Today we're going to keep looking at components that elm-mdl provides, this time focusing on Lists and Cards. Let's get to it.

Project

We're starting off with our chat project, tagged with before_episode_018.4.

Setup

The first thing we'll do is install elm-mdl and set up a basic layout. We've already covered this so I'll just throw it in quickly as a refresher:

elm-package install -y debois/elm-mdl
vim src/Main.elm

We'll add the material css:

import Material.Scheme
-- ...

view : Model -> Html Msg
view model =
    Material.Scheme.top <|
        viewBody model


viewBody : Model -> Html Msg
viewBody model =
    case model.phxSocket of
        Nothing ->
            setUsernameView

        _ ->
            chatInterfaceView model

We'll introduce the Material component store to our model:

import Material
-- ...

type alias Model =
    { username : String
    , chats : Dict String Chat.Model
    , phxSocket : Maybe (Phoenix.Socket.Socket Msg)
    , phxPresences : PresenceState UserPresence
    , users : List User
    , currentChat : Maybe String
    , mdl : Material.Model
    }
-- ...

initialModel : Model
initialModel =
    { username = ""
    , chats =
        Dict.empty
    , phxSocket = Nothing
    , phxPresences = Dict.empty
    , users = []
    , currentChat = Nothing
    , mdl = Material.model
    }

And we'll handle the Material messages:

type Msg
    -- ...
    | Mdl (Material.Msg Msg)

-- ...

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        -- ...
        Mdl msg' ->
            Material.update msg' model

Now we're ready to slip a layout in there:

