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:
@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" />