- Begin every LiveView template with
<Layouts.app flash={@flash} ...>, which wraps all inner content and renders flash messages. Call<.flash_group>only fromlayouts.ex. - Render icons with
<.icon name="hero-x-mark" class="w-5 h-5" />. - Build form inputs with
<.input>fromcore_components.ex. It takes noclassattribute: a passedclasslands after the default one, and the browser keeps the first, so it has no effect. To restyle inputs, changeinput/1itself.
-
Interpolate values with
{...}, both in attributes and in tag bodies. Use<%= ... %>only for block constructs (if,cond,case,for) in tag bodies:<div id={@id}> {@my_assign} <%= if @some_block_condition do %> {@another_assign} <% end %> </div> -
Write a class attribute with several values as a list, and wrap an inline
ifin parentheses:<a class={[ "px-2 text-white", @some_flag && "py-5", if(@other_condition, do: "border-red-500", else: "border-blue-100") ]}>Text</a> -
Generate repeated content with
:foror<%= for item <- @collection do %>. -
Branch three or more ways with
condorcase. Elixir has noelse if. -
Write template comments as
<%!-- comment --%>. -
To show literal
{or}, such as a code snippet, addphx-no-curly-interpolationto the parent tag. Inside it,<%= ... %>still interpolates. -
Give key elements, such as forms and buttons, a unique DOM
id. Tests select elements by these IDs.
- Name a LiveView with a
Livesuffix, under a namespace that matches its domain, such asFlickWeb.Ballots.IndexLiveorFlickWeb.Vote.ResultsLive. - Navigate with
<.link navigate={href}>and<.link patch={href}>in templates, and withpush_navigate/2andpush_patch/2in LiveViews. - Build UI from function components. Reach for a LiveComponent only when part of a page needs its own state and event handling.
Flick assigns its collections as plain lists today, because each list is small. Use a stream when a collection can grow without bound. A stream works like this:
-
The parent element needs
phx-update="stream", which replaces the oldappendandprependmodes, and a DOMid. Each child takes itsidfrom the stream:<div id="messages" phx-update="stream"> <div :for={{id, msg} <- @streams.messages} id={id}> {msg.text} </div> </div> -
Add items with
stream/4, which appends by default (at: -1). Passat: 0to prepend. Remove items withstream_delete/3. -
A stream is not enumerable. To filter or refresh it, refetch the data and call
stream(socket, :messages, messages, reset: true). -
A stream keeps no count. Track a count or an empty flag in a separate assign. An empty state can also be a sibling
<div class="hidden only:block">, when it is the only other child. -
When an assign changes what a streamed item shows, re-insert that item with
stream_insert/3.
-
Write a hook that lives with its template as a colocated hook. Its name starts with a
.:<input type="text" id="user-phone-number" phx-hook=".PhoneNumber" /> <script :type={Phoenix.LiveView.ColocatedHook} name=".PhoneNumber"> export default { mounted() { ... } } </script> -
Put a shared hook in
assets/js/hooks/and register it inassets/js/app.js, asSortableInputsForis. -
When a hook owns its element's content, also set
phx-update="ignore"on that element. A hook that only reorders children the server still patches leaves it off, asSortableInputsFordoes ineditor_live.ex. -
push_event/3returns a new socket. Return it, or rebind it, so the event is sent.
- Build every form from
to_form/2, assigned in the LiveView, and render it with<.simple_form for={@form} id="...">fromcore_components.ex. A small inline form, such as the vote weight form inviewer_live.ex, can use<.form>directly. Read fields as@form[:field]. The changeset stays in the LiveView. - From a changeset,
to_form/1takes the data, params, and errors, and names the params after the schema. ABallotform submits%{"ballot" => ballot_params}. - From
handle_eventparams,to_form/2expects string keys. Passas: :userto nest them under a name. - Nest a form with
<.inputs_for>.