Chat rooms app in Elixir in 15 minutes
This is the showcase post that touches a tiny part of what Elixir and the accompanying tooling can help you build in very little time.
The goal
In this post we are going to build a very simple chat app with multiple rooms. Anyone with the link will be able to connect and say something. We won’t be storing the history of messages for the case of simplicity.
If you are lost, or just want to skip over some hoops, the source of the app we build here is available at Github.
Preparations
This walkthrough will require Elixir 1.0.2+ as a dependency for Phoenix Framework. Please install it before you start.
Get the latest Phoenix Framework.
$ git clone https://github.com/phoenixframework/phoenix.git && cd phoenix && git checkout v0.6.2 && mix do deps.get, compile
Create a new phoenix app. This creates a new app that is configured to use Phoenix Framework. You don’t need the phoenix folder that was checked out of the repo in the previous step.
$ mix phoenix.new chatex ~/tmp/chatex
Compile and launch your app skeleton.
$ cd ~/tmp/chatex
$ mix do deps.get, compile
$ mix phoenix.start
You should see the root page at http://localhost:4000/ now.
Layouts and assets
When we created a Phoenix app, the mix task initialized directory structure for us:
config
folder is for your config fileslib
is for standalone library code- Static files go into the
priv
directory (much likepublic
in Rails) test
folder has familiar to Rails developers structure for testsweb
folder contains your web application code (much listapp
in Rails)
Throw in jquery-2.1.1.min.js and bootstrap.min.js (3.3.1) into priv/static/js
and bootstrap.min.css into priv/static/css
.
Update the layout web/templates/layout/application.html.eex
to include our new assets and clean up a bit. Here’s what I got:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
<title>Chatex</title>
<link rel="stylesheet" href="/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<%= @inner %>
</div>
<script type="text/javascript" src="/js/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="/js/bootstrap.min.js"></script>
</body>
</html>
If you are familiar with Rails layouts, you should be at home with the notion of inserting generated pages inside the broader layout file. Here we have <%= @inner %>
marking the place where the generated page content goes.
Routing
Open the web/router.ex
file. What you see shouldn’t be terribly hard to comprehend. You can read more on routes in officinal Routing guide. Let’s just define three routes.
scope "/", Chatex do
pipe_through :browser
get "/", RoomsController, :show
get "/:id", RoomsController, :show
post "/message/:room_id", MessagesController, :create, as: :new_message
end
Controllers
Let’s add Chatex.RoomsController
:
defmodule Chatex.RoomsController do
use Phoenix.Controller
@default_room "Lobby"
@doc "shows a specific room with room_id specified"
def show(conn, %{ "id" => room_id }) do
conn |> render_room room_id
end
@doc "shows a default room -- Lobby"
def show(conn, _params) do
conn |> render_room @default_room
end
# renders the room with given ID
defp render_room(conn, room_id) do
conn |> render :show, room_id: room_id
end
end
We add two show
clauses – one for the default room and one for the room with ID.
You can read more about controllers in the official controllers guide.
With that out of the way, let’s move to views. We will get back to controllers when sending actual messages.
Views
Views are slightly different from what we’ve seen in Rails. They consist of two parts – presenter module and templates. Read more on views in the official views guide and templates guide.
For now we’ll create an empty view module web/views/rooms_view.ex
:
defmodule Chatex.RoomsView do
use Chatex.View
end
And then create the template for the show action:
<div class="row">
<div class="col-sm-4">
<h2>Rooms</h2>
<ul id="rooms">
<li><a href="<%= Chatex.Router.Helpers.lobby_path(:show) %>">Lobby</a></li>
<li><a href="<%= Chatex.Router.Helpers.room_path(:show, "Help") %>">Help</a></li>
</ul>
</div>
<div class="col-sm-8">
<h1><%= @room_id %></h1>
<div id="chatbox"></div>
<div id="chatline">
<form class="form" action="<%= Chatex.Router.Helpers.new_message_path(:create, @room_id) %>">
<input type="hidden" name="room_id" id="room_id" value="<%= @room_id %>">
<div class="form-group">
<div class="row">
<div class="col-sm-4">
<input type="text" name="user" id="user" placholder="User name" class="form-control"/>
</div>
<div class="col-sm-7">
<input type="text" name="body" id="body" placholder="Message" class="form-control"/>
</div>
<div class="col-sm-1">
<input type="submit" class="btn btn-default" value="Say" />
</div>
</div>
</div>
</form>
</div>
</div>
</div>
If you restart your app at this point, you should be able to change pages by clicking on the Lobby and Help in the sidebar.
Channels
We will need some interactivity in the app. Phoenix Framework comes with built-in WebSockets and PubSup support, which are super fun to work with.
First, we define the channel in the routes and mount it on the /ws
path:
defmodule Chatex.Router do
use Phoenix.Router
use Phoenix.Router.Socket, mount: "/ws"
channel "messages", Chatex.MessagesChannel
# ...
end
Second, let’s create the web/channel/messages_channel.ex
that will be talking to the WebSockets clients:
defmodule Chatex.MessagesChannel do
use Phoenix.Channel
@doc "Called when the user connects to the room."
def join(socket, _room_id, _message) do
{ :ok, socket }
end
@doc "Called when the user disconnects."
def leave(socket, _message) do
socket
end
end
This is a bare bones channel that does literally nothing. It accepts connections to any rooms and does nothing when a user leaves.
On the client side, we need to create a JavaScript file priv/static/js/room.js
and include it in the layout file along with /js/phoenix.js
after jQuery and Bootstrap.
$(function() {
// new message form submission handler
var form = $("form");
form.on("submit", function(e) {
e.preventDefault();
var url = form.attr('action'),
body = $("#body"),
user = $("#user");
$.post(url, { message: { user: user.val(), body: body.val() } }, function(data) {
body.val('').focus();
});
});
// connection to the channel
var socket = new Phoenix.Socket("/ws");
socket.join("messages", $("#room_id").val(), {}, function(channel) {
channel.on("new:message", function(data) {
var div = $("<div class='alert alert-info'></div>").text(data.user + " said: " + data.body);
$("#chatbox").append(div);
});
});
});
Here we listen for form submissions and then sending message[user]
and message[body]
to the MessagesController#create
. We also connect to the room topic via WebSocket and listen for new messages to display.
Now let’s create MessagesController
that will broadcast our message to the members of the same room.
defmodule Chatex.MessagesController do
use Phoenix.Controller
plug :action
@doc "Broadcasts a message to the members of the #room_id."
def create(conn, %{ "room_id" => room_id, "message" => %{ "user" => user, "body" => body } }) do
Phoenix.Channel.broadcast "messages", room_id, "new:message", %{ user: user, body: body }
conn |> text "ok"
end
end
The controller handles the POST request to create a message in a certain room by broadcasting it to all users in the “messages” channel listening to topic with the ID of the room.
Try restarting the app, opening several browsers windows (some in the same room, some in different) and test how messages are delivered.
Testing
In this demo app I completely ignored the testing aspect of development for the sake of time and space. Testing deserves a separate series of posts. It’s vast territory.
Exercises to consider
- Try using
phoenix_haml
instead ofEex
- Think about how could you list the history of 10 last entered rooms in the sidebar instead of static links
- Do you think you can create a separate channel that will send
:new_room
messages whenever someone enters a new room and then update the sidebar list of rooms dynamically? - How about treating rooms as hashtags (#lobby), and broadcast messages to listeners of all hashtags found in a message ("#lobby and #help users, hello!")?
- Can you sanitize room id so that it allows only alphanumerics and dashes?
Conclusion
We haven’t even scratched the surface of what’s possible in Elixir and Phoenix Framework, but I hope you’ve got an idea of how simple it is to build interactive web apps with them.
The general idea is that it’s not a request-response infrastructure, like Rails. Your application gears are always spinning. You can have long running processes with progress reported, lengthy calculations, real-time games, dynamic interfaces and much more. All of this sits on the base of reliable multi-core-ready foundation with supervisors, processes, self-recovery and native tools for distribution, scaling and monitoring.
Have fun! Any comments or questions are welcome.