Skip to content

Commit d8f3576

Browse files
zaaackalfonsogarciacaro
authored andcommitted
Support ofFunction
1 parent e5ed303 commit d8f3576

File tree

4 files changed

+168
-98
lines changed

4 files changed

+168
-98
lines changed

Samples/SSRSample/src/Client/View.fs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,30 @@ type MyReactComp(initProps: MyProp) as self =
6464
do self.setInitState { text="my state" }
6565

6666
override x.render() =
67-
div [] [ str (sprintf "prop: %s state: %s" x.props.text x.state.text) ]
67+
div []
68+
[ span [] [ str (sprintf "prop: %s state: %s" x.props.text x.state.text) ]
69+
span [] [ ofArray x.children ] ]
6870

6971

7072

73+
type [<Pojo>] FnCompProps = {
74+
text: string
75+
}
76+
77+
let fnComp (props: FnCompProps) =
78+
div []
79+
[ span [] [ str (sprintf "prop: %s" props.text) ] ]
80+
81+
type [<Pojo>] FnCompWithChildrenProps = {
82+
children: React.ReactElement array
83+
text: string
84+
}
85+
86+
let fnCompWithChildren (props: FnCompWithChildrenProps) =
87+
div []
88+
[ span [] [ str (sprintf "prop: %s" props.text) ]
89+
span [] [ ofArray props.children ] ]
90+
7191
let view (model: Model) (dispatch) =
7292
div []
7393
[ h1 [] [ str "SAFE Template" ]
@@ -146,5 +166,15 @@ let view (model: Model) (dispatch) =
146166
hybridView jsComp jsCompServer { text="I'm rendered by a js Component!" }
147167
]
148168

149-
ofType<MyReactComp, _, _> { text="my prop" } []
169+
div [] [
170+
span [] [ str "Test ofType:" ]
171+
ofType<MyReactComp, _, _> { text="my prop" } [ span [] [ str "I'm rendered by children!"] ]
172+
]
173+
174+
div [] [
175+
span [] [ str "Test ofFunction:" ]
176+
ofFunction fnComp { text = "I'm rendered by Function Component!"} []
177+
ofFunction fnCompWithChildren { text = "I'm rendered by Function Component!"; children=[||]} [ span [] [ str "I'm rendered by children!"] ]
178+
]
179+
150180
]

src/Fable.React/Fable.Helpers.Isomorphic.fs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ module Components =
3131
open Components
3232
open Fable.Helpers.React
3333

34+
/// Isomorphic helper function for conditional executaion
35+
/// it will execute `clientFn model` in the client side and `serverFn model` in the server side
36+
let inline hybridExec clientFn serverFn model =
37+
ServerRenderingInternal.hybridExec clientFn serverFn model
3438

3539
let hybridView (clientView: 'model -> ReactElement) (serverView: 'model -> ReactElement) (model: 'model) =
3640
#if FABLE_COMPILER
@@ -39,3 +43,27 @@ let hybridView (clientView: 'model -> ReactElement) (serverView: 'model -> React
3943
serverView model
4044
#endif
4145

46+
47+
48+
// /// Isomorphic helper function for Fable.Core.JsInterop.import,
49+
// /// 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
50+
// [<Emit("""import "$1" from "$2" """)>]
51+
// let inline importOrDefault<'T> selector path =
52+
// Unchecked.defaultof<'T>
53+
54+
// /// Isomorphic helper function for Fable.Core.JsInterop.importAll,
55+
// /// 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
56+
// let inline importAllOrDefault<'T> path =
57+
// importOrDefault<'T> "*" path
58+
59+
60+
61+
// /// Isomorphic helper function for Fable.Core.JsInterop.importDefault,
62+
// /// 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
63+
// let inline importDefaultOrDefault<'T> path =
64+
// importOrDefault<'T> "default" path
65+
66+
// /// Isomorphic helper function for Fable.Core.JsInterop.importSideEffects,
67+
// /// it works exactly the same as importSideEffects in client side, but would ignore in server side instead of throw an runtime error immediately
68+
// let inline importSideEffectsOrDefault path =
69+
// hybridExec importSideEffects ignore path

src/Fable.React/Fable.Helpers.React.fs

Lines changed: 99 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
module Fable.Helpers.React
22

3+
open System
4+
open System.Reflection
5+
open FSharp.Reflection
6+
open FSharp.Reflection.FSharpReflectionExtensions
37
open Fable.Core
48
open Fable.Core.JsInterop
59
open Fable.Import
@@ -756,41 +760,61 @@ with interface ReactElement
756760
let createElement(comp: obj, props: obj, [<ParamList>] children: obj) =
757761
HTMLNode.Text "" :> ReactElement
758762

763+
[<StringEnum>]
759764
type ServerElementType =
760-
| Fragment = 1
761-
| Component = 2
762-
| Tag = 3
765+
| [<CompiledName("t")>] Tag
766+
| [<CompiledName("f")>] Fragment
767+
| [<CompiledName("c")>] Component
763768

764-
let isomorphicElement (tag: obj, props: obj, children: ReactElement list, elementType: ServerElementType) =
769+
let [<Literal>] private ChildrenName = "children"
770+
771+
module ServerRenderingInternal =
772+
let inline hybridExec (clientFn: 'a -> 'b) (serverFn: 'a -> 'b) (input: 'a) =
765773
#if FABLE_COMPILER
766-
let props =
767-
match elementType with
768-
| ServerElementType.Component -> props
769-
| _ -> keyValueList CaseRules.LowerFirst (props :?> IProp list)
770-
createElement(tag, props, children)
774+
clientFn input
771775
#else
772-
match elementType with
773-
| ServerElementType.Tag ->
774-
HTMLNode.Node (string tag, props :?> IProp seq, children) :> ReactElement
775-
| ServerElementType.Fragment ->
776-
HTMLNode.List children :> ReactElement
777-
| ServerElementType.Component ->
778-
let tag = tag :?> System.Type
779-
let comp = System.Activator.CreateInstance(tag, props)
780-
let render = tag.GetMethod("render")
781-
render.Invoke(comp, null) :?> ReactElement
782-
| _ -> HTMLNode.Text "" :> ReactElement
776+
serverFn input
783777
#endif
784778

785-
/// OBSOLETE: Use `ofType`
786-
[<System.Obsolete("Use ofType")>]
787-
let inline com<'T,[<Pojo>]'P,[<Pojo>]'S when 'T :> Component<'P,'S>> (props: 'P) (children: ReactElement list): ReactElement =
788-
isomorphicElement(typedefof<'T>, props, children, ServerElementType.Component)
779+
#if FABLE_COMPILER
780+
let inline createServerElement (tag: obj, props: obj, children: ReactElement list, elementType: ServerElementType) =
781+
createElement(tag, props, children)
782+
let inline createServerElementByFn (f, props, children) =
783+
createElement(f, props, children)
784+
#else
785+
let createServerElement (tag: obj, props: obj, children: ReactElement list, elementType: ServerElementType) =
786+
match elementType with
787+
| ServerElementType.Tag ->
788+
HTMLNode.Node (string tag, props :?> IProp seq, children) :> ReactElement
789+
| ServerElementType.Fragment ->
790+
HTMLNode.List children :> ReactElement
791+
| ServerElementType.Component ->
792+
let tag = tag :?> System.Type
793+
let comp = System.Activator.CreateInstance(tag, props)
794+
let childrenProp = tag.GetProperty(ChildrenName)
795+
childrenProp.SetValue(comp, children |> Seq.toArray)
796+
let render = tag.GetMethod("render")
797+
render.Invoke(comp, null) :?> ReactElement
789798

790-
/// OBSOLETE: Use `ofFunction`
791-
[<System.Obsolete("Use ofFunction")>]
792-
let inline fn<[<Pojo>]'P> (f: 'P -> ReactElement) (props: 'P) (children: ReactElement list): ReactElement =
793-
createElement(f, props, children)
799+
let createServerElementByFn = fun (f, props, children) ->
800+
let propsType = props.GetType()
801+
let props =
802+
if propsType.GetProperty (ChildrenName) |> isNull then
803+
props
804+
else
805+
let values = ResizeArray<obj> ()
806+
let properties = propsType.GetProperties()
807+
for p in properties do
808+
if p.Name = ChildrenName then
809+
values.Add (children |> Seq.toArray)
810+
else
811+
values.Add (FSharpValue.GetRecordField(props, p))
812+
FSharpValue.MakeRecord(propsType, values.ToArray()) :?> 'P
813+
f props
814+
815+
#endif
816+
817+
open ServerRenderingInternal
794818

795819
/// Instantiate an imported React component
796820
let inline from<[<Pojo>]'P> (com: ComponentClass<'P>) (props: 'P) (children: ReactElement list): ReactElement =
@@ -799,12 +823,20 @@ let inline from<[<Pojo>]'P> (com: ComponentClass<'P>) (props: 'P) (children: Rea
799823
/// Instantiate a component from a type inheriting React.Component
800824
/// Example: `ofType<MyComponent,_,_> { myProps = 5 } []`
801825
let inline ofType<'T,[<Pojo>]'P,[<Pojo>]'S when 'T :> Component<'P,'S>> (props: 'P) (children: ReactElement list): ReactElement =
802-
#if FABLE_COMPILER
803-
let obj = typedefof<'T>
804-
#else
805-
let obj = typeof<'T>
806-
#endif
807-
isomorphicElement(obj, props, children, ServerElementType.Component)
826+
let inline clientRender () =
827+
createElement(typedefof<'T>, props, children)
828+
829+
let inline serverRender () =
830+
createServerElement(typeof<'T>, props, children, ServerElementType.Component)
831+
832+
hybridExec clientRender serverRender ()
833+
834+
835+
836+
/// OBSOLETE: Use `ofType`
837+
[<System.Obsolete("Use ofType")>]
838+
let inline com<'T,[<Pojo>]'P,[<Pojo>]'S when 'T :> Component<'P,'S>> (props: 'P) (children: ReactElement list): ReactElement =
839+
ofType<'T, 'P, 'S> props children
808840

809841
/// Instantiate a stateless component from a function
810842
/// Example:
@@ -813,7 +845,13 @@ let inline ofType<'T,[<Pojo>]'P,[<Pojo>]'S when 'T :> Component<'P,'S>> (props:
813845
/// ofFunction Hello { name = "Maxime" } []
814846
/// ```
815847
let inline ofFunction<[<Pojo>]'P> (f: 'P -> ReactElement) (props: 'P) (children: ReactElement list): ReactElement =
816-
createElement(f, props, children)
848+
hybridExec createElement createServerElementByFn (f, props, children)
849+
850+
851+
/// OBSOLETE: Use `ofFunction`
852+
[<System.Obsolete("Use ofFunction")>]
853+
let inline fn<[<Pojo>]'P> (f: 'P -> ReactElement) (props: 'P) (children: ReactElement list): ReactElement =
854+
ofFunction f props children
817855

818856
/// Instantiate an imported React component. The first two arguments must be string literals, "default" can be used for the first one.
819857
/// Example: `ofImport "Map" "leaflet" { x = 10; y = 50 } []`
@@ -879,22 +917,41 @@ let inline ofArray (els: ReactElement array): ReactElement = HTMLNode.List els :
879917

880918
/// Instantiate a DOM React element
881919
let inline domEl (tag: string) (props: IHTMLProp list) (children: ReactElement list): ReactElement =
882-
// createElement(tag, keyValueList CaseRules.LowerFirst props, children)
883-
isomorphicElement(tag, (props |> Seq.cast<IProp>), children, ServerElementType.Tag)
920+
let inline clientRender (tag, props, children) =
921+
createElement(tag, keyValueList CaseRules.LowerFirst props, children)
922+
923+
let inline serverRender (tag, props, children) =
924+
createServerElement(tag, (props |> Seq.cast<IProp>), children, ServerElementType.Tag)
925+
926+
hybridExec clientRender serverRender (tag, props, children)
884927

885928
/// Instantiate a DOM React element (void)
886929
let inline voidEl (tag: string) (props: IHTMLProp list) : ReactElement =
887-
// createElement(tag, keyValueList CaseRules.LowerFirst props, [])
888-
isomorphicElement(tag, (props |> Seq.cast<IProp>), [], ServerElementType.Tag)
930+
let inline clientRender (tag, props, children) =
931+
createElement(tag, keyValueList CaseRules.LowerFirst props, children)
932+
933+
let inline serverRender (tag, props, children) =
934+
createServerElement(tag, (props |> Seq.cast<IProp>), children, ServerElementType.Tag)
935+
936+
hybridExec clientRender serverRender (tag, props, [])
889937

890938
/// Instantiate an SVG React element
891939
let inline svgEl (tag: string) (props: IProp list) (children: ReactElement list): ReactElement =
892-
// createElement(tag, keyValueList CaseRules.LowerFirst props, children)
893-
isomorphicElement(tag, props, children, ServerElementType.Tag)
940+
let inline clientRender (tag, props, children) =
941+
createElement(tag, keyValueList CaseRules.LowerFirst props, children)
942+
let inline serverRender (tag, props, children) =
943+
createServerElement(tag, (props |> Seq.cast<IProp>), children, ServerElementType.Tag)
944+
945+
hybridExec clientRender serverRender (tag, props, children)
894946

895947
/// Instantiate a React fragment
896948
let inline fragment (props: IFragmentProp list) (children: ReactElement list): ReactElement =
897-
isomorphicElement(typedefof<Fragment>, props |> Seq.cast<IProp>, children, ServerElementType.Fragment)
949+
let inline clientRender () =
950+
createElement(typedefof<Fragment>, keyValueList CaseRules.LowerFirst props, children)
951+
let inline serverRender () =
952+
createServerElement(typedefof<Fragment>, (props |> Seq.cast<IProp>), children, ServerElementType.Fragment)
953+
954+
hybridExec clientRender serverRender ()
898955

899956
// Standard elements
900957
let inline a b c = domEl "a" b c

src/Fable.React/Fable.Import.React.fs

Lines changed: 9 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -76,43 +76,42 @@ module React =
7676
/// this.props.name this.state.value
7777
/// div [] [ofString msg]
7878
/// ```
79-
#if FABLE_COMPILER
80-
and [<AbstractClass; Import("Component", "react")>] Component<[<Pojo>]'P, [<Pojo>]'S>(props: 'P) =
79+
and [<AbstractClass; Import("Component", "react")>] Component<[<Pojo>]'P, [<Pojo>]'S>(initProps: 'P) =
8180
[<Emit("$0.props")>]
82-
member __.props: 'P = jsNative
81+
member __.props: 'P = initProps
8382

8483
[<Emit("Array.prototype.concat($0.props.children || [])")>]
85-
member __.children: ReactElement array = jsNative
84+
member val children: ReactElement array = [| |] with get, set
8685

8786
[<Emit("$0.state")>]
88-
member __.state: 'S = jsNative
87+
member val state: 'S = Unchecked.defaultof<'S> with get, set
8988

9089
/// ATTENTION: Within the constructor, use `setInitState`
9190
/// 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.
9291
/// Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
9392
/// 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.
9493
/// 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.
9594
[<Emit("$0.setState($1)")>]
96-
member __.setState(value: 'S): unit = jsNative
95+
member x.setState(value: 'S): unit = x.state <- value
9796

9897
/// Overload of `setState` accepting updater function with the signature: `(prevState, props) => stateChange`
9998
/// 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.
10099
/// 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.
101100
[<Emit("$0.setState($1)")>]
102-
member __.setState(updater: 'S->'P->'S): unit = jsNative
101+
member x.setState(updater: 'S->'P->'S): unit = x.state <- updater x.state x.props
103102

104103
/// This method can only be called in the constructor
105104
[<Emit("this.state = $1")>]
106-
member __.setInitState(value: 'S): unit = jsNative
105+
member x.setInitState(value: 'S): unit = x.state <- value
107106

108107
/// 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().
109108
/// 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.
110109
/// Normally you should try to avoid all uses of forceUpdate() and only read from this.props and this.state in render().
111110
[<Emit("$0.forceUpdate($1)")>]
112-
member __.forceUpdate(?callBack: unit->unit): unit = jsNative
111+
member __.forceUpdate(?callBack: unit->unit): unit = ()
113112

114113
[<Emit("$0.isMounted()")>]
115-
member __.isMounted(): bool = jsNative
114+
member __.isMounted(): bool = false
116115

117116
/// 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.
118117
/// Avoid introducing any side-effects or subscriptions in this method. For those use cases, use componentDidMount() instead.
@@ -171,50 +170,6 @@ module React =
171170

172171
interface ReactElement
173172

174-
#else
175-
and [<AbstractClass>] Component<'P, 'S>(initProps: 'P) =
176-
member __.props: 'P = initProps
177-
178-
member __.children: ReactElement array = [| |]
179-
180-
member val state: 'S = Unchecked.defaultof<'S> with get, set
181-
182-
member x.setState(value: 'S): unit = x.state <- value
183-
member x.setState(updater: 'S->'P->'S): unit = x.state <- updater x.state x.props
184-
185-
member x.setInitState(value: 'S): unit = x.state <- value
186-
187-
member __.forceUpdate(?callBack: unit->unit): unit = ()
188-
189-
member __.isMounted(): bool = false
190-
191-
abstract componentWillMount: unit -> unit
192-
default __.componentWillMount () = ()
193-
abstract componentDidMount: unit -> unit
194-
default __.componentDidMount () = ()
195-
196-
abstract componentWillReceiveProps: nextProps: 'P -> unit
197-
default __.componentWillReceiveProps (_) = ()
198-
199-
abstract shouldComponentUpdate: nextProps: 'P * nextState: 'S -> bool
200-
default __.shouldComponentUpdate (_, _) = true
201-
202-
abstract componentWillUpdate: nextProps: 'P * nextState: 'S -> unit
203-
default __.componentWillUpdate (_, _) = ()
204-
205-
abstract componentDidUpdate: prevProps: 'P * prevState: 'S -> unit
206-
default __.componentDidUpdate (_, _) = ()
207-
208-
abstract componentWillUnmount: unit -> unit
209-
default __.componentWillUnmount () = ()
210-
211-
abstract componentDidCatch: error: Exception * info: obj -> unit
212-
default __.componentDidCatch (_, _) = ()
213-
214-
abstract render: unit -> ReactElement
215-
216-
interface ReactElement
217-
#endif
218173
/// A react component that implements `shouldComponentUpdate()` with a shallow prop and state comparison.
219174
///
220175
/// Usage:

0 commit comments

Comments
 (0)