Francisco Dibar
Francisco Dibar

Reputation: 363

Dynamic form with composable-form

I'm trying to implement a dynamic form in Elm 0.19 using hecrj/composable-form.

I receive a json with the fields, their descriptions, etc, so I don't know beforehand how many fields it will have.

So the traditional way of defining a form:

Form.succeed OutputValues
      |> Form.append field1
      |> Form.append field2

doesn't work because I don't know the OutputValues structure beforehand.

I've seen there is a function Form.list which looks like a promising path, though it seems to expect all fields equal, which is not my case, I may have a text field and a select field for example.

Is there any straight forward way of doing this with this library? Thank you.

Upvotes: 3

Views: 222

Answers (1)

Stephen Reddekopp
Stephen Reddekopp

Reputation: 44

The form library doesn't explicitly support what you're trying to do, but we can make it work!

tldr;

Here's my example of how you can take JSON and create a form: https://ellie-app.com/bJqNh29qnsva1

How to get there

Form.list is definitely the promising path. You're also exactly right that Form.list requires all of the fields to be of the same type. So let's start there! We can make one data structure that can hold them by making a custom type. In my example, I called it DynamicFormFieldValue. We'll make a variant for each kind of field. I created ones for text, integer, and select list. Each one will need to hold the value of the field and all of the extras (like title and default value) to make it show up nicely. This will be what we decode the JSON into, what the form value is, and what the form output will be. The resulting types looks like this:

type alias TextFieldRequirements =
    { name : String
    , default : Maybe String
    }


type alias IntFieldRequirements =
    { name : String
    , default : Maybe Int
    }


type alias SelectFieldRequirements =
    { name : String
    , default : Maybe String
    , options : List ( String, String )
    }


type DynamicFormFieldValue
    = TextField String TextFieldRequirements
    | IntField Int IntFieldRequirements
    | SelectField String SelectFieldRequirements

To display the form, you just need a function that can take the form value and display the appropriate form widget. The form library provides Form.meta to change the form based on the value. So, we will pattern match on the custom type and return Form.textField, Form.numberField, or Form.selectField. Something like this:

dynamicFormField : Int -> Form DynamicFormFieldValue DynamicFormFieldValue
dynamicFormField fieldPosition =
    Form.meta
        (\field ->
            case field of
                TextField textValue ({ name } as requirements) ->
                    Form.textField
                        { parser = \_ -> Ok field
                        , value = \_ -> textValue
                        , update = \value oldValue -> TextField value requirements
                        , error = always Nothing
                        , attributes =
                            { label = name
                            , placeholder = ""
                            }
                        }

                IntField intValue ({ name } as requirements) ->
                    Form.numberField
                        { parser = \_ -> Ok field
                        , value = \_ -> String.fromInt intValue
                        , update = \value oldValue -> IntField (Maybe.withDefault intValue (String.toInt value)) requirements
                        , error = always Nothing
                        , attributes =
                            { label = name
                            , placeholder = ""
                            , step = Nothing
                            , min = Nothing
                            , max = Nothing
                            }
                        }

                SelectField selectValue ({ name, options } as requirements) ->
                    Form.selectField
                        { parser = \_ -> Ok field
                        , value = \_ -> selectValue
                        , update = \value oldValue -> SelectField value requirements
                        , error = always Nothing
                        , attributes =
                            { label = name
                            , placeholder = ""
                            , options = options
                            }
                        }
        )

Hooking this display function up is a bit awkward with the library. Form.list wasn't designed with use-case in mind. We want the list to stay the same length and just be iterated over. To achieve this, we will remove the "add" and "delete" buttons and be forced to provide a dummy default value (which will never get used).

dynamicForm : Form (List DynamicFormFieldValue) (List DynamicFormFieldValue)
dynamicForm =
    Form.list
        { default =
            -- This will never get used
            TextField "" { name = "", default = Nothing }
        , value = \value -> value
        , update = \value oldValue -> value
        , attributes =
            { label = "Dynamic Field Example"
            , add = Nothing
            , delete = Nothing
            }
        }
        dynamicFormField

Hopefully the ellie example demonstrates the rest and you can adapt it to your needs!

Upvotes: 2

Related Questions