4

Is there a pattern in elm for avoiding writing lots of messages just to update individual fields on child elements of your model?

At the moment I'm ending up with code as below, with a message for every input that changes and then a bunch of update logic for each field. What I would like to do is have a message like AChanged that handled all the changes to any property of A. Either by updating the record in a function that generates the message or by passing in a field name and then using that to directly perform an update on the record as you could in Javascript.

module Main exposing (Model)

import Browser exposing (Document, UrlRequest)
import Browser.Navigation as Nav exposing (Key)
import Html exposing (div, input)
import Html.Events exposing (onInput)
import Url exposing (Url)


type alias A =
    { a : String
    , b : String
    , c : String
    , d : String
    }


type alias B =
    { e : String
    , f : String
    , g : String
    , h : String
    }


type alias Model =
    { key : Nav.Key
    , url : Url.Url
    , a : A
    , b : B
    }


type Msg
    = UrlChanged Url.Url
    | LinkClicked Browser.UrlRequest
    | AaChanged String
    | AbChanged String
    | AcChanged String
    | AdChanged String
    | BeChanged String
    | BfChanged String
    | BgChanged String
    | BhChanged String


init : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init flag url key =
    ( Model key url (A "" "" "" "") (B "" "" "" ""), Cmd.none )


subscriptions : Model -> Sub Msg
subscriptions _ =
    Sub.none


view : Model -> Document msg
view model =
    { title = "Mister Mandarin"
    , body =
        div
            [ input [ onInput AaChanged ] []
            , input [ onInput AbChanged ] []
            , input [ onInput AcChanged ] []
            , input [ onInput AdChanged ] []
            , input [ onInput BeChanged ] []
            , input [ onInput BfChanged ] []
            , input [ onInput BgChanged ] []
            , input [ onInput BhChanged ] []
            ]
            []
    }


update : Msg -> Model -> ( Model, Cmd msg )
update msg model =
    case msg of
        LinkClicked urlRequest ->
            case urlRequest of
                Browser.Internal url ->
                    ( model, Nav.pushUrl model.key (Url.toString url) )

                Browser.External href ->
                    ( model, Nav.load href )

        UrlChanged url ->
            ( { model | url = url }
            , Cmd.none
            )

        AaChanged value ->
            let
                a =
                    model.a

                newA =
                    { a | a = value }
            in
            ( { model | a = newA }, Cmd.none )

        AbChanged value ->
            let
                a =
                    model.a

                newA =
                    { a | b = value }
            in
            ( { model | a = newA }, Cmd.none )

        AcChanged value ->
            let
                a =
                    model.a

                newA =
                    { a | c = value }
            in
            ( { model | a = newA }, Cmd.none )

        AdChanged value ->
            let
                a =
                    model.a

                newA =
                    { a | d = value }
            in
            ( { model | a = newA }, Cmd.none )

        BeChanged value ->
            let
                b =
                    model.b

                newB =
                    { b | e = value }
            in
            ( { model | b = newB }, Cmd.none )

        BfChanged value ->
            let
                b =
                    model.b

                newB =
                    { b | f = value }
            in
            ( { model | b = newB }, Cmd.none )

        BgChanged value ->
            let
                b =
                    model.b

                newB =
                    { b | g = value }
            in
            ( { model | b = newB }, Cmd.none )

        BhChanged value ->
            let
                b =
                    model.b

                newB =
                    { b | h = value }
            in
            ( { model | b = newB }, Cmd.none )


main : Program () Model Msg
main =
    Browser.application
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        , onUrlChange = UrlChanged
        , onUrlRequest = LinkClicked
        }
glennsl
  • 28,186
  • 12
  • 57
  • 75
Okkio
  • 147
  • 11
  • Can you share an example of what is the laborous way of "writing lots of update messages". This would help in finding a solution in Elm way. – kaskelotti Mar 18 '19 at 10:03
  • @kaskelotti I've added a detailed example of what I was talking about. Thanks for your input! – Okkio Mar 18 '19 at 12:39

1 Answers1

5

I've taken two vastly different approaches for this problem. The one that gives you the most control (while still helping eliminate verbosity) is to move your logic from the Update to your View with a generalized Msg. Something like: UpdateForm (String -> Model) or UpdateForm (String -> FormModel).

The other approach is to move away from storing input state in your model at all. This has the downside of not allowing you to do things like initialize your inputs or clear them easily. But it's great as a quick and dirty approach to getting basic forms out. In this method, you leverage the fact that input elements with a name attribute become properties of the parent form element1. You can attach a decoder to the form's onSubmit and get the value through Decode.at ["ab", "value"].

1https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-name

Wex
  • 15,539
  • 10
  • 64
  • 107
  • 2
    `move your logic from the update to your view` The Elm package hecrj/composable-form uses this technique, I really like it. https://package.elm-lang.org/packages/hecrj/composable-form/latest/ – Sidney Mar 18 '19 at 17:02
  • 2
    Have tried moving the update logic into the view out and ended up with a lambda like this: onInput <| \value -> AChanged { a | b = value } with a let in above to extract a from the model. Thanks for the help, cuts down the number of messages massively – Okkio Mar 18 '19 at 23:28