Introduction to Phoenix and Elixir
If you’re venturing into the world of real-time web applications, you’re in the right place. Elixir and Phoenix are a dynamic duo that can help you build a robust and scalable chat application. In this article, we’ll dive into the nitty-gritty of creating a chat app using these powerful tools.
Why Elixir and Phoenix?
Elixir, with its asynchronous programming model, is perfect for handling concurrent requests, making it an ideal choice for real-time applications. Phoenix, built on top of Elixir, provides a robust framework for web development, including excellent support for WebSockets and PubSub (Publish/Subscribe) functionality.
Setting Up Your Project
To get started, you need to have Elixir and Phoenix installed on your machine. Here’s how you can create a new Phoenix project:
mix phx.new chatter --no-ecto
cd chatter
mix deps.get
mix ecto.create # If you decide to use Ecto, otherwise skip this step
The --no-ecto
flag is used if you don’t want to include the Ecto database library in your project. However, for a more complete chat application, you’ll likely want to include Ecto to store chat messages persistently.
Creating the Channel
In Phoenix, channels are used to handle real-time communication. Let’s create a channel for our chat room:
mix phx.gen.channel chat_room
This command generates the necessary files for your channel. You’ll need to add the channel to your user_socket.ex
file:
defmodule ChatterWeb.UserSocket do
use Phoenix.Socket
channel "chat_room:lobby", ChatterWeb.ChatRoomChannel
# ...
end
The chat_room:lobby
topic allows clients to subscribe to the channel, similar to how a path in a web request directs to a specific controller.
Setting Up the Channel Module
Open the generated chat_room_channel.ex
file and add the necessary functions to handle messages:
defmodule ChatterWeb.ChatRoomChannel do
use ChatterWeb, :channel
def join("chat_room:lobby", _params, socket) do
{:ok, socket}
end
def handle_in("new_msg", %{"body" => body}, socket) do
broadcast(socket, "new_msg", %{body: body})
{:noreply, socket}
end
end
This code sets up the channel to join and broadcast new messages.
Creating the View
Now, let’s create the HTML template for our chat app. Open lib/chatter_web/templates/page/index.html.eex
and replace its contents with:
<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>
<button id='send-btn'>Send</button>
</div>
<script>
let socket = new Phoenix.Socket("/socket", {params: {token: window.userToken}});
socket.connect();
socket.onConnect(() => console.log("Connected"));
socket.onDisconnect(() => console.log("Disconnected"));
socket.onMessage("new_msg", payload => {
let messageList = document.getElementById('message-list');
let message = document.createElement('div');
message.innerText = payload.body;
messageList.appendChild(message);
});
document.getElementById('send-btn').addEventListener('click', () => {
let message = document.getElementById('message').value;
socket.push("new_msg", {body: message});
document.getElementById('message').value = '';
});
</script>
This script sets up a basic chat interface and connects to the Phoenix socket.
Adding CSS
To make our chat app look a bit more presentable, add some CSS to assets/css/app.css
:
#message-list {
border: 1px solid #777;
height: 400px;
padding: 10px;
overflow: scroll;
margin-bottom: 50px;
}
Using Phoenix LiveView for Real-Time Updates
For a more advanced and seamless real-time experience, you can use Phoenix LiveView. Here’s how you can set it up:
Project Setup with LiveView
If you’re starting from scratch, create a new Phoenix project with LiveView support:
mix phx.new liveview_chat --no-ecto
cd liveview_chat
mix deps.get
Creating the LiveView Module
Generate the necessary files for your LiveView:
mix phx.gen.live Chat ChatLive.Chat
Edit lib/liveview_chat_web/router.ex
to add a route for your chat page:
defmodule LiveviewChatWeb.Router do
use LiveviewChatWeb, :router
scope "/", LiveviewChatWeb do
pipe_through :browser
live "/chat", ChatLive.Chat
end
end
Setting Up the LiveView Controller
In lib/liveview_chat_web/live/chat_live.ex
, define the mount
and handle_event
functions:
defmodule LiveviewChatWeb.ChatLive.Chat do
use Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, assign(socket, :messages, [])}
end
def handle_event("send", %{"text" => text}, socket) do
messages = socket.assigns.messages ++ [text]
{:noreply, assign(socket, :messages, messages)}
end
def render(assigns) do
~H"""
<div id='message-list'>
<%= for message <- @messages do %>
<div><%= message %></div>
<% end %>
</div>
<div class='row form-group'>
<div class='col-md-9'>
<input type='text' id='message' class='form-control' placeholder='Message' />
</div>
<button phx-click='send' phx-value-text='#{gettext("Send")}' >Send</button>
</div>
"""
end
end
Handling Events and Rendering
The handle_event
function handles the “send” event, and the render
function renders the messages.
Persistence Layer
To make your chat app more robust, you’ll want to add a persistence layer. Here’s how you can do it using Ecto:
Generating the Schema and Migration
Generate the schema and migration for your messages:
mix phx.gen.schema Message messages name:string message:string
mix ecto.migrate
Updating the Schema
Update the lib/liveview_chat/message.ex
file to include functions for creating and listing messages:
defmodule LiveviewChat.Message do
use Ecto.Schema
import Ecto.Changeset
schema "messages" do
field :name, :string
field :message, :string
timestamps()
end
def changeset(message, attrs) do
message
|> cast(attrs, [:name, :message])
|> validate_required([:name, :message])
end
def create_message(attrs) do
%Message{}
|> changeset(attrs)
|> LiveviewChat.Repo.insert()
end
def list_messages do
LiveviewChat.Repo.all(Message)
end
end
Integrating with LiveView
Update your LiveView module to use the persistence layer:
defmodule LiveviewChatWeb.ChatLive.Chat do
use Phoenix.LiveView
def mount(_params, _session, socket) do
messages = LiveviewChat.Message.list_messages()
{:ok, assign(socket, :messages, messages)}
end
def handle_event("send", %{"text" => text, "name" => name}, socket) do
case LiveviewChat.Message.create_message(%{name: name, message: text}) do
{:ok, _message} ->
messages = LiveviewChat.Message.list_messages()
{:noreply, assign(socket, :messages, messages)}
{:error, _changeset} ->
{:noreply, socket}
end
end
def render(assigns) do
~H"""
<div id='message-list'>
<%= for message <- @messages do %>
<div><%= message.name %>: <%= message.message %></div>
<% end %>
</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-6'>
<input type='text' id='message' class='form-control' placeholder='Message' />
</div>
<button phx-click='send' phx-value-text='#{gettext("Send")}' phx-value-name='#{gettext("Name")}' >Send</button>
</div>
"""
end
end
Sequence Diagram for Real-Time Messaging
Here’s a sequence diagram to illustrate the flow of real-time messaging using Phoenix and LiveView:
Conclusion
Building a chat application with Phoenix and Elixir is a rewarding experience that leverages the strengths of both technologies. From setting up channels and LiveView to adding a persistence layer, this guide has walked you through the key steps to create a robust and real-time chat app. Whether you’re a seasoned developer or just starting out, Phoenix and Elixir offer a powerful combination for building scalable and interactive web applications.
Happy coding, and remember, in the world of real-time web development, every message counts