Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign upControlling layout and styling #1
Comments
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
hecrj
May 4, 2018
Owner
Hi @russelldavies,
Could you provide some more details about what you want to achieve? It could help me to write better documentation.
I will try to share my point of view on complex form layout and styling. Notice that I am still figuring things out and my thoughts can be all over the place. In any case, I am open to any suggestions! Here I go:
The Form type is meant to be decoupled from any particular rendering strategy.
Form.View.basic is a simple function that allows you to render any Form. It is meant to be a starting point for users of the API, show examples and get used to the package.
For complex use cases, the package will encourage users to build their own renderers. Now, these renderers do not have to be just a simple function like Form.View.basic. They can be new types that build a Form under the hood in different steps. For instance, take a look at the multi-stage form shown in this example:
form : MultiStage.Form Values Msg
form =
MultiStage.build SignUp
|> MultiStage.add emailAndNameForm viewEmailAndNameForm
|> MultiStage.add passwordField viewPassword
|> MultiStage.end favoriteLanguageFieldAs you can see in the code, this pipeline builds a MultiStage.Form using the composability of the Form type. Then, this new kind of form is rendered using MultiStage.view. In the end, MultiStage is just decorating a Form with some Html Never for each stage.
there is no control of layout, they're vertically stacked. Even if I wanted to add some content between fields, it's not possible.
My recommendation would be to build your own type similar to MultiStage. Maybe something like this:
form : Custom.Form Values Msg
form =
Custom.form SubmitMsg
|> Custom.field someField " Adds a field
|> Custom.content someHtml " Adds some HTML
|> Custom.group someFields " Adds a group of fields (rendered horizontally later)As an endnote, offering many different complex rendering strategies is outside the scope of this package. These will probably be offered in different packages, like elm-form-multistage, elm-form-mdl, elm-form-style-elements, etc. Same applies to custom fields.
I am aware that writing documentation about this will be crucial. What do you think?
|
Hi @russelldavies, Could you provide some more details about what you want to achieve? It could help me to write better documentation. I will try to share my point of view on complex form layout and styling. Notice that I am still figuring things out and my thoughts can be all over the place. In any case, I am open to any suggestions! Here I go: The
For complex use cases, the package will encourage users to build their own renderers. Now, these renderers do not have to be just a simple function like form : MultiStage.Form Values Msg
form =
MultiStage.build SignUp
|> MultiStage.add emailAndNameForm viewEmailAndNameForm
|> MultiStage.add passwordField viewPassword
|> MultiStage.end favoriteLanguageFieldAs you can see in the code, this pipeline builds a
My recommendation would be to build your own type similar to form : Custom.Form Values Msg
form =
Custom.form SubmitMsg
|> Custom.field someField " Adds a field
|> Custom.content someHtml " Adds some HTML
|> Custom.group someFields " Adds a group of fields (rendered horizontally later)As an endnote, offering many different complex rendering strategies is outside the scope of this package. These will probably be offered in different packages, like I am aware that writing documentation about this will be crucial. What do you think? |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
russelldavies
May 4, 2018
Contributor
Thanks for the quick reply. I had cloned the repo just before you added the Multistage example, so I'll review that and get back to you.
|
Thanks for the quick reply. I had cloned the repo just before you added the Multistage example, so I'll review that and get back to you. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
russelldavies
May 7, 2018
Contributor
The MultiStage code was helpful in showing me how the rendering can be done however desired, so having more examples certainly helps illustrate how to use it. My thinking had gotten a bit fixed from just looking at Form.View.basic.
In regard to what I'm trying to achieve, my use case is forms for CRUD operations on various models in an admin site. And the layout of each form element can vary depending on the model. For instance, if there was an invoice model:
type alias Invoice =
{ number : Int
, amount : Float
, notes : String
}then perhaps I'd like to have the number and amount fields on one row and the notes field on another row. I use style-elements, generally so I'd make up a form like this:
form invoice errors =
Element.column Styles.None
[]
[ Element.row Styles.None
[]
[ Input.text Styles.Field
[ padding 10 ]
{ onChange = SetNumber
, value = (toString invoice.number)
, label =
Input.placeholder
{ label = Input.labelLeft (el Styles.None [ Attributes.verticalCenter ] (text "Number"))
, text = "Number"
}
, options = [ Input.errorBelow (el Styles.Error [] (text (formErrors Amount errors)) ]
}
, Input.text Styles.Field
[ padding 15 ]
{ onChange = SetAmount
, value = (toString invoice.amount)
, label =
Input.placeholder
{ label = Input.labelRight (el Styles.None [] (text "Amount"))
, text = "Amount"
}
, options = [ Input.errorBelow (el Styles.Error [] (text (formErrors Number errors)) ]
}
]
, Input.multiline Styles.LargeField
[ padding 25 ]
{ onChange = SetNotes
, value = invoice.notes
, label = Input.labelAbove (Element.el Styles.None [] (text "Notes"))
, options = []
}
, Element.button Styles.Button [ onClick Save ] (text "Save")
]Normally, where there is common code, I'd create helper functions but in this case I wrote it out explicitly to show that the Element.Input fields can having varying attributes, styles and options. So in my renderer, I'd have to associate the each form field with an input.
One way I was thinking about doing it would be like:
form : StyleElements.Form Values Msg
form =
StyleElements.build Invoice
|> StyleElements.add numberField numberInput
|> StyleElements.add amountField amountInput
|> StyleElements.add notesField notesInput
|> StyleElements.layout formLayoutwhere numberInput corresponds to the Element.Input.text function above and formLayout would be the rest of the function with inputs removed:
formLayout numberInput amountInput notesInput =
Element.column Styles.None
[]
[ Element.row Styles.None
[]
[ numberInput, amountInput
]
, notesInput
]but this doesn't seem right. Any suggestions?
As an aside, what is the purpose in having the counter in Form.Value.Value, debugging?
|
The In regard to what I'm trying to achieve, my use case is forms for CRUD operations on various models in an admin site. And the layout of each form element can vary depending on the model. For instance, if there was an invoice model: type alias Invoice =
{ number : Int
, amount : Float
, notes : String
}then perhaps I'd like to have the form invoice errors =
Element.column Styles.None
[]
[ Element.row Styles.None
[]
[ Input.text Styles.Field
[ padding 10 ]
{ onChange = SetNumber
, value = (toString invoice.number)
, label =
Input.placeholder
{ label = Input.labelLeft (el Styles.None [ Attributes.verticalCenter ] (text "Number"))
, text = "Number"
}
, options = [ Input.errorBelow (el Styles.Error [] (text (formErrors Amount errors)) ]
}
, Input.text Styles.Field
[ padding 15 ]
{ onChange = SetAmount
, value = (toString invoice.amount)
, label =
Input.placeholder
{ label = Input.labelRight (el Styles.None [] (text "Amount"))
, text = "Amount"
}
, options = [ Input.errorBelow (el Styles.Error [] (text (formErrors Number errors)) ]
}
]
, Input.multiline Styles.LargeField
[ padding 25 ]
{ onChange = SetNotes
, value = invoice.notes
, label = Input.labelAbove (Element.el Styles.None [] (text "Notes"))
, options = []
}
, Element.button Styles.Button [ onClick Save ] (text "Save")
]Normally, where there is common code, I'd create helper functions but in this case I wrote it out explicitly to show that the One way I was thinking about doing it would be like: form : StyleElements.Form Values Msg
form =
StyleElements.build Invoice
|> StyleElements.add numberField numberInput
|> StyleElements.add amountField amountInput
|> StyleElements.add notesField notesInput
|> StyleElements.layout formLayoutwhere formLayout numberInput amountInput notesInput =
Element.column Styles.None
[]
[ Element.row Styles.None
[]
[ numberInput, amountInput
]
, notesInput
]but this doesn't seem right. Any suggestions? As an aside, what is the purpose in having the counter in |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
hecrj
May 7, 2018
Owner
The renderer should use Form.fields to obtain a List ( Field, Maybe Error ) and use that to build the form.
For instance, a TextField contains a label and a placeholder, and it also contains a State which has the current value and a function to update it. Thus, the onChange, value and label attributes that you need in your renderer can (and should) be obtained from each Field.
The two main issues that I see are:
- You need to control the padding for each field.
- You need to group some fields in a row.
For (1), I would probably ask myself first why do I need different paddings for the same type of field. This package is built under the assumption that a specific type of field will be rendered consistently under the same renderer.
Maybe you just need to differentiate an intField from a floatField? If that is the case I think it will be better to create your own Field type so you can differentiate them when rendering. You can do this simply by copy-pasting the Form module into your own MyForm module and extending it as you wish, there is almost no code in there. For instance, you could have:
type Field values
= Text (TextField values)
| Int (TextField values)
| Float (TextField values)
textField : Base.FieldConfig TextField.Attributes String values output -> Form values output
textField =
TextField.text Text
intField : Base.FieldConfig TextField.Attributes String values output -> Form values output
intField =
TextField.text Int
floatField : Base.FieldConfig TextField.Attributes String values output -> Form values output
floatField =
TextField.text FloatThen, you would use MyForm instead of Form.
For (2), you can simply add a Group to your custom Field type:
type Field values
= Text (TextField values)
| Int (TextField values)
| Float (TextField values)
| Group (List (Field values, Maybe Error))
group : Form values output msg -> Form values output msg
group form =
let
builder values =
let
fields =
Base.fields form values
in
( Group fields
, List.head fields |> Maybe.andThen Tuple.second
)
in
Base.custom { builder = builder, result = Base.result form }Now, you can write your form like this:
type Msg
= SaveInvoice Int Float String
form : MyForm.Form Values Msg
form =
let
numberAndAmountFields =
MyForm.empty (,)
|> MyForm.append numberField
|> MyForm.append amountField
|> MyForm.group
in
MyForm.empty (\(number, amount) notes -> SaveInvoice number amount notes)
|> MyForm.append numberAndAmountFields
|> MyForm.append notesFieldThen, you can implement a simple function like Form.View.basic that renders MyForm. For example, Form.View.StyleElements could look like:
view : MyForm values msg -> (values -> msg) -> values -> Element YourStyle YourVariation msg
view form onChange values =
let
fieldElements =
List.map (viewField onChange) (MyForm.fields form values)
button =
case MyForm.result form values of
Ok msg ->
Element.button Styles.Button [ onClick msg ] (text "Save")
Err error ->
-- Validation failed
-- Show a disabled button?
in
Element.column Styles.None
[]
(fieldElements ++ [ button ])
viewField : (values -> msg) -> ( MyForm.Field values, Maybe Error) -> Element YourStyle YourVariation msg
viewField onChange ( field, error ) =
case field of
Form.Text { attributes, state } ->
Input.multiline Styles.LargeField
[ padding 25 ]
{ onChange = state.update >> onChange
, value = Value.raw state.value |> Maybe.withDefault ""
, label =
Input.placeholder
{ label = Input.labelAbove (Element.el Styles.None [] (text attributes.label))
, text = attributes.placeholder
}
, options = []
}
Form.Int { attributes, state } ->
Input.text Styles.Field
[ padding 10 ]
{ onChange = state.update >> onChange
, value = Value.raw state.value |> Maybe.withDefault ""
, label =
Input.placeholder
{ label = Input.labelLeft (el Styles.None [ Attributes.verticalCenter ] (text attributes.label))
, text = attributes.placeholder
}
, options = [ Input.errorBelow (el Styles.Error [] (text (formErrors Amount error)) ]
}
Form.Float { attributes, state } ->
Input.text Styles.Field
[ padding 15 ]
{ onChange = state.update >> onChange
, value = Value.raw state.value |> Maybe.withDefault ""
, label =
Input.placeholder
{ label = Input.labelLeft (el Styles.None [ Attributes.verticalCenter ] (text attributes.label))
, text = attributes.placeholder
}
, options = [ Input.errorBelow (el Styles.Error [] (text (formErrors Amount error)) ]
}
Form.Group fields ->
-- Simple recursion!
Element.row [] (List.map (viewField onChange) fields)You can keep extending your Field type as you wish. However, try to not couple it too much with your rendering needs, just in case you want to render your forms differently in the future :)
Have in mind that I haven't compiled or tried any of the code snippets, they are mostly meant to guide you. On the other hand, the internals like Base.custom are subject to change.
I understand all of this might seem complicated. Maybe you should wait until I release a stable version of the package alongside some documentation and more examples. In any case, feel free to ask me any questions.
As an aside, what is the purpose in having the counter in Form.Value.Value, debugging?
It is necessary to fix an issue with autocompletion. When a form is autocompleted, many events get triggered before the view can be rerendered, causing the first autocompleted values to be lost.
Value.newest allows to fix this:
update : Msg -> Model -> Model
update msg values =
FormChanged newForm ->
{ form |
values =
{ email = Value.newest .email form.values newForm.values
, password = Value.newest .password form.values newForm.values
}
}I think this particular issue will be fixed in Elm 0.19 as onInput events will trigger a synchronous render. Therefore, the counter and Value.newest will probably be removed in the future.
EDIT: I can confirm that this is fixed in the elm-0.19-alpha branch! No need for Value.newest anymore
|
The renderer should use For instance, a The two main issues that I see are:
For (1), I would probably ask myself first why do I need different paddings for the same type of field. This package is built under the assumption that a specific type of field will be rendered consistently under the same renderer. Maybe you just need to differentiate an type Field values
= Text (TextField values)
| Int (TextField values)
| Float (TextField values)
textField : Base.FieldConfig TextField.Attributes String values output -> Form values output
textField =
TextField.text Text
intField : Base.FieldConfig TextField.Attributes String values output -> Form values output
intField =
TextField.text Int
floatField : Base.FieldConfig TextField.Attributes String values output -> Form values output
floatField =
TextField.text FloatThen, you would use For (2), you can simply add a type Field values
= Text (TextField values)
| Int (TextField values)
| Float (TextField values)
| Group (List (Field values, Maybe Error))
group : Form values output msg -> Form values output msg
group form =
let
builder values =
let
fields =
Base.fields form values
in
( Group fields
, List.head fields |> Maybe.andThen Tuple.second
)
in
Base.custom { builder = builder, result = Base.result form }Now, you can write your form like this: type Msg
= SaveInvoice Int Float String
form : MyForm.Form Values Msg
form =
let
numberAndAmountFields =
MyForm.empty (,)
|> MyForm.append numberField
|> MyForm.append amountField
|> MyForm.group
in
MyForm.empty (\(number, amount) notes -> SaveInvoice number amount notes)
|> MyForm.append numberAndAmountFields
|> MyForm.append notesFieldThen, you can implement a simple function like view : MyForm values msg -> (values -> msg) -> values -> Element YourStyle YourVariation msg
view form onChange values =
let
fieldElements =
List.map (viewField onChange) (MyForm.fields form values)
button =
case MyForm.result form values of
Ok msg ->
Element.button Styles.Button [ onClick msg ] (text "Save")
Err error ->
-- Validation failed
-- Show a disabled button?
in
Element.column Styles.None
[]
(fieldElements ++ [ button ])
viewField : (values -> msg) -> ( MyForm.Field values, Maybe Error) -> Element YourStyle YourVariation msg
viewField onChange ( field, error ) =
case field of
Form.Text { attributes, state } ->
Input.multiline Styles.LargeField
[ padding 25 ]
{ onChange = state.update >> onChange
, value = Value.raw state.value |> Maybe.withDefault ""
, label =
Input.placeholder
{ label = Input.labelAbove (Element.el Styles.None [] (text attributes.label))
, text = attributes.placeholder
}
, options = []
}
Form.Int { attributes, state } ->
Input.text Styles.Field
[ padding 10 ]
{ onChange = state.update >> onChange
, value = Value.raw state.value |> Maybe.withDefault ""
, label =
Input.placeholder
{ label = Input.labelLeft (el Styles.None [ Attributes.verticalCenter ] (text attributes.label))
, text = attributes.placeholder
}
, options = [ Input.errorBelow (el Styles.Error [] (text (formErrors Amount error)) ]
}
Form.Float { attributes, state } ->
Input.text Styles.Field
[ padding 15 ]
{ onChange = state.update >> onChange
, value = Value.raw state.value |> Maybe.withDefault ""
, label =
Input.placeholder
{ label = Input.labelLeft (el Styles.None [ Attributes.verticalCenter ] (text attributes.label))
, text = attributes.placeholder
}
, options = [ Input.errorBelow (el Styles.Error [] (text (formErrors Amount error)) ]
}
Form.Group fields ->
-- Simple recursion!
Element.row [] (List.map (viewField onChange) fields)You can keep extending your Have in mind that I haven't compiled or tried any of the code snippets, they are mostly meant to guide you. On the other hand, the internals like I understand all of this might seem complicated. Maybe you should wait until I release a stable version of the package alongside some documentation and more examples. In any case, feel free to ask me any questions.
It is necessary to fix an issue with autocompletion. When a form is autocompleted, many events get triggered before the
update : Msg -> Model -> Model
update msg values =
FormChanged newForm ->
{ form |
values =
{ email = Value.newest .email form.values newForm.values
, password = Value.newest .password form.values newForm.values
}
}I think this particular issue will be fixed in Elm 0.19 as EDIT: I can confirm that this is fixed in the elm-0.19-alpha branch! No need for |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
russelldavies
May 11, 2018
Contributor
What a great response, it really helped me out. My question was somewhat contrived as I wanted to see how possible it would be to handle unusual form layouts. In reality, most of my form elements look the same so I wouldn't be adjusting the styling for each individual element. Regarding layout, defining a Group field type is cool idea I hadn't thought of.
I'm still toying around with different methods to best capture my requirements (and looking at how the API will change from stylish elephants) but I will send it on when I'm done in case it's useful for documentation. I ended up having to carefully reread and fully understand how Form.Base works as I had developed a few misconceptions from just a cursory look; and also because of a few naming inconsistencies which threw me. So plenty of examples would be useful for other people (and me).
|
What a great response, it really helped me out. My question was somewhat contrived as I wanted to see how possible it would be to handle unusual form layouts. In reality, most of my form elements look the same so I wouldn't be adjusting the styling for each individual element. Regarding layout, defining a I'm still toying around with different methods to best capture my requirements (and looking at how the API will change from stylish elephants) but I will send it on when I'm done in case it's useful for documentation. I ended up having to carefully reread and fully understand how |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
hecrj
May 12, 2018
Owner
I had developed a few misconceptions from just a cursory look; and also because of a few naming inconsistencies which threw me.
I have to rethink names and explain better what everything does in there! I'd like to know what threw you specifically. I know Base.custom and Base.field are probably not the best names, I also don't like builder, and Base.field has a quite complex type signature.
I will simplify and publish a first version soon. I will probably target Elm 0.19 first though.
I have to rethink names and explain better what everything does in there! I'd like to know what threw you specifically. I know I will simplify and publish a first version soon. I will probably target Elm 0.19 first though. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
russelldavies
May 13, 2018
Contributor
My biggest misconception was that for some reason I originally thought that something like emailField was not actually a Form.Form. I can't remember my thought process anymore but I think it's because of looking at the examples I thought that it was different somehow and only became part of a form when appended to an empty form. Once I realized this, a lot of things clicked into place as the composability aspect became more much apparent.
I was slightly confused by values in various type signatures thinking it was the value of a field and wondering why it was plural before realizing it was referring to the values model in the caller code. The README also didn't introduce the type of Value so I was kind of confused reading this:
These pipelines of data are composed of fields. Each field describes how to access data from
values(values -> Value a), validate it (a -> Result String b), and how to update it
(Value a -> values -> values), alongside other field attributes.
Something like this might be clearer:
Your form state, i.e. the
valuesof the form, is stored in your own record where each record field is aValue inputwhich stores the raw input of each field and some extra metadata. These pipelines of data are composed of fields. Each field describes how to access data fromvalues(values -> Value input), validate it (input -> Result String output), and how to update it
(Value input -> values -> values), alongside other field attributes.
I made what I think are correct fixes to naming inconsistencies, here. Please let me know if I'm wrong. Using "field" is somewhat problematic because it's overloaded depending on the context. That said, when the context is understood, the word makes sense.
And since you were asking about a name, I would suggest composable-form.
|
My biggest misconception was that for some reason I originally thought that something like I was slightly confused by
Something like this might be clearer:
I made what I think are correct fixes to naming inconsistencies, here. Please let me know if I'm wrong. Using "field" is somewhat problematic because it's overloaded depending on the context. That said, when the context is understood, the word makes sense. And since you were asking about a name, I would suggest composable-form. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
hecrj
May 13, 2018
Owner
My biggest misconception was that for some reason I originally thought that something like emailField was not actually a Form.Form. [...]
Noted! My idea is to present things incrementally in the docs. I will try to follow elm-lang docs in simplicity and try to be as concise as possible. It is probably a good idea to introduce Form with a simple field first, and then introduce empty and append. Also, I think I might rename empty to succeed in order to feel closer to Task and Decode. I might change append too.
The README also didn't introduce the type of Value
The README is a very quick introduction to gather some feedback. It's not what I would call "official documentation" yet!
Also, I might end up dropping the Value type. The counter is no longer needed in Elm 0.19, it is coupling rendering needs with the Form type, and it clutters the API. A renderer can keep track of the state (Clean, Dirty, ...) of the fields using a Dict String FieldState, using the labels of the fields as keys.
I made what I think are correct fixes to naming inconsistencies, here.
Thank you very much! Maybe we could rename Parser to FieldParser and FormResult to FormParser? We should also avoid mentioning field when most operators work with forms (a set of fields). For instance, it is possible to append an address form to another form.
I will open a PR soon so anyone can follow me as I work on the changes and the documentation. Feel free to provide any feedback when I do so!
And since you were asking about a name, I would suggest composable-form.
A friend has also suggested me that name! I like it :)
Noted! My idea is to present things incrementally in the docs. I will try to follow
The README is a very quick introduction to gather some feedback. It's not what I would call "official documentation" yet! Also, I might end up dropping the
Thank you very much! Maybe we could rename I will open a PR soon so anyone can follow me as I work on the changes and the documentation. Feel free to provide any feedback when I do so!
A friend has also suggested me that name! I like it :) |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
russelldavies
May 13, 2018
Contributor
It is probably a good idea to introduce
Formwith a simple field first, and then introduce empty and append.
That's exactly how I'd do it. If I had been introduced this way I don't think I would have had that aforementioned misconception.
Also, I think I might rename
emptytosucceed, in order to feel closer to Task and Decode.
Makes sense.
I might change
appendtoo.
I thought about this too, after reading the Discourse comments, but I'm stuck for a better name.
Maybe we could rename
ParsertoFieldParserandFormResulttoFormParser?
That's clearer, I like it.
I will open a PR soon so anyone can follow me as I work on the changes and the documentation. Feel free to provide any feedback when I do so!
Cool, thanks. Yep, will do.
That's exactly how I'd do it. If I had been introduced this way I don't think I would have had that aforementioned misconception.
Makes sense.
I thought about this too, after reading the Discourse comments, but I'm stuck for a better name.
That's clearer, I like it.
Cool, thanks. Yep, will do. |

russelldavies commentedMay 4, 2018
I like the API and general idea of this a lot but I'm struggling to figure out how to use it with varying layouts and styles of form elements. For instance, in
Form.View.basic, each field is rendered with the same styling and there is no control of layout, they're vertically stacked. Even if I wanted to add some content between fields, it's not possible.Do you have any ideas on how to change the rendering functions to accommodate this?