This week we're looking at using Web Components with Elm. Yesterday we saw how to use Material-styled inputs and buttons - but it'd be nice to have a good looking layout for them to go in. This is where app-layout comes in. Let's look, shall we?

Project

Starting with the knewter/elm_web_components_playground tagged before this episode, we'll bring in the app-layout component and install it like any other web component:

bower install --save app-layout
vim src/index.html
    <link rel="import" href="/bower_components/app-layout/app-layout.html">

Toolbar

There are a lot of elements bundled in this component. Let's start out by just adding an app-toolbar to our site:

vim src/App.elm
view : Model -> Html Msg
view model =
    div
        []
        [ node "app-toolbar"
            []
            [ div
                [ attribute "main-title" "" ]
                [ text "Thousands of Spoons" ]
            ]
        , -- ...
        ]

And this...sort of works. But it's really ugly. Next, we need to bring in some styling. Polymer has a whole guide regarding CSS with web components, but there's a specific section we care about related to custom-style. You can read that guide if you're interested in learning more - for now, let's just add some styles:

vim src/index.html
    <style is="custom-style">
      body {
        /* No margin on body so toolbar can span the screen */
        margin: 0;
      }
      app-toolbar {
        /* Toolbar is the main header, so give it some color */
        background-color: #1E88E5;
        font-family: 'Roboto', Helvetica, sans-serif;
        color: white;
        --app-toolbar-font-size: 24px;
      }
    </style>

With this, when you refresh the page you should get a pretty nice looking result. That's app-toolbar!

Header

A toolbar is good, but it's not quite a header. That's where app-header comes in. It is:

a container element for app-toolbars at the top of the screen that can have scroll effects.

Let's add one, shall we?

vim src/App.elm
-- Let's also take this opportunity to break the view out into a couple of
-- smaller functions:
view : Model -> Html Msg
view model =
    div
        []
        [ header model
        , body model
        ]


header : Model -> Html Msg
header model =
-- We'll wrap the toolbar in an `app-header`
    node "app-header"
        []
        [ node "app-toolbar"
            []
            [ div
                [ attribute "main-title" "" ]
                [ text "Thousands of Spoons" ]
            ]
        ]


-- And leave the body untouched, aside from wrapping the siblings in a div
-- parent.
body : Model -> Html Msg
body model =
    div
        []
        [ text model.message
        , node "paper-input"
            [ attribute "label" "Username" ]
            []
        , div
            []
            [ node "paper-button"
                [ attribute "raised" "raised"
                , style
                    [ ( "background", "#1E88E5" )
                    , ( "color", "white" )
                    ]
                ]
                [ text "Clicky" ]
            ]
        ]

If you refresh the page right now, it seems mostly the same. Not so exciting. Let's actually make the body really tall though, so you can appreciate something that we get for free:

body : Model -> Html Msg
body model =
    div
        [ style
            [ ( "min-height", "2000px" )
            ]
        ]
        [ -- ...
        ]

Now that you can scroll, if you scroll very slowly you can see the header slides out of the way as you scroll. Honestly: this isn't amazing. Let's use one more feature of the header to make this all worthwhile though. We'll say it reveals when you're scrolling up:

header : Model -> Html Msg
header model =
    node "app-header"
        [ attribute "reveals" "" -- <--
        ]
        [ -- ...
        ]

Now, if you refresh...it doesn't appear to do anything any better, at all. What gives?

At this point, Let's introduce the app-header-layout component.

Header Layout

This is a component that's meant to wrap a header and some content. It's easy to use:

header : Model -> Html Msg
header model =
    node "app-header-layout" -- <--
        []
        [ node "app-header"
            [ attribute "reveals" ""
            ]
            [ node "app-toolbar"
                []
                [ div
                    [ attribute "main-title" "" ]
                    [ text "Thousands of Spoons" ]
                ]
            ]
        ]

That makes everything line up nicely, looking pretty good. Here's a commit representing where you should be at this point.

Drawer Layout

To finish up our exploration of the App Layout bits, let's wrap this all in an app-drawer-layout.

This is our header layout, but with a drawer that you can pull out that can have the menu in it. We start by changing our wrapper div to be the drawer layout:

view : Model -> Html Msg
view model =
    node "app-drawer-layout"
        []
        [ node "app-drawer"
            []
            [ text "drawer content" ]
        , header model
        , body model
        ]

Now we have a header and a drawer. The drawer is visible on desktop sizes, but you can't even open it from mobile. You'll want to create an icon that you can click to open it. To get our icons, we want to add paper-icon-button and iron-icons:

bower install --save paper-icon-button
bower install --save iron-icons
vim src/index.html
    <link rel="import" href="/bower_components/iron-icons/iron-icons.html">
    <link rel="import" href="/bower_components/paper-icon-button/paper-icon-button.html">

Finally, we add the button to the header with the attribute drawer-toggle so that the layout wires it up for us to open the drawer:

header : Model -> Html Msg
header model =
    node "app-header-layout"
        []
        [ node "app-header"
            [ attribute "reveals" ""
            ]
            [ node "app-toolbar"
                []
                [ node "paper-icon-button" -- <---
                    [ attribute "icon" "menu"
                    , attribute "drawer-toggle" ""
                    ]
                    []
                , div
                    [ attribute "main-title" "" ]
                    [ text "Thousands of Spoons" ]
                ]
            ]
        ]

With that, you can refresh the page and you've got a pretty full-featured material layout. It was a lot more effort than setting up elm-mdl's layout, for sure. However, it's completely doable and it's not too bad.

Wrap it up in a warm, Elm-y embrace

I don't really want to see all of this junk though. Let's at least make some helpers in a module so that we don't just have node everywhere throughout our app.

We'll wrap up the App bits first:

mkdir src/WebComponents
vim src/WebComponents/App.elm
module WebComponents.App exposing (appDrawer, appDrawerLayout, appToolbar, appHeader, appHeaderLayout)

import Html exposing (Html, Attribute, node)


appDrawer : List (Attribute a) -> List (Html a) -> Html a
appDrawer =
    node "app-drawer"


appDrawerLayout : List (Attribute a) -> List (Html a) -> Html a
appDrawerLayout =
    node "app-drawer-layout"


appToolbar : List (Attribute a) -> List (Html a) -> Html a
appToolbar =
    node "app-toolbar"


appHeader : List (Attribute a) -> List (Html a) -> Html a
appHeader =
    node "app-header"


appHeaderLayout : List (Attribute a) -> List (Html a) -> Html a
appHeaderLayout =
    node "app-header-layout"

Let's replace the nodes in the App module with these functions:

module App exposing (..)
-- ...
import WebComponents.App exposing (appDrawer, appDrawerLayout, appToolbar, appHeader, appHeaderLayout)
-- ...
view : Model -> Html Msg
view model =
    appDrawerLayout
        []
        [ appDrawer
            []
            [ text "drawer content" ]
        , header model
        , body model
        ]


header : Model -> Html Msg
header model =
    appHeaderLayout
        []
        [ appHeader
            [ attribute "reveals" ""
            ]
            [ appToolbar
                []
                [ node "paper-icon-button"
                    [ attribute "icon" "menu"
                    , attribute "drawer-toggle" ""
                    ]
                    []
                , div
                    [ attribute "main-title" "" ]
                    [ text "Thousands of Spoons" ]
                ]
            ]
        ]

And honestly, that's good enough for now. Refresh the page and everything should still look great, but our code at least looks a little cleaner.

Summary

In today's episode we took a pretty deep dive into using Polymer's layout components with Elm. It takes a bit of setup right now, but eventually I would expect this to be nicely wrapped up for the average person to use. Until then, this should be a pretty solid repo to give you a good starting point, if you're interested. I hope you enjoyed it!

Resources

Josh Adams

I've been building web-based software for businesses for over 18 years. In the last four years I realized that functional programming was in fact amazing, and have been pretty eager since then to help people build software better.

  1. Comments for Polymer App Layout

You May Also Like