Elixir Phoenix PubSub Tutorial

From ElixirBlocks
Revision as of 01:07, 24 August 2023 by Admin (talk | contribs)
Jump to: navigation, search

This page is in progress


This tutorial explains the basics of building a Phoenix PubSub chat app.

The goal of this writing is to explain how to write code used to build PubSub applications in Phoenix. I tried to write examples that keep code writing to a minimum. I believe minimalist examples are the best approach toward comprehension.

The tutorial is divided into three parts, each part building on the completion of the previous one.

Project Description

  • Part 1: We create a two route live view app with each route assigned its own page. The two routes are /send and /receive.

The page at /send contains an html form and form submission code. When data is submitted, the /receive page receives the data, and renders it to its page. The code we write for this exercise uses pubsub functions to connect the two LiveView pages.

When complete, you will have two browser tabs open, one opened to /send and one opened to /receive. The /send route sends data to /receive and you view the update in real time.

  • Part 2: We convert the previous app into a single LiveView page that performs both actions on a single route. The end result is the creation of a single page chat application containing real time updates that you view across browser tabs.
  • Part 3: We write code to update your single page app to store chat data in a database.



Part 1

Setup

To begin we will start with an new Phoenix instance. In your terminal create a new phoenix app by typing:

mix phx.new app

When the console prompts you to Fetch and install dependencies, choose yes.

When you see the following instructions configure your database, run mix ecto.create.



We are almost there! The following steps are missing:

    $ cd app

Then configure your database in config/dev.exs and run:

    $ mix ecto.create

Start your Phoenix app with:

    $ mix phx.server

You can also run your app inside IEx (Interactive Elixir) as:

    $ iex -S mix phx.server



If you are new to Elixir and are having trouble with database setup, please fix those issues before proceeding with this tutorial.

When the app is created, open your web browser to localhost:4000. The app will launch. If it doesn't, fix the errors before moving forward.


Create Routes

Go to the router and update it as shown.

directory: app/app_web/router.ex

  scope "/", AppWeb do
    pipe_through :browser
    live "/send", SendLive, :home
    live "/receive", ReceiveLive, :home
    get "/", PageController, :home
  end

Create the LiveView Pages

Create a new folder named live in this directory: App/lib/app_web. The end result will look like this: App/lib/app_web/live.

In the live directory, create two files and name them send_live.ex and receive_live.ex.


Edit Send Live.ex

Open the file send_live.ex in your text editor. Paste the following code into it.

defmodule AppWeb.SendLive do
  use AppWeb, :live_view

  def mount(_params, _session, socket) do
    {:ok, socket}
  end


  def render(assigns) do
    ~H"""
    <div>
      <h1>Send Connection</h1>

     
    </div>
    """
  end
end

Edit Receive_Live.ex

Open the file named receive_live.ex and paste the following code into it:


defmodule AppWeb.ReceiveLive do
  use AppWeb, :live_view

  def mount(_params, _session, socket) do
    {:ok, socket}
  end

  def render(assigns) do
    ~H"""
    <div>
      <h1>Receive Connection</h1>

   
    </div>
    """
  end
end

Validate Your Work

Ensure the code you wrote has no errors by running the server and checking each route.

mix phx.server

Go to:

localhost:4000/send

localhost:4000/receive

The pages should load without error.

Creating a Form and its Event Handler

The first step to send data via a form is to create the form and write an event handler to listen and receive data from the form. You should have enough prerequisite knowledge to know how to do this. If you don't, read this tutorial before you proceed:

Forms and Event Handlers in Elixir Phoenix

Update the send_live.ex to reflect the code below:



defmodule AppWeb.SendLive do
  use AppWeb, :live_view
  
  def mount(_params, _session, socket) do
    {:ok, socket}
  end
  
  
  def handle_event("send", %{"text" => text}, socket) do
    IO.inspect text
    {:noreply, socket}
  end
  


  def render(assigns)do   
   ~H"""
<div>
  <h1>Send Message</h1>

  <form phx-submit="send">
    <input type="text" name="text" />
    <button type="submit">Send</button>
  </form>
</div>

"""
  end
  
end

Ensure it works by submitting form data. In the terminal console you will see the output.

Before We Add the PubSub Code

You are now going to perform these actions.

1. Add code designed to broadcast a change

2. Add code designed to subscribe to that change

Both of these code pieces act similar to event handlers/listeners in that they listen to events that the developer designates and performs an action in response.

Both broadcast code and subscribe code can be placed in different parts of your codebase depending on your goal.

For this exercise you write the broadcast code to your SendLive module and write the subscribe code to your ReceiveLive module.

Add the PubSub Broadcast Code to SendLive Module



