UICards: live UI development in Elm

UICards are a tool for live UI development.

Crafting a UI can suck up a lot of time due to its stateful nature. We’re all familiar with this cycle: make some tweaks, reload the app, navigate to the relevant page, click around and type for a while to get the right state, only to see that the spacing in one of the dropdowns still doesn’t look quite right. Rinse and repeat.

UICards allow you to lay out pieces of UI on a single page, and to have these pieces actually work - responding to clicks, typing and so on.

Combined with live reloading (eg using elm-live), this provides an environment for interactive UI development.

Pieces of UI are organised into multiple cards, and cards can be organised into decks for convenience (mainly to avoid too much scrolling).

Cards are defined in an Elm file which looks something like this:

module Cards exposing (main)

import UICards exposing (card, cardError, deck, show)
import Main as App -- This is your application's main module

initialMenuModel =
    { page = MenuPage { isMenuOpen = False, ... } }

initialCardModel =
    { page = CardPage { cardTitle = "Test card", ... } }

main =
    show App.update
        [ deck "Menu elements"
            [ card "Menu button" initialMenuModel <|
                \model ->
                    case model.page of
                        MenuP page ->
                            App.menuButton page

                        _ ->
                            cardError "Invalid page in the model"

            , card "Menu panel" initialMenuModel <|
                \_ ->
                    App.menuPanel 
                        [ { link = "#settings", name = "Settings" }
                        , { link = "#logout", name = "Logout" } 
                        ]
            ]

        , deck "Card elements"
            [ card "Card" initialCardModel <|
                \model ->
                    case model.page of
                        CardP page ->
                            App.cardHtml page.cardTitle 

                        _ ->
                            cardError "Invalid page in the model"

            , card "Error test" initialCardModel <|
                \_ ->
                    cardError "This is a test"
            ]
        ]

The cards file can simply be compiled with elm make to produce an HTML or JS file.

For live interactive development, elm-live is the easiest option:

$ elm-live src/Cards.elm

The show function is given the update function from your application as its argument, and each card gets an initial model which allows you to render UI elements in the states you’re interested in.

Setup

The UICards package only generates the main function for you - it doesn’t generate any HTML/CSS/JavaScript directly, and it doesn’t run your code (at least for now).

There are a couple of options for using UICards:

  1. Set up a separate project for cards
  2. Keep cards together with the application

If you don’t already have UI code and you want to use UICards to experiment with UI components, or to flesh out the UI before starting on application code, then the first option is probably more convenient. Even if you have application code that you would like to use in your cards, you can still do it by setting up a reference to your application directory in source-directories in elm.json.

If you already have some application code, or you view UI cards as somewhat analogous to tests, or you just prefer to keep everything related to the application within a single project, you can go with the second option.

Prerequisites

Since UICards doesn’t run the code, you need to arrange your own way to run it with live reload (live reload is the whole premise really!). The easiest option for that is elm-live. elm-live automatically watches the source-directories specified in elm.json which gives you maximum flexibility for organising the application and UICards code.

Set up a separate project for cards

If you set up the cards as a separate Elm project, you will need to access the code from your application. You can do that by adding your application directory to the source-directories in elm.json.

Keep cards together with the application

You can also keep cards in a subdirectory together with your application code.

There is a wrinkle that needs to be ironed out in this scenario: your UI cards are effectively an application too, but you already have your actual application with its own main function. How to combine them?

The easiest way is to get elm make to generate JavaScript (instead of index.html), and to put a script into your index.html to load either cards or your application based on what’s compiled.

Assuming your cards main function is in cards/Cards.elm, and your application’s main is in src/Main.elm, you could run either of these commands to generate elm.js:

$ elm-live cards/Cards.elm -- --output elm.js
$ elm-live src/Main.elm -- --output elm.js

One more thing you need to do to make sure that elm-live picks up the changes either way is add both src and cards to source-directories in elm.json.

Now, assuming your application’s main module is called Main, and the cards module is called Cards, index.html can be set up like this, for example:

<!doctype html>
<html>
<head>
    <title>MyApp</title>
    <script src="elm.js"></script>
</head>

<body>
<div></div>
<script>
if (typeof Elm.Cards !== "undefined") {
    let app = Elm.Cards.init()
}
else {
    let app = Elm.Main.init({
        flags: {sessionId: localStorage.getItem("sessionId")}
    })
}
</script>
</body>
</html>

Note that I can have different initialisation code based on the detected Elm module name.

The only thing you need to do to switch between your application and UI cards is to restart elm-live with the appropriate Elm file argument.

Advanced: multiple cards modules

If you have a more complicated app structure where, for example, you’ve extracted individual pages into separate modules with their own model/view/update functions, you could create a separate cards module for each of the pages if it simplifies your card code.

For instance, suppose we have a module called Pages.Registration which has types and functions like this:

type Msg = ...  -- Page-specific messages

type PageMsg = ...  -- Messages returned to the main update 

type alias Model = { ... }  -- Page-specific state

init : Model 

update : Msg -> { a | serverUrl : String } -> Model -> ( Model, Cmd Msg, PageMsg )

page : Model -> Html Msg  -- View function

We could create a module called RegistrationCards in cards/RegistrationCards.elm which would look like this:

module RegistrationCards exposing (main)

import Pages.Registration
import UiCards exposing (..)


initialModel =
    Pages.Registration.init


update msg model =
    let
        ( newModel, cmd, _ ) =
            Pages.Registration.update msg { serverUrl = "https://example.com" } model
    in
    ( newModel, cmd )


main =
    show update
        [ deck "Registration"
            [ card "Registration page" initialModel <|
                \model ->
                    Pages.Registration.page model
            ]
        ]

The interesting thing here is that we can write all of the cards solely in terms of the Pages.Registration functions, without bringing in anything from Main. This makes card code simpler while still allowing us to test the UI and a lot of the interaction aspects of the registration page.

Note that the update function in Pages.Registration doesn’t have the required signature of Msg -> Model -> ( Model, Cmd Msg ) because it takes an extra argument and returns an extra element in the tuple. That’s why we wrap it to produce a conforming update function in RegistrationCards.

Would you like to dive further into Elm?
📢 My book
Practical Elm
skips the basics and gets straight into the nuts-and-bolts of building non-trivial apps.
🛠 Things like building out the UI, communicating with servers, parsing JSON, structuring the application as it grows, testing, and so on.
Practical Elm