Building a Simple Chat App With Elixir and Phoenix

The past few years, more and more applications have been transitioning to websockets for real-time communication, even forcing some frameworks to implement them (such as ActionCable in Rails 5). The Phoenix Framework for Elixir implements it natively, without depending on any external programs such as Redis.

Today, we’re going to build a super simple chat application in Elixir using the Phoenix Framework. We’re going to ignore authentication, authorization and other features so we can quickly go over the basics, and get websockets in Elixir running.

Getting Started

Start by installing Elixir and Phoenix. Optionally, check out my talk on Introduction to Elixir. Create a new phoenix project and call it whatever you like, and start the server so you can see the app in your browser:

1
2
3
4
5
6
# Create a new Phoenix project called chatroom
$ mix phoenix.new chatroom

# Start the server
$ cd chatroom
$ mix phoenix.server

Creating the View

Let’s start with something easy by writing the markup and CSS for our chat app. Open web/templates/page/index.html.eex, and replace its contents with:

1
2
3
4
5
6
7
8
9
10
11
<div id='message-list' class='row'>
</div>

<div class='row form-group'>
  <div class='col-md-3'>
    <input type='text' id='name' class='form-control' placeholder='Name' />
  </div>
  <div class='col-md-9'>
    <input type='text' id='message' class='form-control' placeholder='Message' />
  </div>
</div>

We’ve created an empty div that will list all chat messages and two text fields (one for the user’s name and one for the message). Now open web/static/css/app.css and paste this at the end:

1
2
3
4
5
6
7
#message-list {
  border: 1px solid #777;
  height: 400px;
  padding: 10px;
  overflow: scroll;
  margin-bottom: 50px;
}

Point your browser to localhost:4000, it should look like this:

Phoenix Chat Markup Screenshot

Setting up a new Channel

We’re going to create a new channel called lobby. Open up web/channels/user_socket.ex and add this line:

1
channel "lobby", Chatroom.LobbyChannel

Create a new file called web/channels/lobby_channel.ex and implement the functionality for the new lobby channel. The join method here always returns {:ok, socket} to allow all connections to the channel. The handle_in method is fired every time a new incoming message is received on the socket, which broadcasts that message to all other open sockets.

1
2
3
4
5
6
7
8
9
10
11
12
defmodule Chatroom.LobbyChannel do
  use Phoenix.Channel

  def join("lobby", _payload, socket) do
    {:ok, socket}
  end

  def handle_in("new_message", payload, socket) do
    broadcast! socket, "new_message", payload
    {:noreply, socket}
  end
end

Handling the connections on Client-side

To make things easier, we’ll start by adding jQuery to our web/templates/layouts/app.html.eex:

1
2
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script src="<%= static_path(@conn, "/js/app.js") %>"></script>

Phoenix comes packed with a simple javascript socket client, but it’s disabled by default. Go into your web/static/js/app.js and uncomment the last line:

1
2
// ...
import socket from "./socket"

Go into your web/static/js/socket.js and paste in this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ...

let channel = socket.channel("lobby", {});
let list    = $('#message-list');
let message = $('#message');
let name    = $('#name');

message.on('keypress', event => {
  if (event.keyCode == 13) {
    channel.push('new_message', { name: name.val(), message: message.val() });
    message.val('');
  }
});

channel.on('new_message', payload => {
  list.append(`<b>${payload.name || 'Anonymous'}:</b> ${payload.message}<br>`);
  list.prop({scrollTop: list.prop("scrollHeight")});
});

channel.join()
  .receive("ok", resp => { console.log("Joined successfully", resp) })
  .receive("error", resp => { console.log("Unable to join", resp) })

// ...

Here, we listen for a keypress event on the message text field. Whenever the user enters a message, it’s pushed on the channel and the text field is cleared. When there’s an incoming message on the channel, it’s appended to the div we previously created and scrolled to the bottom.

Chat app working

Going from there

So far we’ve implemented a super simple chat application in Elixir. It’s obviously not perfect and there’s alot of stuff missing. The next logical step would be to add some sort of authentication and authorization, and implement more one-on-one and private chat rooms.

Here are some links you should check out: