This week we've seen how to use Web Components with Elm, and we've built a fairly full-featured layout. Today we'll look at implementing Material Design Cards, Menus, and Floating Action Buttons for more full-featured UX, all powered by the Paper Components. Let's get started.

Project

We're starting with the elm_web_components_playground, tagged before this episode.

I've pulled in a few more paper components and created a module to provide a slightly nicer interface when using them.

Cards

Let's quickly add a card:

vim src/App.elm
module App exposing (..)
-- ...
import Html.Attributes exposing (attribute, style, class)
-- ...
view : Model -> Html Msg
view model =
    appDrawerLayout
        []
        [ appDrawer
            []
            [ text "drawer content" ]
        , header model
        , card model
        ]

card : Model -> Html Msg
card model =
    paperCard
        [ style [ ( "margin", "1em" ) ] ]
        [ div
            [ class "card-content" ]
            [ text "a lonely card" ]
        ]

Here we have the most basic card possible - just a card node, containing a node with class card-content. Let's add a heading to the card:

card : Model -> Html Msg
card model =
    paperCard
        [ style [ ( "margin", "1em" ) ]
        , attribute "heading" "MegaSpoon"
        ]
        [ div
            [ class "card-content" ]
            [ text "a lonely card" ]
        ]

Here's a card with a heading. Cards can also have actions:

card : Model -> Html Msg
card model =
    paperCard
        [ style [ ( "margin", "1em" ) ]
        , attribute "heading" "MegaSpoon"
        ]
        [ div
            [ class "card-content" ]
            [ text "a lonely card" ]
        , div
            [ class "card-actions" ]
            [ button [] [ text "Lower" ]
            , button [] [ text "Raise" ]
            ]
        ]

Cards have elevation. The default is 2 but you can tweak it:

card : Model -> Html Msg
card model =
    paperCard
        [ style [ ( "margin", "1em" ) ]
        , attribute "heading" "MegaSpoon"
        , attribute "elevation" "4"
        ]
        [ div
            [ class "card-content" ]
            [ text "a lonely card" ]
        , div
            [ class "card-actions" ]
            [ button [] [ text "Lower" ]
            , button [] [ text "Raise" ]
            ]
        ]

Let's add a quick model and wire the buttons up to raise and lower the card:

module App exposing (..)
-- ...
import Html.Events exposing (onClick)
-- ...
type alias Model =
    { message : String
    , elevation : Int
    }


init : ( Model, Cmd Msg )
init =
    ( { message = "Your Elm App is working!", elevation = 2 }, Cmd.none )


type Msg
    = Raise
    | Lower


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

        Lower ->
            ( { model | elevation = model.elevation - 1 }, Cmd.none )
-- ...
card : Model -> Html Msg
card model =
    paperCard
        [ style [ ( "margin", "1em" ) ]
        , attribute "heading" "MegaSpoon"
        , attribute "elevation" (toString model.elevation) -- <--
        ]
        [ div
            [ class "card-content" ]
            [ text "a lonely card" ]
        , div
            [ class "card-actions" ]
            [ button [ onClick Lower ] [ text "Lower" ] -- <--
            , button [ onClick Raise ] [ text "Raise" ] -- <--
            ]
        ]

Elevation's only defined across a limited range, so this breaks at the boundaries. You can make a card's shadow animate when its elevation changes:

card : Model -> Html Msg
card model =
    paperCard
        [ style [ ( "margin", "1em" ) ]
        , attribute "heading" "MegaSpoon"
        , attribute "elevation" (toString model.elevation)
        , attribute "animated" "true" -- <--
        ]
        [ div
            [ class "card-content" ]
            [ text "a lonely card" ]
        , div
            [ class "card-actions" ]
            [ button [ onClick Lower ] [ text "Lower" ]
            , button [ onClick Raise ] [ text "Raise" ]
            ]
        ]

Cards can also have images:

card : Model -> Html Msg
card model =
    paperCard
        [ style [ ( "margin", "1em" ) ]
        , attribute "heading" "MegaSpoon"
        , attribute "image" "https://unsplash.it/420/230"
        , attribute "elevation" (toString model.elevation)
        , attribute "animated" "true"
        ]
        [ div
            [ class "card-content" ]
            [ text "a lonely card" ]
        , div
            [ class "card-actions" ]
            [ button [ onClick Lower ] [ text "Lower" ]
            , button [ onClick Raise ] [ text "Raise" ]
            ]
        ]

Menu

But enough about cards for now. Let's look at Menus. For now, I'm concerned about just making the drawer look nicer. Let's put a paper-menu there.

view : Model -> Html Msg
view model =
    appDrawerLayout
        []
        [ appDrawer
            []
            [ paperMenu []
                (List.map (\x -> paperItem [] [ text x ]) [ "One", "Two", "Three", "Four" ])
            ]
        , header model
        , card model
        ]

Oops, I forgot to install paper-item apparently:

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

Refresh again, and we have a nice looking menu. There are various options, but this is good for now.

Floating Action Button

A common element in Material Design is the Floating Action Button, or FAB. Let's see how easy it is to add one:

card : Model -> Html Msg
card model =
    paperCard
        [ -- ...
        ]
        [ div
            [ class "card-content" ]
            [ paperFab
                [ attribute "icon" "add" ]
                []
            , text "a lonely card"
            ]
        , -- ...
        ]

Generally you'll find one of these hovering over something else. We can do that:

card : Model -> Html Msg
card model =
    paperCard
        [ -- ...
        ]
        [ div
            [ class "card-content"
            , style [ ( "position", "relative" ) ] -- <--
            ]
            [ paperFab
                [ attribute "icon" "add"
                , style [ ( "position", "absolute" ), ( "right", "16px" ), ( "top", "-32px" ) ] -- <--
                ]
                []
            , text "a lonely card"
            ]
        , -- ...
        ]

That's roughly the look I was going for, though I can't really say what this card is for precisely.

Summary

In today's episode we quickly looked at building Cards, Menus, and Floating Action Buttons with Polymer through Elm. I hope you enjoyed it. See you soon!

Resources

Preparing for this episode

Before starting this episode, I brought in the paper-card, paper-fab, paper-menu, and paper-item components:

bower install --save PolymerElements/paper-card PolymerElements/paper-fab paper-menu paper-item
vim src/index.html
    <link rel="import" href="/bower_components/paper-card/paper-card.html">
    <link rel="import" href="/bower_components/paper-fab/paper-fab.html">
    <link rel="import" href="/bower_components/paper-menu/paper-menu.html">
    <link rel="import" href="/bower_components/paper-item/paper-item.html">

I also dropped in a Paper module I created to wrap these components in slightly nicer-looking functions:

vim src/WebComponents/Paper.elm
module WebComponents.Paper exposing (input, button, iconButton, paperMenu, paperItem, paperCard, paperFab)

import Html exposing (Html, Attribute, node)


input : List (Attribute a) -> List (Html a) -> Html a
input =
    node "paper-input"


button : List (Attribute a) -> List (Html a) -> Html a
button =
    node "paper-button"


iconButton : List (Attribute a) -> List (Html a) -> Html a
iconButton =
    node "paper-icon-button"


paperMenu : List (Attribute a) -> List (Html a) -> Html a
paperMenu =
    node "paper-menu"


paperItem : List (Attribute a) -> List (Html a) -> Html a
paperItem =
    node "paper-item"


paperCard : List (Attribute a) -> List (Html a) -> Html a
paperCard =
    node "paper-card"


paperFab : List (Attribute a) -> List (Html a) -> Html a
paperFab =
    node "paper-fab"