Building a chat application using Elixir and Phoenix

11 Jul 2015

I’ve been looking at
Phoenix channels lately,
and going through
Chris McCord’s Phoenix chat example
gives a great intro to
getting started with it.
In this post,
we’ll be walking through the process of
building the same app
step by step.

We’ll use Elixir 1.0.5 and Phoenix 1.0.0.
(This was originally written for Phenix 0.14,
but was later updated for 1.0.0.)
We’ll also be using ES6
instead of Javascript,
but anyone familiar with JS
should be able to follow along.
I’ll add notes explaining the ES6 code
wherever we’re looking at features
not available in Javascript.

Our app will be called “Chatter”,
and will contain a single page
where all connected users
can post chat messages.
Let’s get started,
and generate the project:

mix phoenix.new chatter

Add the HTML markup

The first thing we’ll do is
add the HTML markup for the form.
We’ll be adding the chat functionality
to the default page
generated by themix command.
Replace the html code
in web/templates/pages/index.html.eex
with this:

We will render the chat messages
to the #messages div.
To keep things simple,
users will enter their username
and a message
to the respective input fields
and hit enter to send the message.

Add jQuery

We’re going to be using jQuery
for the client side code.
Add the following line
above the first <script> tag
in web/templates/layouts/app.html.eex.

<script src="http://code.jquery.com/jquery-2.1.4.min.js"></script>

(I’m using this approach for loading jQuery
only so that we can get started quickly.
A better approach would be to
use Bower to fetch jquery,
since that would also help you manage
other Javascript libraries as well.)

Setup app.js

Let’s set up the front end code
before we start writing the Elixir code
to handle the messages.
We will be writing our code
in web/static/app.js.
The App.init() function
will be invoked on page load
and it is will set up handlers for form submission
and for receiving messages over the channel.

If you’re unfamiliar with ES6 syntax,
here’s an outline of what’s happening here:
The first line imports
the Socket object from
web/static/vendon/phoenix.js.
The static init() adds the function
to the App object so that
we can invoke it as App.init()
The $( () => ... ) line
uses the arrow syntax for functions
and is equivalent to
$(function() { App.init() }).

If you’re not familiar with jQuery,
the $() function gets called
when the page is loaded.
Save this file and make sure
the text “Initialized”
is getting logged to the browser console.

Handling form submission on the client side

Before we look at Phoenix channels,
let’s flesh out the init() function
to handle form submission.
For now, we will log the form inputs
to the browser console.

We need to submit the fields
when the user hits enter
from the #message text field.
Let’s change the App.init() function
so that it handles that event.

Here we are clearing
all the keypress event handlers
and then adding a new one
which logs the contents
of the username and message inputs
to the console
when the enter key is pressed
(key code = 13).
We’re using the sting interpolation
feature of ES6 in the
console.log line.

Adding channel routes

A UserSocket module
will be present in
web/channels/user_socket.ex.
This module is used for
handling socket authentication
in a single place.

We just need to add one line to the module.

channel"rooms:*",Chatter.RoomChannel

With this route in place,
whenever we send a message from the browser
with a topic that starts with rooms:,
it will be handled by RoomChannel.

Joining a channel

Having set up the socket routes,
let’s go back to the font end.
We’ll change the App.init() function
so that it connects to the channel
on the page load event.

Here, we’re only considering
a room with the topic rooms:lobby.
The join/3 function takes
the topic (“rooms:lobby”),
an authentication message,
and the socket.
We return the tuple
{:ok, socket}
to indicate that
the user has connected successfully.

Handling the :ok response

When the server responds with ok message,
we need to handle the response on the client.
We’ve previously seen how we can respond
to the :error message.
We can similarly handle the :ok response.
Append the new receive hook
to the channel in App.init():

channel.join().receive("error",()=>console.log("Failed to connect")).receive("ok",()=>console.log("Connected"))

This time you will see the message “Connected”
on the browser console.
(You might need to restart the Phoenix server first).

Pushing messages from client to server

The next thing we want to do
is to push the data to the server
when the user submits a message.
We will modify
the keypress event handler
like this:

Receiving broadcast messages from the client

In order to handle the broadcast message,
we will add the following code
at the end of App.init() function
in app.js:

channel.on("new:message",msg=>console.log(msg.body))

To see this working,
open localhost:4000
in two separate tabs,
and submit a message.
You will see the message body
logged to the console in both tabs.

Rendering the messages to the page

So far we’ve been
logging the messages to the console.
Let’s start displaying the messages
on the browser
and make this look like a real chat app.

For this,
we will add a renderMessage function
and call that instead of console.log
when we receive a new message.
We already have a #messages div
in the template,
and we can append the messages to it
by making these changes:

At this point,
you will be able to
send chat messages
between the two tabs
and see the messages
displayed in the page.

Sanitizing input

Since we are appending the messages directly
to the #messages div,
someone could include
malicious Javascript in their message
and have it executed in the browser
of all the subscribers of the channel.

As an example,
try sending a message containing the text
<script>alert("LOLOLOL!")</script>
from one of the tabs.
This will result in
an alert box being opened in
all the tabs
that are connected to the channel.

To prevent this,
we make the following changes
to the renderMessage function:

Now you can try sending the same message
and you will see that
the text is displayed
exactly as you sent it,
and the script doesn’t get executed.

Next steps

Now that we have created a common lobby
for our chatroom,
you could also allow
users to connect to
rooms with a different names.
Or handle other cases
like broadcasting a message
when a new user joins.
If you wish to explore
Phoenix channels and
try adding such features,
take a look at the links below.

Links

Hi, I’m Nithin Bekal.
I work at Shopify in Ottawa, Canada.
Previously, co-founder of
CrowdStudio.in and
WowMakers.
Ruby is my preferred programming language,
and the topic of most of my articles here,
but I'm also a big fan of Elixir.
Tweet to me at @nithinbekal.