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:
- We receive a message
- The user types something in the message box
- 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.