This week we're going to focus on making our resource show views a lot more pleasant. We've got a mockup showing what we're hoping to accomplish as well

Let's talk through how we'll plan to implement it, and get to writing some code.

NOTE: This is a lengthy video, but you can probably get through the text version on your own much faster. I spent a decent bit of time talking about things like reasons behind particular CSS rules, etc.

Project

We're working on the time-tracker project, tagged with before_episode_029.2.

We'll begin with a look at the mockup. We can talk about the general areas that are present in this view and plan out the implementation.

Talking through our plan

First, we have a "Details Card" at the top, showing our avatar and name, some summary information, and navigation tabs to drill into more detailed data for the user.

Then, in this mockup we're showing the contents of the "Information" tab. It contains a few segmented bits of details for the user. Let's call these "Info Panels". An Info Panel consists of an icon, a title, and some HTML content. That content is in a 2-column grid here, but it's possible we'll eventually have a variation that doesn't follow that requirement so let's just allow free-form HTML inside of them going forward.

Roughing out the structure

We'll start out just roughing out the view to have a series of functions that outline the general structure of the page.

vim src/View/Users/Show.elm
module View.Users.Show exposing (view, header)
-- ...
import Html.Attributes exposing (href, style)
-- ...
view : Model -> Int -> Html Msg
view model id =
    case model.usersModel.shownUser of
        Nothing ->
            text "No user here, sorry bud."

        Just user ->
-- We'll add a showUser function that handles our skeleton
            showUser model user


showUser : Model -> User -> Html Msg
showUser model user =
-- We'll add separate functions for the details card and the view that
-- represents the information tab being selected
    div [ style [ ( "width", "80%" ), ( "margin", "0 auto" ) ] ]
        [ detailsCard model user
        , information model user
        ]


-- Our details card will eventually be an mdl card, but for now it's just a
-- placeholder.
detailsCard : Model -> User -> Html Msg
detailsCard model user =
    div [] [ text "details" ]


-- We'll add our information function, which will show each of our panels
information : Model -> User -> Html Msg
information model user =
    div []
        [ generalInfo model user
        , paymentInfo model user
        , jobInfo model user
        ]


-- And each panel is just a placeholder for now.
generalInfo : Model -> User -> Html Msg
generalInfo model user =
    div [] [ text "general info" ]


paymentInfo : Model -> User -> Html Msg
paymentInfo model user =
    div [] [ text "payment info" ]


jobInfo : Model -> User -> Html Msg
jobInfo model user =
    div [] [ text "job info" ]

Implementing the Details Card

Next, we'll implement a details card with embedded tabs for navigation.

module View.Users.Show exposing (view, header)
-- ...
import Html exposing (Html, text, h2, div, a, span)
import Html.Attributes exposing (href, style, src)
import Material.Card as Card
import Material.Elevation as Elevation
import Material.Tabs as Tabs
import Material.Options as Options
import Material.Icon as Icon
import Material.Color as Color
-- ...
detailsCard : Model -> User -> Html Msg
detailsCard model user =
    Card.view
        [ Elevation.e2
        , Options.css "width" "100%"
        ]
        [ Card.title
            []
            [ Card.head [] [ text user.name ]
            , Card.subhead [] [ text "IT Staff" ]
            ]
        , Card.actions []
            [ Tabs.render Mdl
                [ 10, 0 ]
                model.mdl
                [ Tabs.ripple
                , Tabs.activeTab 3
                , Options.css "cursor" "pointer"
                ]
                [ Tabs.textLabel [] "TIMELINE"
                , Tabs.textLabel [] "CONNECTIONS"
                , Tabs.textLabel [] "PROJECTS"
                , Tabs.textLabel [] "INFORMATION"
                ]
                []
            ]
        ]

This gives us a decent first-pass approximation of the details card. It's obviously missing the avatar image, secondary details, and the background picture. We'll add the secondary details - we'll call these the stats - and some helper functions to reduce boilerplate:

-- This is a helper function for inline-aligned icon and text, like we have for
-- each stat
iconText : String -> String -> Html Msg
iconText icon content =
    div []
        [ Icon.view
            icon
            [ Options.css "vertical-align" "middle", Options.css "margin-right" "0.25em" ]
        , span
            [ style [ ( "vertical-align", "middle" ) ] ]
            [ text content ]
        ]

detailsCard : Model -> User -> Html Msg
detailsCard model user =
    let
