Shared UI + its event handlers in one module — for drawers, pickers, and other components used across many pages.
defmodule MyAppWeb.DetailDrawer do
use StarView.Component
def drawer(assigns) do
~H"""
<div id="detail-drawer">
<input data-on:change={event("change_title:#{@item.id}")} />
</div>
"""
end
def handle_event("change_title:" <> _id, _signals, %{method: "POST"} = conn) do
# ... update, then patch (SSE is already started by the dispatcher)
patch_signals(conn, %{saved: true})
end
endPages embed the UI as plain function components and need no
handle_event clauses for it — events route to this module through
StarView.Dispatch. use StarView.Component marks the module
dispatchable (the same marker as use StarView controllers), so the
dispatch route is the allowlist. Wire it with StarView.Router:
star_view_components "/ds"which defaults to POST. Declare extra unsafe :event_methods explicitly;
read-only GET requires a get_events: [Module] allowlist. A plain
post "/ds/:module/:event", StarView.Dispatch, [], alias: false route
(the installer default) also works, but only for the default POST
events.
Unlike StarView.Page, event/2 here targets the component's dispatch
URL: <base>/<encoded-module>/<event>. The base defaults to /ds and
is read client-side from <body data-ds-base="...">, so it must match
the base you give star_view_components/1. Two common setups:
# Router: star_view_components "/ds" (the default — no layout
# attribute needed)
# Custom base and/or app path prefix — declare it once in the root
# layout, including the dispatch segment:
# Router: scope "/:workspace_slug" do star_view_components "/ds" end
<Layout.app conn={@conn}
body_attrs={%{"data-ds-base" => workspace_path(@current_scope.workspace.slug) <> "/ds"}}>
...
</Layout.app>Colocation only: no server-side component state, no lifecycle. State lives in signals, the DOM, and the database.
Pass checked_events: true to derive an event contract from the actual
handler clauses and check literal local event/1,2 references in ~STAR.
See StarView.Events.