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.
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:
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.
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.
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
.
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.
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
.