mvsmal / fable-material-ui

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

Is it possible to use JSS based styles ? #4

Closed marcpiechura closed 6 years ago

marcpiechura commented 6 years ago

Hi @mvsmal ,

first of all, thanks for the bindings, they're very well designed :+1:

I'm currently trying to port this example and while most of it is working just fine, I'm struggling with this line in the style [theme.breakpoints.up(400 + theme.spacing.unit * 3 * 2)] .

Currently my code looks like this

let theme = createMuiTheme [
    ThemeProp.Palette [
        PaletteProp.Primary [         
            PaletteIntentionProp.Light Colors.blue.``300``
            PaletteIntentionProp.Main Colors.blue.``500``
            PaletteIntentionProp.Dark Colors.blue.``700``

        ]
    ]
]

let layoutStyle = 
    Style [
        Width "400px"
        Display "block"
        MarginLeft "auto"
        MarginRight "auto"       
    ]

let paperStyle = 
    Style[ 
        MarginTop (theme.spacing.unit * 8); 
        Display "flex"; 
        FlexDirection "Column";
        PaddingLeft (theme.spacing.unit * 2)
        PaddingTop (theme.spacing.unit * 3)
        PaddingRight (theme.spacing.unit * 2)
        PaddingBottom (theme.spacing.unit * 3)  
        CSSProp.AlignItems "center"] 

let avatarStyle = 
    Style [
        CSSProp.Margin theme.spacing.unit
        BackgroundColor theme.palette.secondary.main
    ]

let formStyle = 
    Style [
        Width "100%"
        MarginTop theme.spacing.unit        
    ]

let submitStyle = Style [ MarginTop (theme.spacing.unit * 3); Width "100%" ]

let view (model : Model) (dispatch : Msg -> unit) =    
    muiThemeProvider [Theme <| ProviderTheme.Theme theme] [
        cssBaseline []

        main [ layoutStyle ] [
            paper [ paperStyle ] [
                avatar [avatarStyle] [
                   icon [] [str "lock"] 
                ]

                typography [ Variant TypographyVariant.Headline ] [str "Sign in"]

                form [formStyle] [
                    formControl [HTMLAttr.Required true; Style [ Width "100%"; CSSProp.MarginTop "16px"; CSSProp.MarginBottom "8px" ]  ] [
                        inputLabel [HtmlFor "email"] [str "Email Address*"]                        
                        input [Id "email"; HTMLAttr.Name "email"; AutoComplete "email"; AutoFocus true]
                    ]

                    formControl [HTMLAttr.Required true;  Style [ Width "100%"; CSSProp.MarginTop "16px"; CSSProp.MarginBottom "8px"] ] [
                        inputLabel [HtmlFor "password"] [str "Password*"]
                        input [ HTMLAttr.Name "password"; HTMLAttr.Type "password"; Id "password"; AutoComplete "current-password" ]
                    ]

                    button [ HTMLAttr.Type "submit"; submitStyle; ButtonProp.Variant ButtonVariant.Raised;MaterialProp.Color ComponentColor.Primary ] [str"Sign in"] 
                ]
            ]
        ]        
    ]

So my question is, how could I implement the breakpoint behavior or more generally, would it be possible to create the styles function from the example via "Fable dynamic" and use in combination with the withStyles binding ?

Thanks in advance!

mvsmal commented 6 years ago

Hi @marcpiechura,

I see that you are using styles a bit incorrectly (without withStyles HOC). Look at this code:

let theme = createMuiTheme [
                ThemeProp.Palette [
                    PaletteProp.Primary [
                        PaletteIntentionProp.Light Colors.blue.``300``
                        PaletteIntentionProp.Main Colors.blue.``500``
                        PaletteIntentionProp.Dark Colors.blue.``700``

                    ]
                ]
    ]
let rootView props =
    muiThemeProvider [Theme <| ProviderTheme.Theme theme] [
        cssBaseline []

        main [ ClassName !!props?classes?layout ] [
            paper [ ClassName !!props?classes?paper ] [
                avatar [ ClassName !!props?classes?avatar ] [
                   icon [] [str "lock"]
                ]

                typography [ Variant TypographyVariant.Headline ] [str "Sign in"]

                form [ ClassName !!props?classes?form ] [
                    formControl [HTMLAttr.Required true; Style [ Width "100%"; CSSProp.MarginTop "16px"; CSSProp.MarginBottom "8px" ]  ] [
                        inputLabel [HtmlFor "email"] [str "Email Address*"]
                        input [Id "email"; HTMLAttr.Name "email"; AutoComplete "email"; AutoFocus true]
                    ]

                    formControl [HTMLAttr.Required true;  Style [ Width "100%"; CSSProp.MarginTop "16px"; CSSProp.MarginBottom "8px"] ] [
                        inputLabel [HtmlFor "password"] [str "Password*"]
                        input [ HTMLAttr.Name "password"; HTMLAttr.Type "password"; Id "password"; AutoComplete "current-password" ]
                    ]

                    button [ ClassName !!props?classes?submit; HTMLAttr.Type "submit"; ButtonProp.Variant ButtonVariant.Raised;MaterialProp.Color ComponentColor.Primary ] [str"Sign in"]
                ]
            ]
        ]
    ]

let styles (theme : ITheme) : IStyles list =
    [
        Styles.Layout [
            Width "400px"
            Display "block"
            MarginLeft "auto"
            MarginRight "auto" ]
        Styles.Paper [
            MarginTop (theme.spacing.unit * 8)
            Display "flex"
            FlexDirection "Column"
            PaddingLeft (theme.spacing.unit * 2)
            PaddingTop (theme.spacing.unit * 3)
            PaddingRight (theme.spacing.unit * 2)
            PaddingBottom (theme.spacing.unit * 3)
            CSSProp.AlignItems "center" ]
        Styles.Avatar [
            CSSProp.Margin theme.spacing.unit
            BackgroundColor theme.palette.secondary.main ]
        Styles.Form [
            Width "100%"
            MarginTop theme.spacing.unit ]
        Styles.Custom (
            "submit",
            keyValueList CaseRules.LowerFirst [ MarginTop (theme.spacing.unit * 3); Width "100%" ]
        )
    ]

let view model dispatch : ReactElement =
    withStyles (StyleType.Func styles) [] rootView [] []

The difference is that I define styles with ClassKeys, pass them to withStyles HOC and then my function rootView receives props with a classes property, which contains css class names, created by material-ui. You can create a record type for props and avoid Fable dynamic (?) calls.

As for breakpoints, since theme.breakpoints.up() and similar methods dynamically create media query string, unfortunately it is not possible to use CSSProp.Custom at the moment:

let styles (theme : ITheme) : IStyles list =
    [
        Styles.Layout [
            // ...
            CSSProp.Custom (
              // THIS DOES NOT WORK IN FABLE 1.3.7
              theme.breakpoints.up(400 + theme.spacing.unit * 3 * 2 |> U2.Case2),
              keyValueList CaseRules.LowerFirst [ Width 400 ])
        ]
        // ...
    ]

So you have to provide a literal value, smth like "@media (min-width:448px)".

I created a corresponding issue in Fable repository: https://github.com/fable-compiler/Fable/issues/1542

marcpiechura commented 6 years ago

Thanks, works like a charm ๐Ÿ‘

But now I'm struggling with storing the username/password in my model :-) I'm handling the state change and value binding the same way as I've done it in my other Elmish projects with Fulma/Bulma ,but it seems that I need to do something different in combination with MaterialUi.

For example, the password field looks like this

formControl formControlProps [                    
    inputLabel [HtmlFor "password" ] [str "Passwort"]
    input [ 
        HTMLAttr.Name "password"
        HTMLAttr.Type "password"
        Id "password"
        AutoComplete "current-password"
        HTMLAttr.ReadOnly model.IsAuthenticating
        HTMLAttr.Value (Option.defaultValue "" model.Password)
        Fable.Helpers.React.Props.OnChange(fun e -> Some !!e.target?value |> PasswordChanged |> dispatch)
    ]
]

