13

It's a really basic question but I didn't find any example.
I have a view like this :

view address model =
  div []
    [ div [] [ text <|"ID : " ++ toString model.id ]
    , form
        []
        [ input [ value model.title ] []
        , textarea [ value model.content ] []
        , button [ onClick address ( SubmitPost model ) ] [ text "Submit" ] // Here is the issue, I want to send my updated model
        ]
    ]

So it display a form with the content inside.
So if I write in my input and textarea to update the content, how do I "catch" my updated model on the onClick event on the button to send it?

BoumTAC
  • 3,531
  • 6
  • 32
  • 44

3 Answers3

29

The standard way to handle forms in Elm is to trigger updates to your model whenever anything changes on the form. You will typically see some kind of on event attribute attached to each form element.

For your example, you'll want to use on "input" to fire events that update your model with the latest value. But before we can do that, we'll need to create some actions that respond to updates from either field.

type Action
  = SubmitPost
  | UpdateTitle String
  | UpdateContent String

I took the liberty of changing your SubmitPost Model action to just SubmitPost. Since we're changing your code to always be up to date, you don't need anything other than the action SubmitPost to trigger an event that does the submission.

Now that you have the additional actions, you'll need to handle them in the update function:

update action model =
  case action of
    UpdateTitle s -> 
      ({ model | title = s }, Effects.none)
    UpdateContent s -> 
      ({ model | content = s }, Effects.none)
    ...

We can now add the on attributes onto your text fields to trigger updates whenever anything changes. "input" is the event that browsers will fire when text content changes, and it gives you more coverage than just watching for something like keypress events.

view address model =
  div []
    [ div [] [ text <| "ID : " ++ toString model.id ]
    , form
      []
      [ input
        [ value model.title
        , on "input" targetValue (Signal.message address << UpdateTitle)
        ]
        []
      , textarea
        [ value model.content
        , on "input" targetValue (Signal.message address << UpdateContent)
        ]
        []
      , button [ onClick address SubmitPost ] [ text "Submit" ]
      ]
    ]

The targetValue decoder is a Json Decoder which inspects the javascript event that was fired, drilling down to the event.target.value field inside the javascript object, which contains the full value of the text field.

JuanCaicedo
  • 3,132
  • 2
  • 14
  • 36
