F6.05 · part 1 of 3
Templates & assigns
A HEEx template has exactly one input: its assigns. The controller builds a map
— %{courses: [...]} — and the template reads it through
@courses, which is sugar for assigns.courses. Everything the
template can show comes from that map; it does not call a context, query a table, or reach into process state.
That constraint is what makes a template a pure function of its assigns — same assigns,
same HTML — and it is the property F6.06 will exploit to render only what changed. Inside the markup you
have three workhorses: curly interpolation to render a value, the :for
attribute to loop over a list, and the :if attribute to show markup conditionally.
Values are HTML-escaped by default, so a course title containing a tag is shown as text, not
executed — the cross-site-scripting hole closes without you doing anything. Links use the verified
~p sigil from F6.02, so a typo in a route path is a compile error rather than a broken
anchor.
Four parts: the constructs a template uses, how an assign becomes rendered markup, a real index template, and the discipline that keeps queries out of it.
Three constructs
Most of a template is plain HTML; the dynamic parts are three constructs. Interpolation with
{ } renders an assign’s value into the markup. The :for
attribute repeats an element for each item in a list assign. The :if attribute
includes an element only when a condition holds. Select one.
HEEx constructs · select one
construct: Interpolation
does: render an assign's value
:for and :if are attributes on a real element,
not block tags wrapped around one. That keeps the markup readable as HTML and lets the compiler attach them to
the node they modify.
An assign, rendered
Concretely: the controller assigns @courses, a list of two structs;
:for renders one list item per struct; interpolation fills each title; the result is a
static <ul> with dynamic contents. HEEx compiles the static parts once and keeps
only the dynamic values separate, which is why re-rendering with a changed list is cheap.
@courses → :for → rendered list
An assign is the whole contract between controller and template. If a value is not in assigns, the template cannot show it — which is exactly the boundary you want between fetching data and displaying it.
A real template
An index template for the catalog: a heading, a list built with :for, a verified
~p link per course, interpolation for the title, and a conditional badge with
:if. Note there is no Repo, no
Catalog call — only assigns.
<%!-- lib/portal_web/controllers/course_html/index.html.heex --%> <h1>Courses</h1> <ul class="courses"> <li :for={course <- @courses} class="course"> <.link navigate={~p"/courses/#{course.id}"}>{course.title}</.link> <span :if={course.published} class="badge">published</span> </li> </ul> <p :if={@courses == []}>No courses yet.</p>
Read it as HTML with three superpowers: a loop, a guard, and value holes. A designer can edit the markup without learning the domain, because the domain is entirely upstream in the assigns.
Where the data comes from
The assign is set in the controller, by a single facade call. The template never fetches. The contrast below
shows the rule: fetch in the controller through Portal, render in the template. A
query embedded in HEEx compiles and works, but it welds markup to storage and runs database calls during
rendering — the same coupling the context boundary forbids one layer down.
# RIGHT — the controller fetches through the facade; the template only renders def index(conn, _params) do render(conn, :index, courses: Portal.list_courses()) # assign from the facade end # WRONG — a query inside the template couples markup to storage and the DB # <li :for={c <- Portal.Repo.all(Course)}>{c.title}</li> # the template should receive @courses, never build the list itself
controller fetches
One facade call sets @courses; the data layer stays upstream.
template renders
HEEx reads assigns and produces escaped HTML — no queries, no logic.
Next: function components & slots
— the <li> markup above is a candidate to extract into a reusable
<.course_card>.