[018.2] `elm-mdl` Introduction


An introduction to using Google's Material Design language in Elm, with elm-mdl. Covers the basics, as well as the Layout module.

Subscribe now

Elm has a great package for producing nice looking sites using Google's Material Design language with little effort, in elm-mdl, so let's take advantage of it!

Project

We'll start off with a new project, and add the package:

mkdir mdl-playground
cd mdl-playground
elm package install -y debois/elm-mdl
vim Main.elm

We'll start off just pasting in the Counter.elm example from the elm-mdl repository's examples:

{- This file re-implements the Elm Counter example (1 counter) with elm-mdl
   buttons. Use this as a starting point for using elm-mdl components in your own
   app.
-}


module Main exposing (..)

import Html.App as App
import Html exposing (..)
import Html.Attributes exposing (href, class, style)
import Material
import Material.Scheme
import Material.Button as Button
import Material.Options exposing (css)


-- MODEL


-- You have to add a field to your model where you track the `Material.Model`.
-- This is referred to as the "model container"
type alias Model =
    { count : Int
    , mdl :
        Material.Model
        -- Boilerplate: model store for any and all Mdl components you use.
    }


-- `Material.model` provides the initial model
model : Model
model =
    { count = 0
    , mdl =
        Material.model
        -- Boilerplate: Always use this initial Mdl model store.
    }



-- ACTION, UPDATE


-- You need to tag `Msg` that are coming from `Mdl` so you can dispatch them
-- appropriately.
type Msg
    = Increase
    | Reset
    | Mdl (Material.Msg Msg)



-- Boilerplate: Msg clause for internal Mdl messages.


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Increase ->
            ( { model | count = model.count + 1 }
            , Cmd.none
            )

        Reset ->
            ( { model | count = 0 }
            , Cmd.none
            )

        -- When the `Mdl` messages come through, update appropriately.
        Mdl msg' ->
            Material.update msg' model



-- VIEW


type alias Mdl =
    Material.Model


view : Model -> Html Msg
view model =
    div
        [ style [ ( "padding", "2rem" ) ] ]
        [ text ("Current count: " ++ toString model.count)
          {- We construct the instances of the Button component that we need, one
             for the increase button, one for the reset button. First, the increase
             button. The first three arguments are:

               - A Msg constructor (`Mdl`), lifting Mdl messages to the Msg type.
               - An instance id (the `[0]`). Every component that uses the same model
                 collection (model.mdl in this file) must have a distinct instance id.
               - A reference to the elm-mdl model collection (`model.mdl`).

             Notice that we do not have to add fields for the increase and reset buttons
             separately to our model; and we did not have to add to our update messages
             to handle their internal events.

             Mdl components are configured with `Options`, similar to `Html.Attributes`.
             The `Button.onClick Increase` option instructs the button to send the `Increase`
             message when clicked. The `css ...` option adds CSS styling to the button.
             See `Material.Options` for details on options.
          -}
        , Button.render Mdl
            [ 0 ]
            model.mdl
            [ Button.onClick Increase
            , css "margin" "0 24px"
            ]
            [ text "Increase" ]
        , Button.render Mdl
            [ 1 ]
            model.mdl
            [ Button.onClick Reset ]
            [ text "Reset" ]
        ]
        |> Material.Scheme.top



-- Load Google Mdl CSS. You'll likely want to do that not in code as we
-- do here, but rather in your master .html file. See the documentation
-- for the `Material` module for details.


main : Program Never
main =
    App.program
        { init = ( model, Cmd.none )
        , view = view
        -- Here we've added no subscriptions, but we'll need to use the `Mdl` subscriptions for some components later.
        , subscriptions = always Sub.none
        , update = update
        }

OK, we can run this in the reactor:

elm-reactor

And visit it at http://localhost:8000. Click some buttons, and you can see material in action. Let's expand on it and start adding some more components.

Layout

Layout provides an overall layout for your application. We'll add one with just a header at first:

import Material.Layout as Layout

view : Model -> Html Msg
view model =
    Layout.render Mdl
        model.mdl
        [ Layout.fixedHeader
        ]
        { header = [ h1 [ style [ ( "padding", "2rem" ) ] ] [ text "Counter" ] ]
        , drawer = []
        , tabs = ( [], [] )
        , main = [ viewBody model ]
        }


-- This used to be the `view`
viewBody : Model -> Html Msg
viewBody model =
    -- ...

