mvsmal / fable-material-ui

Fable bindings for Material-UI https://mvsmal.github.io/fable-material-ui/
MIT License
61 stars 8 forks source link

Improve documentation how to create HOCs with Styles #53

Open halcwb opened 5 years ago

halcwb commented 5 years ago

This thread is closed but is very important to figure out how the creation of HOCs work. Especially the part where you need to pass the model and the dispatch function through a props object. Maybe put this in the documentation as well? The current example just says:

open Fable.Core.JsInterop
open Fable.Helpers.React
open Fable.Helpers.React.Props
open Fable.MaterialUI.Core
open Fable.MaterialUI.Props

let styles : IStyles list = [
    Styles.Root [
        CSSProp.BackgroundColor "red"
    ]
]

let myFun (props : IClassesProps) =
    div [ HTMLAttr.Class !!props.classes?root ] []

let withStylesFun = withStyles<IClassesProps> (StyleType.Styles styles) [] myFun

let view () =
    from withStylesFun createEmpty []

The example in this thread is much more insightful.

Originally posted by @halcwb in https://github.com/mvsmal/fable-material-ui/issues/4#issuecomment-462112975

cmeeren commented 5 years ago

For the record, here's how I do it:

let private styles (theme: ITheme) : IStyles list =
  Styles.Root [
        CSSProp.BackgroundColor "red"
    ]

let private view' (classes: IClasses) model dispatch =
  div [ HTMLAttr.Class !!classes?root ] []

// Boilerplate below to support JSS with Elmish

type private IProps =
  abstract member model: Model with get, set
  abstract member dispatch: (Msg -> unit) with get, set
  inherit IClassesProps

type private Component(p) =
  inherit PureStatelessComponent<IProps>(p)
  let viewFun (p: IProps) = view' p.classes p.model p.dispatch
  let viewWithStyles = withStyles (StyleType.Func styles) [] viewFun
  override this.render() = ReactElementType.create !!viewWithStyles this.props []

let view (model: Model) (dispatch: Msg -> unit) : ReactElement =
  let props = jsOptions<IProps>(fun p ->
    p.model <- model
    p.dispatch <- dispatch)
  ofType<Component,_,_> props []

IProps, Component and view are always defined like this (copy-paste). Only styles and view' varies per component, and view looks like normal Elmish (except it's private and also takes an IClasses parameter).

halcwb commented 5 years ago

I think this code:

let private view' (classes: IClasses) model dispatch =
  div [ HTMLAttr.Class !!props.classes?root ] []

Should be:

let private view' (classes: IClasses) model dispatch =
  div [ HTMLAttr.Class !!classes?root ] []

So classes?root instead of props.classes?root ?

cmeeren commented 5 years ago

Yes sorry, I copied that part from your example but forgot to change it.

Luiz-Monad commented 5 years ago

I found a better way to do this. (related to #4)

I though withStyles was causing performance problems in my App, so I dediced to implement my own HoC to do styling.
Now I think the problem was the way Fable-React is rendering, it didn't solve my problem, but now at least I have a better way to style the components and have the JSS cached.

The way I envisioned usage.

let styles ( theme: Theme ) = {|
    Button = style [
        S.Margin "5vh 0 5vh 0"
    ]
|}
let render styled dispatch model =
    let styled = inferType styles styled
    div styled.Background [   .... omited   ]
let view = render |> withStyles styles 

You need some adapters


/// Declares a style class.
let style cssProps =
    Some { Props = cssProps; ClassName = "" }

/// Runtime generated style class name.
let styleClass style : IHTMLProp list =
    match style with
    | Some s -> [ HTMLAttr.ClassName s.ClassName ]
    | _ -> []

// mui Helper
let div style =
    div <| styleClass style

Code follow: I better make a PR, what do you think ?


// Material UI makeStyles
let makeStyles'<'S, 'O, 'P> ( styles: 'S ) ( options: 'O ) 
    : 'P -> IClassesProps =
    !!((import "makeStyles" "@material-ui/core/styles") $ (styles, options))

// Material UI makeStyles
let makeStyles ( styles : StyleType ) ( options: StyleOption seq ) =
    let opt = keyValueList CaseRules.LowerFirst options
    let styles' =
        match styles with
        | StyleType.Styles styles -> (keyValueList CaseRules.LowerFirst styles |> unbox)
        | StyleType.Func func -> func >> keyValueList CaseRules.LowerFirst
    makeStyles'<_, _, unit> styles' opt 

// Material UI useTheme
let useTheme<'T> () : 'T =
    !!((import "useTheme" "@material-ui/core/styles") $ ())

/// Convert our CssStyle to IStyles list
let createSheet ( styleSheet: ITheme -> 'StyleSheet ) =
    fun theme ->
        let css = styleSheet theme
        let idx k = ( k, css?(k) )
        let conv ( k, v: CssStyle ) = Styles.Custom ( k, v.Props ) :> IStyles    
        JS.Object.keys css |> Seq.map ( idx >> conv ) |> List.ofSeq

/// Convert the IClassesProps to our CssStyle
let applySheet styleSheet ( classes: IClassesProps ) =
    let css: 'StyleSheet = styleSheet ( useTheme<ITheme> () )
    let convBack k v = Some { Props = v.Props; ClassName = k }
    let styleIt k = css?(k) <- convBack classes?(k) css?(k)
    JS.Object.keys css |> Seq.iter styleIt
    css

let inline private uncurryView3 f key dispatch model = 
    f {| pkey = key; dispatch = dispatch; model = model |}

let inline private curryView3 f props =
    let inline inferType ( fn : ( 's -> _ ) -> _ ) ( _ : 's ) = ()
    inferType uncurryView3 props
    f props.pkey props.dispatch props.model 

/// Create a new styled component from a component and a style sheet.
let withStyles styleSheet ( fn: 'StyleSheet -> 'Dispatch -> 'Model -> ReactElement ) =
    let name = ( box styleSheet ).GetHashCode().ToString()
    let sheet = styleSheet |> createSheet |> StyleType.Func |> makeStyles 
    let useStyles = sheet [ StyleOption.Name name ]
    let applyStyles f _ dispatch model = 
        f ( useStyles () |> applySheet styleSheet ) dispatch model
    let fnC = fn |> applyStyles |> curryView3
    let memoComp = 
        FunctionComponent.Of ( fnC, "styled", 
            fun x y -> equalsButFunctions ( x.model ) ( y.model ) )    
    // Defeat Fable currying optimization, so things can be cached.
    fun ( key: string ) ->        
        uncurryView3 memoComp key

/// Useful helper for getting a copy of the anonymous record type to a parameter.
let inline inferType ( fn : _ -> 's ) ( s : 's ) = s