-
Notifications
You must be signed in to change notification settings - Fork 84
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(Feliz): refactor React API to improve the transpilation output
- Move `Feliz.DateParsing`, `Feliz.Helpers`, `Feliz.ReactInterop` modules to separate files. - Convert `Feliz.Internal` internal type into a module and move it to a separate file. Make the module public to allow its definitions being called from the public `React` type's inline members. - Adjust `useEffect` method definition in `Feliz.ReactApi.IReactApi` interface. - Change `ReactInterop` binding definitions to the attribute-based to ensure the correct import statements generated by Fable (otherwise, the path to "ReactInterop.js" will be relative to the "Feliz" package root and not to the root of the referencing site). - Make `Feliz.Helpers.optDispose` function not inline - to reduce the amount of Feliz transpiled code. - Make all the members of `Feliz.React` type inline, and for those that contain some custom logic (beyond the trivial bindings) move the logic into the separate functions of the `Feliz.Internal` module. This way, the `React` type won't be present in the transpiled code. - Switch `Feliz.ReactInterop` module access modifier from `internal` to `public` to allow its definitions being called from the public `React` type's inline members.
- Loading branch information
Showing
8 changed files
with
374 additions
and
320 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
module [<RequireQualifiedAccess>] Feliz.DateParsing | ||
|
||
open System | ||
|
||
let (|Between|_|) (x: int) (y: int) (input: int) = | ||
if input >= x && input <= y | ||
then Some() | ||
else None | ||
let isLeapYear (year: int) = DateTime.IsLeapYear(year) | ||
|
||
let (|Int|_|) (input: string) = | ||
try (Some (int input)) | ||
with _ -> None | ||
/// <summary>Parses a specific yyyy-MM-dd or yyyy-MM-ddTHH:mm date format that comes out of an input element with type="date"</summary> | ||
let parse (input: string) : Option<DateTime> = | ||
try | ||
if String.IsNullOrWhiteSpace input then None | ||
else | ||
let parts = input.Split('-') | ||
let year, month, day, hour, minute = | ||
match parts with | ||
| [| Int year; Int month |] -> year, month, 1, 0, 0 | ||
| [| Int year; Int month; Int day |] -> year, month, day, 0, 0 | ||
| [| Int year; Int month; day |] -> | ||
if day.Contains("T") then | ||
match day.Split('T') with | ||
| [| Int parsedDay; time |] -> | ||
match time.Split ':' with | ||
| [| Int hour; Int minute |] -> | ||
match hour, minute with | ||
| Between 0 59, Between 0 59 -> year, month, parsedDay, hour, minute | ||
| _ -> | ||
-1, 1, 1, 0, 0 | ||
| _ -> | ||
-1, 1, 1, 0, 0 | ||
| _ -> | ||
-1, 1, 1, 0, 0 | ||
else | ||
-1, 1, 1, 0, 0 | ||
| _ -> | ||
-1, 1, 1, 0, 0 | ||
if year <= 0 then | ||
None | ||
else | ||
match month, day with | ||
| 2, Between 1 29 when isLeapYear year -> Some (DateTime(year, month, day, hour, minute, 0)) | ||
| 2, Between 1 28 when not (isLeapYear year) -> Some (DateTime(year, month, day, hour, minute, 0)) | ||
| (1 | 3 | 5 | 7 | 8 | 10 | 12), Between 1 31 -> Some (DateTime(year, month, day, hour, minute, 0)) | ||
| (4 | 6 | 9 | 11), Between 1 30 -> Some (DateTime(year, month, day, hour, minute, 0)) | ||
| _ -> None | ||
with | ||
| _ -> None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
namespace Feliz | ||
|
||
open System | ||
open System.ComponentModel | ||
open Fable.Core | ||
|
||
[<EditorBrowsable(EditorBrowsableState.Never); Erase>] | ||
[<RequireQualifiedAccess>] | ||
module Helpers = | ||
let optDispose (disposeOption: #IDisposable option) = | ||
{ new IDisposable with member _.Dispose () = disposeOption |> Option.iter (fun d -> d.Dispose()) } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
namespace Feliz | ||
|
||
open System | ||
open Fable.Core | ||
open Fable.Core.JsInterop | ||
|
||
|
||
module Internal = | ||
|
||
let propsWithKey (withKey: ('props -> string) option) props = | ||
match withKey with | ||
| Some f -> | ||
props?key <- f props | ||
props | ||
| None -> props | ||
|
||
let functionComponent | ||
(renderElement: 'props -> ReactElement) | ||
(name: string option) | ||
(withKey: ('props -> string) option) | ||
: 'props -> Fable.React.ReactElement = | ||
name |> Option.iter (fun name -> renderElement?displayName <- name) | ||
#if FABLE_COMPILER_3 | ||
Browser.Dom.console.warn("Feliz: using React.functionComponent in Fable 3 is obsolete, please consider using the [<ReactComponent>] attribute instead which makes Feliz output better Javascript code that is compatible with react-refresh") | ||
#endif | ||
fun props -> | ||
let props = props |> propsWithKey withKey | ||
Interop.reactApi.createElement(renderElement, props) | ||
let memo | ||
(renderElement: 'props -> ReactElement) | ||
(name: string option) | ||
(areEqual: ('props -> 'props -> bool) option) | ||
(withKey: ('props -> string) option) | ||
: 'props -> Fable.React.ReactElement = | ||
let memoElementType = Interop.reactApi.memo(renderElement, (defaultArg areEqual (unbox null))) | ||
name |> Option.iter (fun name -> renderElement?displayName <- name) | ||
fun props -> | ||
let props = props |> propsWithKey withKey | ||
Interop.reactApi.createElement(memoElementType, props) | ||
|
||
let inline useMemo (createFunction: unit -> 'a) (dependencies: (obj array) option) = | ||
Interop.reactApi.useMemo createFunction (defaultArg dependencies [||]) | ||
|
||
let createDisposable (dispose: unit -> unit) = | ||
{ new IDisposable with member _.Dispose() = dispose() } | ||
|
||
[<Hook>] | ||
let useDisposable (dispose: unit -> unit) = | ||
useMemo (fun () -> createDisposable dispose) (Some [| dispose |]) | ||
|
||
let inline useEffectDisposableOptWithDeps (effect: unit -> #IDisposable option) (dependencies: obj []) = | ||
ReactInterop.useEffectWithDeps (effect >> Helpers.optDispose) dependencies | ||
|
||
let inline useEffectDisposableOpt (effect: unit -> #IDisposable option) = | ||
ReactInterop.useEffect effect | ||
|
||
let inline useEffectWithDeps (effect: unit -> unit) (dependencies: obj []) = | ||
Interop.reactApi.useEffect(effect, dependencies) | ||
|
||
let inline useEffect (effect: unit -> unit) = | ||
Interop.reactApi.useEffect effect | ||
|
||
|
||
[<Hook>] | ||
let useEffectOnce(effect: unit -> unit) = | ||
let calledOnce = Interop.reactApi.useRefInternal false | ||
|
||
useEffectWithDeps (fun () -> | ||
if not calledOnce.current then | ||
calledOnce.current <- true | ||
effect() | ||
) [||] | ||
|
||
[<Hook>] | ||
let useEffectDisposableOnce (effect: unit -> #IDisposable) = | ||
let destroyFunc = Interop.reactApi.useRefInternal None | ||
let calledOnce = Interop.reactApi.useRefInternal false | ||
let renderAfterCalled = Interop.reactApi.useRefInternal false | ||
|
||
if calledOnce.current then | ||
renderAfterCalled.current <- true | ||
|
||
useEffectDisposableOptWithDeps (fun () -> | ||
if calledOnce.current | ||
then None | ||
else | ||
calledOnce.current <- true | ||
destroyFunc.current <- effect() |> Some | ||
|
||
if renderAfterCalled.current | ||
then destroyFunc.current | ||
else None | ||
) [||] | ||
|
||
[<Hook>] | ||
let useEffectDisposableOptOnce (effect: unit -> #IDisposable option) = | ||
let destroyFunc = Interop.reactApi.useRefInternal None | ||
let calledOnce = Interop.reactApi.useRefInternal false | ||
let renderAfterCalled = Interop.reactApi.useRefInternal false | ||
|
||
if calledOnce.current then | ||
renderAfterCalled.current <- true | ||
|
||
useEffectDisposableOptWithDeps (fun () -> | ||
if calledOnce.current | ||
then None | ||
else | ||
calledOnce.current <- true | ||
destroyFunc.current <- effect() | ||
|
||
if renderAfterCalled.current | ||
then destroyFunc.current | ||
else None | ||
) [||] | ||
|
||
|
||
let createContext<'a> (name: string option) (defaultValue: 'a option) = | ||
let contextObject = Interop.reactApi.createContext (defaultArg defaultValue Fable.Core.JS.undefined<'a>) | ||
name |> Option.iter (fun name -> contextObject?displayName <- name) | ||
contextObject | ||
|
||
let inline useRef<'t>(initialValue: 't) = Interop.reactApi.useRefInternal(initialValue) | ||
|
||
let inline useCallback(callbackFunction: 'a -> 'b) (dependencies: (obj array) option) = | ||
Interop.reactApi.useCallback callbackFunction (defaultArg dependencies [||]) | ||
|
||
let inline useLayoutEffect(effect: unit -> unit) = | ||
ReactInterop.useLayoutEffect | ||
(fun _ -> | ||
effect() | ||
createDisposable ignore) | ||
|
||
[<Hook>] | ||
let useCallbackRef(callback: ('a -> 'b)) = | ||
let lastRenderCallbackRef = useRef(callback) | ||
|
||
let callbackRef = | ||
useCallback (fun (arg: 'a) -> | ||
lastRenderCallbackRef.current(arg) | ||
) (Some [||]) | ||
|
||
useLayoutEffect(fun () -> | ||
// render is commited - it's safe to update the callback | ||
lastRenderCallbackRef.current <- callback | ||
) | ||
|
||
callbackRef | ||
|
||
let forwardRef(render: ('props * IRefValue<'t> -> ReactElement)) : ('props * IRefValue<'t> -> ReactElement) = | ||
let forwardRefType = Interop.reactApi.forwardRef(Func<'props,IRefValue<'t>,ReactElement> (fun props ref -> render(props,ref))) | ||
fun (props, ref) -> | ||
let propsObj = props |> JsInterop.toPlainJsObj | ||
propsObj?ref <- ref | ||
Interop.reactApi.createElement(forwardRefType, propsObj) | ||
|
||
let forwardRefWithName (name: string) (render: ('props * IRefValue<'t> -> ReactElement)) : ('props * IRefValue<'t> -> ReactElement) = | ||
let forwardRefType = Interop.reactApi.forwardRef(Func<'props,IRefValue<'t>,ReactElement> (fun props ref -> render(props,ref))) | ||
render?displayName <- name | ||
fun (props, ref) -> | ||
let propsObj = props |> JsInterop.toPlainJsObj | ||
propsObj?ref <- ref | ||
Interop.reactApi.createElement(forwardRefType, propsObj) | ||
|
||
[<Hook>] | ||
let useCancellationToken () = | ||
let cts = useRef(new System.Threading.CancellationTokenSource()) | ||
let token = useRef(cts.current.Token) | ||
|
||
useEffectDisposableOnce(fun () -> | ||
createDisposable(fun () -> | ||
cts.current.Cancel() | ||
cts.current.Dispose() | ||
) | ||
) | ||
|
||
token |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,83 +1,29 @@ | ||
namespace Feliz | ||
module [<RequireQualifiedAccess>] Feliz.Interop | ||
|
||
open Fable.Core | ||
open Fable.Core.JsInterop | ||
open Fable.React | ||
open Feliz.ReactApi | ||
|
||
[<RequireQualifiedAccess>] | ||
module DateParsing = | ||
open System | ||
let (|Between|_|) (x: int) (y: int) (input: int) = | ||
if input >= x && input <= y | ||
then Some() | ||
else None | ||
let isLeapYear (year: int) = DateTime.IsLeapYear(year) | ||
let reactApi : IReactApi = importDefault "react" | ||
#if FABLE_COMPILER_3 || FABLE_COMPILER_4 | ||
let inline reactElement (name: string) (props: 'a) : ReactElement = import "createElement" "react" | ||
#else | ||
let reactElement (name: string) (props: 'a) : ReactElement = import "createElement" "react" | ||
#endif | ||
let inline mkAttr (key: string) (value: obj) : IReactProperty = unbox (key, value) | ||
[<Emit "undefined">] | ||
let undefined : obj = jsNative | ||
let inline mkStyle (key: string) (value: obj) : IStyleAttribute = unbox (key, value) | ||
let inline svgAttribute (key: string) (value: obj) : ISvgAttribute = unbox (key, value) | ||
let inline reactElementWithChild (name: string) (child: 'a) = | ||
reactElement name (createObj [ "children" ==> ResizeArray [| child |] ]) | ||
let inline reactElementWithChildren (name: string) (children: #seq<ReactElement>) = | ||
reactElement name (createObj [ "children" ==> reactApi.Children.toArray (Array.ofSeq children) ]) | ||
let inline createElement name (properties: IReactProperty list) : ReactElement = | ||
reactElement name (createObj !!properties) | ||
let inline createSvgElement name (properties: ISvgAttribute list) : ReactElement = | ||
reactElement name (createObj !!properties) | ||
|
||
let (|Int|_|) (input: string) = | ||
try (Some (int input)) | ||
with _ -> None | ||
/// <summary>Parses a specific yyyy-MM-dd or yyyy-MM-ddTHH:mm date format that comes out of an input element with type="date"</summary> | ||
let parse (input: string) : Option<DateTime> = | ||
try | ||
if String.IsNullOrWhiteSpace input then None | ||
else | ||
let parts = input.Split('-') | ||
let year, month, day, hour, minute = | ||
match parts with | ||
| [| Int year; Int month |] -> year, month, 1, 0, 0 | ||
| [| Int year; Int month; Int day |] -> year, month, day, 0, 0 | ||
| [| Int year; Int month; day |] -> | ||
if day.Contains("T") then | ||
match day.Split('T') with | ||
| [| Int parsedDay; time |] -> | ||
match time.Split ':' with | ||
| [| Int hour; Int minute |] -> | ||
match hour, minute with | ||
| Between 0 59, Between 0 59 -> year, month, parsedDay, hour, minute | ||
| _ -> | ||
-1, 1, 1, 0, 0 | ||
| _ -> | ||
-1, 1, 1, 0, 0 | ||
| _ -> | ||
-1, 1, 1, 0, 0 | ||
else | ||
-1, 1, 1, 0, 0 | ||
| _ -> | ||
-1, 1, 1, 0, 0 | ||
if year <= 0 then | ||
None | ||
else | ||
match month, day with | ||
| 2, Between 1 29 when isLeapYear year -> Some (DateTime(year, month, day, hour, minute, 0)) | ||
| 2, Between 1 28 when not (isLeapYear year) -> Some (DateTime(year, month, day, hour, minute, 0)) | ||
| (1 | 3 | 5 | 7 | 8 | 10 | 12), Between 1 31 -> Some (DateTime(year, month, day, hour, minute, 0)) | ||
| (4 | 6 | 9 | 11), Between 1 30 -> Some (DateTime(year, month, day, hour, minute, 0)) | ||
| _ -> None | ||
with | ||
| _ -> None | ||
|
||
[<RequireQualifiedAccess>] | ||
module Interop = | ||
let reactApi : IReactApi = importDefault "react" | ||
#if FABLE_COMPILER_3 || FABLE_COMPILER_4 | ||
let inline reactElement (name: string) (props: 'a) : ReactElement = import "createElement" "react" | ||
#else | ||
let reactElement (name: string) (props: 'a) : ReactElement = import "createElement" "react" | ||
#endif | ||
let inline mkAttr (key: string) (value: obj) : IReactProperty = unbox (key, value) | ||
[<Emit "undefined">] | ||
let undefined : obj = jsNative | ||
let inline mkStyle (key: string) (value: obj) : IStyleAttribute = unbox (key, value) | ||
let inline svgAttribute (key: string) (value: obj) : ISvgAttribute = unbox (key, value) | ||
let inline reactElementWithChild (name: string) (child: 'a) = | ||
reactElement name (createObj [ "children" ==> ResizeArray [| child |] ]) | ||
let inline reactElementWithChildren (name: string) (children: #seq<ReactElement>) = | ||
reactElement name (createObj [ "children" ==> reactApi.Children.toArray (Array.ofSeq children) ]) | ||
let inline createElement name (properties: IReactProperty list) : ReactElement = | ||
reactElement name (createObj !!properties) | ||
let inline createSvgElement name (properties: ISvgAttribute list) : ReactElement = | ||
reactElement name (createObj !!properties) | ||
|
||
[<Emit "typeof $0 === 'number'">] | ||
let isTypeofNumber (x: obj) : bool = jsNative | ||
[<Emit "typeof $0 === 'number'">] | ||
let isTypeofNumber (x: obj) : bool = jsNative |
Oops, something went wrong.