Opinionated wrapper LV that pairs the <.popup_host> function
component with the emit-mode PubSub contract.
A host app mounts this LV (typically via live_render) and gets
popup-driven UX for free — no host-side handle_info subscription,
no modal-stack state management, no frame-ref bookkeeping.
What it does
- Subscribes to a host-supplied PubSub topic on connect.
- Optionally renders a "root view" inline (the always-visible LV;
OverviewLiveis the typical pick) by passingsession["root_view"]. - On
{:projects, :opened, ...}— pushes a frame onto the modal stack and renders the target LV inside a<dialog>overlay. On
{:projects, :closed | :saved | :deleted, %{frame_ref: ref}}— pops the top frame iffrefmatches (race-safe against stale events).- Generates a unique
frame_refper push and stamps it into the child LV's session along withmode: "emit"and the host topic, so the child's own emits flow back through this LV. - Caps stack depth at
@max_stack_depth(5) to prevent runaway recursion if a misbehaved LV emits:openedon every mount.
Session contract
"pubsub_topic"(required) — PubSub topic string. The host owns the topic name; this LV does not invent it (so two embeds on the same page can use different topics if needed)."root_view"(optional) —%{"lv" => "Module.Name", "session" => %{...}}. The always-visible LV.:lvis whitelist-validated."wrapper_class"(optional) — outer div class. Defaults to"flex flex-col w-full".
Example mount (from a host app's router)
live "/orders/:id/projects", MyApp.OrderProjectsLive
# ... and in MyApp.OrderProjectsLive's render:
{Phoenix.Component.live_render(@socket, PhoenixKitProjects.Web.PopupHostLive,
id: "projects-popup-host",
session: %{
"pubsub_topic" => "host:orders:" <> @order_id,
"root_view" => %{
"lv" => "PhoenixKitProjects.Web.OverviewLive",
"session" => %{
"wrapper_class" => "flex flex-col w-full px-4 py-6 gap-6"
}
}
})}Whenever the embedded OverviewLive (or anything it transitively
opens) emits :opened, this LV renders the target inside a modal
on the host's existing page. No URL change. No DOM replacement.