Building a chat application using Elixir and Phoenix
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:
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
.
(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.
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.
At this point, you will see the “Connection error” message being logged to the browser console. If you see the Phoenix server log, you will find an exception that looks like this:
This is because
we haven’t added RoomChannel
yet.
Let’s go ahead and add it.
Adding RoomChannel
The first thing we need to do
with RoomChannel
is to
write a function join/3
that will be called
when we connect to the channel.
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()
:
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:
Instead of logging the username and message body to the browser console, we are pushing the message to the server with the name “new:message” and a payload that contains the username and message body.
Handling incoming messages on server
We can handle an incoming message
in RoomChannel
using a handle_in/3
function.
In this case,
we will broadcast the incoming message
to all subscribers.
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
:
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.