Marrying Elm - a self-proclaimed delightful language for creating reliable web-apps - to Cordova, a platform for developing mobile apps in HTML, CSS and JavaScript? Sure, let's do it!

First things first, you'll need to get yourself setup with the basic requirements. Setting up Cordova through its command-line interface is a fairly simple process, and following the official guide should get your pretty far along the way. For the rest of this post, I'll assume you've followed those instructions and have at least basic familiarity with Elm. If you need to get up to speed, do read the Elm guide and don't hesitate joining the wonderfully helpful Elm community on Slack!

Let's dive right in, and set ourselves up with a friendly "Hello world" rendered by Elm. Rather than writing our Elm code in the www directory, however, we'll create a new directory src to hold only our Elm code and nothing else. The www directory is used in the final artifact, so we don't really want it to contain code that still needs to be compiled. Instead, we create this new src directory and have it hold a brand new Main.elm

module Main exposing (..)

import Html exposing (Html, text)

main : Html msg
main =
    text "Hello from Elm!"

Cool, now we have some Elm. We need some plumbing to connect all the pieces, though. Let's go bottom up; and use elm-make src/Main.elm --output www/js/elm.js so our Elm code can be compiled down to JS and placed somewhere in the www directory. Since we don't like comitting compiled artifacts, this is also a good time to add www/js/elm.js as well as elm-stuff to your .gitignore. Since we'll likely want to compile this fairly often during development, we can set up an npm script as a shortcut.

While we're at it, let's make ourselves a little pipeline. First, install some dependencies:

$ npm i --save-dev chokidar-cli concurrently

With those dependencies in place, we can add some scripts to the scripts entry in package.json.

We'll split up the process in 3 discrete steps: 

  1. Take care of getting Elm compiled when files change (build-elm and watch-elm)
  2. Take care of flushing cordova's caches when www/ changes (watch-www)
  3. Ensure the browser platform is running (serve)

Finally, we bundle up those 3 steps into a single script that runs those scripts concurrently.

    // ...
    "scripts": {
        "build-elm": "elm-make --yes src/Main.elm --output www/js/elm.js",
        "watch-elm": "chokidar 'src/**/*.*' -c 'npm run build-elm'",
        "serve": "cordova run browser",
        "watch-www": "chokidar 'www/**/*.*' -c 'cordova prepare browser'",
        "watch": "npm run build-elm && concurrently --kill-others-on-fail \"npm run watch-elm\" \"npm run watch-www\" \"npm run serve\""
    // ...

We're getting close, now. Let's clean up a little and ensure our Elm code is actually loaded, before hooking it up to Cordova.

Remove the contents of the .app div in your index.html, so it's just an empty div with a class. This will allow us to embed the Elm app inside that div. Since we also need to load our generated JavaScript, add an extra script tag, before js/index.js is loaded. As a result, the body should look a little like this.

    <div class="app"></div>
    <script type="text/javascript" src="cordova.js"></script>
    <script type="text/javascript" src="js/elm.js"></script>
    <script type="text/javascript" src="js/index.js"></script>

With all the mumbo jumbo out of the way, we can finally go edit our www/js/index.js file and add instructions to embed our Elm app.

onDeviceReady: function() {
  var elmApp = Elm.Main.embed(document.querySelector(".app");

  // You can do the regular plumbing for subscribing to ports, here.

Now, run npm run watch, wait a moment for everything to start compiling, and watch your browser pop open and present you...

Now that we have our run-of-the-mill hello world setup, let's look at adding a little interactivity. Let's add a counter for how many times the user has tapped our hello message. Keyword: tapped. Since we're on mobile, we need to deal with touch events rather than the regular click events.

The core html library does not support touch events out of the box. Luckily, there are several packages which add varying levels of support for touch events. Let's randomly pick the one I wrote, and add that to our project using elm-package install zwilias/elm-touch-events. Of course, we'll also need to add it to our imports in order to actually use it.

module Main exposing (..)

import Html exposing (Html, div, text)
import Touch

Our counter has very simple state: an integer. It's not really worth defining a record to hold that, and a little misleading to define a type alias for an integer, so let's skip the model setup, and go straight to defining the messages and our update function. We'll only need a single message, and since there's only one, our update function doesn't even really need to look at it. Let's be type-safe, though, and check it anyway.

type Msg
    = Tapped

update : Msg -> Int -> Int
update Tapped count =
    count + 1

Now, our view function will need to actually trigger the Tapped message when the "Hello from Elm!" message is tapped. We can make our lives a little simpler and ignore swipes, multitouch and touch-and-hold, and simply say that when a touchEnd event is triggered on the text, we'll register that as a tap, ignoring the event-specific state we're provided with.

view : Int -> Html Msg
view count =
    div []
        [ div [ Touch.onEnd (\event -> Tapped) ]
            [ text "Hello from Elm!" ]
        , text <| "Tapcount: " ++ toString count

Finally, let's bundle it all up in a Html.beginnerProgram, and initialize our model to 0.

main : Program Never Int Msg
main =
        { model = 0
        , update = update
        , view = view

We like using Fastlane as a build/CI tool. As luck would have it, pairing that up with Elm and Cordova is fairly easy. Since we prefer building alpha and beta builds and distributing those through hockeyapp before pushing a version to the appstore, we use a "pre-build" shell-script that takes care of 2 major things:

  • Materialize a config.xml file based on a template and some parameters. Some parameters like changing the app-id require making changes to config.xml, and (re-)adding the platforms only after that.
  • Running npm install and npm run build-elm to make sure our www directory has our compiled Elm code before the actual mobile apps are built from this.

In order to kick off cordova's build process, we use the fastlane-plugin-cordova gem which can handle both iOS and Android builds.

An example Fastlane setup is included in the Github repository. Note that the Fastlane tasks are intended to be used as part of your continuous integration setup: files are modified and not cleaned up. Take care not to commit those changes to your VCS.

Looking at the bigger picture, using a compile-to-JS language on top of Cordova is a reasonably simple process. It involves some plumbing to get everything neatly connected, but once that plumbing is in place, it's smooth sailing. Using cordova plugins is also a possibility, and most of these expose API's that are a pretty good fit for communicating through ports. Finally, setting up CI leveraging Fastlane shows that Elm doesn't inherently add complexity, other than making sure we actually compile our Elm code before starting the cordova build.

If you're already interested in developing a mobile app using Cordova and wish to use Elm, you now know you definitely can.


A complete repository with the code from this article, plus all the scripts and fastlane setup discussed is available here:

Written by

Ilias Van Peer

Developer & Elm aficionado

Follow @zwilias