Zero-maintenance Always-up-to-date Living Style Guide in Elm!

New Demo

Demo Code

Old Code

It would require zero maintenance and the Style Guide would always been updated.

Along the line of a previous article about “Living Website Style Guide and Documentation in Elm” , let’s see a possible implementation.

The component

As component here we mean just a stateless function that accept several parameters (including a Type) and return a value.

Don’t confuse the type Type with the Elm keyword type.

Example: Button Component

Let’s, for example, build a button component.

The button can have 6 possible types of configurations:

type Type
= Small
| Small_Important
| Large
| Large_Important
| Large_With_Spinner
| Large_Important_With_Spinner

We are using Snake_Case names. In Elm is normal practice to use CamelCase but Snake_Case is easier to read so I am going to use it here. Either way, the compiler is always there to support us if we misspell something.

Let’s see how the component could look like now:

component : List (Html.Attribute msg) -> String -> Type -> Html.Html 
msg
component msgs string type_ =
let
{ size, bgColor, spinner } =
case type_ of
Small ->
{ size = SmallSize
, bgColor = Regular
, spinner = False
}
Small_Important ->
{ size = SmallSize
, bgColor = Important
, spinner = False
}
Large ->
{ size = LargeSize
, bgColor = Regular
, spinner = False
}
Large_Important ->
{ size = LargeSize
, bgColor = Important
, spinner = False
}
Large_With_Spinner ->
{ size = LargeSize
, bgColor = Regular
, spinner = True
}
Large_Important_With_Spinner ->
{ size = LargeSize
, bgColor = Important
, spinner = True
}
textColor =
case bgColor of
Regular ->
TextOnRegular
_ ->
TextOnImportant
in
Html.button
([ Html.Attributes.style
[ ( "background-color", colorToString bgColor )
, ( "height", sizeToString size )
, ( "color", colorToString textColor )
, ( "border-radius", "10px" )
, ( "padding", "0 60px" )
, ( "position", "relative" )
, ( "transition", "all .3s" )
, if spinner then
( "padding", "0 80px 0 40px" )
else
( "padding", "0 60px" )
]
]
++ msgs
)
[ Html.text string
, if spinner then
Html.div
[ Html.Attributes.style
[ ( "box-sizing", "border-box" )
, ( "position", "absolute" )
, ( "top", "50%" )
, ( "right", "24px" )
, ( "width", "20px" )
, ( "height", "20px" )
, ( "margin-top", "-10px" )
, ( "margin-left", "-10px" )
, ( "border-radius", "50%" )
, ( "border", "2px solid transparent" )
, ( "border-top-color",colorToString textColor )
, ( "animation", "spinner .6s linear infinite" )
]
]
[ Html.text "" ]
else
Html.text ""
]

So here we generate html with some inline styling based on the Type. It is a bit verbose but simple to understand and maintain.

colorToString : Color -> String
colorToString color =
case color of
Regular ->
"white"
Important ->
Components.Color.component Components.Color.Elm_Orange
TextOnRegular ->
Components.Color.component Components.Color.Font_Color
TextOnImportant ->
"white"
sizeToString : Size -> String
sizeToString size =
case size of
SmallSize ->
"32px"
LargeSize ->
"64px"

colorToString is using another component that return colors. Everything here is base on types also if the types Color and Size are not exposed externally.

Going back to our component function, we could also have played with classes instead using inline styling. One thing that is left in Sass are the keyframes:

@keyframes spinner
to
transform: rotate(360deg)

The component is done, we can use like this:

import Components.ButtonComponents.Button.component 
[ onClick <| FetchApiData "https://httpbin.org/delay/1" ]
"My IP is..."
Components.Button.Large_Important

How can we now generate the Guide Line about this component?

Introspection

Let’s create a Introspection.elm file and define inside anIntrospection type like this:

type alias Introspection msg a =
{ name : String
, signature : String
, description : String
, usage : String
, usageResult : Html.Html msg
, example : a -> Html msg
, types : List a
}