defmodule AppWeb.SendLive do
  use AppWeb, :live_view
  
  def mount(_params, _session, socket) do
    {:ok, socket}
  end
  
  
  def handle_event("send", %{"text" => text}, socket) do
    IO.inspect text
    AppWeb.Endpoint.broadcast(topic, "message", text) # Broadcast
    {:noreply, socket}
  end
  

  defp topic do #Topic
    "chat"
  end


  def render(assigns)do   
   ~H"""
<div>
  <h1>Send Message</h1>

  <form phx-submit="send">
    <input type="text" name="text" />
    <button type="submit">Send</button>
  </form>
</div>

"""
  end
  
end

Add the PubSub Subscribe Code to ReceiveLive Module



defmodule AppWeb.ReceiveLive do
  use AppWeb, :live_view
  
  def mount(_params, _session, socket) do


    if connected?(socket) do
      AppWeb.Endpoint.subscribe(topic)     # PupSub Subscribe
    end

    {:ok, assign(socket, messages: "")}
  end
  
  def handle_info(%{event: "message", payload: message}, socket) do  # Handle Ifno is needed
    IO.inspect message
    {:noreply, assign(socket, messages: message)}
  end
  

  defp topic do  # Topic
    "chat"
  end

  def render(assigns)do   
   ~H"""
     <div>
    <h1>ChatLive</h1>
    <%= @messages %>


    </div>

     """
  end
  
end

PubSub Code Result and Explanation

Open the app in two browser tabs and set one to /send and one to /receive. Submit a form in /send and you will see the result in /receive

SendLive PubSub Code Explained

The SendLive module contains this code:

 AppWeb.Endpoint.broadcast(topic, "message", text)

In all default Phoenix applications the PubSub code is in the module named Endpoint. The above example is of a PubSub function named broadcast. When working with

 AppWeb.Endpoint 

the function you use with either be

subscribe

or

broadcast

.

NameOfApp.Endpoint.a_broadcast_or_subscribe_function_goes_here

The broadcast function has three arguments. In our examples they are named topic, "message" and text.

 AppWeb.Endpoint.broadcast(topic, "message", text)

The first argument is the name of the topic. The topic is what connects the broadcast and the subscriber. You can have many broadcasters and subscribers all with different topics.

A topic is a string that acts as an ID to connect the broadcast and subscriber. In our example the topic is "chat" and is referenced in a private function named topic.


  defp topic do #Topic
    "chat"
  end

The code will work if you place the string in the broadcast function directly, like this:

AppWeb.Endpoint.broadcast("chat", "message", text) 

The second argument creates another connection similar to the first argument. The second argument connects your broadcast function to an additional

handle_info

function. The handle_info function (in this context) gives you the ability to change state and affect your app in some way.



The handle_info function is not specific to PubSub. It is a function used with Elixir GenServers. For this article I suggest you ignore this fact and simply look at handle_info as a tool to configure a PubSub application. If you want a more in depth explanation it will not be given here.



Keep a mental place holder to remember the following statement:


The PubSub broadcast function needs to connect to two other functions.

  • Subscribe
  • Handle_info


The third argument is the data you want to transmit via the web socket connection. In the example code below the data is a variable named text. This variable could be user submitted form data, data from a database or any other data. It can be in the form of a list , string, map etc.

 AppWeb.Endpoint.broadcast("chat", "message", text)


ReceiveLive PubSub Code Explained

To respond to the broadcast message the app needs 2 things to listen for its transmission.

  • subscribe function
  • handle_info function

Say it again, when you broadcast a PubSub message you need two things to listen for the broadcast transmission.

  • subscribe function
  • handle_info function

Subscribe

The subscribe function gives the module the ability to listen to the broadcast function with the same topic. Without it, the handle_info function will not respond to broadcast changes.

You can look at the code below as boiler plate needed to enable the handle_info function to listen for the broadcast message.

 if connected?(socket) do
      AppWeb.Endpoint.subscribe(topic)     # PupSub Subscribe
    end

The topic is referenced from a private function but it doesn't have to be. You can hardcode the topic and it will work.

    if connected?(socket) do
      AppWeb.Endpoint.subscribe("chat")     # PupSub Subscribe
    end


Handle Info Function

  def handle_info(%{event: "message", payload: message}, socket) do  # Handle Info is needed
    IO.inspect message
    {:noreply, assign(socket, messages: message)}
  end

As previously mentioned, this function responds to the invoking of this function:

AppWeb.Endpoint.broadcast("chat", "message", text)

These two functions are connected via the string "message". The handle_info function has two arguments The first argument is a map that contains two keys.

%{event: "message", payload: message}

The first key is named event. This is assigned the same string as the second argument of:

AppWeb.Endpoint.broadcast("chat", "message", text)


The second key of the map is the payload data.


%{event: "message", payload: message}

The payload is the data that has been received via the third argument of the broadcast function. In this case, it is named text.

AppWeb.Endpoint.broadcast("chat", "message", text)