New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Controlling layout and styling #1

Closed
russelldavies opened this Issue May 4, 2018 · 9 comments

Comments

Projects
None yet
2 participants
@russelldavies
Contributor

russelldavies commented May 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?

@hecrj

This comment has been minimized.

Show comment
Hide comment
@hecrj

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 favoriteLanguageField

As 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?

Owner

hecrj commented May 4, 2018

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 favoriteLanguageField

As 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?

@russelldavies

This comment has been minimized.

Show comment
Hide comment
@russelldavies

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.

Contributor

russelldavies commented May 4, 2018

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.

@russelldavies

This comment has been minimized.

Show comment
Hide comment
@russelldavies

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 formLayout

where 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?

Contributor

russelldavies commented May 7, 2018

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 formLayout

where 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?

@hecrj

This comment has been minimized.

Show comment
Hide comment
@hecrj

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:

  1. You need to control the padding for each field.
  2. 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 Float

Then, 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 notesField

Then, 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.

image

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 🎉

Owner

hecrj commented May 7, 2018

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:

  1. You need to control the padding for each field.
  2. 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 Float

Then, 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 notesField

Then, 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.

image

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 🎉

@russelldavies

This comment has been minimized.

Show comment
Hide comment
@russelldavies

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).

Contributor

russelldavies commented May 11, 2018

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).

@hecrj

This comment has been minimized.

Show comment
Hide comment
@hecrj

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.

Owner

hecrj commented May 12, 2018

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.

@russelldavies

This comment has been minimized.

Show comment
Hide comment
@russelldavies

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 values of the form, is stored in your own record where each record field is a Value input which 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 from values (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.

Contributor

russelldavies commented May 13, 2018

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 values of the form, is stored in your own record where each record field is a Value input which 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 from values (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.

@hecrj

This comment has been minimized.

Show comment
Hide comment
@hecrj

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 :)

Owner

hecrj commented May 13, 2018

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 :)

@russelldavies

This comment has been minimized.

Show comment
Hide comment
@russelldavies

russelldavies May 13, 2018

Contributor

It is probably a good idea to introduce Form with 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 empty to succeed, in order to feel closer to Task and Decode.

Makes sense.

I might change append too.

I thought about this too, after reading the Discourse comments, but I'm stuck for a better name.

Maybe we could rename Parser to FieldParser and FormResult to FormParser?

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.

Contributor

russelldavies commented May 13, 2018

It is probably a good idea to introduce Form with 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 empty to succeed, in order to feel closer to Task and Decode.

Makes sense.

I might change append too.

I thought about this too, after reading the Discourse comments, but I'm stuck for a better name.

Maybe we could rename Parser to FieldParser and FormResult to FormParser?

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.

@hecrj hecrj referenced this issue May 18, 2018

Merged

1.0.0 #2

27 of 27 tasks complete

@hecrj hecrj added this to the 1.0.0 milestone Jun 14, 2018

@hecrj hecrj closed this in #2 Jun 25, 2018

bobwhitelock added a commit to alces-software/flight-blueprint that referenced this issue Aug 10, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment