Skip to content

Instantly share code, notes, and snippets.

@yaycode
Last active April 29, 2023 02:57

Revisions

  1. yaycode renamed this gist Apr 11, 2016. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  2. yaycode revised this gist Apr 11, 2016. 4 changed files with 130 additions and 0 deletions.
    36 changes: 36 additions & 0 deletions app.html.eex
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,36 @@
    <!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>Hello Instachat!</title>
    <link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
    </head>

    <body>
    <div class="container">
    <header class="header">
    <nav role="navigation">
    <ul class="nav nav-pills pull-right">
    <li><a href="http://www.phoenixframework.org/docs">Get Started</a></li>
    </ul>
    </nav>
    <span class="logo"></span>
    </header>

    <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
    <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>

    <main role="main">
    <%= render @view_module, @view_template, assigns %>
    </main>

    </div> <!-- /container -->
    <script src="//code.jquery.com/jquery-1.11.3.min.js"></script>
    <script src="<%= static_path(@conn, "/js/app.js") %>"></script>
    </body>
    </html>
    2 changes: 2 additions & 0 deletions index.html.eex
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,2 @@
    <div id="messages"></div>
    <input id="chat-input" type="text"></input>
    15 changes: 15 additions & 0 deletions room_channel.ex
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,15 @@
    defmodule Instachat.RoomChannel do
    use Phoenix.Channel

    def join("rooms:lobby", _message, socket) do
    {:ok, socket}
    end
    def join(_room, _params, _socket) do
    {:error, %{reason: "you can only join the lobby"}}
    end

    def handle_in("new_message", body, socket) do
    broadcast! socket, "new_message", body
    {:noreply, socket}
    end
    end
    77 changes: 77 additions & 0 deletions socket.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,77 @@
    // NOTE: The contents of this file will only be executed if
    // you uncomment its entry in "web/static/js/app.js".

    // To use Phoenix channels, the first step is to import Socket
    // and connect at the socket path in "lib/my_app/endpoint.ex":
    import {Socket} from "phoenix"

    let socket = new Socket("/socket", {params: {token: window.userToken}})

    // When you connect, you'll often need to authenticate the client.
    // For example, imagine you have an authentication plug, `MyAuth`,
    // which authenticates the session and assigns a `:current_user`.
    // If the current user exists you can assign the user's token in
    // the connection for use in the layout.
    //
    // In your "web/router.ex":
    //
    // pipeline :browser do
    // ...
    // plug MyAuth
    // plug :put_user_token
    // end
    //
    // defp put_user_token(conn, _) do
    // if current_user = conn.assigns[:current_user] do
    // token = Phoenix.Token.sign(conn, "user socket", current_user.id)
    // assign(conn, :user_token, token)
    // else
    // conn
    // end
    // end
    //
    // Now you need to pass this token to JavaScript. You can do so
    // inside a script tag in "web/templates/layout/app.html.eex":
    //
    // <script>window.userToken = "<%= assigns[:user_token] %>";</script>
    //
    // You will need to verify the user token in the "connect/2" function
    // in "web/channels/user_socket.ex":
    //
    // def connect(%{"token" => token}, socket) do
    // # max_age: 1209600 is equivalent to two weeks in seconds
    // case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do
    // {:ok, user_id} ->
    // {:ok, assign(socket, :user, user_id)}
    // {:error, reason} ->
    // :error
    // end
    // end
    //
    // Finally, pass the token on connect as below. Or remove it
    // from connect if you don't care about authentication.

    socket.connect()

    // Now that you are connected, you can join channels with a topic:
    let channel = socket.channel("rooms:lobby", {})
    channel.join()
    .receive("ok", resp => { console.log("Joined successfully", resp) })
    .receive("error", resp => { console.log("Unable to join", resp) })

    export default socket

    // UI Code
    let chatInput = $("#chat-input");
    let messagesContainer = $("#messages");

    chatInput.on("keypress", event => {
    if(event.keyCode === 13){
    channel.push("new_message", {body:chatInput.val()});
    chatInput.val("");
    }
    });

    channel.on("new_message", payload => {
    messagesContainer.append(`<br/>[${Date()}] ${payload.body}`)
    })
  3. yaycode created this gist Apr 11, 2016.
    167 changes: 167 additions & 0 deletions instachat.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,167 @@
    # Instachat

    The Phoenix Framework was built with realtime communication as a first class priority. Using its built in socket handling and channels we can implement a basic, realtime chat application with little effort.

    For this video we’re going to assume that you already have Elixir and Phoenix Setup. You will not need a database as the messages will not be persisted. This tutorial is taken pretty much directly from the Phoenix Documentation.

    ## Setting up the app
    To start let’s generate a standard phoenix application:<br>
    ```bash
    $> mix phoenix.new instachat
    ```
    And get it running:

    ```bash
    $> cd instachat
    $> mix phoenix.server

    ```
    Now in a web browser hitting http://localhost:4000 should give us the phoenix start page.

    ## Setting Up the Socket
    When we ran `$> mix phoenix.new` it created a default **socket** module for us and attached it to the url `/socket`. Let's open up `lib/instachat/endpoint.ex` and check it out:
    ```ruby
    # in file: lib/instachat/endpoint.ex

    socket "/socket", Instachat.UserSocket
    ```
    This is telling Phoenix that all socket connections hitting `/socket` should be handled by the `Instachat.UserSocket` module. This **UserSocket** module is where we handle all the configuration for the socket itself like connecting and routing messages. It lives at `web/channels/user_socket.ex`. Let's open it up and have a look.

    Up at the top we see some commented out code referencing channels:
    ```ruby
    # in file: web/channels/user_socket.ex

    ## Channels
    # channel "rooms:*", Instachat.RoomChannel
    ```
    The `channel "rooms:*", Instachat.RoomChannel` line is boiler plate example code for handling messages coming over this socket. It says, send any messages that come in starting with "rooms:" and ending with anything to the **Instachat.RoomChannel** module. This is good enough for our purposes so let's uncomment that line:
    ```ruby
    # in file: web/channels/user_socket.ex

    ## Channels
    channel "rooms:*", Instachat.RoomChannel
    ```

    ## Setting up the Channel
    The channel module wasn't created for us automatically so let's create it ourselves. It is going to live at `web/channels/room_channel.ex` and here's the boilerplate:
    ```ruby
    #in file: web/channels/room_channel.ex

    defmodule Instachat.RoomChannel do
    use Phoenix.Channel
    end
    ```
    The first thing a channel needs to do is handle connections. We do this by implementing a function called **join** that either returns `{:ok, socket}` on a successful join or `{:error, message}` otherwise. Let's write code that lets users join only if they try to join the lobby, otherwise we'll deny them:
    ```ruby
    #in file: web/channels/room_channel.ex

    defmodule Instachat.RoomChannel do
    use Phoenix.Channel
    def join("rooms:lobby", _message, socket) do
    {:ok, socket}
    end
    def join(_room, _params, _socket) do
    {:error, %{reason: "you can only join the lobby"}}
    end
    end
    ```
    ## Connecting From Javascript
    The boilerplate javascript for connecting to our socket from a web browser has already been written for us but is not being loaded by default. If we open up `web/static/js/app.js` and look down at the bottom we can see that the code to do this is commented out. Let's un-comment that line:
    ```javascript
    //in file: web/static/js/app.js

    import socket from "./socket"
    ```
    Now with our web browser pointed to http://localhost:4000/ and the developer console open we can see the message:
    ```javascript
    Unable to join Object {reason: "unmatched topic"}
    ```
    This is because our javascript is trying to connect to our socket over a **topic** that we aren't handling. Let's open up the javascript and set it to the right topic. This javascript file lives at `web/static/js/socket.js` and the code in concern is down at the bottom:
    ```javascript
    //in file: web/static/js/socket.js

    // Now that you are connected, you can join channels with a topic:
    let channel = socket.channel("topic:subtopic", {})
    ```
    This code is trying to connect to a channel called "topic" with a sub-topic of "subtopic" but we want to connect to "rooms:lobby" Let's go ahead and change that:
    ```javascript
    //in file: web/static/js/socket.js

    // Now that you are connected, you can join channels with a topic:
    let channel = socket.channel("rooms:lobby", {})
    ```
    And now if we check in our browser's console we should see:
    ```javascript
    Joined successfully Object {}
    ```
    This means that we've both connected to the Socket and Joined the Channel

    ## Adding the HTML
    To interact with the chat we're going to need some user interface. Let's add places to input and display messages. Open up `web/templates/page/index.html.eex` and replace its entire contents with:
    ```html
    <!-- in file: web/templates/page/index.html.eex -->

    <div id="messages"></div>
    <input id="chat-input" type="text"></input>
    ```

    ## Hooking up the HTML
    For this demo we're gonna keep it simple and use jQuery. Let's add a CDNd version to the application layout which is located at `web/templates/layout/app.html.eex` right above the application js file:
    ```html
    <!-- in file: web/templates/layout/app.html.eex -->

    </div> <!-- /container -->
    <!-- add the following line -->
    <script src="//code.jquery.com/jquery-1.11.3.min.js"></script>
    <script src="<%= static_path(@conn, "/js/app.js") %>"></script>
    </body>

    ```

    Back in the javascript file for working with this socket let's add some code to hook up the HTML we just added. Down at the bottom of the file add:
    ```javascript
    // in file: web/static/js/socket.js

    // UI Stuff
    let chatInput = $("#chat-input");
    let messagesContainer = $("#messages");

    chatInput.on("keypress", event => {
    if(event.keyCode === 13){
    channel.push("new_message", {body:chatInput.val()});
    chatInput.val("");
    }
    });
    ```
    All this code does is call the **push** method on **channel** when we press the enter key. It gives **push** two arguments, an event name of "new_message" and a payload which is an object containing our message. Channel is going to send this back to our phoenix app. So let's handle it.

    ## Handling Channel Events
    Back in our **RoomChannel** module we need to handle events coming in and broadcast them to all our connected clients. All we have to do is implement a **handle_in** function. Let's add it below our **join** functions:
    ```ruby
    # in file: web/channels/room_channel.ex

    def handle_in("new_message", body, socket) do
    broadcast! socket, "new_message", body
    {:noreply, socket}
    end
    ```
    We can see that we're pattern matching on events with the name of "new_message", then we simply broadcast the message out to all our connected clients, and we return `{:noreply, socket}` which is one of the required return values of **handle_in** and means that the client that sent the message doesn't get anything back from our channel directly. Now we need to receive the broadcast from our Javascript and display the message.
    ## Receiving Events in Javascript
    Back in our Javascript file we need to look out for our "new_message" event and update the messages display when we get one. Down at the bottom of `web/static/js/socket.js` lets add:
    ```javascript
    // in file: web/static/js/socket.js

    channel.on("new_message", payload => {
    messagesContainer.append(`<br/>[${Date()}] ${payload.body}`)
    })
    ```
    This code simply tells the channel to look out for events named "new_message" and to run a function that adds the payload's body to the messages container when we get one. That's it, we should be all done! Let's open up the browser and give it a try.

    ## Testing
    Pointing our browser to http://localhost:4000/ , typing something into the input, and pressing enter we should now see the chat working. If we open up another tab we should be able to see any new messages in both tabs and in fact any connected web browsers should be able to see any new messages!

    ## Wrapping it up
    Phoenix makes it almost dead simple to write realtime applications for the modern web. With sockets we can handle routing of clients to channels and with channels we can handle receiving and broadcasting events to and from clients with ease. And we get to write this all with the power and clarity of Elixir!