Subscribe to access all episodes. View plans →
Published March 19, 2018
Elixir 1.5
Phoenix 1.3
View source on GitHub
Phoenix is the most popular Framework for web development in Elixir.
And one of it’s most exciting parts is the ability to add realtime functionality, which we can do with Channels.
In this episode let’s see how can build a simple chat application with Phoenix using websockets.
Let’s start this from project from the ground-up, so the first thing we’ll need is a fresh Phoenix app - let’s create a new one with the very unoriginal name ‘chat’.
$ mix phx.new chat
And we’ll install the dependencies.
Then we’ll cd into the ‘chat’ directory and create our database:
$ mix ecto.create
And let’s start our app to make sure everything looks good.
$ mix phx.server
And great - we see the familiar “Welcome to Phoenix!” page.
Now that we know everything is working let’s get started on building our chat room.
We’ll need to generate a new channel.
Phoenix provides a generator we can use, we’ll just need to give it a name. Let’s play with the idea of a chat-room and call ours water_cooler
$ mix phx.gen.channel water_cooler
And it creates a water_cooler_channel.ex
module
We’ll look at it in a minute, but first let’s add our channel to the user socket handler.
We’ll send any events with the water_cooler
topic to our new ‘WaterCoolerChannel’
The asterisk is a wildcard, catching any events coming into the water_cooler topic - regardless of the subtopic - and sending them to our WaterCoolerChannel
lib/chat_web/channels/user_socket.ex
defmodule ChatWeb.UserSocket do
use Phoenix.Socket
##channels
channel "water_cooler:*", ChatWeb.WaterCoolerChannel
...
end
With that updated let’s open the water_cooler_channel.ex
that we generated.
It has a join
callback that will handle events for the water_cooler:lobby
topic/subtopic.
In this example that Phoenix creates, it’s giving us an authorized?
function that will always return true. It’s a nice guide for where we could put authorization logic.
In our example we won’t do any authorization, so let’s remove the logic and have it always return the OK tuple with our socket. And since we won’t be using the payload
we can ignore it with the underscore.
The handle_in
ping function we wont use, so let’s remove it.
The handle_in
shout function is in charge of broadcasting any chat events to everyone that’s joined our channel.
Notice that this is set to pattern match on “shout” - we’ll need to remember to use this when we push events out from the client .
And we’ll remove the authorized?
function since we removed our authorization logic in the join
callback .
lib/chat_web/channels/water_cooler_channel.ex
defmodule ChatWeb.WaterCoolerChannel do
use ChatWeb, :channel
def join("water_cooler:lobby", _payload, socket) do
{:ok, socket}
end
#It is also common to receive messages from the client and
#broadcast to everyone in the current topic (water_cooler:lobby).
def handle_in("shout", payload, socket) do
broadcast socket, "shout", payload
{:noreply, socket}
end
end
Now let’s go to our assets/js/socket.js
And we see some code has already been populated that imports and connects our socket to the socket path in our endpoint.ex
module which is then handled by our UserSocket
.
Our default code also shows how we can join channels with a topic. The topic we defined in our WaterCoolerChannel is water_cooler:lobby
so let’s update that here.
It then logs out a message to our browser’s console letting us know if we’ve joined successfully or not.
assets/js/socket.js
...
let channel = socket.channel("water_cooler:lobby", {})
...
Then we’ll open app.js
and import our socket.
assets/js/app.js
import "phoenix_html"
import socket from "./socket"
Now let’s go to the command line and start our server.
$ mix phx.server
Let’s give it a shot. We’ll open our browser and in the console - we see our success message is printed.
Now that we’re joining successfully let’s create a chat window and form we’ll use to post messages with.
But Let’s first customize our site a bit.
We’ll remove the phoenix.css
stylesheet. And in the app.css
I’ll paste in some custom css, which is available in the linked GitHub repo.
Then we’ll open our app.html.eex
template and let’s remove the Phoenix logo.
Template path: lib/chat_web/templates/layout/app.html.eex
...
<div class="container">
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
...
Then in our page’s index.html.eex
template we’ll paste in the chat window and and form that will have a username and message field.
Template path: lib/chat_web/templates/page/index.html.eex
<h2>The Water Cooler</h2>
<div id="chat-box">
</div>
<form id="chat-form">
<input type="text" placeholder="Username" id="user-name"></input>
<textarea placeholder="Your comment" id="user-msg"></textarea>
<button type="submit">Post</button>
</form>
Then let’s open our browser and see what it looks like.
And we see our chat box and form.
Now let’s add some JavaScript to get our chat working, so that when we post a message it’s displayed.
We’ll create a new file in our assets/js
named water_cooler.js
And we’ll create our WaterCooler
object.
The we’ll create a function init
that will take our socket.
And with our socket we’ll join the channel with our “water_cooler:lobby” topic. Now that we’ve joined our channel, we need to send and receive events.
Let’s create a new function listenForChats
that will take our channel.
I’ll paste in our code for sending events.
But let’s walk through this.
We’re get our chat form with the id chat-form
and then listening for when it’s submitting.
When the form is submitted we’re preventing any further action.
then we’re getting the username from the form
And the message from the form.
Then we’re calling channel.push with “shout” and payload of our username and the message. This will send our event to the server, where it will be picked up by our WaterCoolerChannel
.
Now that we’re setup to send events, we need to receive them.
We’ll use channel.on to subscribe to channel events, matching on “shout”
Then we’ll get our chatBox
And our create a new message block
Then we’ll build up our message by getting the name and message from the received payload.
We’ll then append that message to our chatBox.
assets/js/water_cooler.js
let WaterCooler = {
init(socket) {
let channel = socket.channel('water_cooler:lobby', {})
channel.join()
this.listenForChats(channel)
},
listenForChats(channel) {
document.getElementById('chat-form').addEventListener('submit', function(e){
e.preventDefault()
let userName = document.getElementById('user-name').value
let userMsg = document.getElementById('user-msg').value
channel.push('shout', {name: userName, body: userMsg})
document.getElementById('user-name').value = ''
document.getElementById('user-msg').value = ''
})
channel.on('shout', payload => {
let chatBox = document.querySelector('#chat-box')
let msgBlock = document.createElement('p')
msgBlock.insertAdjacentHTML('beforeend', `${payload.name}: ${payload.body}`)
chatBox.appendChild(msgBlock)
})
}
}
export default WaterCooler
Now let’s open our app.js
And we’ll import WaterCooler
Then we’ll call init, passing in our socket to use.
assets/js/app.js
...
import WaterCooler from "./water_cooler"
WaterCooler.init(socket)
And if we go back to our browser and try typing a message.
We see our message is received and broadcasted out to everyone that’s joined.
And let’s try sending a message back - and that works too!
This is great, but when we refresh the page - our chat disappears because it’s not being persisted.
We’ll fix that by saving our chats to the database.
Let’s have Phoenix do the heavy lifting for us here and use the context generator to build out the modules and database migration that we’ll need.
We’ll go to the command line - and we’ll stop our server.
And let’s our new context ‘Chats’
our ecto schema module will be ‘Message’
And our table will be messages with a name column that’s a string
and body column that’s a text.
$ mix phx.gen.context Chats Message messages name:string body:text
Then we’ll migrate our database.
$ mix ecto.migrate
And let’s take a quick look at the message module that was created.
We see our schema with our body and name fields.
and our changeset.
then let’s open the chats context
And it’s been populated with some functions to save and get our messages.
Great, now we need to save our messages when someone posts one in the chat. Let’s do that in our channel.
Now we’ll open our water_cooler_channel.ex
Then let’s alias our ‘chats’ context.
And in our handle_in
function we’ll save the message before it’s broadcasted out.
Now we could make this asynchronous, but let’s keep it simple for the purposes of this episode.
lib/chat_web/channels/water_cooler_channel.ex
...
alias Chat.Chats
...
def handle_in("shout", payload, socket) do
Chats.create_message(payload)
broadcast socket, "shout", payload
{:noreply, socket}
end
...
Now that we’re saving our messages we need to load any existing ones into the chat.
Let’s open our page_controller.ex
We’ll alias our Chat.Chats
module
Then let’s get all our messages
And we’ll pass them in our assigns.
lib/chat_web/controllers/page_controller.ex
defmodule ChatWeb.PageController do
use ChatWeb, :controller
alias Chat.Chats
def index(conn, _params) do
messages = Chats.list_messages()
render conn, "index.html", messages: messages
end
end
Now we can open our index.html.eex
template and render any messages that were returned in our chat box.
Template path: lib/chat_web/templates/page/index.html.eex
<h2>The Water Cooler</h2>
<div id="chat-box">
<%= for message <- @messages do %>
<p><b><%= message.name %>:</b> <%= message.body %></p>
<% end %>
</div>
...
Then let’s restart our server.
And go back to the browser and post some messages.
Then if we do a quick check of the database - we see our messages are there.
And if we go back to our chat and refresh the page - we see our existing messages are loaded.
Bryan Bryce
6 years agoWhat program did you use to check the database?
Alekx
6 years agoHey Bryan, I used Postico.