Integrating Phoenix and Elm — Part 2

Integrating with Phoenix Channels

billperegoy
im-becoming-functional
10 min readApr 11, 2017

--

Where We Left Off

In the previous bog post, we built a relatively simple application intended to allow multiple users to interactively divide tickets. But we never got to the interactive part. In this post, we will do the really fun part. We’ll use Phoenix channels to allow multiple users to interact with the Phoenix server.

Interactive Requirements

In order to turn the view we created above interactive, we will use Phoenix channels. The expected behavior will take on this form.

  1. A user clicks on an available game.
  2. This generates a Phoenix channel event that sends the user_id and ticket_id to the backend.
  3. The server side code updates the corresponding ticket instance, setting the user_id to the supplied value.
  4. After updating the ticket instance, the backend broadcasts the modified ticket instance data back to all subscribed clients.
  5. A channel subscription on the client side causes an update function to run which updates the single modified ticket instance.
  6. The Elm view will update based on the model update. This will cause the selected ticket to move from the remaining column to the owned column.

Since the server broadcasts the ticket ownership changes to all connected clients, this means that all user’s clients will be updated simultaneously without any interaction or manual refresh.

Next we will modify our code to use Phoenix channels to perform this magic.

Phoenix Channels Architecture

Phoenix channels are a way of creating a persistent connection between a client and a server. A channel is a persistent bidirectional connection. Any given channel can support communication on any number of topics. The server sets up the channel and clients can subscribe to any topics they wish to receive messages from as well as send data to a channel/topic combination. On any particular topic, messages are sent with an event type and a payload. The server is programmed to react differently to different event types.

Server messages can be sent to a particular client or broadcast to all clients. In addition, the server can optionally send a response to the sender of any particular message.

In this example, we will keep it simple and create just a a single channel (dividasaurus) with one topic (tickets). Since we only have one message type, all messages sent on this topic will use an event type of ticket_select. This results in a message flow that looks like this.

  1. User clicks on a ticket they would like to select. This creates a ticket_select event with a ticket_id and user_id attached as a payload. This payload is sent to the server on channel dividasaurus and topic tickets.
  2. The server receives this channel message and uses pattern matching to extract the ticket_id and user_id from the payload. It then fetches the appropriate ticket record and updates it with a new user_id.
  3. After updating the database record with the new user_id, the server broadcasts a message of event type ticket_select back to all subscribed clients.
  4. On each subscribed client, the received event triggers a subscription action that invokes a ReceiveMessage message that updates the Elm model to reflect the contents of the updated ticket record.
  5. The model update in each client triggers a re-rendering of the view which will reveal the changed ticket ownership.

The amazing thing is that all of this real-time update happens almost instantaneously. Since the Phoenix channels keep persistent connections open, there is no over head associated with opening and closing http connections.

With that explanation out of the way, let’s dive into the nuts and bolts starting with the server side.

Phoenix Channel Setup

Given that Phoenix comes configured ready to turn on channels, the amount of setup required to implement channels on the sever side is remarkably small. Let’s walk through the process.

First, we need to update the existing web/channels/user_socket.ex file to register the new channel definition. Here’s the pertinent portion of that file after the changes.

The final line is the addition we made. This simple tells phoenix that any messages sent to any topic on the dividasaurus channel should be routed to the TicketChannel module.

With this in place, we can now create the actual ticket channel module. This can be done by creating a file in web/channels/ticket_channel.ex. Here we define two functions. The join function is called when a client requests to join a particular channel/topic and the handle_in function is invoked when a client sends a message to that channel.

The join function is quite simple. The function arguments pattern match on the desired channel:topic name and return a response indicating success. Note that you could create other versions of the join function that match other channel/topic names if you intended to allow joins for multiple topics.

The handle_in callback is similar in nature. Note that the first argument is pattern matched against the event type that we are looking for. It also pattern matches against a payload that includes ticket_id and user_id. Given a matching pattern, I look up the existing ticket with that ticket_id, update it to include the new user_id. I then check the result of this update. If it’s successful, I get the new version of the ticket and broadcast that ticket structure back to all subscribed clients. If it fails, I just print an error and do nothing else. So clients will not in this case be informed of failures. Finally, note that we send no reply from this operation. Instead of the reply, we depend upon the broadcast function to send necessary data back to the clients.

That’s really all there is on the server side. You define a channel module and add the two required callbacks (join and handle_in) and your backend is all set to communicate in a real-time fashion with your front-end. Next we will walk through the steps required to connect this to an Elm front end.

Creating an Elm Front End to Communicate with Phoenix Sockets

Now that we have a backend that can receive and send data via Phoenix sockets. Now we will go through the steps required to integrate this backend with our Elm code.

We need to perform three tasks from the client side.

  1. Join a channel
  2. Synchronously send a message via a channel
  3. Asynchronously receive messages from a channel

Most of the hard work for these tasks has been done for us thanks to the fine fbonetti/elm-phoenix-socket package. This package does much of the heavy lifting and I used the README and chat example extensively in the work I’ll be describing below.

In order to use this package, we need to import the packages as described in the README and add a reference to the socket to our model. This will allow our application to send and receive messages to the socket application. Our new model now looks like this.

So let’s walk through each of the three steps from above and see how it’s done.

Joining a Channel

In order to join a channel, we need to first define an update function that preforms the channel join. Here is the code I’m using to do that. Note that for this application, I am hardcoding the channel and topic name to the name used by our backend.

This code initializes a channel, issues a join command to the socket and updates our model appropriately.

I want to issue this update message on application startup, so I need to define a command that can execute this command.

Lastly, we update our init function to issue this command at startup time.

Note that there are two changes here. We have modified the effect array to include the joinChannel command we just created and we have called a function to initialize the channel.

This initialization function is the final piece to the startup puzzle. Here we set up the socket by providing a URL that corresponds to our local Phoenix server. We also tell the socket to log debug messages to the console.

That’s it for initialization. At this point if you start up the application with the backend running, you should see an indication in your Phoenix log that a client has connected. At this point, we’re all ready to send and receive messages.

Sending Channel Messages

With an initialized channel, we can now start sending messages. We want a user to be able to click on an unchosen ticket, and then send a message to the channel with the ticket_id and current user_id. The message looks like this.

This update function receives two arguments. It encodes those arguments into a JSON payload that will be sent to the socket. We then initialize a push event that describes the channel, topic and event type we want to target. We then attach the payload to that push event and send it to the socket. Most of this code is boiler plate that I borrowed from the example chat application.

We then need to modify the ticket link in our view to produce this message when a link is clicked. The results looks like this.

With this in place, you should be able to click on a ticket and see an update action occurring in your Phoenix logs. Also, if you refresh your browser, you will see a ticket has moved from the remaining column to the owned column. That’s cool, but now let’s allow us to use the data being broadcast from the Phoenix channel to do that update without a refresh.

Receiving Channel Messages

At this point, we can send messages. They are received on the server side and the server is even sending a broadcast response with an updated ticket. But we still don’t have code on the client side to receive and process this data. So let’s make that happen!

We now need to set up our application to listen for events. First, we need to update our socket initialization to tell it to listen for a certain event type on a certain topic. Here is an example that will listen for the one topic we are using in our application. We associate a message type (ReceiveMessage) with this event.

If you have multiple event types or event multiple channels or topics, you can chain more on events to this function.

We also need to set up an Elm subscription to listen for events and emit a PhoenixMsg event when any are received. Our subscriptions function now looks like this.

So, we now have a subscription that will generate a PhoenixMsg for any received event and a filter that directs certain event types to their appropriate update message types.

All that’s left now is to define the ReceiveMessage update function.

This function takes the message, encodes it to JSON and then uses our already existing ticket decoder to convert it into our Elm ticket structure. We then map over the list of existing tickets, replacing the one that matches this ticket id with the newly returned ticket (with the filled in user_id).

This model change will trigger a view re-render which will move the selected ticket from the remaining column to the owned column.

With this change, you can now select a ticket and it will disappear from the left side column and move to the right side column almost instantaneously with no screen redraw or flicker. Any other connected users will see this change instantaneously as well. Behind this seemingly simple operation, we are doing a round trip to the server and using the Elm model and virtual DOM to make it all look seamless.

Summary

This has been a long journey but the results are amazing. Rather that doing a number of discrete Ajax calls and polling for results, the combination of Phoenix channels and Elm subscriptions allows us to create extremely interactive web applications with very little overhead.

Figuring out all of the pieces the first time through took a bit of effort but once you understand the patterns on both the server and client side, it’s really quite easy to create this magical dance between the client and server.

I have a more complete (and continually updated version of this application at https://github.com/billperegoy/elm-divide. The code isn’t exactly what you see here, but it’s a good working example of tying these two technologies together.

Feel free to give me feedback about anything in these two posts. I hope you get as inspired by these technologies and go out there and build something amazing.

--

--

billperegoy
im-becoming-functional

Polyglot programmer exploring the possibilities of functional programming.