import Html.Attributes exposing (value, placeholder, class, type', style) -- <-- added style
import Material.Layout as Layout
-- ...

view : Model -> Html Msg
view model =
    Material.Scheme.top <|
        Layout.render Mdl
            model.mdl
            [ Layout.fixedHeader
            ]
            { header = [ h1 [ style [ ( "padding", "1rem" ) ] ] [ text "Phoenix Elm Chat" ] ]
            , drawer = []
            , tabs = ( [], [] )
            , main =
                [ div
                    [ style [ ( "padding", "1rem" ) ] ]
                    [ viewBody model ]
                ]
            }

If we build and refresh, we can see a pretty basic layout. Next, we're going to move the list of users and rooms into the drawer, and have the drawer open by default on large screens:

view : Model -> Html Msg
view model =
    Material.Scheme.top <|
        Layout.render Mdl
            model.mdl
            [ Layout.fixedHeader
            , Layout.fixedDrawer
            ]
            { header = [ h1 [ style [ ( "padding", "1rem" ) ] ] [ text "Phoenix Elm Chat" ] ]
            , drawer = [ viewDrawer model ]
            , tabs = ( [], [] )
            , main =
                [ div
                    [ style [ ( "padding", "1rem" ) ] ]
                    [ viewBody model ]
                ]
            }


viewDrawer : Model -> Html Msg
viewDrawer model =
    case model.phxSocket of
        Nothing ->
            div [] []

        Just _ ->
            div []
                [ rosterView model
                , roomsView model
                ]

Also, we don't need the lobby button anymore, so let's get rid of that:

chatInterfaceView : Model -> Html Msg
chatInterfaceView model =
    let
        compiled =
            Styles.compile Styles.css

        { class } =
            Styles.mainNamespace
    in
        div [ class [ Styles.ChatClientContainer ] ]
            [ node "style" [ type' "text/css" ] [ text compiled.css ]
            , div [ class [ Styles.ChatWindowContainer ] ]
                [ chatsView model
                ]
            ]

If you build it and refresh, the app looks a bit nicer. Now we'll move on to the actual meat of the episode, Lists and Cards.

Project Implementation

Alright, in the project setup we added a layout, made the drawer open all the time if there's space, and moved the rooms and buddy list into the drawer. Now we want to look at elm-mdl Lists.

Lists

We'll start off by making the list of channels on the left use a Material List.

import Material.List as List
-- We also need options, for `onClick`
import Material.Options as Options
-- ...

roomsView : Model -> Html Msg
roomsView model =
    List.ul
        []
        (List.map
            (roomView model)
            knownRooms
        )


roomView : Model -> String -> Html Msg
roomView model name =
    let
        isListening =
            Dict.member name model.chats
    in
        List.li
            [ Options.attribute <| onClick (ShowChannel name)
            ]
            [ List.content
                []
                [ text name ]
            ]

And this is...not that exciting I guess? It works though. We'll want to add some CSS to make sure that it's clear that you can click on a channel:

roomView : Model -> String -> Html Msg
roomView model name =
    let
        isListening =
            Dict.member name model.chats
    in
        List.li
            [ Options.attribute <| onClick (ShowChannel name)
            , Options.css "cursor" "pointer"
            ]
            [ List.content
                []
                [ text name ]
            ]

It would be nice if we could tell when a given channel was the active one. We'll first look at how we can change the text color of a list at all:

import Material.Color as Color
-- ...
roomView : Model -> String -> Html Msg
roomView model name =
    let
        isListening =
            Dict.member name model.chats
    in
        List.li
            [ Options.attribute <| onClick (ShowChannel name)
            , Options.css "cursor" "pointer"
            , Color.text Color.accent
            ]
            [ List.content
                []
                [ text name ]
            ]

If you refresh the page, our rooms are now all using the accent color. Let's add some rooms so that this is a bit more clear and a bit more of a list:

knownRooms : List String
knownRooms =
    [ "room:lobby"
    , "room:random"
    , "room:gifs"
    ]

So that works, but they shouldn't all be pink all the time - just the active one. We'll use when from Material.Options to make this easy:

import Material.Options as Options exposing (when)
-- ...
roomView : Model -> String -> Html Msg
roomView model name =
    let
        isListening =
            Dict.member name model.chats
    in
        List.li
            [ Options.attribute <| onClick (ShowChannel name)
            , Options.css "cursor" "pointer"
            , Color.text Color.accent `when` (model.currentChat == Just name)
            ]
            [ List.content
                []
                [ text name ]
            ]

That's pretty great. Let's add some icons to the channels just to give some visual interest.

import Material.Icon as Icon
-- ...

roomView : Model -> String -> Html Msg
roomView model name =
    let
        isListening =
            Dict.member name model.chats
    in
        List.li
            [ Options.attribute <| onClick (ShowChannel name)
            , Options.css "cursor" "pointer"
            , Color.text Color.accent `when` (model.currentChat == Just name)
            ]
            [ List.content
                []
                [ List.icon "label_outline" []
                , text name
                ]
            ]

Now, let's use the non-outlined version if we're connected to the given channel:

roomView : Model -> String -> Html Msg
roomView model name =
    let
        isListening =
            Dict.member name model.chats

        iconName =
            case isListening of
                True ->
                    "label"

                False ->
                    "label_outline"
    in
        List.li
            [ Options.attribute <| onClick (ShowChannel name)
            , Options.css "cursor" "pointer"
            , Color.text Color.accent `when` (model.currentChat == Just name)
            ]
            [ List.content
                []
                [ List.icon iconName []
                , text name
                ]
            ]

Alright, now let's give the same treatment to the users, but use an avatar for them. Since we don't have any avatars, we'll use the adorable avatars service to provide them.

rosterView : Model -> Html Msg
rosterView model =
    List.ul
        []
        (List.map
            (userView model)
            model.users
        )


userView : Model -> User -> Html Msg
userView model user =
    let
        chatChannel =
            twoWayChatChannelFor model.username user.name

        isListening =
            Dict.member chatChannel model.chats
    in
        List.li
            [ Options.attribute <| onClick (ChatWithUser user)
            , Options.css "cursor" "pointer"
            , Color.text Color.accent `when` (model.currentChat == Just chatChannel)
            ]
            [ List.content
                []
                [ List.avatarImage ("https://api.adorable.io/avatars/285/" ++ user.name ++ ".png") []
                , text user.name
                ]
            ]

If we refresh and click around a bit, this is looking a lot nicer. So that covers it for lists. Let's move on to cards:

Cards

Cards are a means of grouping related information together. We're going to partially abuse them for now, just to see their API, by making our main chat interface into a card.

-- We don't need to wrap this in a div...
chatView : ( String, Chat.Model ) -> Html Msg
chatView ( channelName, chatModel ) =
    App.map (ChatMsg channelName) (Chat.view chatModel)
vim src/Chat.elm
import Material.Card as Card
import Material.Options as Options
import Material.Elevation as Elevation
-- ...
view : Model -> Html Msg
view model =
    Card.view
        [ Options.css "width" "50%"
        , Elevation.e2
        ]
        [ Card.title
            [ Color.background Color.primary
            , Color.text Color.white
            ]
            [ Card.head [] [ text model.topic ] ]
        , Card.text
            []
            [ messageListView model
            , messageInputView model
            ]
        ]

That gives us a quick overview of how Material Cards work. Looking alright with not a lot of effort!

Summary

There's a lot more to explore with both Cards and Lists, but we got a pretty good bit of ground covered today. In tomorrow's exercise you'll continue using elm-mdl to make the chat interface look tons nicer. I hope you enjoyed it. See you soon!

Resources