This is what we will use to look inside the component.

Some of the String fields are duplication of the actual code. Let’s see how the button component exposes its internal data:

import Introspectionintrospection : Introspection.Introspection msg Type
introspection =
{ name = "Buttons"
, signature = "List (Html.Attribute msg) -> String -> Type ->
Html.Html msg"
, description = "Button accept a type, an Html.Attribute msg
that can be attribute that return a messages, such as onClick,
and a string that is used inside the button."
, usage = """[] "I am a button" Large"""
, usageResult = component [] "I am a button" Large
, types = [ Small, Small_Important, Large, Large_Important,
Large_With_Spinner, Large_Important_With_Spinner ]
, example = component [] "I'm a button"
}

There is a bit of manual work here. For example, when a type is removed or added to the list of types, the modification need to be repeated inside introspection, so the system is not completely DRY but it allows us to automatically generate the style guide that I think is a positive trade off.

Now we can let the Button introspection going through the Introspection.view with

Introspection.view Components.Button.introspection

This is the result:

Button Style Guide

This is all. We have now our up to date Style Guide coming directly from the code.

Making impossible states impossible

Related to making impossible states impossible, what I like of this approach is that is easy to enforce certain patterns. For example in this case spinners can be used only in the large buttons.

Using classes to modify object may lead to unwanted results:

<button class="fs-button-small fs-button-with-spinner">Loading</button>

In this case a spinner is added to a small button, a pattern that was not considered as appropriate by designers.

In the case of Buttons, the component is completely defined by the type, no other parameters.

Other examples: Logo and Colors

But there could be other component that require more flexibility and so they require extra parameters. For example I left the logo size separated from the type. The type signature of this component is:

component : Size -> Type -> Html.Html msg

Where the Size is an Integer.

In this case the introspection will be

introspection : Introspection.Introspection msg Type
introspection =
{ name = "Logo Elm"
, signature = "Type -> Size -> Html.Html msg"
, description = ""
, usage = "128 (Color Orange)"
, usageResult = component 128 (Color Orange)
, types = [ Colorful, Color Orange, Color Green, Color
Light_Blue, Color Blue, Color White, Color Black ]
, example = component 64
}

This is the result:

Logo Style Guide

I treated the same as component also stuff that is not generating any html. For example Color, that we just encountered before talking about Buttons, is just returning the colors used in the website. The component return a String that contain the color. This is the introspection:

introspection : Introspection.Introspection msg Type
introspection =
let
example type_ =
Html.div
[ Html.Attributes.style
[ ( "width", "120px" )
, ( "height", "40px" )
, ( "background-color", component type_ )
, ( "color", "white" )
, ( "padding", "10px" )
, ( "text-align", "left" )
, ( "font-family", "monospace" )
, ( "font-size", "18px" )
]
]
[ Html.text <| component type_ ]
in
{ name = "Colors"
, signature = "Type -> String"
, description = "List of colors used in the app."
, usage = "Elm_Orange"
, usageResult = example Elm_Orange
, types = [ Elm_Orange, Light_Orange, Font_Color ]
, example = example
}

In this case I wrapped the returned value in an html structure for

  • consistentancy with the other introspections that returns html
  • and to make the Style Guide look nicer

This is the result

Colors Style Guide

You can see a live implementation of this idea inside the Elm Spa Boilerplate at http://elm-spa-boilerplate.surge.sh/styleguide (new version at https://elm-spa-boilerplate.guupa.com/framework)

The code is at https://github.com/lucamug/elm-spa-boilerplate

Thank you for reading.

Editing

I eventually published this as a package: http://package.elm-lang.org/packages/lucamug/elm-styleguide-generator/latest

I also published an example of Framework that is compatible with the Style Guide Generator: http://package.elm-lang.org/packages/lucamug/elm-style-framework/latest

This is the result of the two combined together: https://elm-spa-boilerplate.guupa.com/framework

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store