Channels

Now that you have your app up and running we can start adding functionality. The first thing out chat app needs to be able to do is communicate in Phoneix. This is done via a Channel which uses websockets. Websockets create connections similar to pipes or land line telephones being that they are always open or available. The other forms of communication that are typically used on the web are more like sending snail mail-- a message is sent, the message is received and then a reply is sent. An example of this would be HTTP.

Setting up Our Channels

Phoenix as of 1.2, includes a built-in mix task for channels that will create files and do some of the boilerplate work for you. We will need to pass a name into our command, so we will follow the Slack theme, and call it random. Look out for any actions that running the command asks you to complete.

$ mix phoenix.gen.channel random

In web/channels/user_socket.ex add the following below the `## Channels` comment.

## Channels
# channel "room:*", Slackir.RoomChannel
channel "random:lobby", Slackir.RandomChannel # Add this line

In our web/channels/random_channel.ex paste the following

defmodule Slackir.RandomChannel do
  use Slackir.Web, :channel

  def join("random:lobby", payload, socket) do
    if authorized?(payload) do
      {:ok, socket}
    else
      {:error, %{reason: "unauthorized"}}
    end
  end

  # Channels can be used in a request/response fashion
  # by sending replies to requests from the client
  def handle_in("ping", payload, socket) do
    {:reply, {:ok, payload}, socket}
  end

  # It is also common to receive messages from the client and
  # broadcast to everyone in the current topic (random:lobby).
  def handle_in("shout", payload, socket) do
    broadcast socket, "shout", payload
    {:noreply, socket}
  end

  # Add authorization logic here as required.
  defp authorized?(_payload) do
    true
  end
end

Now that we have generated some code let's look at what it does. First the random_channel.ex is the channel equivalent of a controller in Phoenix, being that it handles websocket requests instead of http requests. On line 2 :channel is referring to the Phoenix channel method. It takes a parameter which is used as a namespace for the websocket connections. In this instance the name space is "random:lobby".

The next argument Slackir.RandomChannel is made up of two parts. The first part Slackir is the name of the app. The second part RandomChannel is the name of the module that handles the elixirgirls channel. In Phoenix, it’s conventional to namespace all your modules with your app name first as this prevents namespace errors.

The file, user_channels.ex, is like the router.ex file, but for channels. The topic, for us it's elixirgirls, allows clients, also known as web browsers, to subscribe to the channel. In the same way a path in a web request directs the request to the right controller, the topic directs the socket to the right module, in this case, Slackir.RandomChannel.

Now, let’s look at the join method. def join ("random:lobby", payload, socket) do: takes three arguments. The first argument, the name of the channel, "random:lobby", makes it so that this method is only called when the client is joining the specific channel. The second argument, payload is the request from the user; it can contain auth credentials, a message or anything the user wants. Finally, it takes socket, which is the websocket connection. There’s a test, authorized?, which always returns true. Then, {:ok, socket} just returns a status and the websocket they are connecting to. join always returns {:ok, socket} to allow all connections to the channel.

Once a user joins a channel they can send messages to the channel and they’ll be received by one of the handle_in messages. The first one just returns the payload sent by the user back to them. The second one sends the payload to all the users subscribed to the channel. It’s the second that allows us to build our chat room with just a little front end work. When you send a message to the chat room, this method sends it back to you and to everyone else in the channel.

To handle the connections on the Client-side we’ll start by adding jQuery to our web/templates/layouts/app.html.eex:

<script src="//code.jquery.com/jquery-1.12.4.min.js"></script>

Phoenix comes 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:

import socket from "./socket"

Then go into your web/static/js/socket.js and paste the following:

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

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

channel.on('shout', 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) })

socket.channel("random:lobby", {}) sends the join request to the server, and the server sends the message back to the client. 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.

Next we will want to create the container for our messages: Open 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>
</div>

What we have done here is create an empty div that will list all the chat messages and added two text fields, one for the user’s name and one for the message. Now you will want to open web/static/css/app.css and paste this at the end:

#message-list {
  border: 1px solid #777;
  height: 400px;
  padding: 10px;
  overflow: scroll;
  margin-bottom: 50px;
}