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.
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 (..)
: Program Never Model Msg
main =
main
Ui.program init = init
{ = view
, view = update
, update = \model -> Sub.none
, subscriptions
}
: Model -> Node Msg
view = text [] [ Ui.string "Mobile view" ] view model
-- Web.elm
import App exposing (..)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
: Program Never Model Msg
main =
main
Html.program init = init
{ = view
, view = update
, update = \model -> Sub.none
, subscriptions
}
: Model -> Html Msg
view = text "Web view" view model
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.
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 =
: List Person
{ people : Order
, order
}
-- Each Person is very simple - just a name and age
type alias Person =
: Int
{ age : String
, name
}
-- You can fill in whatever people you want here
init =
=
{ people = "Methusaleh", age = 969 }
[ { name = "Hal", age = 17 }
, { name = "Dante", age = 35 }
, { name
]= ByName
, order
}-- 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
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
: Model -> Node Msg
view =
view model <|
Elements.view []
[ Elements.view [] "People" ]
[ text [] [ Ui.string
]
, textChangeSort ByName) ]
[ onPress ("sort by name" ]
[ Ui.string
, textChangeSort ByAge) ]
[ onPress ("sort by age" ]
[ Ui.string
] ++ List.map viewPerson model.people
: Person -> Node Msg
viewPerson =
viewPerson person
Elements.view [].name ]
[ text [] [ Ui.string person<| toString person.age ]
, text [] [ Ui.string ]
--Web.elm
: Model -> Html Msg
view =
view model div [] <|
"People" ]
[ h1 [] [ text -- Buttons for changing the sorting order
, button ChangeSort ByName) ]
[ onClick ("sort by name" ]
[ text
, buttonChangeSort ByAge) ]
[ onClick ("sort by age" ]
[ text
]++ List.map viewPerson model.people
-- Displays a single person with name and age
: Person -> Html Msg
viewPerson =
viewPerson person div []
.name ]
[ h2 [] [ text person.age) ]
, p [] [ text (toString person ]
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.
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
.