elm christmas

Javascript interop

←Previous postNext post →

Talking to the world outside Elm

A 5 min read written by
Harald Ringvold

Elm is awesome in many ways but sometimes we need to run some javacript. Maybe your company has some shared code that you wish to use or maybe you need to call some of the many Web APIs, most of them not supported natively in Elm.

Today we are going to look at how we communicate with javascript in Elm with flags and ports.

Flags

Let start at the beginning: getting data at startup. Sometimes we might need to send som data to our Elm application to be available at startup. There might be many reasons for this. Passing the chosen locale or data calculated with javascript that need to be available when the Elm app starts. This can be done with flags.

First lets look at how we initiate an Elm app:

<div id="elm"></div>
<script>
  var app = Elm.Main.init({
    node: document.getElementById('elm')
  });
</script>

Assuming the compiled javascript for our Elm app has been loaded, this will start the app and the output inserted in the element with id "elm".

The node property is not the only one available. We can also use the flags property to pass in the needed data:

var app = Elm.Main.init({
  node: document.getElementById('elm'),
  flags: 'String to pass in'
});

On the Elm side we can use for example Browser.element to enable us to receive the flags through the init function.

main : Program String Model Msg
main =
    Browser.element
        { init = init
        , view = view
        , update = update
        , subscriptions = always Sub.none
        }

init : String -> (Model, Cmd Msg)
init flag =
    ({ count = 0, flag = flag}, Cmd.none)

-- rest of function definitions left out for brevity

The second element in the type definition for main is the type of the flag. The init function receives the flags and we can work with them as we please. Note that the type definition for the flag parameter must match the one defined for main.

But what happens if we do not pass a string? What if the value is null or some other value? Elm will try to convert the value passed through flags to the type defined in the init function, but if it fails we are in trouble. Elm will throw an error and the app will fail to start.

This is why it is recommended to define the flags as Json.Decode.Value and use a decoder to process it. This forces you to handle the failing cases and avoid full application crashes. See the official guide for more details on flags: https://guide.elm-lang.org/interop/flags.html

Check out this blogpost for an intro to JSON decoding: https://javascriptplayground.com/json-decoding-in-elm/

Ports

Flags is about getting data into the Elm app at start up. For the situations where we need to send and receive data from javascript we need to use ports.

Ports are a way to pass messages back and forth to javascript. The message can be simple values as String or Int or more complex structures as JSON. If it can be serialized as a string we can send it in ports. Usaually we pass JSON in the ports.

So how does it work? Lets start with a simple example where we send some data out for storage in the browser localstorage.

port module Main exposing (..)

port storeInCache : String -> Cmd msg

First we need to declare that this module defines ports. To create an outgoing port we write the type signature for a function that returns Cmd msg. Our port here takes a string to be stored in localstorage. Lets see how we can use this in javascript.

var app = Elm.Main.init({
  node: document.getElementById('elm')
});

app.ports.storeInCache.subscribe(function(data) {
  localStorage.setItem('cache', JSON.stringify(data));
});

The app value will have a ports object were we can find functions to interact with the ports. Here we are listening for messages on the outgoing storeInCache port and store the value in localstorage.

To receive data in the Elm app we need an incoming port. Let's add that to our example.

port Main exposing (..)

port storeInCache : String -> Cmd msg

port getFromCache : (String -> msg) -> Sub msg

Here we define a port named getFromCache. We will use this to listen for messages from javascript. The first parameter is a function that will be called by the Elm runtime when messages arrive from javascript. The function retruns a Sub msg and makes it possible for us to subscribe to the messages as they arrive.

A simple function call sends the data to Elm from javascript:

app.ports.getFromCache.send(localStorage.setItem('cache'));

Now let's tie things together:

main : Program String Model Msg
main =
    Browser.element
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }

type Msg
    = ReceviedFromCache String
    ...

subscriptions : model -> Sub msg
subscriptions model =
    getFromCache (\value -> ReceviedFromCache value)

In the subscriptions function we call the getFromCache and pass a function that will receive the value som javascript. To use it further in our app we will return a msg so we can store it to our model in the update function.

The examples shown here have been trucated to highlight the important parts. See the full example here: https://ellie-app.com/4d29TzvfK2Ra1

As with flags it is recommended to pass the data as JSON values so we can handle errors gracfully. See the official guide on ports for more details.

It is also recommended to have one outgoing and one incoming port, and not one for every task you need to do in javascript. Check Murphy Randles talk "The Importance of Ports" from Elm Conf 2017 on how you can do this.

←Previous postNext post →