And while my model is updated, the field looses the focus after every key stroke 2018-08-29_14-34-43

So I thought maybe I need to use valueOrDefault from Elmish.React.Helpers instead of HTMLAttr.Value but then the textbox isn't updated at all while the model still contains the updated value. 2018-08-29_14-35-44

So my question is, how do I update my state correctly with MaterialUi ? ๐Ÿ˜„

mvsmal commented 6 years ago

Did you try to use just HTMLAttr.DefaultValue ?

marcpiechura commented 6 years ago

Yup, same behavior as with HMLAttr.Value

marcpiechura commented 6 years ago

I was able to "fix" it by not using withStyles , but that would bring us back to the original issue ๐Ÿ˜„

marcpiechura commented 6 years ago

So I did a bit of researching and I think I found the issue. withStyles is returning a higher order component and according to the react docs HOCs canโ€™t be used in the render/view function. I also found a possible way to integrate HOCs with Fable / React https://github.com/fable-compiler/fable-react/issues/84#issuecomment-407893281

Iโ€™ll try it out and see if it fixes the issue

mvsmal commented 6 years ago

Yes, I also figured out how to properly work with it. I am going to release a new version with breaking changes. I just don't have much time now. So in a couple of days...

marcpiechura commented 6 years ago

Even better :) thanks a lot and no rush, itโ€™s nothing urgend ;-)

mvsmal commented 6 years ago

Now with Fable 1.3.18 you can use this approach:

let styles (theme : ITheme) : IStyles list =
    let breakPoint = theme.breakpoints.up(400 + theme.spacing.unit * 3 * 2 |> U2.Case2)
    [
        Styles.Layout [
            // ...
            CSSProp.Custom (
              breakPoint,
              keyValueList CaseRules.LowerFirst [ Width 400 ])
        ]
        // ...
    ]

And I will provide a withStyles example later on

marcpiechura commented 6 years ago

Works like a charm with the new version, thanks a lot !

marcpiechura commented 6 years ago

Looks like I was to optimistic ;-)

I can now use the breakpoint logic and the new style system but unfortunately I'm still not able to type text into a textbox as described here https://github.com/mvsmal/fable-material-ui/issues/4#issuecomment-416939240

mvsmal commented 6 years ago

Well, I found a way to make it working, even though I don't like it. A simplified version of your code:

let styles (theme : ITheme) : IStyles list =
    [ Styles.Custom ("main", [ Width 300 ] |> keyValueList CaseRules.LowerFirst) ]

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

let rootWithStyles<'a> = withStyles (StyleType.Func styles) []
let rootView model dispatch (props: RootProps) =
    main [ Class !!props.classes?main ] [
        paper [] [
            form [] [
                input [
                    HTMLAttr.DefaultValue model.email
                    DOMAttr.OnChange (fun e -> ChangeEmail e.Value |> dispatch) ]
            ]
        ]
    ]

type RootComp(p) as this =
    inherit PureComponent<RootProps,unit>(p)
    let ws = from (rootView this.props.model this.props.dispatch |> rootWithStyles) this.props []

    override this.render() =
        ws

let view (model: Model) (dispatch: (Msg -> unit)) : ReactElement =
    let props = createEmpty<RootProps>
    props.dispatch <- dispatch
    props.model <- model
    ofType<RootComp,_,_> props []

So as you can see, the RootComp wrapped in withStyles is created only once in the constructor. I don't like it because I had to pass model and dispatch as props. Not sure if we can solve it better.

marcpiechura commented 6 years ago

Alright, that fixes the issue for textboxes, unfortunately I've also added a select which isn't working anymore, if I'm using the components. I've defined it like this ( I also tried to set Name, Id and Value directly via select)


            formControl [ HTMLAttr.Required true; FullWidth true; MaterialProp.Margin FormControlMargin.Normal ] [
                inputLabel [HtmlFor "salutation"] [str "Anrede"]
                select [SelectProp.Input (Import.React.ReactNode.Case1(Import.React.ReactChild.Case1(input [HTMLAttr.Name "salutation"; HTMLAttr.Id "salutation"; HTMLAttr.Value model.Salutation])))
                        DOMAttr.OnChange(fun e -> dispatch  (SalutationChanged !!e.target?value)) ] [  
                    menuItem [HTMLAttr.Value ""] []
                    menuItem [HTMLAttr.Value "Herr"] [str "Herr"]
                    menuItem [HTMLAttr.Value "Frau"] [str "Frau"]
                ]
            ]

The issues is that it doesn't update its value, I can see that the model is updated correctly and if I change something and recompile the value gets updated once.

2018-09-10_18-09-44

marcpiechura commented 6 years ago

I think I found the issue, the view function is only called once, probably because we're caching it in the component :-(

mvsmal commented 6 years ago

Yep, unfortunately it is still unclear how to properly mimic javascript's HOCs behavior in Fable.Elmish. I will dig deeper. Thank you for the effort, you are making this project better

mvsmal commented 6 years ago

Looks like this works well for select, even without type component. The idea is to use model in props.

let selectStyles (theme : ITheme) : IStyles list =
    [ Styles.Custom ("main", [ Width 500 ] |> keyValueList CaseRules.LowerFirst) ]

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

let selectView (props: AppSelectProps) =
    formControl [
        Class !!props.classes?main
        HTMLAttr.Required true
        MaterialProp.Margin FormControlMargin.Normal
    ] [
        inputLabel [HtmlFor "salutation"] [str "Anrede"]
        select [
            MaterialProp.InputProps [
                HTMLAttr.Name "salutation"
                HTMLAttr.Id "salutation"
            ]
            MaterialProp.Value props.model.salutation
            DOMAttr.OnChange (fun e -> !!e.Value |> SalutationChanged |> props.dispatch)
        ] [
            menuItem [HTMLAttr.Value ""] []
            menuItem [HTMLAttr.Value "Herr"] [str "Herr"]
            menuItem [HTMLAttr.Value "Frau"] [str "Frau"]
        ]
    ]
let selectWithStyles<'a> = withStyles (StyleType.Func selectStyles) []
let view (model: Model) (dispatch: (Msg -> unit)) : ReactElement =
    let selectProps = createEmpty<AppSelectProps>
    selectProps.dispatch <- dispatch
    selectProps.model <- model
    from (selectView |> selectWithStyles) selectProps []
marcpiechura commented 6 years ago

Sorry for the late response, I'll try it out tomorrow

marcpiechura commented 6 years ago

@mvsmal I'm sorry but this brings us back to the focus problem :-(

mvsmal commented 6 years ago

It works if you wrap all your Model changing components with styles in a Type component like this:

let selectStyles (theme : ITheme) : IStyles list =
    [ Styles.Root [ Width 500 ] ]

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

let selectView (props: AppProps) =
    formControl [
        Class !!props.classes?root
        HTMLAttr.Required true
        MaterialProp.Margin FormControlMargin.Normal
    ] [
        inputLabel [HtmlFor "salut"] [str "Anrede"]
        select [
            MaterialProp.InputProps [
                HTMLAttr.Name "salut"
                HTMLAttr.Id "salut"
            ]
            MaterialProp.Value props.model.salut
            DOMAttr.OnChange (fun e -> !!e.Value |> SalutChanged |> props.dispatch)
        ] [
            menuItem [HTMLAttr.Value ""] []
            menuItem [HTMLAttr.Value "Herr"] [str "Herr"]
            menuItem [HTMLAttr.Value "Frau"] [str "Frau"]
        ]
    ]

type AppSelect(p) =
    inherit PureComponent<AppProps,unit>(p)

    let selectWithStyles = withStyles (StyleType.Func selectStyles) [] selectView

    override this.render() =
        from selectWithStyles this.props []

let inputStyles (theme : ITheme) : IStyles list =
    [ Styles.Root [ Width 300 ] ]

let inputView (props: AppProps) =
    div [ Class !!props.classes?root ] [
        textField [
            DOMAttr.OnChange (fun e -> e.Value |> ChangeEmail |> props.dispatch)
            HTMLAttr.Value props.model.email
        ] []
    ]

type AppInput(p) =
    inherit PureComponent<AppProps,unit>(p)
    let inputWithStyles = withStyles (StyleType.Func inputStyles) [] inputView

    override this.render() =
        from inputWithStyles this.props []

let rootStyles (theme : ITheme) : IStyles list =
    [ Styles.Root [ Width 1000 ] ]

let rootView (props : AppProps) =
    div [Class !!props.classes?root] [
        ofType<AppInput,_,_> props []
        ofType<AppSelect,_,_> props []
    ]

type RootView(p) =
    inherit PureComponent<AppProps, unit>(p)
    let rootWithStyles = withStyles (StyleType.Func rootStyles) [] rootView

    override this.render() =
        from rootWithStyles this.props []

let view (model: Model) (dispatch: (Msg -> unit)) : ReactElement =
    let props = createEmpty<AppProps>
    props.model <- model
    props.dispatch <- dispatch
    ofType<RootView,_,_> props []
marcpiechura commented 6 years ago

Indeed that works, thanks a lot ๐Ÿ‘

What's your take on this one ? I mean it's a lot of "boilerplate" code and complicates things quite a bit and I'm not sureif it's worth the effort, compared to css/sass which I used as fallback and it worked quite well ?

mvsmal commented 6 years ago

I've simplified it a bit. Now there are no Type components, but still model and dispatch are passed as props, which is fine, I think.

type AppProps =
    abstract member model: Model with get, set
    abstract member dispatch: (Msg -> unit) with get, set
    inherit IClassesProps
let selectStyles (theme : ITheme) : IStyles list =
    [ Styles.Root [ Width 500 ] ]

let selectView (props: AppProps) =
    formControl [
        Class !!props.classes?root
        HTMLAttr.Required true
        MaterialProp.Margin FormControlMargin.Normal
    ] [
        inputLabel [HtmlFor "salut"] [str "Anrede"]
        select [
            MaterialProp.InputProps [
                HTMLAttr.Name "salut"
                HTMLAttr.Id "salut"
            ]
            MaterialProp.Value props.model.salut
            DOMAttr.OnChange (fun e -> !!e.Value |> SalutChanged |> props.dispatch)
        ] [
            menuItem [HTMLAttr.Value ""] []
            menuItem [HTMLAttr.Value "Herr"] [str "Herr"]
            menuItem [HTMLAttr.Value "Frau"] [str "Frau"]
        ]
    ]

let selectWithStyles = withStyles (StyleType.Func selectStyles) [] selectView

let inputStyles (theme : ITheme) : IStyles list =
    [ Styles.Root [ Width 300 ] ]

let inputView (props: AppProps) =
    div [ Class !!props.classes?root ] [
        textField [
            DOMAttr.OnChange (fun e -> e.Value |> ChangeEmail |> props.dispatch)
            HTMLAttr.Value props.model.email
        ] []
    ]

let inputWithStyles = withStyles (StyleType.Func inputStyles) [] inputView

let rootStyles (theme : ITheme) : IStyles list =
    [ Styles.Root [ Width 1000 ] ]

let rootView (props : AppProps) =
    div [Class !!props.classes?root] [
        from selectWithStyles props []
        from inputWithStyles props []
    ]
let rootWithStyles = withStyles (StyleType.Func rootStyles) [] rootView
let view (model: Model) (dispatch: (Msg -> unit)) : ReactElement =
    let props = createEmpty<AppProps>
    props.model <- model
    props.dispatch <- dispatch
    from rootWithStyles props []

Your approach with CSS was utilizing style HTML attribute, which is not the best idea. Starting from version 1 of MaterialUI guys moved from styles to CSS classes, that's why they introduced withStyles, which creates those CSS classes and you can apply them to your elements. Moreover, in some more complex components, you can pass class names to internal components, which is not possible with styles attribute. Example: https://material-ui.com/customization/overrides/#overriding-with-classes

mvsmal commented 6 years ago

Btw, there are other pretty clean ways to style the components: https://material-ui.com/guides/interoperability/

marcpiechura commented 6 years ago

Your approach with CSS was utilizing style HTML attribute, which is not the best idea.

yes, in my question I was actually referring to the approach described in the guide because during this conversation I've moved all Style attributes into sass files and added Class = "style.xyz" instead. Anyway, after reading you explanation and the guide I guess both options are fine :-)

Regarding the last update without the components, I tried it but it doesn't seem to work for parent / child combinations, because you can't pass AppProps down to the child since it's compiled before the AppProps. I tried also to call it like this from childWithStyles (Child.View.createProps props.model.childModel (ChildMsg >> props.model.dispatch)) []

where Child.View.createProps is

let createProps model dispatch =
    let props = createEmpty<ChildProps>
    props.Model <- model
    props.Dispatch <- dispatch
    props

that compiles but results again in the focus problem. Also in general it looks like that with this approach everything is "redrawn" on every update.

On the other hand this works fine even for parent / child combinations ofType<ChildComp,_,_> (Child.View.createProps model.Child (ChildMsg >> dispatch)) [] and only "redraws" the textbox I'm currently in.

So for now I will take the component approach and after splitting my file into three (State.fs, Types.fs and View.fs) it's not so much of a deal ;-)

mvsmal commented 6 years ago

I tried to use Child messages with lazyView2 helpers. It allows to avoid rerendering of other parts. Please, have a look

// Model
type TextMsg =
    | ChangeEmail of string
type SelectMsg =
    | SalutChanged of string

type TextModel = {
    email: string
}

type SelectModel = {
    salut: string
}

type Msg =
    | TextMsg of TextMsg
    | SelectMsg of SelectMsg

type Model = {
    text: TextModel
    select: SelectModel
}

// State
let textInit () : TextModel * Cmd<TextMsg> =
    { email = "" }, []

let textUpdate (msg : TextMsg) (model : TextModel) : TextModel * Cmd<TextMsg> =
    match msg with
    | ChangeEmail e ->
        { model with email = e }, []

let selectInit () : SelectModel * Cmd<SelectMsg> =
    { salut = "" }, []

let selectUpdate (msg : SelectMsg) (model : SelectModel) : SelectModel * Cmd<SelectMsg> =
    match msg with
    | SalutChanged s ->
        { model with salut = s }, []

let init () =
    let (text, textCmd) = textInit ()
    let (select, selectCmd) = selectInit ()

    { text = text; select = select },
        Cmd.batch [ Cmd.Empty
                    Cmd.map TextMsg textCmd
                    Cmd.map SelectMsg selectCmd ]

let update msg model =
  match msg with
  | TextMsg msg ->
    let (text, textCmd) = textUpdate msg model.text
    { model with text = text }, Cmd.map TextMsg textCmd
  | SelectMsg msg ->
    let (select, selectCmd) = selectUpdate msg model.select
    { model with select = select }, Cmd.map SelectMsg selectCmd

// View

type TextProps =
    abstract member model: TextModel with get, set
    abstract member dispatch: (TextMsg -> unit) with get, set
    inherit IClassesProps

type SelectProps =
    abstract member model: SelectModel with get, set
    abstract member dispatch: (SelectMsg -> unit) with get, set
    inherit IClassesProps

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

let selectStyles (theme : ITheme) : IStyles list =
    [ Styles.Root [ Width 500 ] ]

let selectView (props: SelectProps) =
    formControl [
        Class !!props.classes?root
        HTMLAttr.Required true
        MaterialProp.Margin FormControlMargin.Normal
    ] [
        inputLabel [HtmlFor "salut"] [str "Anrede"]
        select [
            MaterialProp.InputProps [
                HTMLAttr.Name "salut"
                HTMLAttr.Id "salut"
            ]
            MaterialProp.Value props.model.salut
            DOMAttr.OnChange (fun e -> !!e.Value |> SalutChanged |> props.dispatch)
        ] [
            menuItem [HTMLAttr.Value ""] []
            menuItem [HTMLAttr.Value "Herr"] [str "Herr"]
            menuItem [HTMLAttr.Value "Frau"] [str "Frau"]
        ]
    ]

