Closed marcpiechura closed 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 ClassKey
s, 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
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
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.
So my question is, how do I update my state correctly with MaterialUi ? ๐
Did you try to use just HTMLAttr.DefaultValue
?
Yup, same behavior as with HMLAttr.Value
I was able to "fix" it by not using withStyles
, but that would bring us back to the original issue ๐
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
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...
Even better :) thanks a lot and no rush, itโs nothing urgend ;-)
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
Works like a charm with the new version, thanks a lot !
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
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.
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.
I think I found the issue, the view function is only called once, probably because we're caching it in the component :-(
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
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 []
Sorry for the late response, I'll try it out tomorrow
@mvsmal I'm sorry but this brings us back to the focus problem :-(
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 []
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 ?
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
Btw, there are other pretty clean ways to style the components: https://material-ui.com/guides/interoperability/
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 ;-)
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 []
@mvsmal yep, that did the trick ๐
@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.
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.
@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.
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.
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.
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
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
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 thewithStyles
binding ?Thanks in advance!