components

reusing ui in your app.

starting simple

Not every component in Elm needs to have it's own Model, Msg, init, update, view defined. In fact, a lot of things can just be a function!

Let's look at an examples of using creating a reusable button in Elm:

module Pages.Top exposing ( Model, Msg, page )

import Element
import Element.Background as Background
import Element.Border as Border
import Element.Font as Font
import Element.Input as Input

type Msg = SignIn | SignOut

view : Element Msg
view =
  Input.button
    [ Font.size 14
    , Font.semiBold
    , Border.solid
    , Border.width 2
    , Border.rounded 4
    , Element.paddingXY 24 8
    , Font.color colors.coral
    , Border.color colors.coral
    , Background.color colors.white
    , Element.pointer
    ]
    { label = Element.text "SignIn"
    , onPress = Just SignIn
    }

Here, our homepage (at src/Pages/Top.elm) defines a bunch of button styles. If we wanted to reuse those styles, we can make a function like this:

module Pages.Top exposing ( Model, Msg, page )

import Element
import Element.Background as Background
import Element.Border as Border
import Element.Font as Font
import Element.Input as Input

viewButton : { label : String, onPress : msg } -> Element msg
viewButton options =
  Input.button
    [ Font.size 14
    , Font.semiBold
    , Border.solid
    , Border.width 2
    , Border.rounded 4
    , Element.paddingXY 24 8
    , Font.color colors.coral
    , Border.color colors.coral
    , Background.color colors.white
    , Element.pointer
    ]
    { label = Element.text options.label
    , onPress = Just options.onPress
    }

type Msg = SignIn | SignOut

view : Element Msg
view =
  Element.column []
    [ viewButton
        { label = "Sign in"
        , onPress = SignIn
        }
    , viewButton
        { label = "Sign out"
        , onPress = SignOut
        }
    ]

By creating that viewButton function, we prevent the need to duplicate our code, and reuse those styles again for the "Sign out" button!

sharing between pages

So we love our button so much that we want to reuse it on the "Share" page (over at src/Pages/Share.elm). The only problem is that all the code we wrote is in the src/Pages/Top.elm file.

So what should we do?

Let's create a module called Ui.elm that has our viewButton function in it:

module Ui exposing ( viewButton )

import Element
import Element.Background as Background
import Element.Border as Border
import Element.Font as Font
import Element.Input as Input


viewButton : { label : String, onPress : msg } -> Element msg
viewButton options =
  Input.button
    [ Font.size 14
    , Font.semiBold
    , Border.solid
    , Border.width 2
    , Border.rounded 4
    , Element.paddingXY 24 8
    , Font.color colors.coral
    , Border.color colors.coral
    , Background.color colors.white
    , Element.pointer
    ]
    { label = Element.text options.label
    , onPress = Just options.onPress
    }

And update src/Pages/Top.elm:

module Pages.Top exposing ( Model, Msg, page )

import Element
import Ui

type Msg = SignIn | SignOut

view : Element Msg
view =
  Element.column []
    [ Ui.viewButton
        { label = "Sign in"
        , onPress = SignIn
        }
    , Ui.viewButton
        { label = "Sign out"
        , onPress = SignOut
        }
    ]

That makes our page a lot shorter, and using Ui.viewButton let's readers know where that function is coming from!

We can now reuse it on src/Pages/Share.elm easily!

module Pages.Share exposing ( Model, Msg, page )

import Element
import Ui

type Msg = ShareOnTwitter

view : Element Msg
view =
  Ui.viewButton
    { label = "Share"
    , onPress = ShareOnTwitter
    }

when to create a new module

In Elm, we usually make a module around data structures. The creator of the language, Evan Czaplicki, has a really great talk about that idea here.

For this site, I made the navbar into it's own file (at src/Components/Navbar.elm), but I could have just as easily made a function in src/Ui.elm that exposed viewNavbar.

Directly mapping ideas from JS frameworks like React may lead you down a frustrating path. What makes sense for scaling a JavaScript app might not translate in Elm!

If you find yourself creating components like this:

module Components.Example exposing
  ( Model
  , Msg
  , init
  , update
  , view
  )

-- code

You'll end up creating a verbosity problem for components (the same one that elm-spa was designed to fix for pages!)

module Pages.Example exposing (Model, Msg, page)

import Components.Foo as Foo
import Components.Bar as Bar
import Components.Baz as Baz

type alias Model =
  { foo : Foo.Model
  , bar : Bar.Model
  , baz : Baz.Model
  }

type Msg
  = FromFoo Foo.Msg
  | FromBar Bar.Msg
  | FromBaz Baz.Msg

view : Model -> Element Msg
view model =
  Element.column []
    [ Element.map FromFoo (Foo.view model.foo)
    , Element.map FromBar (Bar.view model.bar)
    , Element.map FromBaz (Baz.view model.baz)
    ]

update : Msg -> Model -> Model
update msg model =
  case msg of
    FromFoo msg_ ->
      { model | foo = Foo.update msg_ model.foo }
    FromBar msg_ ->
      { model | bar = Bar.update msg_ model.bar }
    FromBaz msg_ ->
      { model | baz = Baz.update msg_ model.baz }

There's nothing wrong with the code in the example above! Maybe Foo needs to be complex!

But start with the simplest strategy first. Maybe Bar and Baz don't need to follow that pattern:

module Pages.Example exposing (Model, Msg, page)

import Components.Foo as Foo
import Ui

type alias Model =
  { user : Maybe String
  , foo : Foo.Model
  }

type Msg
  = FromFoo Foo.Msg
  | SignOut

view : Model -> Element Msg
view model =
  Element.column []
    [ Element.map FromFoo (Foo.view model.foo)
    , Ui.viewBar model.username
    , Ui.viewBaz { onClick = SignOut }
    ]

update : Msg -> Model -> Model
update msg model =
  case msg of
    FromFoo msg_ ->
      { model | foo = Foo.update msg_ model.foo }
    SignOut ->
      { model | user = Nothing }
next up: deploying