From 47d0d4f0aaeb4da7f7d5b5c9da8d2f399d39d14f Mon Sep 17 00:00:00 2001 From: zaaack Date: Thu, 1 Mar 2018 11:29:51 +0800 Subject: [PATCH] Support ofFunction --- Samples/SSRSample/src/Client/View.fs | 34 ++++- src/Fable.React/Fable.Helpers.Isomorphic.fs | 28 ++++ src/Fable.React/Fable.Helpers.React.fs | 141 ++++++++++++++------ src/Fable.React/Fable.Import.React.fs | 63 ++------- 4 files changed, 168 insertions(+), 98 deletions(-) diff --git a/Samples/SSRSample/src/Client/View.fs b/Samples/SSRSample/src/Client/View.fs index ec7496cb..1929dbab 100644 --- a/Samples/SSRSample/src/Client/View.fs +++ b/Samples/SSRSample/src/Client/View.fs @@ -64,10 +64,30 @@ type MyReactComp(initProps: MyProp) as self = do self.setInitState { text="my state" } override x.render() = - div [] [ str (sprintf "prop: %s state: %s" x.props.text x.state.text) ] + div [] + [ span [] [ str (sprintf "prop: %s state: %s" x.props.text x.state.text) ] + span [] [ ofArray x.children ] ] +type [] FnCompProps = { + text: string +} + +let fnComp (props: FnCompProps) = + div [] + [ span [] [ str (sprintf "prop: %s" props.text) ] ] + +type [] FnCompWithChildrenProps = { + children: React.ReactElement array + text: string +} + +let fnCompWithChildren (props: FnCompWithChildrenProps) = + div [] + [ span [] [ str (sprintf "prop: %s" props.text) ] + span [] [ ofArray props.children ] ] + let view (model: Model) (dispatch) = div [] [ h1 [] [ str "SAFE Template" ] @@ -146,5 +166,15 @@ let view (model: Model) (dispatch) = hybridView jsComp jsCompServer { text="I'm rendered by a js Component!" } ] - ofType { text="my prop" } [] + div [] [ + span [] [ str "Test ofType:" ] + ofType { text="my prop" } [ span [] [ str "I'm rendered by children!"] ] + ] + + div [] [ + span [] [ str "Test ofFunction:" ] + ofFunction fnComp { text = "I'm rendered by Function Component!"} [] + ofFunction fnCompWithChildren { text = "I'm rendered by Function Component!"; children=[||]} [ span [] [ str "I'm rendered by children!"] ] + ] + ] diff --git a/src/Fable.React/Fable.Helpers.Isomorphic.fs b/src/Fable.React/Fable.Helpers.Isomorphic.fs index ae75d8f1..cfcb7c15 100644 --- a/src/Fable.React/Fable.Helpers.Isomorphic.fs +++ b/src/Fable.React/Fable.Helpers.Isomorphic.fs @@ -31,6 +31,10 @@ module Components = open Components open Fable.Helpers.React +/// Isomorphic helper function for conditional executaion +/// it will execute `clientFn model` in the client side and `serverFn model` in the server side +let inline hybridExec clientFn serverFn model = + ServerRenderingInternal.hybridExec clientFn serverFn model let hybridView (clientView: 'model -> ReactElement) (serverView: 'model -> ReactElement) (model: 'model) = #if FABLE_COMPILER @@ -39,3 +43,27 @@ let hybridView (clientView: 'model -> ReactElement) (serverView: 'model -> React serverView model #endif + + +// /// Isomorphic helper function for Fable.Core.JsInterop.import, +// /// it works exactly the same as import in client side, but would return Unchecked.defaultof<'T> in server side instead of throw an runtime error immediately +// [] +// let inline importOrDefault<'T> selector path = +// Unchecked.defaultof<'T> + +// /// Isomorphic helper function for Fable.Core.JsInterop.importAll, +// /// it works exactly the same as importAll in client side, but would return Unchecked.defaultof<'T> in server side instead of throw an runtime error immediately +// let inline importAllOrDefault<'T> path = +// importOrDefault<'T> "*" path + + + +// /// Isomorphic helper function for Fable.Core.JsInterop.importDefault, +// /// it works exactly the same as importDefault in client side, but would return Unchecked.defaultof<'T> in server side instead of throw an runtime error immediately +// let inline importDefaultOrDefault<'T> path = +// importOrDefault<'T> "default" path + +// /// Isomorphic helper function for Fable.Core.JsInterop.importSideEffects, +// /// it works exactly the same as importSideEffects in client side, but would ignore in server side instead of throw an runtime error immediately +// let inline importSideEffectsOrDefault path = +// hybridExec importSideEffects ignore path diff --git a/src/Fable.React/Fable.Helpers.React.fs b/src/Fable.React/Fable.Helpers.React.fs index bd7a478e..f1929c0d 100644 --- a/src/Fable.React/Fable.Helpers.React.fs +++ b/src/Fable.React/Fable.Helpers.React.fs @@ -1,5 +1,9 @@ module Fable.Helpers.React +open System +open System.Reflection +open FSharp.Reflection +open FSharp.Reflection.FSharpReflectionExtensions open Fable.Core open Fable.Core.JsInterop open Fable.Import @@ -756,41 +760,61 @@ with interface ReactElement let createElement(comp: obj, props: obj, [] children: obj) = HTMLNode.Text "" :> ReactElement +[] type ServerElementType = -| Fragment = 1 -| Component = 2 -| Tag = 3 +| [] Tag +| [] Fragment +| [] Component -let isomorphicElement (tag: obj, props: obj, children: ReactElement list, elementType: ServerElementType) = +let [] private ChildrenName = "children" + +module ServerRenderingInternal = + let inline hybridExec (clientFn: 'a -> 'b) (serverFn: 'a -> 'b) (input: 'a) = #if FABLE_COMPILER - let props = - match elementType with - | ServerElementType.Component -> props - | _ -> keyValueList CaseRules.LowerFirst (props :?> IProp list) - createElement(tag, props, children) + clientFn input #else - match elementType with - | ServerElementType.Tag -> - HTMLNode.Node (string tag, props :?> IProp seq, children) :> ReactElement - | ServerElementType.Fragment -> - HTMLNode.List children :> ReactElement - | ServerElementType.Component -> - let tag = tag :?> System.Type - let comp = System.Activator.CreateInstance(tag, props) - let render = tag.GetMethod("render") - render.Invoke(comp, null) :?> ReactElement - | _ -> HTMLNode.Text "" :> ReactElement + serverFn input #endif -/// OBSOLETE: Use `ofType` -[] -let inline com<'T,[]'P,[]'S when 'T :> Component<'P,'S>> (props: 'P) (children: ReactElement list): ReactElement = - isomorphicElement(typedefof<'T>, props, children, ServerElementType.Component) +#if FABLE_COMPILER + let inline createServerElement (tag: obj, props: obj, children: ReactElement list, elementType: ServerElementType) = + createElement(tag, props, children) + let inline createServerElementByFn (f, props, children) = + createElement(f, props, children) +#else + let createServerElement (tag: obj, props: obj, children: ReactElement list, elementType: ServerElementType) = + match elementType with + | ServerElementType.Tag -> + HTMLNode.Node (string tag, props :?> IProp seq, children) :> ReactElement + | ServerElementType.Fragment -> + HTMLNode.List children :> ReactElement + | ServerElementType.Component -> + let tag = tag :?> System.Type + let comp = System.Activator.CreateInstance(tag, props) + let childrenProp = tag.GetProperty(ChildrenName) + childrenProp.SetValue(comp, children |> Seq.toArray) + let render = tag.GetMethod("render") + render.Invoke(comp, null) :?> ReactElement -/// OBSOLETE: Use `ofFunction` -[] -let inline fn<[]'P> (f: 'P -> ReactElement) (props: 'P) (children: ReactElement list): ReactElement = - createElement(f, props, children) + let createServerElementByFn = fun (f, props, children) -> + let propsType = props.GetType() + let props = + if propsType.GetProperty (ChildrenName) |> isNull then + props + else + let values = ResizeArray () + let properties = propsType.GetProperties() + for p in properties do + if p.Name = ChildrenName then + values.Add (children |> Seq.toArray) + else + values.Add (FSharpValue.GetRecordField(props, p)) + FSharpValue.MakeRecord(propsType, values.ToArray()) :?> 'P + f props + +#endif + +open ServerRenderingInternal /// Instantiate an imported React component let inline from<[]'P> (com: ComponentClass<'P>) (props: 'P) (children: ReactElement list): ReactElement = @@ -799,12 +823,20 @@ let inline from<[]'P> (com: ComponentClass<'P>) (props: 'P) (children: Rea /// Instantiate a component from a type inheriting React.Component /// Example: `ofType { myProps = 5 } []` let inline ofType<'T,[]'P,[]'S when 'T :> Component<'P,'S>> (props: 'P) (children: ReactElement list): ReactElement = -#if FABLE_COMPILER - let obj = typedefof<'T> -#else - let obj = typeof<'T> -#endif - isomorphicElement(obj, props, children, ServerElementType.Component) + let inline clientRender () = + createElement(typedefof<'T>, props, children) + + let inline serverRender () = + createServerElement(typeof<'T>, props, children, ServerElementType.Component) + + hybridExec clientRender serverRender () + + + +/// OBSOLETE: Use `ofType` +[] +let inline com<'T,[]'P,[]'S when 'T :> Component<'P,'S>> (props: 'P) (children: ReactElement list): ReactElement = + ofType<'T, 'P, 'S> props children /// Instantiate a stateless component from a function /// Example: @@ -813,7 +845,13 @@ let inline ofType<'T,[]'P,[]'S when 'T :> Component<'P,'S>> (props: /// ofFunction Hello { name = "Maxime" } [] /// ``` let inline ofFunction<[]'P> (f: 'P -> ReactElement) (props: 'P) (children: ReactElement list): ReactElement = - createElement(f, props, children) + hybridExec createElement createServerElementByFn (f, props, children) + + +/// OBSOLETE: Use `ofFunction` +[] +let inline fn<[]'P> (f: 'P -> ReactElement) (props: 'P) (children: ReactElement list): ReactElement = + ofFunction f props children /// Instantiate an imported React component. The first two arguments must be string literals, "default" can be used for the first one. /// Example: `ofImport "Map" "leaflet" { x = 10; y = 50 } []` @@ -879,22 +917,41 @@ let inline ofArray (els: ReactElement array): ReactElement = HTMLNode.List els : /// Instantiate a DOM React element let inline domEl (tag: string) (props: IHTMLProp list) (children: ReactElement list): ReactElement = -// createElement(tag, keyValueList CaseRules.LowerFirst props, children) - isomorphicElement(tag, (props |> Seq.cast), children, ServerElementType.Tag) + let inline clientRender (tag, props, children) = + createElement(tag, keyValueList CaseRules.LowerFirst props, children) + + let inline serverRender (tag, props, children) = + createServerElement(tag, (props |> Seq.cast), children, ServerElementType.Tag) + + hybridExec clientRender serverRender (tag, props, children) /// Instantiate a DOM React element (void) let inline voidEl (tag: string) (props: IHTMLProp list) : ReactElement = -// createElement(tag, keyValueList CaseRules.LowerFirst props, []) - isomorphicElement(tag, (props |> Seq.cast), [], ServerElementType.Tag) + let inline clientRender (tag, props, children) = + createElement(tag, keyValueList CaseRules.LowerFirst props, children) + + let inline serverRender (tag, props, children) = + createServerElement(tag, (props |> Seq.cast), children, ServerElementType.Tag) + + hybridExec clientRender serverRender (tag, props, []) /// Instantiate an SVG React element let inline svgEl (tag: string) (props: IProp list) (children: ReactElement list): ReactElement = -// createElement(tag, keyValueList CaseRules.LowerFirst props, children) - isomorphicElement(tag, props, children, ServerElementType.Tag) + let inline clientRender (tag, props, children) = + createElement(tag, keyValueList CaseRules.LowerFirst props, children) + let inline serverRender (tag, props, children) = + createServerElement(tag, (props |> Seq.cast), children, ServerElementType.Tag) + + hybridExec clientRender serverRender (tag, props, children) /// Instantiate a React fragment let inline fragment (props: IFragmentProp list) (children: ReactElement list): ReactElement = - isomorphicElement(typedefof, props |> Seq.cast, children, ServerElementType.Fragment) + let inline clientRender () = + createElement(typedefof, keyValueList CaseRules.LowerFirst props, children) + let inline serverRender () = + createServerElement(typedefof, (props |> Seq.cast), children, ServerElementType.Fragment) + + hybridExec clientRender serverRender () // Standard elements let inline a b c = domEl "a" b c diff --git a/src/Fable.React/Fable.Import.React.fs b/src/Fable.React/Fable.Import.React.fs index 6d24a772..14853a1b 100644 --- a/src/Fable.React/Fable.Import.React.fs +++ b/src/Fable.React/Fable.Import.React.fs @@ -76,16 +76,15 @@ module React = /// this.props.name this.state.value /// div [] [ofString msg] /// ``` -#if FABLE_COMPILER - and [] Component<[]'P, []'S>(props: 'P) = + and [] Component<[]'P, []'S>(initProps: 'P) = [] - member __.props: 'P = jsNative + member __.props: 'P = initProps [] - member __.children: ReactElement array = jsNative + member val children: ReactElement array = [| |] with get, set [] - member __.state: 'S = jsNative + member val state: 'S = Unchecked.defaultof<'S> with get, set /// ATTENTION: Within the constructor, use `setInitState` /// Enqueues changes to the component state and tells React that this component and its children need to be re-rendered with the updated state. This is the primary method you use to update the user interface in response to event handlers and server responses. @@ -93,26 +92,26 @@ module React = /// setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below. /// setState() will always lead to a re-render unless shouldComponentUpdate() returns false. If mutable objects are being used and conditional rendering logic cannot be implemented in shouldComponentUpdate(), calling setState() only when the new state differs from the previous state will avoid unnecessary re-renders. [] - member __.setState(value: 'S): unit = jsNative + member x.setState(value: 'S): unit = x.state <- value /// Overload of `setState` accepting updater function with the signature: `(prevState, props) => stateChange` /// prevState is a reference to the previous state. It should not be directly mutated. Instead, changes should be represented by building a new object based on the input from prevState and props. /// Both prevState and props received by the updater function are guaranteed to be up-to-date. The output of the updater is shallowly merged with prevState. [] - member __.setState(updater: 'S->'P->'S): unit = jsNative + member x.setState(updater: 'S->'P->'S): unit = x.state <- updater x.state x.props /// This method can only be called in the constructor [] - member __.setInitState(value: 'S): unit = jsNative + member x.setInitState(value: 'S): unit = x.state <- value /// By default, when your component’s state or props change, your component will re-render. If your render() method depends on some other data, you can tell React that the component needs re-rendering by calling forceUpdate(). /// Calling forceUpdate() will cause render() to be called on the component, skipping shouldComponentUpdate(). This will trigger the normal lifecycle methods for child components, including the shouldComponentUpdate() method of each child. React will still only update the DOM if the markup changes. /// Normally you should try to avoid all uses of forceUpdate() and only read from this.props and this.state in render(). [] - member __.forceUpdate(?callBack: unit->unit): unit = jsNative + member __.forceUpdate(?callBack: unit->unit): unit = () [] - member __.isMounted(): bool = jsNative + member __.isMounted(): bool = false /// Invoked immediately before mounting occurs. It is called before render(), therefore calling setState() synchronously in this method will not trigger an extra rendering. Generally, we recommend using the constructor() instead. /// Avoid introducing any side-effects or subscriptions in this method. For those use cases, use componentDidMount() instead. @@ -171,50 +170,6 @@ module React = interface ReactElement -#else - and [] Component<'P, 'S>(initProps: 'P) = - member __.props: 'P = initProps - - member __.children: ReactElement array = [| |] - - member val state: 'S = Unchecked.defaultof<'S> with get, set - - member x.setState(value: 'S): unit = x.state <- value - member x.setState(updater: 'S->'P->'S): unit = x.state <- updater x.state x.props - - member x.setInitState(value: 'S): unit = x.state <- value - - member __.forceUpdate(?callBack: unit->unit): unit = () - - member __.isMounted(): bool = false - - abstract componentWillMount: unit -> unit - default __.componentWillMount () = () - abstract componentDidMount: unit -> unit - default __.componentDidMount () = () - - abstract componentWillReceiveProps: nextProps: 'P -> unit - default __.componentWillReceiveProps (_) = () - - abstract shouldComponentUpdate: nextProps: 'P * nextState: 'S -> bool - default __.shouldComponentUpdate (_, _) = true - - abstract componentWillUpdate: nextProps: 'P * nextState: 'S -> unit - default __.componentWillUpdate (_, _) = () - - abstract componentDidUpdate: prevProps: 'P * prevState: 'S -> unit - default __.componentDidUpdate (_, _) = () - - abstract componentWillUnmount: unit -> unit - default __.componentWillUnmount () = () - - abstract componentDidCatch: error: Exception * info: obj -> unit - default __.componentDidCatch (_, _) = () - - abstract render: unit -> ReactElement - - interface ReactElement -#endif /// A react component that implements `shouldComponentUpdate()` with a shallow prop and state comparison. /// /// Usage: