Skip to content

Guide 36 of 53

Verniz Views and Markup

Components as functions, HTML-like markup, safe interpolation and server-rendered Views without a template language.

On this page

A Verniz component is a Zolo function that returns View. There is no component base class, registration table or second expression language.

Markup is Zolo

use std::html::{article, h2, p, View}

fn PostCard(title: str, summary: str) -> View {
    <article class="post">
        <h2>{title}</h2>
        <p>{summary}</p>
    </article>
}

The parser lowers this to the same representation as builder syntax:

article(class: "post") {
    h2() { title }
    p() { summary }
}

Use whichever form is clearer. Components and HTML elements follow the same call rules, including named arguments and default parameters.

Forms and definition lists

Use a form id to identify it in the DOM, and a definition list to pair terms with their descriptions:

use std::html::{View, form, label, input, button, dl, dt, dd}

fn RepositoryPanel(repository: str, references: int) -> View {
    <>
        <form id="repository-filter">
            <label>Repository <input name="repository" value={repository} /></label>
            <button type="submit">Open</button>
        </form>
        <dl class="repository-facts">
            <dt>Repository</dt>
            <dd>{repository}</dd>
            <dt>Published references</dt>
            <dd id="reference-count">{references}</dd>
        </dl>
    </>
}

dl, dt and dd support class, id, style, role, hidden and tabindex. Builder syntax uses the same contract: form(id: "filter") { "Contents" } binds its trailing block to children.

Text, expressions and control flow

Inside markup, text is literal. Braces return to the full Zolo language:

fn Results(items: [str], show_empty: bool) -> View {
    <section>
        {if items.len() == 0 && show_empty {
            <p>No results.</p>
        }}
        {for item in items {
            <p key={item}>{item}</p>
        }}
    </section>
}

Use HTML comments inside markup: <!-- note -->. Zolo // and /* */ comments are accepted between tag attributes, where the parser is back in tag syntax.

Safe by default

Interpolated text and attribute values are escaped. A title containing <script> becomes text, not executable markup. Use raw(value) only for HTML that has already crossed a trust boundary safely.

<p>{untrusted_text}</p>
{raw(trusted_fragment)}

Documents, fragments and route results

Use document, head and body for complete pages, or fragment for siblings without a wrapper. render(view) converts a View to a string when you need explicit serialization.

An HTTP route may return a View directly. The server renders it and sets the HTML content type. It also serves the Verniz client runtime at /_zolo/client.js when a page includes client_script().

Raw-text elements

The bodies of <style> and <script> are parsed as CSS and JavaScript text, not as markup. A static <style> inside a component is scoped and hoisted by default; write <style global> for document-wide CSS.

Components for native renderers

Tags also call component functions that return a renderer's own record type. std::ui::collect(children) evaluates a trailing child block and preserves its values in statement order, including values emitted by if and for. It restores the enclosing frame when a child throws and does not convert records into HTML text nodes.

use std::ui::{collect}

@naming(returns: [.SnakeCase, .PascalCase])
struct UiNode { text: str, children: [Any] }
fn Label(value: str) -> UiNode { UiNode.new(text: value, children: []) }
fn Panel(children: Any = nil) -> UiNode {
    UiNode.new(text: "panel", children: collect(children))
}

let panel = <Panel><Label value="First" /><Label value="Second" /></Panel>

Native libraries can use this collection contract to pass components to their renderer. The <>...</> shorthand still creates an HTML fragment; a native library supplies its own ordinary fragment component. Standalone wasm-aot currently rejects native component frames explicitly.

An event loop can own safe development reload points with begin_updates(), poll_updates() and end_updates() from std::ui. A poll returns an aggregate invalidation hint. Renderers that stage views or native commands can pass poll_updates(on_begin, on_end): on_begin() returns an opaque snapshot and on_end(snapshot, success) runs for each protected entry or module replay. Separate results let a renderer reject a failed entry even when another update in the same batch succeeds. Supply both callbacks together; ordinary compiled applications do not execute them.

Next: Islands, Signals and Actions.

Conditional elements with v:if

Use v:if={condition} when a condition decides whether an entire element exists. A false condition skips the component call, its properties and its children. In this example, expensive() runs only for visible elements. The same rule applies to HTML and to components collected with std::ui.

use std::html::{div, span, render}
use std::ui::{collect}

var calls = 0
fn expensive() -> str { calls += 1; return "visible" }
let html = <div>
    <span v:if={false}>{expensive()}</span>
    <span v:if={true}>{expensive()}</span>
</div>
assert_eq(render(html), "<div><span>visible</span></div>")
assert_eq(calls, 1)

struct UiNode { text: str, children: [Any] }
fn Label(value: str) -> UiNode { UiNode.new(text: value, children: []) }
fn Panel(children: Any = nil) -> UiNode { UiNode.new(text: "panel", children: collect(children)) }
let panel = <Panel>
    <Label v:if={false} value={expensive()}/>
    <Label v:if={true} value={expensive()}/>
</Panel>
assert_eq(panel.children.len(), 1)
assert_eq(panel.children[0].text, "visible")
assert_eq(calls, 2)
print(render(html))
print(panel.children[0].text)
print(calls)

The Boolean condition runs once. An absent element contributes no child. v:if accepts an explicit expression and may appear once per element. Ordinary {if ...} blocks remain useful for several elements. Reactive updates use the existing conditional render path.

This example runs in the browser playground's embedded VM and in the CLI's VM, native and LLVM modes. Standalone wasm-aot currently rejects the std::ui component collection used here because it requires exception transport (TryCallEnv). Catching a failed runtime Boolean check also requires exception transport (TryCall), which wasm-aot currently rejects. v:if itself works in wasm-aot when the component's dependencies are supported.

By Example

DOCS / FEEDBACK

Did this page leave a question?

Tell us where the explanation lost you. Documentation is part of the language experience.

Global index

Find your way through Zolo

Try an idea

Start here

9 results

9 results

enespt-br