Would not be nice if a website could produce its own Living Style Guide?
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:
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:
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
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