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
session["max_stack_depth"](defaults to 5; accepts an integer in1..20, anything outside that band resets to the default) 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"."max_stack_depth"(optional) — positive integer in1..20overriding the default 5-frame cap. Values outside that band are clamped to the default with a logged warning."modal_box_class"(optional) — daisyUImodal-boxsizing overrides. Defaults to"w-11/12 max-w-6xl"(91% viewport, capped at 72rem). Pass a different size class ("max-w-4xl","max-w-7xl", etc.) if a host page wants a narrower or wider modal.
Example: dashboard root view (OverviewLive)
For a dedicated admin page that lists projects/tasks/templates with modal-stacked detail views:
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"
}
}
})}Example: single project show inside a host record's edit page
The common host-app shape — a host record (order, ticket, etc.) has one linked project and the edit page embeds its detail view:
{Phoenix.Component.live_render(@socket, PhoenixKitProjects.Web.PopupHostLive,
id: "embedded-project-host-#{@host_record.uuid}",
session: %{
"pubsub_topic" => "host:foo:" <> @host_record.uuid,
"root_view" => %{
"lv" => "PhoenixKitProjects.Web.ProjectShowLive",
"session" => %{"id" => @host_record.project_uuid}
},
"locale" => @locale
})}Whenever the embedded LV (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.