---
title: Bridging Elm and JavaScript with Ports
teaser: 'Elm is strongly-typed; JavaScript is not. Communication between the two is
  paramount for large applications and ports enable this.

  '
tags: elm,javascript,web
author: Josh Clayton
published_on: 2017-10-25
---

[Elm] is a programming language for building client-side web applications that
compiles to JavaScript. Because it's strongly-typed and pure, Elm provides a
handy mechanism for interacting with the "unsafe world" that is JavaScript to
help maintain boundaries between the two.

That mechanism is ports.

[Elm]: http://elm-lang.org/

## Why Ports?

Because Elm relies on an explicit understanding of the data it describes via
the type system, it needs to make guarantees when interacting with JavaScript
so developers can trust the compiler to verify accuracy in the program. Elm
touts "no runtime exceptions", and while I've personally found ways to break
that (hint: [don't attempt to convert user input into a regular expression][break]),
I trust the compiler to help spot issues.

[break]: https://medium.com/@eeue56/top-6-ways-to-make-your-elm-app-crash-at-runtime-562b2fa92d70

Because of these guarantees, ports are necessary to guard what flows into the
Elm application from JavaScript, and allows developers to format data from Elm
flowing outward to JavaScript. Ports act as a bridge between two different
worlds, and introduce confidence that communication between the two languages
is occurring correctly.

### Limited Interface

Ports allow for basic primitives to flow to and from JavaScript and Elm,
[including JavaScript objects, arrays, boolean values, numbers, and
strings][border-protection]. Functions cannot be passed directly, even though
they're first-class objects in JavaScript. Similarly, functions cannot be
passed from Elm to JavaScript to be called afterward.

[border-protection]: https://guide.elm-lang.org/interop/javascript.html#customs-and-border-protection

Because of this limited interface, it's arguably easier to ensure both Elm and
JavaScript are able to communicate; there's a much smaller language with which
they can speak to each other.

### Forced Constraints and Forced Failure

While I've written at length about [Elm decoders], their purpose is to parse
JSON structures into Elm-specific data structures. Elm provides a `Result a b`
primitive type that describes both success and failure; with this, the Elm code
will then explicitly handle when parsing JSON fails, and provide information to
the developer (and perhaps customer) that something went wrong.

Forced error handling means explicit messaging to customers; edge-cases around
unexpected things happening crop up much less frequently.

[Elm decoders]: https://thoughtbot.com/blog/decoding-json-structures-with-elm

### Managing Side-Effects

Port interaction is handled entirely through Elm as asynchronous messages. This
means all **inbound** port interaction is handled either with Elm
**subscriptions** (JavaScript sending data over a port to Elm) and **outbound**
port interaction with Elm **commands** (Elm sending data to JavaScript over a
port).

Because of this, communication via ports is often isolated to a single file,
ensuring all code with side-effects is isolated from the rest of the codebase.

```elm
-- outbound port
port initialized : List GoogleMapMarker.Model -> Cmd a


-- outbound port
port selectLatLon : GoogleMapMarker.Model -> Cmd a


-- inbound port
port clickedMapMarker : (Int -> msg) -> Sub msg
```

## Example Application

For this article, I built an [Elm application] that plots [thoughtbot offices]
on a Google Map. The data is seeded via JavaScript objects and passed into the
Elm application with a flag, and once the data is decoded, it's plotted on the
map.

[Elm application]: https://github.com/joshuaclayton/elm-ports-example
[thoughtbot offices]: https://thoughtbot.com/locations

![Elm Ports Example Application Screenshot](https://images.thoughtbot.com/jc-bridging-elm-and-javascript-with-ports/Xne6Kv5QPyBT8AXIbgxw_elm-ports-example-screenshot.png)

## Elm to JavaScript

Let's start by sending information from Elm to JavaScript. This occurs at the
top-level `initial` and `update` functions within the Elm application as a
command, and we'll need a way to encode an Elm structure into JSON.

### Sending Messages to Ports

The first port we'll define will be used to initialize the Google Map itself;
with this, we'll provide a list of geographical coordinates (latitude and
longitude) that we can iterate over to plot on the map.

Laying the foundation of the data model, let's look at what a Google Map marker
looks like:

```elm
module GoogleMapMarker
    exposing
        ( Model
        , fromOffice
        )

import Json.Encode as Encode
import Office.Model as Office
import Address.Model as Address


type alias Model =
    Encode.Value


fromOffice : Office.Model -> Model
fromOffice ({ id } as office) =
    Encode.object
        [ ( "id", encodeOfficeId id )
        , ( "latLng", encodeLatLon office.address.geo )
        ]
```

To make this list of offices available to JavaScript, we'll begin by defining a
port:

```elm
port initialized : List GoogleMapMarker.Model -> Cmd a
```

This defines a function that takes a list of `GoogleMapMarker.Model` and
returns a polymorphic `Cmd a`; with this in place, we can call `initialized`
from `initial` (which we've told Elm to use when starting the application) and
provide the list of coordinates:

```elm
initial : Flags.Model -> ( Model, Cmd Msg )
initial flags =
    let
        initialModel =
            initialModelFromFlags flags

        initialLatLngs =
            List.map GoogleMapMarker.fromOffice initialModel.offices
    in
        ( initialModel, initialized initialLatLngs )
```

When we embed the Elm application, we'll subscribe to the `initialized` port,
initialize the map, and register the coordinates. Let's take a look at the
JavaScript necessary to do so:

```javascript
const flags = {
  offices: [
    {
      id: 1,
      name: "Boston",
      // ...
    }
  ]
}

document.addEventListener("DOMContentLoaded", () => {
  const app = Elm.Main.embed(document.getElementById("main"), flags);

  app.ports.initialized.subscribe(latLngs => {
    window.requestAnimationFrame(() => {
      const map = new Map(
        window.google,
        document.getElementById("map")
      );
      map.registerLatLngs(latLngs);
    });
  });
});
```

As you can see, we've extracted interacting with the Google Map into a new
JavaScript class, `Map`. Its constructor accepts a reference to `google`, as
well as the element we'll be embedding the map within. The `registerLatLngs`
function takes the list of coordinates, plots the markers, and updates map
boundaries so all points are displayed at the appropriate zoom level.

Of note is the use of `window.requestAnimationFrame()`; because we're outside
of Elm's rendering in the JavaScript side of this port, we wrap modification of
the Elm-controlled DOM in this callback to ensure smooth rendering.

### Encoding Data Structures with `Json.Encode`

Looking at the type signature for `initialized`, we can see it expects a list
of `GoogleMapMarker.Model`. Ports in Elm can provide data to JavaScript in two
ways: send along *only* Elm primitives like `String` or `Bool`, or use the
`Json.Encode.Value` type to represent a JSON structure, and have the developer
define how a structure is encoded.

Let's look at the `GoogleMapMarker` module to dig into encoding.

```elm
module GoogleMapMarker
    exposing
        ( Model
        , fromOffice
        )

import Json.Encode as Encode
import Office.Model as Office
import Address.Model as Address


type alias Model =
    Encode.Value


fromOffice : Office.Model -> Model
fromOffice ({ id } as office) =
    Encode.object
        [ ( "id", encodeOfficeId id )
        , ( "latLng", encodeLatLon office.address.geo )
        ]


encodeOfficeId : Office.Id -> Encode.Value
encodeOfficeId (Office.Id id) =
    Encode.int id


encodeLatLon : Address.LatLon -> Encode.Value
encodeLatLon latLon =
    Encode.object
        [ ( "lat", Encode.float latLon.latitude )
        , ( "lng", Encode.float latLon.longitude )
        ]
```

Here, we alias `Model` to be `Json.Encode.Value`, and provide the function
`fromOffice` to handle encoding. The JSON generated should be straightforward:

```json
{
  "id": 1,
  "latLng": {
    "lat": 42.356157,
    "lng": -71.061634
  }
}
```

### JavaScript Data Structures

With the JSON structure in place, let's look quickly at the `registerLatLngs`
function from `Map` and see how the data is used in JavaScript.

```javascript
export default class Map {
  // ...

  registerLatLngs(latLngs) {
    const bounds = new this.google.maps.LatLngBounds();

    latLngs.forEach(o => {
      const gLatLng = o.latLng;
      bounds.extend(gLatLng);

      const marker = new this.google.maps.Marker({
        position: gLatLng,
        map: this.map
      });
    });

    this.map.fitBounds(bounds);
  }
}
```

Because `registerLatLngs` receives an array of JavaScript objects, we can
iterate over the list and build markers for Google Maps, as well as extend the
map bounds, in a single `forEach`. Google Maps expects a specific structure for
the coordinate, captured in `latLng`, so we pass it through directly.

### Messages to Ports via `update`

With our markers rendered, the next thing we'll cover is selecting an office in
Elm, which should pan the map to the appropriate marker.

First, let's create a new port for selecting a particular office:

```elm
port selectLatLon : GoogleMapMarker.Model -> Cmd a
```

In the `update` function, we'll now handle the new message:

```elm
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        SelectOffice office ->
            { model | selectedOffice = Just office }
                |> navigateMapsToOffice

        -- other messages


navigateMapsToOffice : Model -> ( Model, Cmd Msg )
navigateMapsToOffice model =
    let
        cmd =
            case model.selectedOffice of
                Nothing ->
                    Cmd.none

                Just office ->
                    selectLatLon <| GoogleMapMarker.fromOffice office
    in
        ( model, cmd )
```

When a selected office exists, we use the `selectLatLon` port to notify the
JavaScript.

The underlying JavaScript needs to be updated a bit so the new port can also
reference the map:

```javascript
document.addEventListener("DOMContentLoaded", () => {
  const app = Elm.Main.embed(document.getElementById("main"), flags);

  let map; // make map available

  app.ports.initialized.subscribe(latLngs => {
    window.requestAnimationFrame(() => {
      map = new Map(
        window.google,
        document.getElementById("map")
      ); // assign to map
      map.registerLatLngs(latLngs);
    });
  });

  app.ports.selectLatLon.subscribe(latLng => {
    map.selectLatLng(latLng); // call a function on our now-available map
  });
});
```

Within our `Map`, we add the function `selectLatLng` to handle map interaction:

```javascript
export default class Map {
  // ...

  selectLatLng(o) {
    this.map.panTo(o.latLng);
    this.map.setZoom(12);
  }
}
```

When selecting an office, the map now pans to the appropriate position!

## JavaScript to Elm

With outgoing ports configured, the final step we need to take is capturing
clicking on a marker to select the office Elm-side.

### Subscriptions

While outgoing ports are handled as side-effects at the top-level `update`
function, accepting data from incoming ports is handled via subscriptions.

Let's look at the incoming port and `subscriptions` functions to see how these
fit together:

```elm
subscriptions : Model -> Sub Msg
subscriptions _ =
    clickedMapMarker (\id -> SelectOfficeById <| Office.Id id)


port clickedMapMarker : (Int -> a) -> Sub a
```

In this example, our subscriptions operate regardless of model state, and we
ingest an `Int`, transform it to an `Office.Id`, and wrap it in the
`SelectOfficeById` message. With this, we'll need to wire up clicking on a
marker to send the `id` property, and handle this message in `update`.

```elm
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        SelectOfficeById id ->
            selectOfficeById id model
                |> navigateMapsToOffice

        -- other messages
```

We'll leverage `navigateMapsToOffice` again, which will ensure the map is
panned correctly.

We'll also need to configure the callback when clicking on a marker, so let's
update the JavaScript:

```javascript
document.addEventListener("DOMContentLoaded", () => {
  const app = Elm.Main.embed(document.getElementById("main"), flags);

  let map;

  app.ports.initialized.subscribe(latLngs => {
    window.requestAnimationFrame(() => {
      map = new Map(
        window.google,
        document.getElementById("map"),
        app.ports.clickedMapMarker.send // pass the inbound port function
      );
      map.registerLatLngs(latLngs);
    });
  });

  app.ports.selectLatLon.subscribe(latLng => {
    map.selectLatLng(latLng);
  });
});
```

Finally, when we register the initial set of coordinates, we configure a
listener on the "click" event on the marker:

```javascript
export default class Map {

  registerLatLngs(latLngs) {
    const bounds = new this.google.maps.LatLngBounds();

    latLngs.forEach(o => {
      const gLatLng = o.latLng;
      bounds.extend(gLatLng);

      const marker = new this.google.maps.Marker({
        position: gLatLng,
        map: this.map
      });

      marker.addListener("click", () => {
        this.clickedCallback(o.id); // trigger the callback with the office ID
      });
    });

    this.map.fitBounds(bounds);
  }
}
```

Similar to sending data out of the Elm application, it's able to handle
decoding primitive data structures with ease. In this case, we take the
identifier (an `Int`) and do some simple wrapping to an `Office.Id` in the
`subscriptions` function above.

If you need to decode larger data structures, the process is largely the same,
but you'll need to build decoders for your Elm types, which you can read more
about [in another post].

[in another post]: https://thoughtbot.com/blog/decoding-json-structures-with-elm

## What's Next for Ports

Ports in Elm are a robust mechanism of sending data between the worlds of Elm
and JavaScript. It supports primitive data structures out of the box, resulting
in quick prototyping and validation, while allowing for the raw power of Elm's
JSON encoders and decoders when necessary.

Beyond encoding and decoding data, because data passed to Elm still flows
through the top-level `update` function, behavior triggered from JavaScript is
easy to reason about because it abides by the same rules as all other actions
within the application. [Murphy Randle] talked about a different approach to
ports at ElmConf 2017 that, while I haven't tried, seems very appealing. I
encourage you to watch [his talk].

[Murphy Randle]: https://github.com/splodingsocks
[his talk]: https://www.youtube.com/watch?v=P3pL85n9_5s
