---
title: Views and Templates
description: How Glacier's compiled HTML templates work, from props, expressions and escaping to layouts, partials, form helpers, PDFs and static files.
category: Views and Controllers
order: 3
---
Glacier's views are HTML templates in `src/views/`. Each one declares the
values it needs, and is compiled into a Rust struct when the app builds, so
a template that uses a field the model doesn't have, or a route that
doesn't exist, stops the build instead of failing in front of a visitor.
After reading this guide, you will know:
- Where templates live and how they become structs.
- How to write expressions, conditions and loops, and how output is escaped.
- How layouts and partials fit together.
- Which helpers templates can use, including the form helpers.
- What `html!` and `#[derive(Template)]` are for.
- How files in `public/` are served.
The examples come from the `notes` app introduced in
[Routing](routing.html).
## Templates
### Where Templates Live
Templates are `.html` files under `src/views/`, one folder per controller,
named after the action that renders them. `render` in `Notes.index` uses
`src/views/notes/index.html` (see
[Controllers](controllers.html#rendering-a-template)). The `notes` app has:
```text
$ find src/views -name '*.html' | sort
src/views/admin/dashboard.html
src/views/docs/index.html
src/views/docs/show.html
src/views/layouts/admin.html
src/views/layouts/app.html
src/views/notes/_form.html
src/views/notes/edit.html
src/views/notes/index.html
src/views/notes/new.html
src/views/notes/preview.html
src/views/notes/show.html
src/views/pages/home.html
src/views/settings/show.html
src/views/shared/_flash.html
```
### Templates Are Structs
The app turns the folder into Rust as it compiles, with no file of yours:
it reads `src/views/` and makes one
struct per file, in a module per folder. `notes/index.html` becomes
`views::notes::Index`, `layouts/app.html` becomes `views::layouts::App`,
and `notes/_form.html` becomes `views::notes::Form` (a leading `_` is
dropped). Folder names must be valid Rust identifiers, and file names
snake_case words.
Templates see every model, every path helper, what every folder under
`src/` holds (a `Price` from `src/clients/`, say), the app's own
functions and Glacier's prelude, and the helpers described
[below](#helpers). An app that wants to choose for itself writes
`src/views.rs`: `pub use` lines for what templates see, then
`glacier::views!();`.
### Props
A template's first line declares its props: the values it's rendered
with, and their Rust types:
```html
{% props notes: Array<Note>, pinned: Option<Note>, page: glacier::pagination::Page, total: i64 %}
<h1>Notes</h1>
{%- if let Some(note) = pinned %}
<p class="pinned">Pinned: {{ link_to(¬e.title, note) }}</p>
{%- end %}
{% if notes.is_empty() -%}
<p>No notes yet.</p>
{%- else -%}
<.table headers={vec!["Title".to_owned(), "Body".to_owned(), String::new()]}>
{%- for note in notes %}
<tr>
<td>{{ note.title }}</td>
<td>{{ excerpt(¬e.body) }}</td>
<td>
{{ link_to("Show", note) }}
{{ link_to("Edit", edit_note_path(note.id)) }}
</td>
</tr>
{%- end %}
</.table>
{{ paginate(&page, total as u64) }}
{%- end %}
<p><a href="{{ new_note_path() }}">New note</a></p>
```
`{% props notes: Array<Note>, pinned: Option<Note>, page: glacier::pagination::Page, total: i64 %}`
makes `Index` a struct with those four fields, which
`render notes, pinned: pinned, page: page, total: total` fills in. The props line must come before anything else in the file, at most
once, and leaves no blank line in the output. A template with no props,
such as `pages/home.html`, needs no props line.
Templates own their data, so props are owned values like `Note` and
`Array<Note>`, or `'static` references such as `&'static str`, never
borrows.
## Template Syntax
| `{{ expr }}` | Outputs a Rust expression, HTML-escaped |
| `{% if cond %} … {% else if cond %} … {% else %} … {% end %}` | Conditions; `if let` works too |
| `{% for pattern in expr %} … {% end %}` | Loops |
| `{% let pattern = expr %}` | A local variable, until the end of the enclosing block |
| `{# comment #}` | A comment, left out of the output |
| `{%-`, `-%}`, `{{-`, `-}}` | Trims whitespace before or after the tag |
`{% endif %}` and `{% endfor %}` work as well as `{% end %}`. Any other tag,
such as `{% while %}`, is a compile error.
### Expressions (updated)
Anything between `{{` and `}}` is a Rust expression. Props are in scope by
name, and so are the helpers and everything templates see
([above](#templates-are-structs)):
```html
{% props note: Note %}
<h1>Note</h1>
<p>Written on {{ note.inserted_at.format("%-d %B %Y").to_string() }} ({{ note.inserted_at.time_ago() }})</p>
<.card title={note.title.clone()}>
<:header><small>Note #{{ note.id }}</small></:header>
<div class="body">{{ sanitize(¬e.body) }}</div>
</.card>
<p>
<a href="{{ edit_note_path(note.id) }}">Edit</a>
<a href="{{ notes_path(after: note.id) }}">Back</a>
</p>
<button type="button" commandfor="delete-note" command="show-modal">Delete</button>
<.modal id="delete-note" title="Delete this note?">
<p>It can't be brought back.</p>
{{ form_tag(note.to_path(), "delete") }}<.button kind="danger">Delete</.button></form>
</.modal>
```
The template can call methods and macros, as with
`note.inserted_at.format("%-d %B %Y").to_string()` above, and the
text, number and time helpers, as `note.inserted_at.time_ago()` (see
[Text and Number Helpers](glacier_language.html#text-and-number-helpers)):
```elixir
test "formats the date it was written" do
note = insert(Note)
res = get note
assert res.body =~ format!("Written on {}", note.inserted_at.format("%-d %B %Y"))
assert res.body =~ "(just now)"
travel(3.days())
assert get(note).body =~ "(3 days ago)"
# A path with a query: `notes_path(after: note.id)`.
assert get(note).body =~ format!("href=\"/notes?after={}\"", note.id)
end
```
`{{ }}` outputs strings, `char`s, numbers and `bool`s, nested templates,
and the trusted HTML that `raw()`, `link_to` and the form helpers return.
Your own types can be output too, by implementing `glacier::view::Render`
for them. `Some(value)` outputs the value and `None` outputs nothing.
Anything else, such as a timestamp, is a compile error until it's turned
into one of those, for example with `.to_string()`.
A path helper takes query parameters as keywords, as it does in
`.glacier` code: `notes_path(after: note.id)` above is
`/notes?after=7`, each value escaped.
`self` isn't available in templates; use the props by name.
### Escaping
Text output with `{{ }}` is HTML-escaped: `&`, `<`, `>`, `"` and `'` become
entities. A note titled `<script>alert(1)</script>` is shown as text,
not run:
```elixir
test "escapes what it prints" do
note = insert(Note, title: "<script>alert(1)</script>")
res = get note
assert res.body =~ "<script>alert(1)</script>"
refute res.body =~ "<script>alert"
end
```
To output HTML as it is, wrap it in `raw(…)`:
```html
{% props body: &'static str %}
{{ raw(body) }}
<p>{{ link_to("All docs", docs_path()) }}</p>
```
WARNING: `raw` outputs exactly what it's given. Only pass it HTML your app
wrote, never anything a visitor sent.
For HTML a visitor did send, such as a note's body, `sanitize(…)` keeps
only safe markup, as Rails' `sanitize` does. Paragraphs, emphasis, lists,
headings, links and images stay. Scripts, styles, event handlers such as
`onerror`, and links to `javascript:` go. `notes/show.html` prints the
body that way:
```elixir
test "keeps a body's safe HTML only" do
note = insert(Note, body: "<em>Soon</em>, <a href=\"javascript:alert(1)\">see</a><script>alert(1)</script>")
res = get note
assert res.body =~ "<em>Soon</em>, <a>see</a>"
refute res.body =~ "alert(1)"
end
```
`strip_tags(…)` goes further and keeps only the text:
`strip_tags("<b>Bold</b> move")` is `Bold move`.
### Conditions and Loops
`{% if %}` takes any Rust condition, including `if let`, and
`{% else if %}` and `{% else %}` add branches. `{% for %}` loops over
anything iterable. Looping over a prop or a variable borrows it, so
`{% for note in notes %}` gives `&Note`s and leaves `notes` usable
afterwards. Both appear in `notes/index.html` above.
### Whitespace Control
A `-` just inside a tag removes all the whitespace on that side of it,
including newlines. `notes/index.html` uses it so the empty list renders
cleanly:
```elixir
test "says when there are no notes" do
res = get "/notes"
assert res.body =~ "<h1>Notes</h1>\n\n<p>No notes yet.</p>"
end
```
The `-%}` after `{% if notes.is_empty() %}` removed the newline before
`<p>`, and `{%-` before `{% else %}` the one after it.
## Layouts
`render` puts the page inside a layout, `src/views/layouts/app.html` by
default:
```html
{% props title: String, content: Rendered %}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ title }} · notes</title>
<link rel="stylesheet" href="/app.css">
</head>
<body>
{{ shared::Flash {}.render() }}
{{ content }}
</body>
</html>
```
A layout is a template whose props include `content: Rendered`, which is
the rendered page. `title` is the prop that `render … title: "Notes"`
sets. Layout structs also implement `Default`, so any layout prop an
action doesn't set is left at its type's default value.
```elixir
test "renders the page inside the layout" do
res = get "/notes"
assert res.body =~ "<!doctype html>"
assert res.body =~ "<title>Notes · notes</title>"
assert res.body =~ "<h1>Notes</h1>"
assert res.body =~ "<meta name=\"csrf-token\" content=\""
end
```
Another file in `src/views/layouts/` is another layout, which an action
chooses with `layout: :admin`, and `layout: :none` renders a page on its
own (see [Controllers](controllers.html#rendering-a-template)). The
`notes` app's admin pages use `layouts/admin.html`:
```html
{% props title: String, notes_count: i64, content: Rendered %}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ title }} · notes admin</title>
<link rel="stylesheet" href="/app.css">
</head>
<body class="admin">
<header>Admin area · {{ notes_count }} notes · {{ form_tag(admin_session_path(), "delete") }}<button>Lock</button></form></header>
{{ shared::Flash {}.render() }}
{{ content }}
</body>
</html>
```
A layout has no named blocks for a template to fill; it gets values only
through its props.
### Values Every Page Shows (new)
Some of what a layout shows comes from the database on every request: a
count in the header, an account's custom styles. Rather than every action
passing it, middleware loads it once and `assign`s it to the layout's
prop of that name, as Phoenix's `assign` does. The admin pipeline in
`notes` runs this middleware:
```glacier
# What every admin page's header shows, loaded once per request: the
# admin layout's `notes_count` prop.
module AdminCounts do
implements Middleware
def call(ctx, next) do
assign("notes_count", Note.count()!)
next(ctx)
end
end
```
and the admin layout above has a `notes_count: i64` prop, which shows it:
```glacier
test "the admin layout shows what the pipeline assigned" do
insert(Note)
insert(Note)
post "/admin/session", passcode: "let me in"
assert get("/admin").body =~ "Admin area · 2 notes"
end
```
A prop an action sets wins over the assigned value; a prop neither sets
is its type's default. The value must have the prop's type, so
`assign("notes_count", 2)` (an `i32`) leaves an `i64` prop at `0`, and
logs a warning saying so.
## Partials
A partial is a template whose file name starts with `_`. It's a struct like
any other, so a template renders it by building the struct and calling
`.render()` inside `{{ }}`. `notes/new.html` and `notes/edit.html` share
the form in `notes/_form.html`:
```html
{% props note: Note %}
<h1>Edit note</h1>
{%- let path = note.to_path() %}
{{ Form { note }.render() }}
<p>
<a href="{{ path }}">Show</a>
<a href="{{ notes_path() }}">Back</a>
</p>
```
Inside the same folder, the struct's name is enough: `Form { note }`. From
anywhere else, name the folder, as both layouts do with the flash
messages in `shared/_flash.html`:
```html
{%- if let Some(notice) = flash("notice") %}
<p class="flash">{{ notice }}</p>
{%- end %}
{%- if let Some(alert) = flash("alert") %}
<p class="flash alert">{{ alert }}</p>
{%- end %}
```
```elixir
test "renders the flash partial in either layout" do
post "/admin/session", passcode: "let me in"
res = get "/admin"
assert res.body =~ "<p class=\"flash\">Admin area unlocked.</p>"
end
```
A partial's props are checked like any struct's fields, so leaving one out
is a compile error.
## Components (new)
A component is a piece of page an app uses again and again, such as a
button or a card, called from any template with a tag, as Phoenix's HEEx
writes them: `<.button kind="danger">Delete</.button>`. Each one is a
template in `src/components/`, beside `src/views/`, named for its tag
(`button.html` for `<.button>`), whose props are its attributes:
```html
{% props kind: String = "primary", inner_block: Slot %}
<button class="button button-{{ kind }}">{{ inner_block }}</button>
```
A prop with a default (`kind: String = "primary"`) may be left out of
the call. A prop of type `Slot` takes content rather than a value:
`inner_block` is what the caller writes between the tags, and any other
slot, such as `header` here, is written inside the call as
`<:header>…</:header>`. `{{ inner_block }}` renders a slot. A slot with a
default (`Slot::default()`, nothing) is optional:
```html
{% props title: String, header: Slot = Slot::default(), inner_block: Slot %}
<section class="card">
<header><h2>{{ title }}</h2>{{ header }}</header>
{{ inner_block }}
</section>
```
An attribute's value is text in quotes (`kind="danger"`) or an expression
in braces (`title={note.title.clone()}`), and one with neither
(`disabled`) is `true`. `notes/show.html` puts the note in a card and its
delete button in a button:
```html
{% props note: Note %}
<h1>Note</h1>
<p>Written on {{ note.inserted_at.format("%-d %B %Y").to_string() }} ({{ note.inserted_at.time_ago() }})</p>
<.card title={note.title.clone()}>
<:header><small>Note #{{ note.id }}</small></:header>
<div class="body">{{ sanitize(¬e.body) }}</div>
</.card>
<p>
<a href="{{ edit_note_path(note.id) }}">Edit</a>
<a href="{{ notes_path(after: note.id) }}">Back</a>
</p>
<button type="button" commandfor="delete-note" command="show-modal">Delete</button>
<.modal id="delete-note" title="Delete this note?">
<p>It can't be brought back.</p>
{{ form_tag(note.to_path(), "delete") }}<.button kind="danger">Delete</.button></form>
</.modal>
```
```elixir
test "shows a note in a card, with a button" do
note = insert(Note, title: "Shopping", body: "Eggs")
body = get(note).body
assert body =~ "<section class=\"card\">"
assert body =~ "<h2>Shopping</h2><small>Note ##{note.id}</small>"
assert body =~ "<div class=\"body\">Eggs</div>"
assert body =~ "<button class=\"button button-danger\">Delete</button>"
end
```
Glacier reads a component's props when it builds the pages that use it,
so a call that leaves out a required attribute, names one the component
doesn't take, or gives it the wrong type, is a build error that says so:
```text
error: notes/show.html:6: `<.card>` needs `title`
error: notes/show.html:18: `<.button>` takes no `size`; it takes `kind`, and the slots `inner_block`
```
A component is a template like any other, so it can use helpers, other
components and its own `if` and `for`.
### Previewing Components (new)
`/dev/components`, in development, shows every component's examples on
one page, in the app's own layout and stylesheet, for working on them
side by side. A component's examples are a template beside it,
`<name>.preview.html`, which uses its tag with the values to show; it
takes no props:
```html
<.card title="Shopping">
<:header><small>Note #7</small></:header>
<p>Eggs, flour, milk.</p>
</.card>
```
```glacier
test "each component's preview renders, for /dev/components" do
previews = notes::views::component_previews()
names = previews.iter().map(fn (name, _) -> *name end).collect::<Array<_>>()
assert names == ["button", "card", "modal", "select", "table"]
page = glacier::view::gallery_html(previews)
assert page =~ "<a href=\"#button\">button</a>"
assert page =~ "Delete this note?"
end
```
`views::component_previews()` gives each one rendered, by name.
### The Component Library (new)
`glacier new` writes a whole component library into `src/components/`,
the app's own to change, as Phoenix's `core_components.ex` and shadcn's
components are: about 140 components, from buttons, fields, tables and
dialogs to navigation, layouts, charts drawn on the server, and the
GOV.UK patterns for long forms (error summaries, summary lists, task
lists, date inputs). Each is plain HTML styled by a `ui-*` class, with
its variant and size as attributes, and works without JavaScript:
```html
{% props variant: String = "", size: String = "", r#type: String = "submit", inner_block: Slot %}
<button class="ui-btn"{% if !variant.is_empty() %} data-variant="{{ variant }}"{% end %}{% if !size.is_empty() %} data-size="{{ size }}"{% end %} type="{{ r#type }}">{{ inner_block }}</button>
```
Its stylesheet is `public/ui/components.css`, and everything it looks
like comes from the tokens in `public/ui/tokens.css`: colours (light, and
dark when the system or `data-theme="dark"` asks), type, spacing, radii
and shadows. Change a token and every component follows; with Tailwind,
`bg-primary` and `text-muted-foreground` follow too. The Geist fonts it
uses are in `public/fonts/`.
Form controls take a field and do the wiring: the label, the
`id`/`for`, the value to show, `aria-invalid`, and the errors under the
control. `Field::named("settings[theme]", &theme)` is a field for a form
without a record, and `options([...])` makes a select's options:
```html
<.native_select field={Field::named("settings[theme]", "dark")} options={options([("light", "Light"), ("dark", "Dark"), ("system", "Match the system")])} />
```
A dialog is the browser's own `<dialog>`: a button with
`commandfor="its-id" command="show-modal"` opens it and a `<form
method="dialog">` button closes it, without JavaScript. Browsers older
than Chrome 135, Safari 26 and Firefox 144 don't know `commandfor` yet;
`public/ui/ui.js`, which the layout loads, does it for them:
```html
<button class="ui-btn" data-variant="destructive" type="button" commandfor="delete-project" command="show-modal">Delete project</button>
<.dialog id="delete-project" title="Delete ‘Acme Inc.’?" description="This removes 12 projects. It can't be undone.">
<div class="ui-dialog-footer">
<form method="dialog"><.button variant="outline">Cancel</.button></form>
<.button variant="destructive">Delete project</.button>
</div>
</.dialog>
```
Every component has a preview beside it, so `/dev/components` shows
the whole library in the app's own layout:
```glacier
test "every component's preview renders" do
res = get "/dev/components"
assert res.status == :ok
assert_select(res, "section h2 code", count: 141)
assert_select(res, "section#button button.ui-btn[data-variant=destructive]", text: "Delete project")
assert_select(res, "section#dialog dialog.ui-dialog h2", text: "Delete ‘Acme Inc.’?")
end
```
## Helpers (updated)
These functions are available in every template:
| `link_to(text, target)` | `<a href="…">text</a>`, to a path or a record; the text is escaped |
| `form_for(&record)` | An opening `<form>` tag for a record, with the CSRF token |
| `form_tag(action, method)` | An opening `<form>` tag for any path and method, with the CSRF token |
| `csrf_field()` | The hidden CSRF token field, for a form written by hand |
| `csrf_token()` | The CSRF token itself, for a `<meta name="csrf-token">` tag that scripts read |
| `flash("notice")` | The flash message of that kind, as an `Option<String>` |
| `raw(html)` | HTML output as it is, without escaping |
| `sanitize(html)` | HTML with only safe tags and attributes kept (see [Escaping](#escaping)) |
| `strip_tags(html)` | The text of some HTML, every tag removed |
| `markdown(text)` | Markdown as HTML, cleaned as `sanitize` cleans (see [Markdown](#markdown)) |
| `paginate(&page, total)` | Links to a list's other pages (see [Pagination](#pagination)) |
The path helpers from the route table, such as `notes_path()` and
`edit_note_path(note.id)`, are available too, and so are records' own
paths, `note.to_path()`. Through the prelude, templates also have `t`,
`t_count`, `t_with` and `locale` for translations, and the text and
number helpers (`body.truncated(140)`, `price.to_currency("GBP")`).
### Markdown (new)
`markdown(¬e.body)` turns Markdown into HTML: CommonMark, with
GitHub's tables, strikethrough (`~~gone~~`), task lists (`- [x] done`)
and footnotes. Then it's cleaned as `sanitize` cleans it, so HTML written
into the Markdown, a `<script>` or a `javascript:` link, is dropped:
```html
<div class="body">{{ markdown(¬e.body) }}</div>
```
```glacier
test "previews a note's body as Markdown, safely" do
note = insert(Note, title: "List", body: "Buy *milk* and ~~eggs~~.\n\n- [x] bread\n\n<script>alert(1)</script>")
res = get preview_note_path(note.id)
assert res.body =~ "<em>milk</em>"
assert res.body =~ "<del>eggs</del>"
assert_select(res, "li input[type=checkbox][checked]")
refute res.body =~ "<script"
end
```
### Your Own Helpers
An app's own helpers go in `src/helpers/`, one module per file, as Rails'
`app/helpers/` does: `CommentsHelper` in
`src/helpers/comments_helper.glacier`. Templates call their functions by
name, as they do `link_to`. The `notes` app has one that cuts a long
note short for its list:
```elixir
# Helpers for the notes pages. Templates call them by name, as they do
# `link_to`: `{{ excerpt(¬e.body) }}`.
module NotesHelper do
# The start of a long text, cut at a word, for lists.
def excerpt(text: &str) -> String do
if text.chars().count() <= 40 do
return text.to_owned()
end
cut: String = text.chars().take(40).collect()
start = cut.rsplit_once(' ').map(fn (start, _) -> start end).unwrap_or(&cut)
"#{start}…"
end
end
```
`src/views/notes/index.html` shows each note's body with
`{{ excerpt(¬e.body) }}`:
```elixir
test "calls the app's own helpers" do
insert(Note, body: "A long note, about many things, that goes on and on")
res = get "/notes"
assert res.body =~ "<td>A long note, about many things, that…</td>"
end
```
Every helper is available in every template, whichever file it's in. An
app with only a few can keep them in one module, `Helpers` in
`src/helpers.glacier`, in place of the folder. Controllers and other code
call them through their module: `NotesHelper.excerpt(¬e.body)`.
An app helper named like one of Glacier's, its own `markdown` say, is the
one templates call.
### Pagination
`paginate(&page, total)` links to the other pages of a list split with
`glacier::pagination::Page`, as Kaminari's `paginate` does. The links
include previous and next, the first and last pages, and two either side
of the current one, with `…` for the gaps. They keep the request's other
query params. A list that fits on one page gets no links.
`notes/index.html` ends with it, below its table, and the action takes the
page from `?page=`:
```elixir
test "lists twenty notes a page" do
for i in 1..=25 do
insert(Note, title: format!("Note {i}"))
end
first = get "/notes"
assert first.body =~ "Note 25"
refute first.body =~ "Note 5<"
assert first.body =~ "<a href=\"/notes?page=2\" rel=\"next\">Next ›</a>"
second = get "/notes?page=2"
assert second.body =~ "Note 5<"
assert second.body =~ "<span class=\"current\" aria-current=\"page\">2</span>"
end
```
The links come in a `<nav class="pagination">`. The current page is a
`span.current`, a gap is a `span.gap`, and "Previous" or "Next" with no
page to go to is a `span.disabled`, for styling.
### Links
`link_to("Show", note)` links to the record's page, and
`link_to("Edit", edit_note_path(note.id))` to any path, as in
`notes/index.html` above. The docs index links to each page:
```html
{% props slugs: Array<&'static str> %}
<h1>Docs</h1>
<ul>
{%- for slug in slugs %}
<li>{{ link_to(slug, doc_path(slug)) }}</li>
{%- end %}
</ul>
```
```elixir
test "links with link_to" do
res = get "/docs"
assert res.body =~ "<li><a href=\"/docs/routing\">routing</a></li>"
end
```
For a link with attributes, such as a class, write the `<a>` tag yourself
with a path helper in its `href`, as `notes/show.html` does.
### Forms (updated)
`form_for(¬e)` opens the right form for a record: a `POST` to `/notes` for a new note, and a `PATCH`
to `/notes/7` for a saved one. `form_tag(path, method)` does the same for
any path. Both add the CSRF token (except to a `get` form), and, for
`patch`, `put` and `delete`, a hidden `_method` field, since forms can only send `GET` and `POST` (see
[Routing](routing.html#method-override)). Both write only the opening
tag, so close the form with `</form>`:
```html
{% props note: Note %}
{{ form_for(¬e) }}
<.input field={note.field("title")} />
<.input field={note.field("body")} type="textarea" />
<.button>Save</.button>
</form>
```
```elixir
test "opens the right form for a new note and a saved one" do
res = get "/notes/new"
assert res.body =~ "<form method=\"post\" action=\"/notes\"><input type=\"hidden\" name=\"_csrf_token\""
note = insert(Note)
res = get edit_note_path(note.id)
assert res.body =~ format!("<form method=\"post\" action=\"/notes/{}\">", note.id)
assert res.body =~ "<input type=\"hidden\" name=\"_method\" value=\"patch\">"
end
```
The fields are `<.input>` components. `note.field("title")` gives what
an input needs, as Phoenix's `form[:title]` does: the param's name, an id
for the label, the value to show (what was typed, if the note failed to
save) and the field's errors. The `input` component draws a label, the
input (`type="textarea"` for a text area, any other `type` for an
`<input>`) and the errors under it:
```html
{% props field: Field, r#type: String = "text", label: String = "", inner_block: Slot = Slot::default() %}
<p class="field">
<label for="{{ field.id }}">{% if label.is_empty() %}{{ field.label }}{% else %}{{ label }}{% end %}</label>
{%- if r#type == "textarea" %}
<textarea id="{{ field.id }}" name="{{ field.name }}">{{ field.value }}</textarea>
{%- else %}
<input type="{{ r#type }}" id="{{ field.id }}" name="{{ field.name }}" value="{{ field.value }}">
{%- end %}
{{ inner_block }}
{%- for message in field.errors.iter() %}
<small class="error">{{ field.label }} {{ message }}</small>
{%- end %}
</p>
```
```elixir
test "draws each field with its label and value" do
note = insert(Note, title: "Shopping", body: "Eggs")
body = get(edit_note_path(note.id)).body
assert body =~ "<label for=\"title\">Title</label>"
assert body =~ "<input type=\"text\" id=\"title\" name=\"title\" value=\"Shopping\">"
assert body =~ "<textarea id=\"body\" name=\"body\">Eggs</textarea>"
end
```
A changeset has `field` too, for a form re-shown from one.
`note.text("title")` and `note.errors_on("title")`, the parts `field`
gathers, are covered in [Models](models.html), for a field written by
hand.
```elixir
test "shows a form's errors" do
res = post "/notes", params_for(Note), title: ""
assert res.body =~ "<small class=\"error\">Title can't be blank</small>"
end
```
`form_tag` builds the delete button on `notes/show.html` and the settings
form, whose field names nest under `settings` for `require` (see
[Controllers](controllers.html#nested-params-with-require)):
```html
{% props theme: String %}
<h1>Settings</h1>
{{ form_tag(settings_path(), "patch") }}
<.select field={Field::named("settings[theme]", &theme)} options={options([("light", "Light"), ("dark", "Dark")])} />
<button>Save</button>
</form>
```
Neither helper sets an `enctype`, which a form with a file field needs;
[File Uploads](file_uploads.html) covers those forms.
NOTE: There's no `select` component or helpers for formatting numbers,
dates or text yet. Write the HTML, and use Rust's formatting where you
need it.
## Translations (new)
An app in more than one language keeps its text in
`config/locales/<locale>.yml`, as Rails does, compiled into the app. The
shop sells in English and French:
```yaml
en:
shop:
title: Shop
products:
zero: Nothing on sale yet
one: "%{count} product on sale"
other: "%{count} products on sale"
receipt_subject: "Your receipt for %{product}"
```
```yaml
fr:
shop:
title: Boutique
products:
one: "%{count} produit en vente"
other: "%{count} produits en vente"
receipt_subject: "Votre reçu pour %{product}"
```
`t("shop.title")` is the text for the request's locale, and
`t_count("shop.products", n)` picks the plural form for `n`, filling in
`%{count}`:
```html
<h1>{{ t("shop.title") }}</h1>
<p>{{ t_count("shop.products", products.len() as i64) }}</p>
```
The form comes from the locale's rules: `one` and `other` in English and
French (where 0 is `one` too), `one`, `few` and `many` in Russian or
Polish, a single `other` in Japanese, and so on. A `zero` form, when
there is one, is used for 0 in any language. In `.glacier`, `t` takes the
count and any other `%{name}` by name: `t("inbox", count: n, name:
user.name)`. A missing translation reads `Translation missing:
fr.shop.title`.
### The Request's Locale
The `Locales` middleware picks each request's locale from those the app
has, the first being the default:
```glacier
# English unless the browser (or `?locale=fr`) asks for French.
middleware Locales.new(["en", "fr"])
```
It takes, in order, a `locale` param (`?locale=fr`, which it remembers
in a `locale` cookie, so a language switcher is a link), that cookie,
and the best match in the browser's `Accept-Language` (`fr-CA` matches
`fr`). An unknown `?locale=` is a `400 Bad Request`. Where a signed-in
user has a language of their own, `set_locale(&user.locale)` in a
middleware or action sets it for the rest of the request. `locale()`
reads it anywhere, to scope a query, say.
```glacier
test "speaks the browser's language" do
assert get("/").body =~ "Nothing on sale yet"
insert(Product, name: "Field Guide")
assert get("/").body =~ "1 product on sale"
glacier::testing::header("accept-language", "fr-FR,fr;q=0.9,en;q=0.5")
res = get "/"
assert res.body =~ "Boutique"
assert res.body =~ "1 produit en vente"
end
test "keeps a locale asked for" do
insert_list(Product, 2)
assert get("/?locale=fr").body =~ "2 produits en vente"
# Remembered in a cookie, for the pages after.
assert get("/").body =~ "2 produits en vente"
end
```
## Inline Templates and Rust
### `html!`
`html!("…")` compiles a template written inline in Rust or `.glacier`. It
uses the same syntax, sees the local variables where it's written, and
gives a `Rendered`, which `.into_response()` turns into an HTML response:
```elixir
def show(ctx) do
path = ctx.param("path").unwrap_or_default()
html!("<p>You asked for {{ path }}</p>").into_response()
end
```
It escapes like any template:
```elixir
test "escapes values in html!" do
res = get "/attachments/%3Cb%3E"
assert res.body == "<p>You asked for <b></p>"
end
```
### `#[derive(Template)]`
The structs `views!()` makes are ordinary Rust structs with
`#[derive(glacier::view::Template)]` and
`#[template(path = "notes/index.html")]`. You can write such a struct
yourself in Rust, with `path = …` for a file under `src/views/` or
`source = "…"` for a template in a string. Its fields are the template's
values, and `.render()` renders it.
## Checked When the App Compiles
Templates are compiled with the app, so mistakes are build errors. A
template that can't be parsed names the file and line:
```text
error: src/views/notes/show.html:2: unclosed `{% if %}`; add `{% end %}`
--> src/lib.rs:3:1
|
```
A mistake in a Rust expression, such as a field the model doesn't have or
a path helper that doesn't exist, is reported by the Rust compiler. Its
message names the missing field or function, but points at the
`glacier::app!()` line in `src/lib.rs` rather than at the template;
[Routing](routing.html#path-helpers) shows one.
## Changing Templates Under `glacier server` (new)
`glacier server` rebuilds and restarts the app when a file under `src/`
or `config/` changes. Most template edits don't need that. A template is
compiled into its code (expressions, `if`s, loops, components and their
attributes) and the text around it. When only the text changes (markup,
classes, wording), the running app reads the new text from the file at
its next render, so there's no build:
```text
[glacier] reloaded src/views/pages/home.html (no rebuild needed)
```
Change an expression, a condition or a component's attributes and it
rebuilds as before, keeping the previous version running until the new
one is up:
```text
[glacier] changed: src/views/pages/home.html
[glacier] restarted in 2.1s
```
Either way the page in the browser reloads itself. Under `glacier
server`, HTML pages get a small script that waits on
`/__glacier/reload` and reloads the page when a template's text or a
file in `public/` changes, and when the server comes back after a
restart. When the app uses Tailwind, a template edit also rebuilds the
stylesheet, so a new class works straight away.
This applies only to debug builds run by `glacier server`. Tests and
release builds use the compiled text, and pages get no script.
## PDFs
`glacier::pdf::render_template` prints a template to a PDF with headless
Chrome, as Rails apps do with Grover or a Puppeteer service.
`Response::download` sends it to be saved, like Rails' `send_data`. The
`services` app prints a paid order's receipt:
```elixir
# GET /orders/:id/receipt.pdf: a paid order's receipt, printed from the
# orders/receipt.html template by headless Chrome.
def receipt do
order = Order.find(params[:id])!
if not order.status.is_paid() do
return head :not_found
end
filename = "receipt-#{order.id}.pdf"
options = glacier::pdf::PdfOptions.a4().footer(r#"<div style="font-size:8px; margin: 0 10mm">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>"#)
pdf = glacier::pdf::render_template(Receipt { order: order }, &options)!
Response.download(pdf, &filename)
end
```
The template is printed on its own, without the app's layout, so
`orders/receipt.html` is a whole page with its own `<style>`.
`PdfOptions` sets the rest:
- `a4()` and `letter()` set the paper, with backgrounds printed and 10 mm
margins. `.landscape()` turns the page and `.margins(top, right, bottom,
left)` takes millimetres.
- `.header(html)` and `.footer(html)` repeat on every page. Chrome fills
elements with the classes `pageNumber`, `totalPages`, `date` and
`title`. Leave room for them with the margins, and give them a font
size, as Chrome's default is tiny.
- `.allow(prefix)` lets the page load from URLs starting with `prefix`.
Each PDF starts its own Chrome process (the one at `CHROME_PATH`, or
where Chrome or Chromium is usually installed), so expect a second or two
each. Print in a [background job](background_jobs.html) when that's too
long for a request. The page runs with JavaScript off, and every request
it makes fails unless it's for a `data:` URL or an allowed prefix, so HTML
built from user input can't reach the network or the machine's own
services. Put images and fonts inline with
`glacier::pdf::data_url("image/png", &bytes)`; printing waits for the
page to load, so they're drawn.
In tests nothing is printed. Each render answers with a small placeholder
PDF, and `rendered_pdfs()` returns the HTML and options it was given:
```elixir
test "a paid order's receipt is a PDF" do
order = insert(Order, price_id: "price_basic")
assert get(format!("/orders/{}/receipt.pdf", order.id)).status == :not_found
order.status = OrderStatus::Paid
order.save()!
res = get format!("/orders/{}/receipt.pdf", order.id)
assert res.status == :ok
assert res.header("content-type") == Some("application/pdf")
assert res.header("content-disposition").unwrap() =~ format!("attachment; filename=\"receipt-{}.pdf\"", order.id)
# Nothing is printed in a test: the HTML is kept to check instead.
printed = rendered_pdfs()
assert printed.len() == 1
assert printed[0].html =~ "paid by ada@example.com"
assert printed[0].options.footer.as_deref().unwrap() =~ "totalPages"
end
```
`Response::inline(bytes, name)` sends a file for the browser to show,
such as a PDF in its viewer, instead of saving it.
## Styling with Tailwind (new)
A new app is styled with [Tailwind CSS](https://tailwindcss.com) as
well as the component library: templates use Tailwind's classes
(`class="flex gap-2"`) for their own layout, and the build turns
`assets/app.css` into `public/assets/app.css`, with only the classes the
app's templates use. The `ui` app's is what `glacier new` writes:
```css
/* The app's stylesheet: Tailwind CSS (https://tailwindcss.com), built
into public/assets/app.css by every build. The colours, fonts and radii
are the component library's, from public/ui/tokens.css: change them
there, and `bg-primary`, `text-muted-foreground` and the rest follow. */
@import "tailwindcss" source("../src");
/* Tailwind's `font-sans` and `rounded-md` read the same names, which
tokens.css sets after this file. */
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-success: var(--success);
--color-warning: var(--warning);
--color-info: var(--info);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
}
/* Plain HTML looks right too: some generated pages (sign-in, the
scaffold's simple forms) use bare elements. */
@layer base {
body {
@apply mx-auto my-16 max-w-2xl bg-background px-4 font-sans text-foreground;
}
h1 {
@apply mb-4 text-3xl font-extrabold tracking-tight;
}
h2 {
@apply mt-8 mb-3 text-xl font-semibold tracking-tight;
}
p,
form,
ul,
table {
@apply my-3;
}
a {
@apply text-primary underline underline-offset-4;
}
label {
@apply text-sm font-medium;
}
input:not([type="checkbox"], [type="radio"], [type="hidden"]):not([class]),
textarea:not([class]),
select:not([class]) {
@apply mt-1 block h-9 w-full rounded-md border border-input bg-transparent px-3 text-sm;
}
button:not([class]) {
@apply h-9 cursor-pointer rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground;
}
code {
@apply rounded-sm bg-muted px-1 font-mono text-sm;
}
.error {
@apply mt-1 block text-sm text-destructive;
}
}
```
`@import "tailwindcss" source("../src")` brings in Tailwind and has it
look for classes in `src/`, in templates, components and `.glacier`
files alike. `@theme inline` maps Tailwind's colours to the library's
tokens, so `bg-primary`, `text-muted-foreground` and `border-border`
follow `public/ui/tokens.css`, dark mode included. The `@layer base`
rules style plain HTML, so the generated sign-in pages and simple forms,
which use bare `<input>`s and `<button>`s, look right without classes.
There's no Node and nothing to install. The build uses Tailwind's
standalone program, a pinned release that Glacier downloads the first
time into `~/.cache/glacier/` and checks against its known SHA-256
before running it. Every build builds the stylesheet, `cargo build`,
`cargo test` and `glacier server` alike, so it follows the templates as
you change them, and release builds minify it and compile it into the
binary with the rest of `public/`. `public/assets/` is in `.gitignore`:
it's built, never edited.
To use a Tailwind of your own (offline, or another version), set
`GLACIER_TAILWIND_PATH` to its program. An app without `assets/app.css`
doesn't use Tailwind: `glacier new --no-tailwind` makes one with a plain
`public/app.css` (the component library is the same), and apps made
before 0.5 keep theirs. To move one over, add `assets/app.css`, link
`/assets/app.css` in the layout, and add `/public/assets/` to
`.gitignore`.
## JavaScript with Vite (new)
An app whose JavaScript needs npm packages or bundling can use
[Vite](https://vite.dev). Node is needed for this, and only for this.
`glacier new --vite` sets it up: a `package.json`, a `vite.config.js`,
`assets/js/app.js` as the entry point, and one line in the layout's
`<head>`:
```html
{{ vite("assets/js/app.js") }}
```
Run `npm install` once, then `glacier server` starts Vite's dev server
beside the app (on port 5173, or `GLACIER_VITE_PORT`). The `vite` helper
then links the entry from it, with Vite's client, so changes to scripts
and styles it bundles apply in place, without a reload:
```html
<script type="module" src="http://localhost:5173/@vite/client"></script>
<script type="module" src="http://localhost:5173/assets/js/app.js"></script>
```
A release build (`cargo build --release`, or `glacier build --release`)
runs `vite build` first. It writes hashed files and a manifest to
`public/build/`, and they're compiled into the binary with the rest of
`public/`. `vite(…)` then reads the manifest and links the built files,
with the entry's CSS and its imported modules preloaded, as Rails' and
Laravel's Vite helpers do:
```html
<link rel="stylesheet" href="/build/assets/app-5UjPuW-k.css">
<link rel="modulepreload" href="/build/assets/shared-B7PI925R.js">
<script type="module" src="/build/assets/app-4ed993c7.js"></script>
```
`vite(["assets/js/app.js", "assets/css/print.css"])` links several
entries; each must be in `vite.config.js`'s `input`. An entry missing
from the manifest, or a missing manifest, is logged and leaves an HTML
comment in its place, so the page still renders. `node_modules/` and
`public/build/` are in the generated `.gitignore`.
Vite is optional. Tailwind needs no Node, and without Vite an app's own
scripts go in `public/` and are linked directly, as the component
library's `/ui/ui.js` is. Modern browsers load
`<script type="module">` and its `import`s without a bundler.
## Static Files
Files in `public/` are served at the root of the site, so
`public/app.css` is `/app.css` (the notes app's plain stylesheet) and
`public/assets/app.css` is `/assets/app.css`. An app with a `public/` folder serves it
without a line of its own, after the middleware in its settings
(`src/app.glacier`):
```glacier
# The app. Its config, database, migrations, seeds, jobs, static files and
# routes are added for you; these are the settings of its own.
app do
alias glacier.middleware.MethodOverride
alias glacier.middleware.Logger
# Before the logger, so logs show the overridden method.
middleware MethodOverride
middleware Logger
end
```
```elixir
test "serves files from public/" do
res = get "/app.css"
assert res.status == :ok
assert res.headers["content-type"] == "text/css; charset=utf-8"
end
```
`glacier::static_files!("public")` serves the folder from disk in debug
builds. In release builds it compiles every file in it into the binary
(skipping names that start with a dot), so a deploy is one file. Either
way, it answers only `GET` and `HEAD`, sets an `ETag` so browsers can
revalidate with `304 Not Modified`, and passes any request that doesn't
name a file on to the router.