Stable, collision-free DOM ids for components.
Every interactive component needs an id for two reasons:
- Accessibility.
aria-controls,aria-labelledbyandaria-describedbyreference other elements by id, so those elements need ids that are unique on the page. - LiveView DOM patching. morphdom matches old and new elements by id. Without a stable
id, an Alpine-initialised element can be replaced rather than updated, discarding its
state even with
pineDom()wired up.
Generate once, not per render
unique/1 returns a different value on every call, so it must be resolved through
assign_new/3, which memoises it in the component's assigns:
# Correct — resolved once, stable across re-renders.
assigns = assign_new(assigns, :id, fn -> Id.unique("pine-modal") end)
# Wrong — a new id on every render; morphdom sees a brand new element each time.
~H|<div id={Id.unique("pine-modal")}>|Note the memoisation only holds within a live view's assigns lifecycle. A component rendered
from a controller (a "dead" view) generates a fresh id per render, which is harmless in the
browser but makes exact-value assertions in tests non-deterministic — pass an explicit id
in tests, or assert on shape rather than value.
Summary
Functions
Ensures assigns.id is set, generating one from prefix when the caller did not pass one.
Derives a child id from a parent id.
Returns a process-unique id with the given prefix.
Functions
Ensures assigns.id is set, generating one from prefix when the caller did not pass one.
assigns = ensure_id(assigns, "pine-modal")Why not assign_new/3
assign_new/3 only fires when the key is absent. A component that declares
attr :id, :string, default: nilalways has :id present — Phoenix injects the default before the function body runs — so
assign_new(assigns, :id, fn -> unique(...) end) never fires and @id stays nil. Every
scoped_id(@id, "panel") downstream then raises, and every aria-controls silently
becomes empty.
This helper checks the value, not the key, which is what the situation actually calls for.
Derives a child id from a parent id.
iex> scoped_id("pine-accordion-1f9a", "panel")
"pine-accordion-1f9a-panel"Use this for every sub-element that needs to be referenced — it keeps aria-controls and
friends pointing at real, unique elements without a second assign_new.
Returns a process-unique id with the given prefix.
iex> Id.unique("pine-accordion")
"pine-accordion-1f9a"Uses System.unique_integer/1, which is monotonic per VM — no randomness, so nothing here
depends on the seeding of :rand.