-
Notifications
You must be signed in to change notification settings - Fork 2
Description
[EDITED]
"Almost" straight from the documentation.
Almost because of the bonus of having a reactive banner on each LiveView.
The banner is rendered in each LiveView (just a different "module_id" assign, which is NOT a dom_id)
<Users.display user_id={@user_id} module_id="users-map" />the
user_idcomes from themount/3
The PhoenixComponent "Users" contains a DOM element that will be filled by JS, where you set phx-udpate="ignore" since the innerContent is managed by the reactive component.
defmodule LiveviewPwaWeb.Users do
use Phoenix.Component
@moduledoc false
attr :user_id, :string, required: true
attr :module_id, :string, required: true
def display(assigns) do
~H"""
<div>
<br />
<p class="text-sm text-gray-600 mt-4 mb-2">
User ID:
<span
id="user-id"
class="inline-flex items-center px-2 py-1 text-xs font-medium border border-midnightblue text-midnightblue rounded-full"
>
{@user_id}
</span>
</p>
<p id={@module_id} phx-update="ignore"></p>
<br />
</div>
"""
end
endUse a Channel
Server-side, you just:
Presence.trackon the "user-id" when it mounts (on each navigation to a given page)pushthe "user-id" to the client. This is because I wanted to isolate the current user from the other users in the Presence.list- get the
Presence.listandpushit to the connected client(s)
defmodule LiveviewPwa.PresenceChannel do
use Phoenix.Channel
alias LiveviewPwa.Presence
require Logger
@moduledoc false
@impl true
def join("proxy:presence", _params, socket) do
send(self(), :after_join)
{:ok, socket}
end
@impl true
def handle_info(:after_join, socket) do
user_id = socket.assigns.user_id
{:ok, _} = Presence.track(socket, user_id, %{id: user_id})
send(self(), :push_users)
{:noreply, socket}
end
def handle_info(:push_users, socket) do
users = Presence.list(socket)
# pass the user_id to the channel
:ok = push(socket, "user", %{from: socket.assigns.user_id})
# push the presence state to the channel
:ok = push(socket, "presence_state", users)
{:noreply, socket}
end
endIn the main JS script ("app.js"), call this function that targets given ids (the "module_id" defined above, set in ) to display the presence list.
It contains the client-side logic of the Channel. The difficulty here is that you want to support navigation, meaning your banner with the users logged-in is present and reactive on each page.
- a listener
channel.on("user")(to detect the current user) - a listener on navigation (event "phx:page-loading-stop" to a page that uses this channel.
- a
new Presenceobject that contains thepresence.onSynclistener to react on Presence changes when the Channel is connected - a rendering JS component (named
MountUsershere). It receives the updated "userIDs", the current user ID and the DOM element ID on where to mount)
import { CONFIG } from "@js/main";
export async function setPresence(userSocket, topic, user_token) {
const [{ Presence }, { useChannel }, { MountUsers }] = await Promise.all([
import("phoenix"),
import("@js/user_socket/useChannel"),
import("@js/components/mountUsers"),
]);
const channel = await useChannel(userSocket, topic, { user_token });
const presence = new Presence(channel);
let userID = null;
let userIDs = [];
let usersComponent = null;
// Track user ID sent from backend
channel.on("user", ({ from }) => {
userID = from;
});
// Utility: determine which DOM ID to use
const getTargetEl = () => {
const path = window.location.pathname;
const id =
path === "/yjs"
? CONFIG.NAVIDS.yjs.id
: path === "/map"
? CONFIG.NAVIDS.map.id
: CONFIG.NAVIDS.elec.id;
return document.getElementById(id);
};
// Render or update the component, if possible
const tryRender = () => {
const el = getTargetEl();
if (!el) return;
// Avoid duplicate mounting
if (usersComponent) {
if (el === usersComponent.el) {
console.log("[tryRender] Same element, updating...");
usersComponent.update({ userIDs, userID, el });
return;
} else {
console.log(
"[tryRender] Different element, disposing old, mounting new..."
);
usersComponent.dispose();
usersComponent = MountUsers({ userIDs, userID, el });
}
} else {
console.log("[tryRender] No existing component, mounting...");
usersComponent = MountUsers({ userIDs, userID, el });
}
// usersComponent.el = el;
};
// Sync presence when changed
presence.onSync(() => {
console.log("[presence.onSync]");
userIDs = presence.list((id, _meta) => id);
tryRender();
});
// Handle LiveView navigation
window.addEventListener("phx:page-loading-stop", () => {
console.log("[phx:page-loading-stop]");
tryRender();
});
}where the helper useChannel is:
export function useChannel(socket, topic, params) {
return new Promise((resolve, reject) => {
if (!socket) {
reject(new Error("Socket not found"));
return;
}
const channel = socket.channel(topic, params);
channel
.join()
.receive("ok", () => {
console.log(`Joined successfully Channel : ${topic}`);
resolve(channel);
})
.receive("error", (resp) => {
console.log(`Unable to join ${topic}`, resp.reason);
reject(new Error(resp.reason));
});
channel.onClose(() => console.log(`Channel closed: ${topic}`));
});
}The last bit is the reactive JS component called "MountUsers" above. . I used SolidJS. One great power is the integration with LiveView. You can return a "disposer" , and possibly any function such as the "update" to mutates the props, thus render selectively. check in "setPresence" how we "dispose" or "udpate".
import { render } from "solid-js/web";
import { For, createSignal, batch } from "solid-js";
export function MountUsers(props) {
console.log("[MountUsers]");
const [users, setUsers] = createSignal(props.userIDs);
const [dom, setDom] = createSignal(props.el);
const dispose =
props.el &&
render(
() => (
<p class="text-sm text-midnightblue mt-4 mb-4" id="users">
<span class="mr-2">{users().length}</span>
Online user(s):  
<For each={users()}>
{(user) => (
<span
class={[
Number(user) !== Number(props.userID) ? "bg-bisque" : null,
"inline-flex mr-2 items-center px-2 py-1 text-xs font-medium border border-midnightblue text-midnightblue rounded-full",
].join(" ")}
>
{user}
</span>
)}
</For>
</p>
),
dom()
);
const update = (newProps) => {
batch(() => {
setUsers(newProps.userIDs);
setDom(newProps.el);
});
};
return {
update,
dispose,
};
}