Elm Everywhere

Elm is a nice language for front-end applications. It has some cool features like a near-complete lack of runtime exceptions, well-managed state, and user-friendly compiler messages. This tutorial is meant for people with a bit of experience in Elm, and presumes you know how to read Elm code and know the basics of the Elm Architecture.

In this tutorial, we’ll be using the Elm Native UI library to help us create one codebase that will build to Android, iOS, and Web apps. This will allow us to reuse large chunks of logic, but also lets us customize each app as we see fit.

Setup

There are detailed Elm Native UI setup instructions in the Elm Native UI repo, so I won’t repeat them here. Once you have the basic Counter app working, come back here!

We’ll edit package.json and replace the compilation commands (change Main.elm to Mobile.elm and Web.elm). This will allow us to compile both versions of the app with one npm run compile command.

"precompile": "rm -f elm-mobile.js elm-web.js",
"compile": "elm-make Mobile.elm --output elm-mobile.js &&
            elm-make Web.elm --output elm-web.js",

We’ll also change index.android.js and index.ios.js so that we require our new mobile build file in them with const Elm = require('./elm-mobile');.

Finally, we’ll make two more files: Mobile.elm and Web.elm. You can delete the old Main.elm.

-- Mobile.elm

import App exposing (..)
import NativeUi as Ui exposing (Node)
import NativeUi.Style as Style exposing (defaultTransform)
import NativeUi.Elements as Elements exposing (..)
import NativeUi.Events exposing (..)

main : Program Never Model Msg
main =
  Ui.program 
    { init = init
    , view = view
    , update = update 
    , subscriptions = \model -> Sub.none
    }

view : Model -> Node Msg
view model = text [] [ Ui.string "Mobile view" ]
-- Web.elm

import App exposing (..)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)

main : Program Never Model Msg
main =
  Html.program 
    { init = init
    , view = view
    , update = update 
    , subscriptions = \model -> Sub.none
    }

view : Model -> Html Msg
view model = text "Web view"

As you can see, these files have a lot in common. The main functions in each will serve as the entry point for our Mobile and Web versions of the app. Each main initializes a program, whether a Ui.program or an Html.program. These are passed our init, update, and subscriptions (which we’ll add to App.elm in a moment). Each module here also has a separate view function, with similar types. Just swap Ui’s Node for Html! We’ll extend these view functions later, hence the various imports.

Application Logic

Setup complete! Let’s get started on making something useful.

Let’s create a new file: App.elm, which will contain our “business logic”. That logic consists of maintaining a list of people, and viewing them as sorted by either their names or their ages. There’s a lot going on here, so I’ll explain it in comments.

-- App.elm

module App exposing (..)

import List exposing (sortBy)

-- Our model is a list of individuals and a sorting order
type alias Model =
  { people : List Person
  , order : Order
  }

-- Each Person is very simple - just a name and age
type alias Person =
  { age : Int
  , name : String
  }

-- You can fill in whatever people you want here
init =
  { people = 
    [ { name = "Methusaleh", age = 969  } 
    , { name = "Hal", age = 17  }
    , { name = "Dante", age = 35  }
    ]
  , order = ByName
  }
  -- The operator (!) is an easy way to call Cmd.batch
  ! []

-- We can sort either by name or age
type Order =
    ByName
  | ByAge

-- The only Msg our app has right now provides a new
-- sorting order
type Msg =
  ChangeSort Order

update msg model =
  -- case isn't necessary with only one msg,
  -- but it's easier to extend this way
  case msg of
    ChangeSort order ->
      -- Function that will help us order our List of Persons
      let sortFunction =
        case order of
          -- You can guess what these do!
          ByName -> sortBy .name
          ByAge -> sortBy .age
      in
        -- Our update returns a newly sorted list of people,
        -- as well as maintains the order they were sorted by.
        -- Note that you don't actually need both people and
        -- order in the model. You could sort in view code
        -- based on just the order or maintain a list of people
        -- in the order you need, storing just the list
        { model 
        | people = sortFunction model.people
        , order = order
        }
        ! [] -- That Cmd.batch thing again

Making the Views

Try compiling with npm run compile and running both the web and mobile versions of the app. While there’s plenty of logic going on, users can’t see any of it until we make better views! Let’s go back to Mobile.elm and Web.elm.

-- Mobile.elm

view : Model -> Node Msg
view model =
  Elements.view [] <| 
  [ Elements.view [] 
    [ text [] [ Ui.string "People" ]
  ] 
  , text
    [ onPress (ChangeSort ByName) ]
    [ Ui.string "sort by name" ]
  , text
    [ onPress (ChangeSort ByAge) ]
    [ Ui.string "sort by age" ]
  ] 
  ++ List.map viewPerson model.people

viewPerson : Person -> Node Msg
viewPerson person =
  Elements.view []
    [ text [] [ Ui.string person.name ] 
    , text [] [ Ui.string <| toString person.age ]
    ]
--Web.elm

view : Model -> Html Msg
view model =
  div [] <| 
    [ h1 [] [ text "People" ]
    -- Buttons for changing the sorting order
    , button 
      [ onClick (ChangeSort ByName) ]
      [ text "sort by name" ]
    , button
      [ onClick (ChangeSort ByAge) ]
      [ text "sort by age" ]
    ]
    ++ List.map viewPerson model.people

-- Displays a single person with name and age
viewPerson : Person -> Html Msg
viewPerson person =
  div []
    [ h2 [] [ text person.name ] 
    , p [] [ text (toString person.age) ]
    ]

Of course, in a real app you would also have to manage styling for each view, whether with external stylesheets, inline bits of CSS, or with typesafe CSS provided by an Elm library.

Conclusion

As you can see, most types, program logic, updates, etc. can be written once in App.elm and used in both the mobile and web views. This permits a good chunk of code to be reused across several platforms, easing maintainability and increasing speed of adding new features. However, there is still some repetitive boilerplate (especially in view functions) which could be reduced by further refactoring. My own larger apps have chunks of view code shared across platforms that are included from both Mobile.elm and Web.elm.


❮ The Siren Song of Meta-Altruism
Effects of going one level too deep
Hidden Recursion ❯
Definitional flavors of the gamma function