Elm ports is one of the main ways to interact with JavaScript. However I feel like I need to tell you that if you can get away with it, you should just use flags. Flags pass in some initial value to Elm to start with and is the easiest way no doubt. However if you want more back and forth communication ports is the way.

High-Level Overview

First, here’s the picture:

Ports are basically the pub-sub pattern (also called The Observer Pattern). A port is the connecting pipe between Elm and JavaScript.

Each port is only one direction. Either Elm ->JavaScript or JavaScript -> Elm. So naturally if you want two way communication you just have to make two.

Making a Port

Some Setup

If you’re going to follow along follow these steps (or perhaps just look at the code)

Step 1: Setup an Main.elm file with the full Html.program

Step 2: A .js file that can be blank for now

Step 3: An index.html file that includes both elm.js (compiled elm file) and the whatever.js file that contains the code you want to interact with.

From Elm to JavaScript

Annotate the module by putting a port at the beginning

Create a port to JavaScript with a type signature like

port toJs : String -> Cmd msg

This will creates a function called toJs that creates Cmd s (commands). If you remember you can return Cmd as the second part of the tuple in the update function! Let’s do that.

update msg model =

case msg of SendToJs str ->

( model, toJs str )

This will magically send str to JavaScript land! Well only to those who subscribe to it. To subscribe to it you do:

var node = document.getElementById('view');

var app = Elm.Main.embed(node); // receive something from Elm

app.ports.toJs.subscribe(function (str) {

console.log("got from Elm:", str);

});

From JavaScript to Elm

In JS land you send something via the send method on the port. Like so:

var node = document.getElementById('view');

var app = Elm.Main.embed(node);

app.ports.toElm.send("undefined is not a function");

Naturally JS says it’s customary phrase.

So we define a new port:

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

This creates a function, that takes a function that can turn strings JS sends and turns it into a Msg (again can be any value that’s compatible between the two) This one returns a Sub instead of a Cmd so we need to subscribe to in Elm:

subscriptions : Model -> Sub Msg

subscriptions model =

toElm UpdateStr

Basically this says:

whenever JS sends me something over the toElm port send the UpdateStr message to the update function.

We can handle this Msg just like any other in our update function:

update msg model =

case msg of

UpdateStr str ->

( { model | message = str }, Cmd.none )

Basically I’m just storing the string JS gave me to a part of my model.

🚨 WARNING: Run-time Exceptions Ahead! 🚨

While I’ve used String to keep this simple but this is dangerous.

If JavaScript, (in it’s infinite wisdom), decides to send something that’s not a string (like null, undefined, NaN, rubber duck, a function, etc.) then Elm will throw a run-time exception and stop it’s run loop.

This means Elm will not be able to accept any input or respond in any way. It’s dead.

The way to make your elm code bullet proof again to change the type of the port to a Value instead. This means that you’ll have to write a Json.Decoder for it. This way if JS decides to send you a bad value it will just return an Err instead of throwing an exception.

See the diff here

import Json.Encode exposing (Value)

import Json.Decode as Decode -- Farther down in the file.... -- port toElm : (Value -> msg) -> Sub msg -- SUBSCRIPTIONS subscriptions : Model -> Sub Msg

subscriptions model =

toElm (decodeValue) decodeValue : Value -> Msg

decodeValue x =

let

result =

Decode.decodeValue Decode.string x

in

case result of

Ok string ->

UpdateStr string Err _ ->

UpdateStr "Silly JavaScript, you can't kill me!"

That’s it!

Here’s the picture again:

Here’s a link to the working repo: https://github.com/justgage/complete-elm-port-example