let selectWithStyles = withStyles (StyleType.Func selectStyles) [] selectView

let selectFun model dispatch =
    let selectProps = createEmpty<SelectProps>
    selectProps.model <- model
    selectProps.dispatch <- dispatch
    from selectWithStyles selectProps []

let inputStyles (theme : ITheme) : IStyles list =
    [ Styles.Root [ Width 300 ] ]

let inputView (props: TextProps) =
    div [ Class !!props.classes?root ] [
        textField [
            DOMAttr.OnChange (fun e -> e.Value |> ChangeEmail |> props.dispatch)
            HTMLAttr.Value props.model.email
        ] []
    ]

let inputWithStyles = withStyles (StyleType.Func inputStyles) [] inputView

let inputFun model dispatch =
    let textProps = createEmpty<TextProps>
    textProps.model <- model
    textProps.dispatch <- dispatch
    from inputWithStyles textProps []

let rootStyles (theme : ITheme) : IStyles list =
    [ Styles.Root [ Width 1000 ] ]

let rootView (props : AppProps) =
    div [Class !!props.classes?root] [
        lazyView2 selectFun props.model.select (SelectMsg >> props.dispatch)
        lazyView2 inputFun props.model.text (TextMsg >> props.dispatch)
    ]

let rootWithStyles = withStyles (StyleType.Func rootStyles) [] rootView
let view (model: Model) (dispatch: (Msg -> unit)) : ReactElement =
    let props = createEmpty<AppProps>
    props.model <- model
    props.dispatch <- dispatch
    from rootWithStyles props []
marcpiechura commented 6 years ago

@mvsmal yep, that did the trick ๐Ÿ‘

cmeeren commented 5 years ago

@mvsmal In your last example, is the lazyView2 needed for proper functionality, or is it just a performance improvement? I tried with and without, and things seem to work the same, but I can't be entirely sure since I've just started trying out JSS with Elmish and MUI.

cmeeren commented 5 years ago

Is it possible to use a functional component as the wrapper? E.g.

let private styles (theme: ITheme) : IStyles list =
  []

let private view' (classes: IClasses) model dispatch =
  div [] []

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

let private comp (p: IProps) =
  let viewFun (p: IProps) = view' p.classes p.model p.dispatch
  let inputWithStyles = withStyles (StyleType.Func styles) [] viewFun
  from inputWithStyles p []

let view (model: Model) (dispatch: Msg -> unit) : ReactElement =
  let props = createEmpty<IProps>
  props.model <- model
  props.dispatch <- dispatch
  ofFunction comp props []

I can't immediately see anything wrong when testing this, but I know little of react components/props and it may just be in my specific case this (seems to) work well.

mvsmal commented 5 years ago

@cmeeren Regarding lazyView2, it depends on a case. lazyView2 helps to avoid rerendering, and might be helpful. However, if the code works well without it for you, feel free to omit it.

As for the functional component wrapper, I don't see much of a difference in your code comparing to mine above. I need to check in detail, but my main concern is this part:

let private comp (p: IProps) =
  let viewFun (p: IProps) = view' p.classes p.model p.dispatch
  let inputWithStyles = withStyles (StyleType.Func styles) [] viewFun
  from inputWithStyles p []

withStyles is a HOC, and must me applied only once for the whole lifetime of the app. Otherwise it will always create a new underlying component and interfere the model updates, especially with OnChange events. Hopefully with React hooks we won't need this thing anymore.

cmeeren commented 5 years ago

Oh, right. I can confirm that with a functional component it's applied once for each re-render.

Back to classes, then. But would it be better to use PureStatelessComponent instead of PureComponent? Seems to work fine.

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.

Luiz-Monad 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:

I guess I figured it out here https://github.com/mvsmal/fable-material-ui/issues/53#issuecomment-500174365