📖 phoenix-views-templates
Render views and templates in Phoenix using HEEx templates, function components, slots, and assigns
Overview
Phoenix uses HEEx (HTML+EEx) templates for rendering dynamic HTML content. HEEx provides compile-time validation, security through automatic escaping, and a component-based architecture. Views in Phoenix are modules that organize template rendering logic and house reusable function components.
View Module Structure
Phoenix view modules use the embed_templates macro to load HEEx templates from a directory:
defmodule HelloWeb.HelloHTML do
use HelloWeb, :html
embed_templates "hello_html/*"
end
This automatically creates functions for each .html.heex file in the hello_html/ directory.
HEEx Templates
Basic Template Structure
HEEx templates combine HTML with embedded Elixir expressions:
<section>
<h2>Hello World, from Phoenix!</h2>
</section>
Interpolating Dynamic Content
Use <%= ... %> to interpolate Elixir expressions into HTML:
<section>
<h2>Hello World, from <%= @messenger %>!</h2>
</section>
The @ symbol accesses assigns passed from the controller.
Multi-line Expressions
For expressions without output, omit the =:
<% # This is a comment %>
<% user_name = String.upcase(@user.name) %>
<p>Welcome, <%= user_name %>!</p>
Working with Assigns
Assigns are key-value pairs passed from controllers to templates:
# Controller
def show(conn, %{"messenger" => messenger}) do
render(conn, :show, messenger: messenger, receiver: "Dweezil")
end
<!-- Template -->
<section>
<h2>Hello <%= @receiver %>, from <%= @messenger %>!</h2>
</section>
All assigns are accessed with the @ prefix in templates.
Conditional Rendering
Using if/else
HEEx supports conditional rendering with if/else blocks:
<%= if some_condition? do %>
<p>Some condition is true for user: <%= @username %></p>
<% else %>
<p>Some condition is false for user: <%= @username %></p>
<% end %>
Using unless
For negative conditions:
<%= unless @user.premium do %>
<div class="upgrade-banner">
Upgrade to premium for more features!
</div>
<% end %>
Pattern Matching with case
For multiple conditions:
<%= case @status do %>
<% :pending -> %>
<span class="badge badge-warning">Pending</span>
<% :approved -> %>
<span class="badge badge-success">Approved</span>
<% :rejected -> %>
<span class="badge badge-danger">Rejected</span>
<% end %>
Looping and Iteration
For Comprehensions
Generate dynamic lists using for:
<table>
<tr>
<th>Number</th>
<th>Power</th>
</tr>
<%= for number <- 1..10 do %>
<tr>
<td><%= number %></td>
<td><%= number * number %></td>
</tr>
<% end %>
</table>
Iterating Over Collections
Loop through lists or maps:
<ul>
<%= for post <- @posts do %>
<li>
<h3><%= post.title %></h3>
<p><%= post.excerpt %></p>
</li>
<% end %>
</ul>
Shorthand :for Attribute
HEEx provides cleaner syntax for simple iterations:
<ul>
<li :for={item <- @items}><%= item.name %></li>
</ul>
Accessing Index
Get the iteration index with Enum.with_index/2:
<%= for {item, index} <- Enum.with_index(@items) do %>
<div class="item-<%= index %>">
<%= item.name %>
</div>
<% end %>
Function Components
Function components are reusable UI elements defined as Elixir functions that return HEEx templates.
Defining Function Components
Use the attr macro to declare attributes and the ~H sigil for the template:
defmodule HelloWeb.HelloHTML do
use HelloWeb, :html
embed_templates "hello_html/*"
attr :messenger, :string, required: true
def greet(assigns) do
~H"""
<h2>Hello World, from <%= @messenger %>!</h2>
"""
end
end
Using Function Components
Invoke components with the <.component_name /> syntax:
<section>
<.greet messenger={@messenger} />
</section>
Optional Attributes with Defaults
Define optional attributes with default values:
attr :messenger, :string, default: nil
attr :class, :string, default: "greeting"
def greet(assigns) do
~H"""
<h2 class={@class}>
Hello World<%= if @messenger, do: ", from #{@messenger}" %>!
</h2>
"""
end
Multiple Attribute Types
Components can accept various attribute types:
attr :title, :string, required: true
attr :count, :integer, default: 0
attr :active, :boolean, default: false
attr :user, :map, required: true
attr :items, :list, default: []
def card(assigns) do
~H"""
<div class={"card" <> if @active, do: " active", else: ""}>
<h3><%= @title %></h3>
<p>Count: <%= @count %></p>
<p>User: <%= @user.name %></p>
<ul>
<li :for={item <- @items}><%= item %></li>
</ul>
</div>
"""
end
Components with Computed Values
Use assign/2 to compute values within components:
attr :x, :integer, required: true
attr :y, :integer, required: true
attr :title, :string, required: true
def sum_component(assigns) do
assigns = assign(assigns, sum: assigns.x + assigns.y)
~H"""
<h1><%= @title %></h1>
<p>Sum: <%= @sum %></p>
"""
end
Slots
Slots allow components to accept blocks of content, enabling powerful composition patterns.
Defining and Using Slots
Define a slot and render it:
slot :inner_block, required: true
def card(assigns) do
~H"""
<div class="card">
<%= render_slot(@inner_block) %>
</div>
"""
end
Use the component with content:
<.card>
<h2>Card Title</h2>
<p>Card content goes here</p>
</.card>
Named Slots
Components can have multiple named slots:
slot :header, required: true
slot :body, required: true
slot :footer
def panel(assigns) do
~H"""
<div class="panel">
<div class="panel-header">
<%= render_slot(@header) %>
</div>
<div class="panel-body">
<%= render_slot(@body) %>
</div>
<%= if @footer != [] do %>
<div class="panel-footer">
<%= render_slot(@footer) %>
</div>
<% end %>
</div>
"""
end
Usage:
<.panel>
<:header>
<h2>Panel Title</h2>
</:header>
<:body>
<p>Panel content</p>
</:body>
<:footer>
<button>Close</button>
</:footer>
</.panel>
Slots with Attributes
Slots can accept attributes for more dynamic rendering:
slot :item, required: true do
attr :title, :string, required: true
attr :highlighted, :boolean, default: false
end
def list(assigns) do
~H"""
<ul>
<%= for item <- @item do %>
<li class={if item.highlighted, do: "highlight"}>
<%= item.title %>: <%= render_slot(item) %>
</li>
<% end %>
</ul>
"""
end
Rendering Child Templates
Rendering Other Templates
Include child templates within a parent:
<%= render("child_template.html", assigns) %>
Rendering Components from Other Modules
Call components from different modules:
<MyApp.Components.button text="Click me" />
Or with aliasing:
alias MyApp.Components
# In template:
<Components.button text="Click me" />
Layout Templates
Using Layouts
Layouts wrap rendered templates. Configure the layout in the controller:
def controller do
quote do
use Phoenix.Controller,
formats: [:html, :json],
layouts: [html: HelloWeb.Layouts]
...
end
end
Root Layout
The root layout includes the @inner_content placeholder:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>My App</title>
</head>
<body>
<%= @inner_content %>
</body>
</html>
App Layout Component
Nest layouts using components:
<Layouts.app flash={@flash}>
<section>
<h2>Hello World, from <%= @messenger %>!</h2>
</section>
</Layouts.app>
Disabling Layouts
Render without a layout:
def home(conn, _params) do
render(conn, :home, layout: false)
end
LiveView Integration
Delegating to Phoenix Views
LiveView can delegate rendering to existing view modules:
defmodule AppWeb.ThermostatLive do
use Phoenix.LiveView
def render(assigns) do
Phoenix.View.render(AppWeb.PageView, "page.html", assigns)
end
end
Embedding LiveView in Templates
Render LiveView components within static templates:
<h1>Temperature Control</h1>
<%= live_render(@conn, AppWeb.ThermostatLive) %>
Function Components in LiveView
Define and use function components in LiveView:
def weather_greeting(assigns) do
~H"""
<div title="My div" class={@class}>
<p>Hello <%= @name %></p>
<MyApp.Weather.city name="Kraków"/>
</div>
"""
end
Testing Views
Testing View Rendering
Test views directly using render_to_string/4:
defmodule HelloWeb.ErrorHTMLTest do
use HelloWeb.ConnCase, async: true
import Phoenix.Template
test "renders 404.html" do
assert render_to_string(HelloWeb.ErrorHTML, "404", "html", []) == "Not Found"
end
test "renders 500.html" do
assert render_to_string(HelloWeb.ErrorHTML, "500", "html", []) == "Internal Server Error"
end
end
Testing Function Components
Test components in isolation:
import Phoenix.LiveViewTest
test "renders greet component" do
assigns = %{messenger: "Phoenix"}
html = rendered_to_string(~H"""
<HelloWeb.HelloHTML.greet messenger={@messenger} />
""")
assert html =~ "Hello World, from Phoenix!"
end
When to Use This Skill
Use this skill when you need to:
- Create dynamic HTML templates for Phoenix applications
- Build reusable function components for consistent UI elements
- Implement conditional rendering based on application state
- Render lists and tables with dynamic data
- Create complex layouts with nested components and slots
- Integrate LiveView components with static templates
- Test view rendering logic and component behavior
- Build accessible and semantic HTML structures
- Implement responsive designs with dynamic classes
- Create forms with validation feedback
- Display flash messages and user notifications
- Render navigation menus and breadcrumbs
- Build card-based layouts and dashboards
- Implement pagination controls
Best Practices
- Use function components - Encapsulate reusable UI patterns in components
- Declare attributes explicitly - Use
attrmacro for all component attributes - Provide default values - Make components flexible with sensible defaults
- Use semantic HTML - Choose appropriate HTML elements for accessibility
- Leverage slots - Use slots for flexible component composition
- Keep templates simple - Move complex logic to controller or context
- Use :for shorthand - Prefer
:forattribute for simple iterations - Avoid inline styles - Use CSS classes for styling
- Test components - Write tests for complex component logic
- Document components - Add docstrings to explain component usage
- Use verified routes - Always use
~psigil in templates - Escape user content - Let HEEx handle escaping automatically
- Optimize renders - Minimize computation in template code
- Use descriptive names - Name components and attributes clearly
- Follow conventions - Stick to Phoenix naming patterns
Common Pitfalls
- Putting logic in templates - Complex business logic belongs in contexts, not views
- Not escaping HTML - Using
raw/1without sanitizing user input - Deeply nested templates - Creating hard-to-maintain template hierarchies
- Missing attribute declarations - Not using
attrmacro for component attributes - Overusing inline conditionals - Making templates hard to read
- Not using components - Repeating markup instead of extracting components
- Forgetting slot checks - Not checking if optional slots are provided
- Mixing concerns - Combining data fetching with presentation logic
- Large template files - Creating monolithic templates instead of components
- Inconsistent formatting - Not following HEEx formatting conventions
- Using EEx instead of HEEx - Missing compile-time validation benefits
- Ignoring accessibility - Not adding ARIA labels and semantic markup
- Hardcoding values - Not using assigns for configurable content
- Not testing edge cases - Missing nil checks and empty state handling
- Excessive nesting - Creating deeply nested component trees