Chad Gilbert
  • 36,115
  • 4
  • 89
  • 97
  • 14
    @BoumTAC this is not "hacking", this is how you code in Elm. – halfzebra Apr 03 '16 at 21:42
  • 2
    @halfzebra I understand, but I also understand we are in 2016 not 2005. I also understand people will never use elm if it's like this – BoumTAC Apr 04 '16 at 07:38
  • 26
    @BoumTAC stackoverflow is a place where people come to see or ask the answer to questions of varying complexity. Of course, you are welcome to your opinion, however, this is not the platform for such things, and comments like yours could lead less knowledgeable people to follow your lead. halfzebra made a valid comment about this not being 'hacking' and it sounds like your frustration has gotten the better of you. Elm takes a different mindset but it really is a valuable way of reasoning about data flow, compared to two way binding. Hang in there. – Lloyd Moore Oct 30 '16 at 17:57
  • 2
    @BoumTAC Interestingly, redux, the most popular javascript state management library in 2017, takes its [influence](http://redux.js.org/#influences) from Elm. The constraints within which the Elm architecture works guides you in a direction that makes it easy to build scalable/maintainable apps, maybe at the expense of looking complex for a trivial form binding. – khaledh Mar 05 '17 at 19:36
  • Data binding is overrated anyway. The control and conceptual consistency and gotchaless-ness and not-having-to-relearn-everything-for-every-new-UI-toolkit you gain in writing explicit handlers is well worth the few minutes spent on boilerplate. – Rei Miyasaka Jan 28 '20 at 19:16
7

Full example on ellie for elm-0.18, based on http://musigma.org/elm/2016/11/28/elm.html

Save below file as Main.elm

module Main exposing (main)

import Html exposing (Html, div, text, form, textarea, button, input)
import Html.Attributes exposing (type_, action, value, disabled)
import Html.Events exposing (onSubmit, onInput)
import Http
import Json.Decode as Json
import Json.Encode


type alias Model =
    { newComment : NewComment
    , comments : List Comment
    }


emptyModel : Model
emptyModel =
    { newComment = emptyNewComment
    , comments = []
    }


emptyNewComment =
    NewComment -1 "" ""


type alias NewComment =
    { userId : Int
    , title : String
    , body : String
    }


type Msg
    = AddComment
    | UpdateComment NewComment
    | AddCommentHttp (Result Http.Error Comment)


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        AddComment ->
            let
                newComment =
                    Debug.log "model.newComment" model.newComment
            in
                ( { model | newComment = emptyNewComment }, postComment newComment )

        UpdateComment newComment ->
            ( { model | newComment = newComment }, Cmd.none )

        AddCommentHttp (Ok response) ->
            let
                _ =
                    Debug.log "response" response
            in
                ( { model | comments = model.comments ++ [ response ] }, Cmd.none )

        AddCommentHttp (Err err) ->
            let
                _ =
                    Debug.log "err" err
            in
                ( model, Cmd.none )


postComment newComment =
    Http.send AddCommentHttp
        (Http.post "https://jsonplaceholder.typicode.com/posts"
            (encodeNewComment newComment)
            decodeComment
        )


encodeNewComment : NewComment -> Http.Body
encodeNewComment newComment =
    Http.jsonBody <|
        Json.Encode.object
            [ ( "title", Json.Encode.string newComment.title )
            , ( "body", Json.Encode.string newComment.body )
            , ( "userId", Json.Encode.int newComment.userId )
            ]


type alias Comment =
    { title : String
    , body : String
    , userId : Int
    , id : Int
    }


decodeComment : Json.Decoder Comment
decodeComment =
    Json.map4 Comment
        (Json.field "title" Json.string)
        (Json.field "body" Json.string)
        (Json.field "userId" Json.int)
        (Json.field "id" Json.int)


view : Model -> Html Msg
view model =
    div [] <|
        [ viewForm model.newComment UpdateComment AddComment
        ]
            ++ List.map (\comment -> div [] [ text <| toString comment ]) model.comments


viewForm : NewComment -> (NewComment -> msg) -> msg -> Html msg
viewForm newComment toUpdateComment addComment =
    form
        [ onSubmit addComment, action "javascript:void(0);" ]
        [ div []
            [ input
                [ value newComment.title
                , onInput (\v -> toUpdateComment { newComment | title = v })
                ]
                []
            ]
        , textarea
            [ value newComment.body
            , onInput (\v -> toUpdateComment { newComment | body = v })
            ]
            []
        , div []
            [ button
                [ type_ "submit"
                , disabled <| isEmpty newComment.title || isEmpty newComment.body
                ]
                [ text "Add Comment" ]
            ]
        ]


isEmpty : String -> Bool
isEmpty =
    String.isEmpty << String.trim


main : Program Never Model Msg
main =
    Html.program
        { view = view
        , update = update
        , subscriptions = \_ -> Sub.none
        , init = ( emptyModel, Cmd.none )
        }

and run:

elm package install -y elm-lang/http
elm-reactor

Open in web browser http://localhost:8000/Main.elm

rofrol
  • 14,438
  • 7
  • 79
  • 77
  • Very nice! But it t took some time for me to figure out that "title" in `onInput <| (toUpdateComment << \title -> NewComment 1 title newComment.body)` will come directly from `onInput`. – farmio May 24 '17 at 05:01
  • 1
    I've changed it to `onInput (\v -> toUpdateComment { newComment | title = v })`. Should be more clear – rofrol May 24 '17 at 06:07
  • For anyone wondering what `(\v -> toUpdateComment { newComment | title = v })` means. It's a lambda function with a single parameter `v`. Here is another example of a lambda `(\ x y -> x * y )` – Strix Jul 16 '21 at 07:33
5

This is the "newest" way that I've found to define an HTML form in Elm (0.18) is below. Notice it hooks into the onSubmit property of the form tag rather than an onClick of a particular button.

view : Model -> Html Msg
view model =
    Html.form
        [ class "my-form"
        , onWithOptions
            "submit"
            { preventDefault = True, stopPropagation = False }
            (Json.Decode.succeed SubmitPost)
        ]
        [ button []
            [ text "Submit"
            ]
        ]
Don Park
  • 457
  • 5
  • 6
  • 2
    What do you do in your `update` function when you receive a `SubmitPost` method – John F. Miller Oct 05 '17 at 07:19
  • Looks like `action "javascript:void(0);"` is not needed. https://ellie-app.com/9zTH65GHXa1/0 and `onSubmit` already has `preventDefault` set to `True` https://github.com/elm-lang/html/blob/2.0.0/src/Html/Events.elm#L130 – rofrol Feb 11 '18 at 16:45