choonkeat / formdata / FormData

Using Dict.Any and a few helper functions to manage the state of a form

type alias Model =
    { userForm : FormData UserFields User
    }

type alias User =
    { firstname : String
    , hobbies : List Hobby
    }

type UserFields
    = Firstname
    | Hobbies Hobby

type Hobby = Soccer | Football | Basketball

stringHobby : Hobby -> String
stringHobby hobby =
    case hobby of
        Soccer -> "soccer"
        Football -> "football"
        Basketball -> "basketball"

stringUserFields : UserFields -> String
stringUserFields field =
    case field of
        Firstname ->
            "firstname"
        Hobbies h ->
            "hobbies " ++ stringHobby h

model : Model
model =
    { userForm = FormData.init stringUserFields []
    }

currentForm : FormData UserFields User
currentForm =
    model.userForm
        |> onInput Firstname "Alice"
        |> onCheck (Hobbies Soccer) True
        |> onCheck (Hobbies Football) True
        |> onCheck (Hobbies Soccer) False
        |> onCheck (Hobbies Basketball) True

currentForm
--> FormData.init stringUserFields
-->    [ (Firstname, "Alice"), (Hobbies Basketball, ""), (Hobbies Football, "") ]


parseDontValidate : List (UserFields, String) -> ( Maybe User, List ( Maybe UserFields, String ) )
parseDontValidate keyValueList =
    let
        initalUserWithErrors =
            ( { firstname = "", hobbies = [] }
            , [ ( Just Firstname, "cannot be blank")
              , ( Nothing, "must choose one hobby" )
              ]
            )
        --
        buildUserErrs (k, s) (partUser, partErrs) =
            case k of
                Firstname ->
                    ( { partUser | firstname = s }
                    , if s /= "" then
                          List.filter (\(maybeK, _) -> maybeK /= Just k) partErrs
                      else
                          partErrs
                    )
                Hobbies h ->
                    ( { partUser | hobbies = h :: partUser.hobbies }
                    , List.filter (\(maybeK, _) -> maybeK /= Nothing) partErrs
                    )
        --
        (value, errs) =
            List.foldl buildUserErrs initalUserWithErrors keyValueList
    in
    if [] == errs then
        (Just { value | hobbies = List.reverse value.hobbies }, [])
    else
        (Nothing, errs)

model.userForm
    |> FormData.parse parseDontValidate
--> ( Nothing , errorsFrom stringUserFields [ ( Just Firstname, "cannot be blank"), ( Nothing, "must choose one hobby") ] )

model.userForm
    |> FormData.onInput Firstname "Alice"
    |> FormData.parse parseDontValidate
--> ( Nothing , errorsFrom stringUserFields [ ( Nothing, "must choose one hobby") ] )

model.userForm
    |> FormData.onInput Firstname "Alice"
    |> FormData.onCheck (Hobbies Football) True
    |> FormData.parse parseDontValidate
--> ( Just  { firstname = "Alice", hobbies = [Football] }, errorsFrom stringUserFields [] )

Types


type FormData k a

The type that holds all the state.

init : (k -> String) -> List ( k, String ) -> FormData k a

The types parsed by Config will determine what types we are managing here

(k -> String)

Note that it's important to make sure every key is turned to different comparable. Otherwise keys would conflict and overwrite each other.


type Data a
    = Invalid
    | Valid a
    | Submitting a

The 3 states your data can be in

onSubmit : Basics.Bool -> FormData k a -> FormData k a

Toggles whether the FormData is submitting


type Errors k err

Holds error values for faster lookup

errorsFrom : (k -> String) -> List ( Maybe k, err ) -> Errors k err

Builds Errors value from List; used inside FormData.parse

errorAt : Maybe k -> Errors k err -> Maybe err

Lookup if a Maybe k has error

Input

value : k -> FormData k a -> String

What's the current value that we should set for form element of field k

view =
    input
        [ value (FormData.value Firstname formdata) ]
        []

onInput : k -> String -> FormData k a -> FormData k a

For handling onInput; updates the state based on incoming user input. The incoming value is stored as-is

view formdata =
    input
        [ value (FormData.value Firstname formdata)
        , onInput (OnUserFormInput Firstname)
        ]
        []

update msg model =
    case msg of
        OnUserFormInput k s ->
            ( { model | userForm = formdata.onInput k s model.userForm }
            , Cmd.none
            )

Checkbox

isChecked : k -> FormData k a -> Basics.Bool

Is String a chosen option for field k?

view formdata =
    label []
        [ text "Hobbies "
        , input
            [ checked (FormData.isChecked (Hobbies Basketball) formdata)
            , type_ "checkbox"
            ]
            []
        ]

onCheck : k -> Basics.Bool -> FormData k a -> FormData k a

For handling onCheck; stores multiple values for a single k

view formdata =
    label []
        [ text "Hobbies "
        , input
            [ checked (FormData.isChecked (Hobbies Basketball) formdata)
            , onCheck (OnUserFormCheck (Hobbies Basketball))
            , type_ "checkbox"
            ]
            []
        ]

update msg model =
    case msg of
        OnUserFormCheck k b ->
            ( { model | userForm = FormData.onCheck k b model.userForm }
            , Cmd.none
            )

Parse, don't validate

parse : (List ( k, String ) -> ( Maybe a, List ( Maybe k, err ) )) -> FormData k a -> ( Data a, Errors k err )

Before submitting, we should try to obtain a value (and a list of errors) from our FormData

( maybeUser, errors ) =
    FormData.parse parseDontValidate model.userForm

If we get Nothing, disable the submit button. Otherwise, wire up the onSubmit handler

button
    [ case maybeUser of
        Just user ->
            onSubmit (Save user)

        Nothing ->
            disabled True
    ]
    [ text "Submit" ]

If there's an error, show it

div
    [ input [ onInput (OnInput Name), type_ "text", placeholder "Name" ] []
    , case List.head (List.filter (\( k, v ) -> k == Just Name) errors) of
        Just ( _, err ) ->
            p [] [ small [] [ text err ] ]

        Nothing ->
            text ""
    ]

Extra

in order to not show errors which user hasn't attempted, it would be nice to track which inputs were visited

onVisited : Maybe k -> FormData k a -> FormData k a

can be used with onBlur to track which inputs were visited

view formdata =
    input
        [ onBlur (OnUserFormBlur Firstname)
        ]
        []

update msg model =
    case msg of
        OnUserFormBlur k ->
            ( { model | userForm = formdata.onVisited k model.userForm }
            , Cmd.none
            )

hadVisited : Maybe k -> FormData k a -> Basics.Bool

inquire which inputs were visited

visitedErrors : FormData k a -> Errors k err -> Errors k err

Filter Errors value against the visited tracker

keyValues : FormData k a -> List ( k, String )

Returns the form state as a List of (k, String) tuple

This is the data that