Function Tags

Function tags provide an HTML-like syntax for calling template functions, making component composition intuitive and readable.

Basic Syntax

Function tags look like HTML tags but start with <@ and call template functions:

@* Self-closing function tag *@
<@header::apply title="Page Title" />

@* Function tag with content *@
<@layout::apply title="My App">
    
Page content goes here

The apply Shorthand

If the function name is apply, you can omit it from the tag:

@* These are equivalent: *@
<@layout::apply title="Page">Content
<@layout title="Page">Content

@* Self-closing shorthand: *@
<@header::apply title="Nav" />
<@header title="Nav" />

This shorthand is especially useful since apply is the conventional name for a template's main render function.

Attribute Types

Function tags support several types of attributes:

String Literals (Quoted)

<@button label="Click me" />
<@modal title="Confirm Action" />

Expressions (With @)

@* Simple variable *@
<@user_card user=@current_user />

@* Complex expression *@
<@pagination total=@(pages.len()) current=@page_number />

@* Method calls *@
<@list items=@users.iter().filter(|u| u.active).collect() />

Boolean Flags

@* Just the name = true *@
<@button label="Submit" disabled />
<@input type="text" readonly required />

Type-Aware Coercion

Attribute values are automatically coerced to match the expected parameter type. No @ needed for primitive literals:

@fn input(name: String, required: bool, max_length: i32, ratio: f64) {
    
}

@* All of these work - values coerced to parameter type *@
<@input name="email" required max_length=100 ratio=0.5 />
<@input name="age" required=true max_length=3 ratio=1.0 />
<@input name="note" required=false max_length=500 ratio=0.75 />

Coercion rules:

  • bool: attribute presence = true; =true/=false → bool literal
  • i8-i128, isize: numeric strings → signed integer literal
  • u8-u128, usize: numeric strings → unsigned integer literal
  • f32, f64: float strings → float literal
  • Whitespace is trimmed: max=" 42 " works correctly
  • Complex types still need @: data=@some_expression

Variable Shorthand

When a parameter name matches a variable name, use the shorthand:

@* Instead of: *@
<@user_card user=@user active=@active />

@* Use shorthand: *@
<@user_card @user @active />

@* For references: *@
<@form @&validation_errors />
Important: Don't mix quotes with @

A common mistake is putting @ inside quotes. This is wrong:

@* ❌ WRONG - @ inside quotes is literal text *@
<@component name="@user.name" />

@* ✅ CORRECT - use unquoted expression *@
<@component name=@user.name />

Default Parameters

Template functions can have default parameter values. When calling, you can omit any parameter that has a default:

@* Define a function with defaults *@
@fn button(
    label: String,
    variant: String = "primary",
    size: String = "md",
    disabled: bool = false,
) {
    
}

@* Call with all defaults *@
<@button label="Click" />

@* Override just variant *@
<@button label="Cancel" variant="secondary" />

@* Override multiple *@
<@button label="Delete" variant="danger" size="lg" />

@* Set boolean flag *@
<@button label="Wait..." disabled />

Default Value Types

Default values can be any valid Rust expression:

@fn card(
    title: String,
    class: String = "card",
    max_width: u32 = 400,
    show_border: bool = true,
    icon: Option = None,
    theme: Theme = Theme::Light,
) {
    ...
}

Closure and Callback Defaults

Both render callbacks and regular closures can have default values:

Render Callback Defaults

@* Render callback with default template content *@
@fn layout(header: @() = @() {
    
Default Header
}) {
@@header()
} @* Alternative: lambda with @@Out type *@ @fn layout(header: @() = |_: @Out| {
Default Header
}) {
@@header()
} @* With parameters *@ @fn data_table(row_renderer: @(&Item) = @(item: &Item) { @@item.name }) { @@for item in items { @@row_renderer(item) }
}

Regular Closure Defaults

@* Closure with default implementation *@
@fn apply(square: |a: i32| -> i32 = |a: i32| { a * a }) {
    
Result: @@square(7)
} @* Filter function with default *@ @fn filtered_list( items: Vec, filter: |&Item| -> bool = |item: &Item| { item.active } ) {
    @@for item in items.iter().filter(filter) {
  • @@item.name
  • }
}

Content Parameter

If a function's last parameter is of type Content, the tag body automatically becomes that parameter:

@* Define function with Content parameter *@
@fn card(title: String, content: Content) {
    

@title

@safe(content.render())
} @* The tag body becomes the content parameter *@ <@card title="Welcome">

This entire section becomes the content parameter.

It can contain any HTML or template code.

Optional Content

Make content optional with Option<Content>:

@fn modal(
    title: String,
    footer: Option = None,
    content: Content,
) {
    
}

Closing Tags

Function tags can be closed in several ways:

Full Closing Tag

<@layout::apply title="Page">
    Content here

Shorthand </@>

Closes the nearest open function tag:

<@layout title="Page">
    <@sidebar>
        
Sidebar content
@* Closes sidebar *@
Main content
@* Closes layout *@

Self-Closing

<@icon name="check" />
<@button label="Submit" />

Calling Components from Components

Function tags shine when building component hierarchies:

@import "layouts/base.wtz" as base
@import "components/header.wtz" as header
@import "components/sidebar.wtz" as sidebar
@import "components/footer.wtz" as footer

@fn apply(title: String, user: Option, content: Content) {
    <@base title=@title>
        <@header title=@title user=@user />

        
<@sidebar user=@user />
@safe(content.render())
<@footer /> }

Nested Function Tags

Function tags can be deeply nested:

<@page_layout title="Dashboard">
    <@content_area>
        <@card title="Statistics">
            <@stats_grid>
                @for stat in statistics {
                    <@stat_item
                        label=@stat.label
                        value=@stat.value
                        icon=@stat.icon
                    />
                }
            
        

        <@card title="Recent Activity">
            @for item in activity {
                <@activity_item @item />
            }
        
    

Conditional Function Tags

Use control flow with function tags:

@* Conditionally render different components *@
@if user.is_premium {
    <@premium_dashboard @user />
} else {
    <@basic_dashboard @user />
}

@* Conditional inside function tag content *@
<@card title="User Profile">
    @if let Some(avatar) = user.avatar {
        <@avatar src=@avatar />
    }
    

@user.name

Conditional Attributes

Use @if inside function tags for conditional attributes:

<@button
    label="Submit"
    @if is_loading { disabled }
    @if is_primary { variant="primary" }
/>

<@input
    name="email"
    @if let Some(err) = errors.get("email") {
        error=@err
        class="invalid"
    }
/>

Underscore Parameter Convention

Function parameters often use underscore prefixes to avoid conflicts with Rust keywords. Function tags can omit these underscores:

@* Function with underscore parameters *@
@fn button(_type: String = "button", _class: String = "btn") {
    
}

@* Call without underscores (preferred) *@
<@button type="submit" class="btn-primary" />

@* With underscores also works *@
<@button _type="submit" _class="btn-primary" />

Complete Example

Here's a complete example showing a page layout with multiple components:

@* templates/layouts/app.wtz *@
@import "components/nav.wtz" as nav
@import "components/footer.wtz" as footer

@use crate::models::User

@fn apply(
    title: String,
    user: Option = None,
    show_sidebar: bool = true,
    content: Content,
) {
    
    
    
        @title - MyApp
        
    
    
        <@nav user=@user />

        
@if show_sidebar { }
@safe(content.render())
<@footer /> } @* Usage from another template: *@ <@app::apply title="Dashboard" user=@current_user>

Welcome back!

<@dashboard_widgets user=@current_user />