If you refresh, you can see we're now using a header on our page. We'll skip the drawer for now, but let's add some tabs:

view : Model -> Html Msg
view model =
    Layout.render Mdl
        model.mdl
        [ Layout.fixedHeader
        ]
        { header = [ h1 [ style [ ( "padding", "2rem" ) ] ] [ text "Counter" ] ]
        , drawer = []
        , tabs = ( [ text "Milk", text "Oranges" ], [] )
        , main = [ viewBody model ]
        }

If you refresh, you can see we have tabs. However, those tabs don't do anything yet. We can tell the layout what messages we'd like it to give us when we select a tab:


type Msg
    -- ...
    | SelectTab Int


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        -- ...
        SelectTab num ->
            let
                _ =
                    Debug.log "SelectTab: " num
            in
                model ! []


view : Model -> Html Msg
view model =
    Layout.render Mdl
        model.mdl
        [ Layout.fixedHeader
        , Layout.onSelectTab SelectTab -- <-- Add this
        ]
        { header = [ h1 [ style [ ( "padding", "2rem" ) ] ] [ text "Counter" ] ]
        , drawer = []
        , tabs = ( [ text "Milk", text "Oranges" ], [] )
        , main = [ viewBody model ]
        }

Now if we open up the developer tools we can see the selected tab's index when we click the tab. Let's provide a couple of different views depending on the selected tab.

type alias Model =
    { count : Int
    , mdl :
        Material.Model
        -- Boilerplate: model store for any and all Mdl components you use.
    , selectedTab : Int
    }


model : Model
model =
    { count = 0
    , mdl =
        Material.model
        -- Boilerplate: Always use this initial Mdl model store.
    , selectedTab = 0
    }


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        -- ...
        SelectTab num ->
            { model | selectedTab = num } ! []


viewBody : Model -> Html Msg
viewBody model =
    case model.selectedTab of
        0 ->
            viewCounter model

        1 ->
            text "something else"

        _ ->
            text "404"


-- Renamed again!
viewCounter : Model -> Html Msg
viewCounter model =
    -- ...

And if you check it out now...it's awful. Here's why. Our viewCounter function is what's adding on the Material CSS, so let's remove it from there and add it to our main view function instead. Ultimately, you want this included in your html file probably.

view : Model -> Html Msg
view model =
    Material.Scheme.top <|
        Layout.render Mdl
            -- ...

Now let's just play with one more thing today - colors. You can specify a color scheme to the Material.Scheme.topWithScheme function. Let's try Teal with a LightGreen highlight:

import Material.Color as Color

view : Model -> Html Msg
view model =
    Material.Scheme.topWithScheme Color.Teal Color.LightGreen <|
        Layout.render Mdl
            -- ...

This is nice but we don't have any highlights. We can tell the layout which tab index is selected:

view : Model -> Html Msg
view model =
    Material.Scheme.topWithScheme Color.Teal Color.LightGreen <|
        Layout.render Mdl
            model.mdl
            [ Layout.fixedHeader
            , Layout.selectedTab model.selectedTab
            , Layout.onSelectTab SelectTab
            ]
            { header = [ h1 [ style [ ( "padding", "2rem" ) ] ] [ text "Counter" ] ]
            , drawer = []
            , tabs = ( [ text "Milk", text "Oranges" ], [] )
            , main = [ viewBody model ]
            }

Alright, and that works. One last trick - that second element in the tabs 2-tuple is a list of styles. Let's set their background to a lighter version of our scheme's primary color:

view : Model -> Html Msg
view model =
    Material.Scheme.topWithScheme Color.Teal Color.LightGreen <|
        Layout.render Mdl
            model.mdl
            [ Layout.fixedHeader
            , Layout.selectedTab model.selectedTab
            , Layout.onSelectTab SelectTab
            ]
            { header = [ h1 [ style [ ( "padding", "2rem" ) ] ] [ text "Counter" ] ]
            , drawer = []
            , tabs = ( [ text "Milk", text "Oranges" ], [ Color.background (Color.color Color.Teal Color.S400) ] )
            , main = [ viewBody model ]
            }

Pretty easy!

Summary

In today's episode we had a quick look at introducing Material Design Lite into a project, showing both how to get started with it as well as how to use the Layout module to build layouts rapidly.

I'm thrilled that this early in Elm's existence we have such a fantastically well-constructed general purpose layout package. See you soon!

Resources

Published on 08.09.2016