Creating realtime applications with PusherJS and Elm.
We’re big fans of Elm Lang at Pusher, a functional reactive language that compiles to JavaScript. We recently attended an Elm hacknight in London and ever since we’ve been thinking about how to integrate Pusher into an Elm application. Longer term we’d love to build a native Elm library for Pusher but in the mean time you can successfully integrate by using PusherJS and Elm’s interopability with ports together.
We’ve put our proof of concept onto GitHub if you’re keen to check the code out locally and play around with it. Remember that you’ll need a small server running, such as our Sinatra Pusher Server. Please note that we’re assuming a familiarity with Elm for the rest of this post; if you’re not familiar the Elm docs are a great place to start. We also won’t cover every single bit of the application in this tutorial, I recommend running the application locally if you’d like to really explore the full application.
The typical Pusher integration for a client is as follows:
POST
requests to your server with dataWe’re able to use elm-http to make requests from Elm, so we’re able to keep that part native Elm. Once our server triggers events though we’ll use a small bit of JavaScript.
The JavaScript will fetch our events and then send them back into our Elm application using Elm ports. Ports were introduced in Elm 0.11 and are designed exactly for this purpose; making it easy to communicate between JavaScript and Elm. Although we won’t cover it in this post you’re also able to send data from your Elm application to JavaScript through ports too.
Here’s all the JavaScript we’ll need to write:
1var myApp = Elm.fullscreen(Elm.PusherApp, { 2 newMessage: '' 3}); 4 5var pusher = new Pusher('APP_KEY'); 6 7var channel = pusher.subscribe('messages'); 8 9channel.bind('new_message', function(data) { 10 myApp.ports.newMessage.send(data.text); 11});
First we initialise our Elm application in fullscreen mode. For each port that we need (we just have the one, newMessage
) we have to give it an initial value. In our case we just use the empty string. Finally, we then use the PusherJS library to subscribe to any new_message
events on the messages
channel. When we get an event we grab the text
property from the object and send that through to the newMessage
port.
From the Elm side, a port is just a signal of data, where the data is what’s fed in from the JavaScript side. In our case this will be a Signal String
, because we’re just sending through the text from our event. There’s no reason we couldn’t send an entire object though if we wanted to.
All we have to do is define the newMessage
port:
1port newMessage : Signal String
And that’s it! We now have a newMessage
signal that we can get data from. You can treat this signal just like any other in Elm; there’s no additional special casing because it’s coming from a port. It’s as much a signal as any of the signals Elm provides out of the box for you.
Because we’re following the Elm Architecture, we have an update
method that expects to take an action, model and return the new model. In our case the user actions are as follows:
1type Action 2 = NoOp 3 | NewMessage String 4 | SendMessage 5 | UpdateField String
NewMessage String
is the action we want to generate when we get a new value from the newMessage
signal. UpdateField String
is how we’ll keep track of the value of the input box the user can type their new message in. SendMessage
is triggered when the user clicks the button to send their message.
Normally we’d define our update
function as follows:
1update : Action -> Model -> Model
But in this case one of the actions has a side effect; the SendMessage
action will trigger an HTTP request to our server. Asynchronous tasks like this are represented using the Task module. We use Tasks to represent asynchronous actions that may succeed or fail, such as HTTP requests. At this point I should also thank Peter Damoc, whose answer to my question on the Elm Discuss Mailing List really helped me with this.
The type annotation for our update
method looks like so:
1update : Action -> (Model, Task () ()) -> (Model, Task () ())
Our update
will be called with an Action
and then a tuple. The first item will be our model, and the second will be a Task
that will succeed or fail with an empty tuple. Note that in this tutorial we’re not going to discuss error handling, and presume that our requests will always succeed. update
is now expected to return the new model and then a task that will succeed or fail. For most of our actions we won’t need to return an actual task to be executed, so we can return Task.succeed ()
, a Task that when run will immediately succeed with the given value.
Let’s take a look at our full update
function:
1update : Action -> ( Model, Task () () ) -> ( Model, Task () () ) 2update action ( model, _ ) = 3 case action of 4 NewMessage string -> 5 ( { model | messages = string :: model.messages } 6 , Task.succeed () 7 ) 8 9 UpdateField string -> 10 ( { model | field = string }, Task.succeed () ) 11 12 NoOp -> 13 ( model, Task.succeed () ) 14 15 SendMessage -> 16 ( { model | field = "" }, postJson model.field )
Note that all of the actions return Task.succeed ()
(which I tend to think of as a “blank task”), but SendMessage
returns postJson model.field
, which will return a task.
You’ll remember earlier that I mentioned we won’t deal with errors in this tutorial, and additionally we don’t need to deal with the return responses of HTTP requests. The tasks returned by Elm-HTTP don’t match the type we need of Task () ()
but instead return Task Http.RawError Http.Response
. We need to map that into a Task () ()
; this can be done with a silenceTask
method:
1silenceTask : Task x a -> Task () () 2silenceTask task = 3 task 4 |> Task.map (\_ -> ()) 5 |> Task.mapError (\_ -> ())
Thanks again to Peter Damoc for suggesting this implementation on the Elm mailing list. In a real application we’d definitely want to deal with errors, and we’ll look more at error handling in a future post.
Now we have a way to take any Task x a
and change it into a task that will succeed or fail with ()
, we can use this in our postJson
method:
1postJson : String -> Task () () 2postJson str = 3 silenceTask 4 <| Http.send 5 Http.defaultSettings 6 { verb = "POST" 7 , headers = 8 [ ( "Content-Type", "application/json" ) 9 , ( "Accept", "application/json" ) 10 ] 11 , url = "http://localhost:5000/messages" 12 , body = Http.string (jsonBody str) 13 }
This method uses Http.send
to create a custom HTTP request – whilst Elm HTTP does provide Http.post
, it doesn’t allow us yet to customise the headers, and in our case we need more control over the request. We take the response of this and pass it into silenceTask
to transform the Task
returned into the type that we need. We encode the body using jsonBody
, which uses Elm’s JSON.Encode
module, which I’ve imported as JSEncode
:
1jsonBody : String -> String 2jsonBody str = 3 JSEncode.encode 4 0 5 (JSEncode.object 6 [ ( "text", JSEncode.string str ) ] 7 )
With this we’re now able to fill in the text field and get the data sent to the server.
Now our update
method can give us back the new model along with a task to run, we need to actually run them! To do this in Elm we give a task to a port, which will cause it to execute. A port can also take a signal of tasks, which is what we’re going to do here.
First, let’s take a look at our signals in the app:
1newMessageSignal : Signal Action 2newMessageSignal = 3 Signal.map (\str -> (NewMessage str)) newMessage 4 5inputSignal : Signal Action 6inputSignal = 7 Signal.mergeMany [ actions.signal, newMessageSignal ] 8 9modelAndTask : Signal ( Model, Task () () ) 10modelAndTask = 11 Signal.foldp update ( initialModel, Task.succeed () ) inputSignal 12 13modelSignal : Signal Model 14modelSignal = 15 Signal.map fst modelAndTask 16 17tasksSignal : Signal (Task () ()) 18tasksSignal = 19 Signal.map snd modelAndTask
modelAndTask
is the typical method you’ll see in nearly all Elm applications, it maintains the state of the application using Signal.foldp
. We have inputSignal
as a signal of all the user inputs, which includes the inputs from the newMessage
port (mapped to create NewMessage
actions) and any from actions
, which tracks user events such as mouse clicks.
Once we have modelAndTask
which is a signal of our model and the current task we can then create tasksSignal
by applying snd
to modelAndTask
. This creates a signal of tasks that change over time. All we now need to do is pass this to a port to have them run:
1port tasks : Signal (Task () ()) 2port tasks = 3 tasksSignal
Elm’s signals, ports and tasks are confusing at first and I’ll happily confess to a lot of head bashing whilst working on this application and blog post. However, once things begin to click they become very powerful and understandable. I’d urge you to pull down this repo, have a play and get a feel for how signals and tasks operate. I’d love to hear how you get on with Elm and any thoughts you might have – feel free to tweet me and let me know.