Tutorial
Send events from JS to a LiveView component
This post was updated 01 May - 2020
Let's say your app uses a JavaScript library that needs to interact with your app. For example, a LiveView component. That is possible with the built-in Phoenix Channels and Phoenix PubSub.
STEP 1 - Generate a channel
Generate a new channel called App
mix phx.gen.channel App
In user_socket.ex
add:
channel "app:*", TutorialWeb.AppChannel
Update the AppChannel
component so it looks like:
defmodule TutorialWeb.AppChannel do
use TutorialWeb, :channel
def join("app:" <> token, _payload, socket) do
{:ok, assign(socket, :channel, "app:#{token}")}
end
# can be triggered by from the frontend js by:
# channel.push('paginate', message)
def handle_in("paginate", payload, socket) do
Phoenix.PubSub.broadcast(Tutorial.PubSub, socket.assigns.channel, {"paginate", payload})
{:noreply, socket}
end
def handle_info(_, socket), do: {:noreply, socket}
end
NOTE: I want every page request to have a unique channel name. And that channel name needs to be exposed to any LiveView component. So for this purpose, I will use the csrf-token
that is generated in the page header on every request.
In JavaScript, I can do this with:
import {Socket} from "phoenix"
let socket = new Socket("/socket", {params: {token: window.userToken}})
socket.connect()
let channel = socket.channel(`app:${document.querySelector("meta[name='csrf-token']").getAttribute("content")}`, {})
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
Now, I have a channel name that looks something like:
app:FxBlXhI8Kj0WSQUrCi0vX0UIAhguFVZ9gZ5spuCEfqAg9Ou6radTlX01
So, if I send a channel event in JS to the channel, I have a handle_in
function that handles the event:
def handle_in("paginate", payload, socket) do
Phoenix.PubSub.broadcast(Tutorial.PubSub, socket.assigns.channel, {"paginate", payload})
{:noreply, socket}
end
And in this function, I use Phoenix PubSub to broadcast the event to any subscribers.
STEP 2 - Subscribe to JavaScript events
It's easy to set up a LiveView component to subscribe and handle events from Phoenix PubSub. In the [last tutorial](), I set up a LiveView component for handling pagination. I want to revisit that component and set up a subscription. Open lib/tutorialweb/live/productlist_live.ex
and in the mount
function add:
if connected?(socket), do: Phoenix.PubSub.subscribe(Tutorial.PubSub, "app:#{csrf_token}")
So it looks like:
def mount(%{"csrf_token" => csrf_token} = _session, socket) do
if connected?(socket), do: Phoenix.PubSub.subscribe(Tutorial.PubSub, "app:#{csrf_token}")
assigns = [
conn: socket,
csrf_token: csrf_token
]
{:ok, assign(socket, assigns)}
end
This means, when the component is connected, subscribe to anything that happens on the channel that corresponds to the current csrf_token
. This means that the channel will be unique per browser request.
The last piece here is to actually handle the messages we subscribe to.
Add in the same file:
def handle_info({"paginate", %{"page" => page}}, socket) do
{:noreply, live_redirect(socket, to: Routes.live_path(socket, ProductListLive, page: page))}
end
def handle_info(_, socket), do: {:noreply, socket}
STEP 3 - Send the message from JS
Now that the backend code is wired up, I can add the final piece of the JavaScript. I will visit the page: http://localhost:4000/products
and expect that I will send a pagination event after 5 seconds and navigate to page 3 http://localhost:4000/products?page=3
setTimeout(() => {
channel.push('paginate', {page: '3'})
}, 3000)
Final Result
After a few moments, the timeout in setTimeout
will kick in, and the page will live_redirect to page 3.