Tutorial
Send events from JS to a LiveView component
This post was updated 01 May - 2020
csrfliveviewphoenixjavascript
Let say you 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
Generata 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 look 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 fot this purpose, I will use the `csrf-token` than is generated in the page header on every request.
In javascript, I can to 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
Its easy to set up the 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/tutorial_web/live/product_list_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 curren `csrf_token`. This means, that if 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 time out in `setTimeout` will kick in and the page will live_redirect to page 3
