Introduction

Recently I came across the need for an easy UI to display information coming from a websocket. A niche option but one I’ve had a pleasant experience with previously is Elm, a functional language designed specifically for web applications.

Previously, the standard approach for connecting to a websocket was through the elm/websocket package, has been recently broken after some of the recent changes to the Elm language. In the mean time while the package and others are being updated, there’s a simple workaround by using another Elm feature called ports which allow for interop with JavaScript code.

Outline of what we’ll be building

To demonstrate how to use ports to send and receive messages we will build a small chat-like application that can receive a message or send one. The focus of this post is on websockets and not Elm itself, so I won’t outline every detail of the application but the full source is available here.

Getting a basic Elm project running

To start an elm project, simply make a new folder and use elm to create a elm.json to track dependencies and an empty Main.elm file.

mkdir websockets_project
cd websockets_project
elm init

We need a basic index page in the root of our repository too. The following includes the JavaScript we’ll generate and a small script section we’ll add to later.

<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <title>Main</title>
  <script src="app.js"></script>
</head>

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

I use elm-live to run Elm applications locally which includes nice features like hot-reloading (that preserves the state of the application) and also let’s us export our application to a JavaScript file which is included in the index.html file above. To follow along, you can do

elm-live src/Main.elm -- --output="app.js"

If you go to localhost:8000 in a browser and see a message from the compiler complaining about the empty Main.elm file then you’re in the right place.

Writing Model and Msg

The most central part of our application are the Model and Msg types which encode the full state of our application and the possible actions that can happen respectively.

First we want to describe the Message type. There are two key parts to a message, the direction and the contents. We use a sum type called MessageType to describe who sent the message and the contents will just be a String.

type MessageType
    = Incoming
    | Outgoing

type alias Message =
    { contents :  String
    , messageType : MessageType }

The state of the application includes two things. The list of messages that have been either sent or received and the contents of the message box that the user types their message into.

type alias Model =
    { messages : List Message
    , messageBoxValue : String
    }

Next we define the Msg type. The possible events that can happen in the application are:

  1. We receive a message
  2. The user types something in the message box
  3. The user clicks the send button

We model each of these using the Msg type.

type Msg
    = GotMessage String
    | SendMessage
    | MessageBoxChanged String

Defining the ports

Ports in Elm operate in a single direction, either from Elm to JavaScript or the other way. Therefore to be able to both send and receive messages we need two.

port incomingMessage : (String -> msg) -> Sub msg
port outgoingMessage : String -> Cmd msg

Recall that the GotMessage variant of the Msg type takes a String type argument. This means in a way that it is a function with the type signature String -> msg, which we can see matches the first argument of the type signature for incomingMessage.

Indeed, we register our “callback” for incoming messages by calling incomingMessage with GotMessage as the argument.

subscriptions : Model -> Sub Msg
subscriptions model =
    incomingMessage GotMessage

Now when a String is sent through the port a GotMessage event occurs, and we can process the new message in the update function.

Sending a String to JavaScript is a little different. Notice from the type signature of outgoingMessage if we pass in a String we get a Cmd msg back. Cmd Msg are sent via the return value of calling update, so to send a message in response to an event, we pass it back along with a Model type.

In our case, we want to send an outbound message through to the websocket on the SendMessage variant of Msg, passing in the contents of the message box.

SendMessage ->
    let
        --  Calculate the new model
    in
        (newModel, outgoingMessage model.messageBoxValue)

Handling messages in JavaScript

Now that we have written the plumbing in Elm plumbing we need to handle the messages on the JavaScript side and pass them to the websocket. We won’t include a new JavaScript file (but you probably should) and will just add to the existing index.html instead.

To simulate a real websocket, I use wscat to host a local websocket I can type messages into. If you want to follow along with my approach you can run wscat -l 9000 otherwise just replace ws://localhost:9000 with your address of interest.

To create a websocket in JavaScript, all we need to do is the following.

let ws = new WebSocket("ws://localhost:9000")

Next we need to register two callbacks. One that listens for incoming messages over the websocket and sends them to Elm and another that listens for outgoing messages from Elm and sends them over the websocket.

The first looks like this

ws.onmessage = function(msg) {
    app.ports.incomingMessage.send(msg.data)
}

and the second looks like this.

app.ports.outgoingMessage.subscribe(function(msg) {
    ws.send(msg)
})

That’s it! If you refresh your page you should see a new connection if you are using wscat and be able to send messages to and from Elm over the websocket.