view!() { /* proc-macro */ }Expand description
The view! macro is Topcoat’s HTML templating syntax. It tries to be unsurprising by staying close to real HTML instead of inventing a Rust-shaped HTML dialect. That means:
- HTML elements use their real names.
- HTML void elements, such as
<br>,<hr>,<img>,<input>,<meta>, and<link>, are written without closing tags. - Non-void elements need matching closing tags.
- Attribute names can use HTML separators like
-,:, and.:data-post-id,aria-label,xmlns:xlink,hx-get,class.active. - Rust keywords are still valid HTML attribute names, so
type="button"andfor="email"work as expected.
Unlike HTML however, text nodes must be quoted.
view! {
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="/app.css">
</head>
<body>
<label for="email">"Email"</label>
<input type="email" id="email" aria-label="Email address">
<hr>
</body>
</html>
}Element names can use dashes, so custom elements fit naturally:
view! {
<my-widget data-widget-id="profile"></my-widget>
}§Rust Expressions
Use parentheses to interpolate a Rust expression into markup.
In child position, the expression becomes a node:
view! {
<h1>"Hello, " (user.name) "!"</h1>
(sidebar)
}In attribute value position, the expression becomes the value:
view! {
<a href=(url) aria-current=(is_current)>"Open"</a>
}The same parenthesized expression syntax can also be used for dynamic attribute names and dynamic element names:
let tag = "section";
let attr = "data-state";
view! {
<(tag) (attr)="ready">"Loaded"</(tag)>
}Due to a limitation in Rust macros, text nodes must be quoted:
view! {
<p>"This is text"</p>
<p>(computed_text)</p>
}§Control Flow
Control flow in view! is Rust control flow with markup bodies. The macro lowers these constructs into ordinary Rust statements that append to the view being built.
§if
Use if, else if, and else to choose which markup is emitted.
view! {
if user.is_some() {
<a href="/account">"Account"</a>
} else {
<a href="/login">"Sign in"</a>
}
}In attributes, each branch emits attributes instead of child nodes:
view! {
<a
href="/posts"
if current {
aria-current="page"
class="active"
}
>
"Posts"
</a>
}§for
Use for pat in expr { ... } to render the body once for each item.
view! {
<ul>
for post in posts {
<li>
<a href=(post.url)>(post.title)</a>
</li>
}
</ul>
}In attributes, a loop can emit zero or more attributes. This is useful when you already have attributes represented as data:
view! {
<div
for (name, value) in attrs {
(name)=(value)
}
></div>
}§match
Use match to choose markup from patterns. Match arms can also use guards.
view! {
match status {
Status::Draft => <span>"Draft"</span>,
Status::Published { title } => <a href="/posts">(title)</a>,
Status::Archived if show_archived => <span>"Archived"</span>,
_ => "",
}
}A match arm body is one view node. If a branch needs multiple sibling nodes, wrap them in a block:
view! {
match user {
Some(user) => {
<h1>(user.name)</h1>
<p>"Signed in"</p>
},
None => <a href="/login">"Sign in"</a>,
}
}In attributes, each arm can emit attribute nodes:
view! {
<article
match state {
State::Open => class="open",
State::Closed => aria-disabled=(true),
}
></article>
}§let
Use let pat = expr; to bind values for later nodes in the same body.
view! {
<article>
let title = post.title.trim();
<h1>(title)</h1>
<a href=(post.url)>"Read"</a>
</article>
}The same works in an attribute list. The binding is in scope for attributes that follow it:
view! {
<a
let href = post.url();
href=(href)
data-slug=(post.slug)
>
(post.title)
</a>
}§Components
Components are called inside view! with a call syntax similar to functions. The macro introduces named parameters with the comma-separated name: value syntax to improve readability for components with many (optional) parameters. If the component has a child property, you may pass any number of view nodes at the end of parameter list. These do not need to be comma-separated:
view! {
panel(
// Named title parameter:
title: "Profile",
// Child nodes:
<p>"Account details"</p>
badge(
label: "Active",
tone: "success",
)
)
}The child nodes desugar to:
view! {
panel(
title: "Profile",
// Named child parameter:
child: view! {
<p>"Account details"</p>
badge(
label: "Active",
tone: "success",
)
}?
)
}See how to define components in the component macro guide.
§Conditional Attributes
Expression attributes can remove themselves from the rendered markup.
When an attribute value evaluates to false or None, the whole attribute is omitted. This matches the required boolean HTML attributes behavior.
let is_disabled = false;
let is_current = true;
let maybe_title: Option<&str> = None;
view! {
<button
disabled=(is_disabled)
aria-current=(is_current.then_some("page"))
title=(maybe_title)
>
"Save"
</button>
}The rendered opening tag includes aria-current="page", but leaves out disabled and title completely.
This omission logic applies to expression attributes. Literal attributes are always present:
view! {
<button disabled="false">"Still disabled in HTML"</button>
}For reusable runtime attribute collections, use the attributes! macro. It has the same attribute syntax as the view! macro but generates an topcoat::view::Attributes value that can be passed around and inserted into an element as an attribute fragment.
To assemble a class attribute value from static and conditional parts, use the class! macro. It builds a topcoat::view::Class value whose entries join with single spaces, and the attribute is omitted entirely when no entry is present.
§Status Codes And Response Headers
A view can declare the status code and headers of the HTTP response it renders into. A StatusCode in node position sets the response status, and a HeaderMap or a single (HeaderName, HeaderValue) pair adds response headers. None of them render any content.
view! {
(StatusCode::NOT_FOUND)
((header::CACHE_CONTROL, HeaderValue::from_static("no-store")))
<h1>"Page not found"</h1>
}Competing declarations resolve by render order: the first status code rendered wins, and for each header name the first part that mentions it provides all of that name’s values. Placement therefore decides precedence between a layout and the pages it wraps. A declaration placed before the layout’s slot overrides whatever the page declares; placed after the slot it is a fallback the page can override:
#[layout("/docs")]
async fn docs_layout(slot: Slot<'_>) -> Result {
view! {
<main>(slot.await?)</main>
((header::CACHE_CONTROL, HeaderValue::from_static("max-age=60")))
}
}Every page under /docs now gets Cache-Control: max-age=60 unless it declares its own Cache-Control.
A status code in node position never renders text. To display one, render one of its accessors instead, such as (status.as_u16()).
These declarations require the router feature (or the topcoat-view crate’s http feature) and take effect when the rendered view becomes a response; rendering a view to a plain string discards them.
§Rendering Outside A Component
Inside a component, #[page], or #[layout], the request context is in scope implicitly, so view! can render components and reactive markup with no ceremony. In a plain function you need to pass it at the start of the view! macro explicitely:
async fn render(cx: &Cx) -> Result {
view! { cx => greeting(name: "World") }
}§Custom Values In Markup
The macro accepts dynamic Rust values by routing them through small runtime traits. Implement the trait for the position where your type should be accepted:
NodeViewPartsfor values used as child nodes:(value).AttributeValueViewPartsfor values used as attribute values:name=(value).AttributeKeyViewPartsfor values used as dynamic attribute names:(name)="value".AttributeViewPartsfor values that emit one or more full attributes in APIs that accept complete attribute fragments.ElementNameViewPartsfor values used as dynamic element names:<(name)>...</(name)>.
Each trait method receives a PartsWriter for the position being filled. Everything pushed through its push_* methods is escaped or validated for that position when the view renders; push_str_unescaped is the only opt-out and must only be given trusted markup.
For example, a type can opt into child-node rendering by implementing NodeViewParts:
use topcoat::{context::Cx, view::{NodeViewParts, PartsWriter}};
struct Badge(String);
impl NodeViewParts for Badge {
fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
parts.push_str(self.0);
}
}
view! {
<p>(Badge("New".to_owned()))</p>
}For attribute values, implement AttributeValueViewParts. Its attribute_present method controls whether the containing attribute is rendered at all.
use topcoat::{context::Cx, view::{AttributeValueViewParts, PartsWriter}};
struct DataId(Option<String>);
impl AttributeValueViewParts for DataId {
fn attribute_present(&self) -> bool {
self.0.is_some()
}
fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
if let Some(value) = self.0 {
parts.push_str(value);
}
}
}
view! {
<article data-id=(DataId(Some("post-1".to_owned())))></article>
}