-- Our stats will be centered and spread out nicely with flexbox.
        stats : List (Html Msg) -> Html Msg
        stats =
            div [ style [ ( "display", "flex" ), ( "flex-direction", "row" ), ( "justify-content", "space-around" ) ] ]
    in
        Card.view
            [ Elevation.e2
            , Options.css "width" "100%"
            ]
            [ Card.title
                []
                [ Card.head [] [ text user.name ]
                , Card.subhead [] [ text "IT Staff" ]
                ]
-- By default the secondary text has width of 90%, but we want it to fill the
-- whole card so we'll use CSS calc to do that.
            , Card.text [ Options.css "width" "calc(100% - 32px)" ]
-- Then we'll use our stats helper functions
                [ stats
                    [ iconText "email" "user@example.com"
                    , iconText "history" "3h 28m"
                    , iconText "access_time" "57h 12m"
                    , iconText "assignment_turned_in" "Projects: 20"
                    , iconText "assessment" "Open Tasks: 8"
                    ]
                ]
            , Card.actions []
                [ Tabs.render Mdl
                    [ 10, 0 ]
                    model.mdl
                    [ Tabs.ripple
                    , Tabs.activeTab 3
                    , Options.css "cursor" "pointer"
                    ]
                    [ Tabs.textLabel [] "TIMELINE"
                    , Tabs.textLabel [] "CONNECTIONS"
                    , Tabs.textLabel [] "PROJECTS"
                    , Tabs.textLabel [] "INFORMATION"
                    ]
                    []
                ]
            ]

Alright, so that's a little bit nicer. We'd really like to justify the tabs a bit better but I don't see where we have access to add CSS to the tabbar that holds them so we'll ignore that for now.

Next, let's make the header portion of the card have a black background since I don't have an image handy to use as the background, change the text to white, and center it all:

        Card.view
            [ Elevation.e2
            , Options.css "width" "100%"
            ]
            [ Card.title
                -- OK This isn't *exactly* black but it's better
                [ Options.css "background-color" "#202736"
                , Options.css "align-items" "center"
                ]
                -- We can't use the Card.head function here because we can't
                -- successfully set the `align-self` property to center in this
                -- release of elm-mdl, sadly - I'd expect not to need this hack
                -- in the future.
                [ Options.styled Html.h1
                    [ Options.cs "mdl-card__title-text"
                    , Options.css "align-self" "center"
                    , Options.css "color" "#ffffff"
                    ]
                    [ text user.name ]
                , Card.subhead [ Options.css "color" "#ffffff" ] [ text "IT Staff" ]
                ]
                -- ...

So that's looking kind of close. Next, let's add an avatar image, centered and poking out of the card a bit:

detailsCard : Model -> User -> Html Msg
detailsCard model user =
    let
        -- ...
        avatarUrl : String
        avatarUrl =
            "https://api.adorable.io/avatars/100/" ++ user.name ++ ".png"
    in
        Card.view
            [ -- ...
            , Options.css "overflow" "visible"
            , Options.css "margin-top" "66px"
            ]
            [ Card.title
                [ -- ...
                ]
                [ Options.img
                    [ Elevation.e4
                    , Options.css "border-radius" "50%"
                    , Options.css "position" "relative"
                    , Options.css "top" "-66px"
                    , Options.css "margin-bottom" "-33px"
                    ]
                    [ src avatarUrl ]
                -- ...

Now we've got a nice avatar image. We'd also like to remove that space below the tabs as it looks a bit quirky. It's caused by padding in the actions section of the card so we can override its padding to solve this:

            , Card.actions
                [ Options.css "padding" "0" ]
                [ -- ...
                ]

With that, the details card is in pretty good shape. Let's move our focus onto creating the Info Panels now.

Info Panels

We'll start by defining an infoPanel function that takes an icon string, a title string, and a list of content, and use that in each of our individual info functions:

infoPanel : String -> String -> List (Html Msg) -> Html Msg
infoPanel icon title content =
    Card.view
        [ Elevation.e2
        , Options.css "width" "100%"
        , Options.css "margin-top" "2em"
        ]
        [ Card.title
            [ Color.background Color.primary
            , Color.text Color.white
            ]
            [ iconText icon title
            ]
        , Card.text [] content
        ]


generalInfo : Model -> User -> Html Msg
generalInfo model user =
    infoPanel "info"
        "General Information"
        []


paymentInfo : Model -> User -> Html Msg
paymentInfo model user =
    infoPanel "credit_card"
        "Payment Information"
        []


jobInfo : Model -> User -> Html Msg
jobInfo model user =
    infoPanel "work"
        "Job Information"
        []

That's looking pretty nice already. Next, we'd like to introduce a helper function for each of the interior bits of info. We'll just call this function info and provide it with an icon name, a title, and a list of content as well, just like our infoPanel takes:

info : String -> String -> List (Html Msg) -> Html Msg
info icon title content =
    div
        [ style [ ( "padding", "1em" ) ] ]
        [ span
            [ style [ ( "font-weight", "600" ) ] ]
            [ iconText icon title ]
        , div
            [ style [ ( "margin-left", "2em" ) ] ]
            content
        ]


generalInfo : Model -> User -> Html Msg
generalInfo model user =
    infoPanel "info"
        "General Information"
        [ info "date_range"
            "Date of Admission"
            [ text "March 24th, 2016" ]
        , info "person"
            "Full Name"
            [ text user.name ]
        , info "person_outline"
            "Username"
            [ text "Kyle_89" ]
        ]

I won't bother with filling all of this out here or handling the two columns.

Summary

This is a pretty good start and it didn't take us very long at all. Tomorrow we'll look at adding some of the other tabs, and I'll eventually wire this all up to our backend. I also anticipate pulling these helpers out of this view function in relatively short order as they look like patterns we should reuse in various places in our app. See you soon!

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 Making the Users Show View Nicer

You May Also Like