Elm Radio Episode 55: Use the Platform

Hello everyone,

We have a new Elm Radio episode today: Use the Platform. We explore how the pendulum has swung away from using platform primitives and how it’s swinging back towards using them in the frontend world, and we discuss what that means for your Elm code.

Hope you enjoy!

8 Likes

@dillonkearns A candidate for the show notes: Refactoring Evan's viewInput function in elm-todomvc - DEV Community.

3 Likes

When I heard the episode, I felt kind of sheepish that form and form fields handling in Elm doesn’t exactly “use the platform”

  1. We busy ourselves storing user input values even though the browser’s form field components manages their own state (the values we store after we process the user input String, e.g. into Time.Posix, is fine though)
  2. Our form fields don’t need (and probably doesn’t have) name attribute, maybe unless it’s a radio button

Then I recall a really old DOM api that I used long ago: document.forms actually contains all the forms on the page and their current values.

Could we… just Json.Decode that data structure to retrieve the form values managed by the browser components? Then we can totally avoid (1). In fact, this means unless a field actually needs special Elm handling (e.g. conversion into custom types) we don’t actually need to handle the field (wiring up value model.currentValue, onInput, …). So number of msg no longer grows with the number of form fields

Long story short, I tried it and it works. Please try it out and let me know your thoughts on using the platform even more :tada: https://nativeform.netlify.app GitHub - choonkeat/nativeform

Notes:

  • form fields now need name attribute
  • form needs an id
  • initial value to edit form should be set with defaultValue or defaultChecked
  • form id, field names are now stringly typed, but I suppose some api design elbow grease could require supplying a custom type + toString function instead?
  • passing document.forms around the app is still a big no no imo. always use a parseDontValidate function to extract the values and store/pass those values around instead
3 Likes

Does the document value in the model change on its own when the form is updated. I’m just wondering isn’t the document value mutable then

Yes it does. In the demo example

  1. We pass window.document into the Elm app only once, via Flags, to store in our Model

     var app = Elm.Main.init({
         node: document.getElementById('myapp'),
         flags: {
             document: document
         }
     })
    
  2. Upon any event, e.g. onBlur, onChange,… we can json decode from model.document on demand to extract the current form values out of it

    update msg model =
        case msg of
            OnFormChange formId ->
                ( { model
                    | decodedForm =
                        Json.Decode.decodeValue (NativeForm.decoder formId) model.document
                            |> Result.withDefault []
                }
                , Cmd.none
                )
    
  3. You’ll see the decoded form values displayed under Raw output on the demo page

  4. Don’t stop there: parse that raw form values into the type we want

  5. You’ll see the final Result Errors ParsedInfo displayed under Parsed output on the demo page

yes it is.

:smiling_imp:

https://ellie-app.com/hqkGCFbphXNa1

1 Like

yaaa… but that’s kind of off-topic

Let’s say I have a form with a bunch of checkboxes. Is it possible to implement Check All/Uncheck All buttons with this NativeForm approach?

I’m also curious how does this approach work out when you have a dynamic list of forms. Say user with dynamic no of addresses

As a decoder for window.document, means NativeForm itself is not actively involved in setting form field values. However you get it working, NativeForm can retrieve all the values from checked boxes correctly.

Option 1. Standard Elm way

Managing the checked attribute of affected input[type=checkbox]

  1. Storing a list of checked states
    type alias Model = Array { a | checked : Bool }
    
  2. Wire up msgs to set checked to True or False
    case msg of
        OnCheck index bool ->
            ( Array.Extra.update index (\a -> { a | checked = bool }) model, Cmd.none )
    
        OnCheckAll bool ->
            ( Array.map (\a -> { a | checked = bool }) model, Cmd.none )
    
  3. Rendering the checkboxes with checked attribute set accordingly + wiring up OnCheck
    Array.indexedMap
        (\index item ->
            input [ checked item.checked, type_ "checkbox", onCheck (OnCheck index) ] []
        ) model
    
  4. Have buttons toggling OnCheckAll
    button [ onClick (OnCheckAll True), type_ "button" ] [ text "Check all" ]
    button [ onClick (OnCheckAll False), type_ "button" ] [ text "Check none" ]
    

Option 2: Standard jQuery way

lol but true

Option 3: using Html.Keyed + defaultChecked

  1. Manage an external state for checkAll : { count : Int, maybeBool : Maybe Bool }

  2. Wire up msg and provide buttons

    OnCheckAll bool ->
        ( { model
            | checkAll =
                { maybeBool = Just bool
                , count = model.checkAll.count + 1
    
    button [ onClick (OnCheckAll True), type_ "button" ] [ text "Check all" ]
    button [ onClick (OnCheckAll False), type_ "button" ] [ text "Check none" ]
    
  3. Wrap the original list of checkboxes in Html.Keyed with checkAll as unique identifier

    Html.Keyed.node "div"
        []
        [ ( stringFromCheckAll model.checkAll
          , renderCheckboxes model
        ]
    
  4. Set defaultValue of each checkbox input to prefer checkAll if present

    input
        [ defaultChecked (Maybe.withDefault False model.checkAll.maybeBool)
        , type_ "checkbox"
        ]
        []
    

You manage the list of addresses the usual way and map them each into input fields.

As long as the form field name attribute is the same, will get be getting back a ManyValues (List String) from NativeForm.valuesDict. See mycheckbox related code.

Or you could provide unique name attribute for each, e.g. rails-style "address[1]", and extract them (see Raw output section in the demo app)

1 Like

Alas not enough: we have to remember to trigger the NativeForm.decoder since making our DOM tree change doesn’t do much else (unless we setup subscriptions for MutationObserver for our form elements…)

Hello @choonkeat, thanks for the discussion! It’s cool to see your exploration here.

I’ve been exploring “using the platform” more for my upcoming elm-pages v3 release as well. For elm-pages v3, there are several benefits that come with that approach

  1. You can start out simple (no Model state for forms is possible, as you describe)
  2. You can progressively enhance form state with client validation and Model state, while still using the form values as the basis of the page, and for serializing the data
  3. The form submissions work before the JS hydrates for the page

I’ll share more on this soon along with a release announcement!

I’ve been trying to use the submitter from Html.Events.onSubmit, because that captures an additional nuance of forms which is that the button that triggers the submission will pass along its name/value pair in the form submission if it has those attributes.

I’m still finalizing some details of the API to take advantage of this, but one of the neat things about using the platform here is that you can progressively enhance the built-in browser form submission behavior to do form submissions client-side, while emulating the built-in browser form submission behavior. This makes it more robust (works while hydrating), but also reduces glue code because you can use this platform behavior to send data to the server.

One note here, I think it’s worth distinguishing between “use the platform” as the only thing you can use, vs. the basis of what you build (progressive enhancement). You can also think of this as graceful degradation. Users with different experiences (assistive devices, slow connections, small screens, certain permissions disabled) should still be able to use the web application’s core features. They may miss out on some bells and whistles, but should be able to accomplish the core workflows.

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.