Skip to content

Commit

Permalink
Support ofFunction
Browse files Browse the repository at this point in the history
  • Loading branch information
zaaack committed Mar 1, 2018
1 parent ba2f35c commit 47d0d4f
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 98 deletions.
34 changes: 32 additions & 2 deletions Samples/SSRSample/src/Client/View.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 [<Pojo>] FnCompProps = {
text: string
}

let fnComp (props: FnCompProps) =
div []
[ span [] [ str (sprintf "prop: %s" props.text) ] ]

type [<Pojo>] 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" ]
Expand Down Expand Up @@ -146,5 +166,15 @@ let view (model: Model) (dispatch) =
hybridView jsComp jsCompServer { text="I'm rendered by a js Component!" }
]

ofType<MyReactComp, _, _> { text="my prop" } []
div [] [
span [] [ str "Test ofType:" ]
ofType<MyReactComp, _, _> { 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!"] ]
]

]
28 changes: 28 additions & 0 deletions src/Fable.React/Fable.Helpers.Isomorphic.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
// [<Emit("""import "$1" from "$2" """)>]
// 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
141 changes: 99 additions & 42 deletions src/Fable.React/Fable.Helpers.React.fs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -756,41 +760,61 @@ with interface ReactElement
let createElement(comp: obj, props: obj, [<ParamList>] children: obj) =
HTMLNode.Text "" :> ReactElement

[<StringEnum>]
type ServerElementType =
| Fragment = 1
| Component = 2
| Tag = 3
| [<CompiledName("t")>] Tag
| [<CompiledName("f")>] Fragment
| [<CompiledName("c")>] Component

let isomorphicElement (tag: obj, props: obj, children: ReactElement list, elementType: ServerElementType) =
let [<Literal>] 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`
[<System.Obsolete("Use ofType")>]
let inline com<'T,[<Pojo>]'P,[<Pojo>]'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`
[<System.Obsolete("Use ofFunction")>]
let inline fn<[<Pojo>]'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<obj> ()
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<[<Pojo>]'P> (com: ComponentClass<'P>) (props: 'P) (children: ReactElement list): ReactElement =
Expand All @@ -799,12 +823,20 @@ let inline from<[<Pojo>]'P> (com: ComponentClass<'P>) (props: 'P) (children: Rea
/// Instantiate a component from a type inheriting React.Component
/// Example: `ofType<MyComponent,_,_> { myProps = 5 } []`
let inline ofType<'T,[<Pojo>]'P,[<Pojo>]'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`
[<System.Obsolete("Use ofType")>]
let inline com<'T,[<Pojo>]'P,[<Pojo>]'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:
Expand All @@ -813,7 +845,13 @@ let inline ofType<'T,[<Pojo>]'P,[<Pojo>]'S when 'T :> Component<'P,'S>> (props:
/// ofFunction Hello { name = "Maxime" } []
/// ```
let inline ofFunction<[<Pojo>]'P> (f: 'P -> ReactElement) (props: 'P) (children: ReactElement list): ReactElement =
createElement(f, props, children)
hybridExec createElement createServerElementByFn (f, props, children)


/// OBSOLETE: Use `ofFunction`
[<System.Obsolete("Use ofFunction")>]
let inline fn<[<Pojo>]'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 } []`
Expand Down Expand Up @@ -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<IProp>), 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<IProp>), 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<IProp>), [], 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<IProp>), 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<IProp>), 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<Fragment>, props |> Seq.cast<IProp>, children, ServerElementType.Fragment)
let inline clientRender () =
createElement(typedefof<Fragment>, keyValueList CaseRules.LowerFirst props, children)
let inline serverRender () =
createServerElement(typedefof<Fragment>, (props |> Seq.cast<IProp>), children, ServerElementType.Fragment)

hybridExec clientRender serverRender ()

// Standard elements
let inline a b c = domEl "a" b c
Expand Down
63 changes: 9 additions & 54 deletions src/Fable.React/Fable.Import.React.fs
Original file line number Diff line number Diff line change
Expand Up @@ -76,43 +76,42 @@ module React =
/// this.props.name this.state.value
/// div [] [ofString msg]
/// ```
#if FABLE_COMPILER
and [<AbstractClass; Import("Component", "react")>] Component<[<Pojo>]'P, [<Pojo>]'S>(props: 'P) =
and [<AbstractClass; Import("Component", "react")>] Component<[<Pojo>]'P, [<Pojo>]'S>(initProps: 'P) =
[<Emit("$0.props")>]
member __.props: 'P = jsNative
member __.props: 'P = initProps

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

[<Emit("$0.state")>]
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.
/// 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.
/// 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.
[<Emit("$0.setState($1)")>]
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.
[<Emit("$0.setState($1)")>]
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
[<Emit("this.state = $1")>]
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().
[<Emit("$0.forceUpdate($1)")>]
member __.forceUpdate(?callBack: unit->unit): unit = jsNative
member __.forceUpdate(?callBack: unit->unit): unit = ()

[<Emit("$0.isMounted()")>]
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.
Expand Down Expand Up @@ -171,50 +170,6 @@ module React =

interface ReactElement

#else
and [<AbstractClass>] 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:
Expand Down

0 comments on commit 47d0d4f

Please sign in to comment.