Components

Waltzing components work like React components: composable, reusable functions with props (parameters) and children (Content). Function tags provide JSX-like syntax for calling them.

@import vs @use

Waltzing has two import mechanisms with different purposes:

Key Difference

@import imports other template files (.wtz). @use imports Rust types from your crate.

@import - Template Files

Import other .wtz template files to use their functions as components:

@* Relative path (from current file's directory) *@
@import "button.wtz" as button
@import "../layouts/base.wtz" as layout
@import "./forms/input.wtz" as input

@* Absolute path (from templates root) *@
@import /layouts/base.wtz as layout
@import /components/ui/card.wtz as card
@import /components/forms/input.wtz as input

The alias after as becomes the component name you use in function tags:

@import /components/button.wtz as btn
@import /layouts/app.wtz as app

@fn apply() {
    <@app title="Home">
        <@btn label="Click me" />
    </@app>
}

@use - Rust Types

Import Rust types, traits, and functions from your crate:

@use crate::models::User
@use crate::models::{Post, Comment, Tag}
@use crate::helpers::format_date
@use std::collections::HashMap

@fn apply(user: User, posts: Vec<Post>) {
    <h1>@user.name</h1>
    @for post in posts {
        <p>@post.title - @format_date(post.created_at)</p>
    }
}

Component Composition

The real power of Waltzing is composing components together, just like React:

Building a UI Kit

@* components/button.wtz *@
@fn apply(
    label: String,
    variant: String = "primary",
    size: String = "md",
    disabled: bool = false,
    _type: String = "button",
) {
    <button
        type="@_type"
        class="btn btn-@variant btn-@size"
        @if disabled { disabled }
    >
        @label
    </button>
}

@* components/icon.wtz *@
@fn apply(name: String, size: u32 = 24, class: String = "") {
    <svg class="icon @class" width="@size" height="@size">
        <use href="/icons.svg#@name" />
    </svg>
}

@* components/icon_button.wtz - composing button + icon *@
@import /components/button.wtz as button
@import /components/icon.wtz as icon

@fn apply(
    icon_name: String,
    label: String = "",
    variant: String = "ghost",
    size: String = "md",
) {
    <@button variant=@variant size=@size>
        <@icon name=@icon_name />
        @if !label.is_empty() {
            <span class="ml-2">@label</span>
        }
    </@button>
}

Layout Components

@* layouts/base.wtz *@
@fn apply(
    title: String,
    description: String = "",
    content: Content,
) {
    <!DOCTYPE html>
    <html>
    <head>
        <title>@title</title>
        @if !description.is_empty() {
            <meta name="description" content="@description" />
        }
    </head>
    <body>
        @safe(content.render())
    </body>
    </html>
}

@* layouts/app.wtz - extends base with nav/footer *@
@import /layouts/base.wtz as base
@import /components/nav.wtz as nav
@import /components/footer.wtz as footer

@fn apply(
    title: String,
    user: Option<User> = None,
    show_footer: bool = true,
    content: Content,
) {
    <@base title=@title>
        <@nav user=@user />
        <main class="container">
            @safe(content.render())
        </main>
        @if show_footer {
            <@footer />
        }
    </@base>
}

@* layouts/dashboard.wtz - extends app with sidebar *@
@import /layouts/app.wtz as app
@import /components/sidebar.wtz as sidebar

@fn apply(
    title: String,
    user: User,
    active_nav: String = "",
    content: Content,
) {
    <@app title=@title user=@(Some(user.clone()))>
        <div class="dashboard-layout">
            <@sidebar user=@user active=@active_nav />
            <div class="dashboard-content">
                @safe(content.render())
            </div>
        </div>
    </@app>
}

Form Components

@* components/forms/input.wtz *@
@fn apply(
    name: String,
    _type: String = "text",
    label: String = "",
    value: String = "",
    placeholder: String = "",
    required: bool = false,
    error: Option<String> = None,
) {
    <div class="form-group @if error.is_some() { has-error }">
        @if !label.is_empty() {
            <label for="@name">@label</label>
        }
        <input
            type="@_type"
            id="@name"
            name="@name"
            value="@value"
            @if !placeholder.is_empty() { placeholder="@placeholder" }
            @if required { required }
            class="form-control"
        />
        @if let Some(err) = error {
            <span class="error-message">@err</span>
        }
    </div>
}

@* components/forms/form.wtz *@
@fn apply(
    action: String,
    method: String = "POST",
    class: String = "",
    content: Content,
) {
    <form action="@action" method="@method" class="form @class">
        @safe(content.render())
    </form>
}

@* pages/login.wtz - using form components *@
@import /layouts/app.wtz as app
@import /components/forms/form.wtz as form
@import /components/forms/input.wtz as input
@import /components/button.wtz as button

@use crate::forms::LoginForm

@fn apply(form_data: LoginForm, errors: HashMap<String, String>) {
    <@app title="Login">
        <@form action="/login">
            <@input
                name="email"
                type="email"
                label="Email"
                value=@form_data.email
                error=@errors.get("email").cloned()
                required
            />
            <@input
                name="password"
                type="password"
                label="Password"
                error=@errors.get("password").cloned()
                required
            />
            <@button label="Sign In" type="submit" />
        </@form>
    </@app>
}

Card System

@* components/card.wtz *@
@fn apply(
    title: String = "",
    subtitle: String = "",
    variant: String = "default",
    header: Option<Content> = None,
    footer: Option<Content> = None,
    content: Content,
) {
    <div class="card card-@variant">
        @if header.is_some() || !title.is_empty() {
            <div class="card-header">
                @if let Some(h) = header {
                    @safe(h.render())
                } else {
                    <h3>@title</h3>
                    @if !subtitle.is_empty() {
                        <p class="subtitle">@subtitle</p>
                    }
                }
            </div>
        }
        <div class="card-body">
            @safe(content.render())
        </div>
        @if let Some(f) = footer {
            <div class="card-footer">
                @safe(f.render())
            </div>
        }
    </div>
}

@* Using cards in a dashboard *@
@import /layouts/dashboard.wtz as dashboard
@import /components/card.wtz as card
@import /components/button.wtz as button
@import /components/stats_grid.wtz as stats

@fn apply(user: User, stats: DashboardStats) {
    <@dashboard title="Dashboard" user=@user active_nav="dashboard">
        <@stats data=@stats />

        <div class="grid grid-cols-2 gap-4">
            <@card title="Recent Activity">
                @for item in stats.recent_activity {
                    <div class="activity-item">@item.description</div>
                }
            </@card>

            <@card title="Quick Actions" variant="highlight">
                <@button label="New Project" variant="primary" />
                <@button label="Invite Team" variant="secondary" />
            </@card>
        </div>
    </@dashboard>
}

Default Parameters Power

Default parameters make components flexible without requiring every prop. This is what makes Waltzing feel like React:

@* A highly configurable modal component *@
@fn modal(
    title: String,
    size: String = "md",           @* sm, md, lg, xl, full *@
    closable: bool = true,
    show_overlay: bool = true,
    overlay_closes: bool = true,
    footer: Option<Content> = None,
    content: Content,
) {
    <div class="modal-wrapper">
        @if show_overlay {
            <div
                class="modal-overlay"
                @if overlay_closes { @click="closeModal()" }
            ></div>
        }
        <div class="modal modal-@size">
            <div class="modal-header">
                <h2>@title</h2>
                @if closable {
                    <button class="modal-close" @click="closeModal()">×</button>
                }
            </div>
            <div class="modal-body">
                @safe(content.render())
            </div>
            @if let Some(f) = footer {
                <div class="modal-footer">
                    @safe(f.render())
                </div>
            }
        </div>
    </div>
}

@* Simple usage - just title and content *@
<@modal title="Confirm">
    <p>Are you sure?</p>
</@modal>

@* Full customization *@
<@modal
    title="Edit Profile"
    size="lg"
    closable=@false
    overlay_closes=@false
>
    <@form action="/profile">...</@form>
</@modal>

The apply Convention

By convention, a template's main function is named apply. This enables shorthand syntax:

@* These are equivalent: *@
<@layout::apply title="Home">...</@layout::apply>
<@layout title="Home">...</@layout>

@* Self-closing: *@
<@button::apply label="Click" />
<@button label="Click" />