Elm - Real World Use Cases

Complex Forms

Sometimes, we need more complex forms in our application. Using common Elm approach with a message per input field can result in a lot of boilerplate code. Luckily there are some libraries to help with that. One of them is etaque/elm-form. At the price of losing some type safety (field names are using strings), we get nice validation API similar to JSON decoders API with some basic validation and a possibility to create our own, support for nested fields and lists.

First, we need to define a type that should be represented in our form, e.g., a person.

type alias Person =
    { name : String
    , age : Int
    }

Then, we define validation (notice that the API is basically the same as using Json.Decode module):

import Form
import Form.Validate as Validate


personValidation : Validate.Validation CustomFormError Person
personValidation =
    Validate.map2 Person
        (Validate.field "name" Validate.string)
        (Validate.field "age" Validate.int)

After that, we can initialize the form, either empty:

initPersonForm : Form.Form CustomFormError Person
initPersonForm =
    Form.initial [] personValidation

Or using existing person data (notice that the API for the initial data is basically the same as using Json.Encode module):

import Form.Field as Field


initPersonFormWithPerson : Person -> Form.Form CustomFormError Person
initPersonFormWithPerson { age, name } =
    let
        initials =
            [ ( "name", Field.string name )
            , ( "age", Field.int age )
            ]
    in
    Form.initial initials personValidation

The library comes with functions for generating views. We need to create our own functions for converting form errors to string (because we can add our own errors). Also, all fields are represented as string or bool (for checkboxes), so we cannot really use for example input type number.

import Form
import Form.Input as Input
import Html exposing (Html)
import Html.Attributes as Attributes
import Html.Events as Events


viewForm : Form.Form () Person -> Html Form.Msg
viewForm form =
    let
        viewError { liveError } =
            case liveError of
                Just error ->
                    Html.p [ Attributes.class "error" ]
                        [ Html.text (errorToString error) ]

                Nothing ->
                    Html.text ""

        nameField =
            Form.getFieldAsString "name" form

        ageField =
            Form.getFieldAsString "age" form
    in
    form [ Events.onSubmit Form.Submit ]
        [ Html.label [] [ Html.text "name" ]
        , Input.textInput nameField []
        , viewError nameField
        , Html.label [] [ Html.text "age" ]
        , Input.textInput ageField []
        , viewError ageField
        , Html.button [ Attributes.type_ "submit" ] [ Html.text "Submit" ]
        ]

The last thing is handling Form messages. If the message is Form.Submit, and the form is valid, we want to handle submitting the form. To get the output, there is a function Form.getOutput, it either returns Just the type in the form or Nothing if the form contains invalid values.

We want to use Form.update function for other messages to update the form model.

type Msg
    = FormMsg Form.Msg

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        FormMsg formMsg ->
            case ( formMsg, Form.getOutput model.personForm ) of
                ( Form.Submit, Just form ) ->
                    -- The output is the original type (Person in this case)
                    -- We get it only if the validation passes
                    let
                        _ =
                            Debug.log "Submit form" form
                    in
                    ( model, Cmd.none )

                _ ->
                    ( { model | personForm = Form.update personValidation formMsg model.personForm }
                    , Cmd.none
                    )

SVG

Elm has a package elm/svg for creating SVG images in Elm. The API looks like corresponding packages for Html. Now we have Svg module with functions for SVG elements (e.g., rect or circle), Svg.Attributes for the attributes used by SVG elements (e.g., strokeWidth or x) and Svg.Events for JavaScript events, same as Html.Events.

SVG is good for visualisations. We can start at SVG element reference to find the elements we need. Here’s an example from package documentation of drawing a rounded rectangle:

import Html exposing (Html)
import Svg
import Svg.Attributes as Attributes

roundRect : Html msg
roundRect =
    Svg.svg
      [ Attributes.width "120"
      , Attributes.height "120"
      , Attributes.viewBox "0 0 120 120"
      ]
      [ Svg.rect
        [ Attributes.x "10"
        , Attributes.y "10"
        , Attributes.width "100"
        , Attributes.height "100"
        , Attributes.rx "15"
        , Attributes.ry "15"
        ] 
        []
      ]

Files

Since Elm 0.19 there is elm/file package for working with files. We can use it for allowing users to download a file generated in Elm, or for uploading files.

Example of download a markdown file generated from a string in Elm:

import File.Download as Download

save : String -> Cmd msg
save markdown =
  Download.string "draft.md" "text/markdown" markdown

Example of file select:

import File.Select as Select

type Msg
  = ImagesRequested
  | ImagesLoaded File (List File)

requestImages : Cmd Msg
requestImages =
  Select.files [ "image/png", "image/jpg" ] ImagesLoaded

If we get File from the previous example, we can send it to the server using elm/http package. There is a fileBody function for that. We can explore examples in elm/file package to see how it works.

Graph QL

There is a couple of packages for working with GraphQL in Elm.

dillonkearns/elm-graphql

This is one of the most popular GraphQL packages for Elm. It comes with a command-line code generator to create type-safe Elm code for GraphQL endpoint. Then, we get type-safe GraphQL queries. It is also eliminating GraphQL features in favour of Elm language constructs.

Here’s an example from the documentation.

The GraphQL query:

query {
  human(id: "1001") {
    name
    homePlanet
  }
}

It looks like this in the Elm code (StarWars packages are auto-generated using the command line tool):

import Graphql.Operation as Operation
import Graphql.SelectionSet as SelectionSet
import StarWars.Object.Human as HumanObject
import StarWars.Query as Query
import StarWars.Scalar as Scalar


query : SelectionSet.SelectionSet (Maybe Human) Operation.RootQuery
query =
    Query.human { id = Scalar.Id "1001" } humanSelection


type alias Human =
    { name : String
    , homePlanet : Maybe String
    }


humanSelection : SelectionSet.SelectionSet Human HumanObject
humanSelection =
    SelectionSet.map2 Human
        Human.name
        Human.homePlanet

jamesmacaulay/elm-graphql

This is another popular package for GraphQL. It provides an interface for working with GraphQL queries and schemas in Elm. Building requests works similarly as JSON decoders.

Here’s an example from the documentation, we need to define the types by ourselves:

type alias Photo =
    { url : String
    , caption : String
    }

type alias User =
    { name : String
    , photos : List Photo
    }

Then we build a query document using the library:

userQuery : Document Query User { vars | userID : String }
userQuery =
    let
        userIDVar =
            Var.required "userID" .userID Var.id

        photo =
            object Photo
                |> with (field "url" [] string)
                |> with (field "caption" [] string)

        user =
            object User
                |> with (field "name" [] string)
                |> with (field "photos" [] (list photo))

        queryRoot =
            extract
                (field "user"
                    [ ( "id", Arg.variable userIDVar ) ]
                    user
                )
    in
    queryDocument queryRoot

The document would be encoded into this string:

query ($userID: ID!) {
  user(id: $userID) {
    name
    photos {
      url
      caption
    }
  }
}

WebSockets

There is a package elm-lang/websockets, however, it was not yet updated to Elm 0.19.1 - it remains in 0.19.0 ELM version. It should be updated at some point in the future. There is a 3rd party package billstclair/elm-websocket-client that is converting the original package to Elm 0.19.1.

The other option is to use ports and to implement WebSocket interactions on the JavaScript side.

Materials

Further Reading


Licenses and Attributions


Speak Your Mind

-->