Making the Users Show View Nicer
Adding a much more full-featured resource show view.
Tagged with elm
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.
We're working on the time-tracker project, tagged with
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.
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.
We'll start out just roughing out the view to have a series of functions that outline the general structure of the page.
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" ]
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" "email@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
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.
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
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.
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!
knewter/time-trackerbefore this episode.
knewter/time-trackerafter this episode.
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.