Advanced Features

The less common things: content that renders once per page, tags of your own, and picking a component while the page renders.

The @__once__ Macro

The @__once__ macro ensures content is rendered only once per @render call, even if the component is used multiple times.

Basic Usage

A component that carries its own stylesheet or script tag would repeat it on every use. Wrap that content in @__once__ and the render writes it the first time it is reached and skips it afterwards.

using HypertextTemplates
using HypertextTemplates.Elements

@component function include_jquery()
    @__once__ begin
        @script {src = "https://code.jquery.com/jquery-3.6.0.min.js"}
    end
end

@deftag macro include_jquery end

# The script tag appears only once in the output
html = @render @div begin
    @include_jquery  # Renders script
    @include_jquery  # Skips script (already rendered)
    @include_jquery  # Skips script (already rendered)
end

# Note: Only one script tag appears!

<div>
  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</div>

Common Use Cases

CSS Dependencies

using HypertextTemplates
using HypertextTemplates.Elements

@component function styled_button(; text, variant = "primary")
    @__once__ begin
        @style """
        .btn {
            padding: 0.5rem 1rem;
            border: none;
            border-radius: 0.25rem;
            cursor: pointer;
        }
        .btn-primary { background: #007bff; color: white; }
        .btn-danger { background: #dc3545; color: white; }
        """
    end

    @button {class = "btn btn-$variant"} $text
end

@deftag macro styled_button end

# Style block rendered once, buttons rendered multiple times
html = @render @div begin
    @styled_button {text = "Save", variant = "primary"}
    @styled_button {text = "Delete", variant = "danger"}
    @styled_button {text = "Cancel"}
end
# Note: The <style> block appears only once at the beginning!

<div><style>.btn {
    padding: 0.5rem 1rem;
    border: none;
    border-radius: 0.25rem;
    cursor: pointer;
}
.btn-primary { background: #007bff; color: white; }
.btn-danger { background: #dc3545; color: white; }
</style><button class="btn btn-primary">Save</button><button class="btn btn-danger">Delete</button><button class="btn btn-primary">Cancel</button></div>

JavaScript Initialization

using HypertextTemplates
using HypertextTemplates.Elements

@component function data_table(; id, data)
    @__once__ begin
        @script """
        function initDataTable(id) {
            // Initialize table sorting, filtering, etc.
            console.log('Initializing table:', id);
        }
        """
    end

    @table {id} begin
        @thead @tr begin
            @th "Name"
            @th "Value"
        end
        @tbody begin
            for item in data
                @tr begin
                    @td $(item.name)
                    @td $(item.value)
                end
            end
        end
    end

    @script "initDataTable('$id');"
end

@deftag macro data_table end

# Multiple tables, but init function defined once
data1 = [(name = "A", value = 1), (name = "B", value = 2)]
data2 = [(name = "X", value = 10), (name = "Y", value = 20)]

html = @render @div begin
    @data_table {id = "table1", data = data1}
    @data_table {id = "table2", data = data2}
end

# Note: initDataTable function defined once, but called twice

<div>
  <script>
  function initDataTable(id) {
    // Initialize table sorting, filtering, etc.
    console.log("Initializing table:", id);
  }
  </script>
  <table id="table1">
    <thead>
      <tr>
        <th>Name</th>
        <th>Value</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>A</td>
        <td>1</td>
      </tr>
      <tr>
        <td>B</td>
        <td>2</td>
      </tr>
    </tbody>
  </table>
  <script>
  initDataTable("table1");
  </script>
  <table id="table2">
    <thead>
      <tr>
        <th>Name</th>
        <th>Value</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>X</td>
        <td>10</td>
      </tr>
      <tr>
        <td>Y</td>
        <td>20</td>
      </tr>
    </tbody>
  </table>
  <script>
  initDataTable("table2");
  </script>
</div>

Scoping Behavior

@__once__ is scoped to the current render context:

using HypertextTemplates
using HypertextTemplates.Elements

@component function scoped_script()
    @__once__ begin
        @script "console.log('Script loaded');"
    end
    @p "Component rendered"
end

@deftag macro scoped_script end

# First render - includes script
html1 = @render @div begin
    @scoped_script
end

<div>
  <script>
  console.log("Script loaded");
  </script>
  <p>Component rendered</p>
</div>

# Second render - includes script again (different render context)
html2 = @render @div begin
    @scoped_script
end

<div>
  <script>
  console.log("Script loaded");
  </script>
  <p>Component rendered</p>
</div>

# Within same render - deduplication works
html3 = @render @div begin
    @section @scoped_script  # Included
    @section @scoped_script  # Skipped
end

<div>
  <section>
    <script>
    console.log("Script loaded");
    </script>
    <p>Component rendered</p>
  </section>
  <section>
    <p>Component rendered</p>
  </section>
</div>

The @deftag Macro

Create custom macros for components and elements for cleaner syntax.

Basic Definition

@deftag defines a macro that expands to the @< call. A component or a custom element is then written the way @div and @p are, with {} props and a content block.

using HypertextTemplates
using HypertextTemplates.Elements

# Define a component
@component function alert(; type = "info", dismissible = false)
    @div {class = "alert alert-$type"} begin
        @__slot__
        if dismissible
            @button {class = "close"} "×"
        end
    end
end

# Create a macro for it
@deftag macro alert end

# Now use as a regular element macro
html = @render @alert {type = "warning", dismissible = true} begin
    @strong "Warning!" " Something went wrong."
end

<div
  class="alert alert-warning"><strong>Warning! Something went wrong.</strong><button class="close">×</button></div>

# Also works without dismissible
html2 = @render @alert {type = "info"} "This is an info message."

<div class="alert alert-info">This is an info message.</div>

Custom Element Tags

using HypertextTemplates
using HypertextTemplates.Elements

# Define custom elements with macros
@element "my-custom-element" my_custom_element
@deftag macro my_custom_element end

# Use the custom element
html = @render @my_custom_element {prop = "value"} "Content"

<my-custom-element prop="value">Content</my-custom-element>

# Also works with nested content
@element "custom-card" custom_card
@deftag macro custom_card end

html2 = @render @custom_card {variant = "primary"} begin
    @h3 "Card Title"
    @p "Card content goes here"
end

<custom-card variant="primary">
  <h3>Card Title</h3>
  <p>Card content goes here</p>
</custom-card>

Module-Scoped Tags

using HypertextTemplates
using HypertextTemplates.Elements

module UI
    using HypertextTemplates
    using HypertextTemplates.Elements

    @component function button(; variant = "default")
        Elements.@button {class = "ui-btn ui-btn-$variant"} @__slot__
    end

    # Create and export the macro
    @deftag macro button end
    export @button
end

# Use from outside the module
html = @render @UI.button {variant = "primary"} "Click me"

<button class="ui-btn ui-btn-primary">Click me</button>

# Also can import and use directly
using .UI: @button
html2 = @render @button {variant = "success"} "Save"

<button class="ui-btn ui-btn-success">Save</button>

Text Interpolation with $

The $ syntax provides convenient text interpolation, similar to string interpolation.

Basic Interpolation

$value expands to @text value. Same thing, two spellings, and both escape what they write.

using HypertextTemplates
using HypertextTemplates.Elements

name = "Julia"
version = 1.9

html = @render @div begin
    @h1 "Welcome to " $name " v" $version
end
# Equivalent to:
# @h1 "Welcome to " @text name " v" @text version

<div>
  <h1>Welcome to Julia v1.9</h1>
</div>

Expression Interpolation

using HypertextTemplates
using HypertextTemplates.Elements

items = ["apple", "banana", "cherry"]

html = @render @ul begin
    for (i, item) in enumerate(items)
        @li "Item " $i ": " $(uppercase(item))
    end
end

<ul>
  <li>Item 1: APPLE</li>
  <li>Item 2: BANANA</li>
  <li>Item 3: CHERRY</li>
</ul>

Nested Interpolation

using HypertextTemplates
using HypertextTemplates.Elements

@component function price_display(; amount, currency = "\$")
    @span {class = "price"} $currency $(round(amount, digits=2))
end

@deftag macro price_display end

html = @render @div begin
    @text "Total: "
    @price_display {amount = 99.999}
    @text " (Tax included: "
    @price_display {amount = 99.999 * 1.1} ")"
end

<div>Total: <span class="price">$100.0</span> (Tax included: <span class="price">$110.0</span></div>

Dynamic Component Selection

The @< macro enables dynamic component rendering.

Component as Variable

@<component_var {props} takes the component from a variable. Which one renders is decided as the page renders. Hold it in a variable, pass it in as a prop, look it up in a Dict.

using HypertextTemplates
using HypertextTemplates.Elements

# Define the specific components first
@component function error_message(; content)
    @div {class = "error"} @strong "Error: " $content
end

@component function warning_message(; content)
    @div {class = "warning"} @strong "Warning: " $content
end

@component function info_message(; content)
    @div {class = "info"} @strong "Info: " $content
end

# Select component based on condition
@component function message(; type, content)
    component = if type == "error"
        error_message
    elseif type == "warning"
        warning_message
    else
        info_message
    end

    @<component {content}
end

@deftag macro message end

# Usage
html = @render @div begin
    @message {type = "error", content = "File not found"}
    @message {type = "warning", content = "Disk space low"}
    @message {type = "info", content = "Process completed"}
end

<div>
  <div class="error"><strong>Error: File not found</strong></div>
  <div class="warning"><strong>Warning: Disk space low</strong></div>
  <div class="info"><strong>Info: Process completed</strong></div>
</div>

Polymorphic Components

using HypertextTemplates
using HypertextTemplates.Elements

@component function flexible_container(; tag = div, class_name = "container")
    @<tag {class = class_name} begin
        @__slot__
    end
end

@deftag macro flexible_container end

# Use with different HTML elements
html1 = @render @flexible_container {tag = section} begin
    @h1 "Section Title"
    @p "Section content"
end

<section class="container">
  <h1>Section Title</h1>
  <p>Section content</p>
</section>

html2 = @render @flexible_container {tag = article, class_name = "article-container"} begin
    @h2 "Article Title"
    @p "Article content"
end

<article class="article-container">
  <h2>Article Title</h2>
  <p>Article content</p>
</article>

Component Maps

using HypertextTemplates
using HypertextTemplates.Elements

# Define field components
@component function text_field(; name, options = nothing)
    @input {type = "text", name, id = name}
end

@component function email_field(; name, options = nothing)
    @input {type = "email", name, id = name}
end

@component function select_field(; name, options = [])
    @select {name, id = name} begin
        for opt in options
            @option {value = opt.value} $(opt.label)
        end
    end
end

# Map of field types to components
const FIELD_COMPONENTS = Dict(
    :text => text_field,
    :email => email_field,
    :select => select_field
)

@component function form_field(; type, name, label, options = nothing)
    component = get(FIELD_COMPONENTS, type, text_field)

    @div {class = "form-field"} begin
        Elements.@label {"for" := name} $label
        @<component {name, options}
    end
end

@deftag macro form_field end

# Usage examples
html = @render @form begin
    @form_field {type = :text, name = "username", label = "Username:"}
    @form_field {type = :email, name = "email", label = "Email:"}
    @form_field {
        type = :select,
        name = "country",
        label = "Country:",
        options = [
            (value = "us", label = "United States"),
            (value = "uk", label = "United Kingdom"),
            (value = "ca", label = "Canada")
        ]
    }
end

<form>
  <div
    class="form-field"><label for="username">Username:</label><input type="text" name="username" id="username"></div>
  <div
    class="form-field"><label for="email">Email:</label><input type="email" name="email" id="email"></div>
  <div
    class="form-field"><label for="country">Country:</label><select name="country" id="country"><option value="us">United States</option><option value="uk">United Kingdom</option><option value="ca">Canada</option></select></div>
</form>

Advanced Patterns

Memoization

Cache expensive computations:

using HypertextTemplates
using HypertextTemplates.Elements

const COMPUTED_CACHE = Dict{Any,Any}()

@component function memoized(; key, compute)
    value = get!(COMPUTED_CACHE, key) do
        compute()
    end

    @div {class = "memoized"} $value
end

@deftag macro memoized end

# Simulate expensive calculation
function expensive_calculation(id)
    println("Computing for id=$id...")
    return "Result for $id"
end

# First call computes
html1 = @render @memoized {
    key = "expensive-42",
    compute = () -> expensive_calculation(42)
}
println("First render: ", html1)

# Second call uses cache (no "Computing..." message)
html2 = @render @memoized {
    key = "expensive-42",
    compute = () -> expensive_calculation(42)
}
println("Second render (cached): ", html2)

# Different key computes again
html3 = @render @memoized {
    key = "expensive-99",
    compute = () -> expensive_calculation(99)
}
println("Different key: ", html3)
Computing for id=42...
First render: <div class="memoized">Result for 42</div>
Second render (cached): <div class="memoized">Result for 42</div>
Computing for id=99...
Different key: <div class="memoized">Result for 99</div>

Render Props Pattern

Pass rendering functions as props:

using HypertextTemplates
using HypertextTemplates.Elements

# Simulate data fetching
function fetch_data(url)
    if url == "/api/users"
        # Simulate successful fetch
        data = [(name = "Alice",), (name = "Bob",), (name = "Charlie",)]
        return (data = data, error = nothing, loading = false)
    elseif url == "/api/error"
        return (data = nothing, error = "Network error", loading = false)
    else
        return (data = nothing, error = nothing, loading = true)
    end
end

@component function data_fetcher(; url, render_loading, render_error, render_success)
    result = fetch_data(url)

    if result.loading
        @<render_loading
    elseif !isnothing(result.error)
        @<render_error {error = result.error}
    else
        @<render_success {data = result.data}
    end
end

@deftag macro data_fetcher end

# Define render components
@component function loading_comp()
    @p {class = "loading"} "Loading..."
end
@component function error_comp(; error)
    @p {class = "error"} "Error: " $error
end
@component function success_comp(; data)
    @ul begin
        for user in data
            @li $(user.name)
        end
    end
end

# Success case
println("Success case:")
html1 = @render @data_fetcher {
    url = "/api/users",
    render_loading = loading_comp,
    render_error = error_comp,
    render_success = success_comp
}

<ul>
  <li>Alice</li>
  <li>Bob</li>
  <li>Charlie</li>
</ul>

# Error case
println("\nError case:")
html2 = @render @data_fetcher {
    url = "/api/error",
    render_loading = loading_comp,
    render_error = error_comp,
    render_success = success_comp
}

<p class="error">Error: Network error</p>

Template Inheritance

Build template inheritance systems:

using HypertextTemplates
using HypertextTemplates.Elements

@component function base_layout(; title = "Default Title")
    @html begin
        @head begin
            @title $title
            @__slot__ head  # Additional head content
        end
        @body begin
            @header @__slot__ header
            Elements.@main @__slot__     # Default slot for main content
            @footer @__slot__ footer
        end
    end
end

@deftag macro base_layout end

@component function blog_layout(; post)
    @base_layout {title = post.title} begin
        # Main content
        @article begin
            @h1 $(post.title)
            @div {class = "content"} $(SafeString(post.html))
        end

        # Named slots
        head := @meta {name = "author", content = post.author}

        header := @nav @a {href = "/blog"} "← Back to Blog"

        footer := @p "Published: " $(post.date)
    end
end

@deftag macro blog_layout end

# Example blog post
post = (
    title = "Understanding Julia Macros",
    author = "Jane Developer",
    date = "2024-01-15",
    html = "<p>Julia macros are powerful metaprogramming tools...</p>"
)

html = @render @blog_layout {post}

<!DOCTYPE html>
<html>
  <head>
    <title>Understanding Julia Macros</title>
    <meta name="author" content="Jane Developer">
  </head>
  <body>
    <header>
      <nav><a href="/blog">← Back to Blog</a></nav>
    </header>
    <main>
      <article>
        <h1>Understanding Julia Macros</h1>
        <div class="content">
          <p>Julia macros are powerful metaprogramming tools...</p>
        </div>
      </article>
    </main>
    <footer>
      <p>Published: 2024-01-15</p>
    </footer>
  </body>
</html>

Lazy Loading Pattern

Load data on-demand during rendering:

using HypertextTemplates
using HypertextTemplates.Elements

const DATA_CACHE = Dict{Any,Any}()

# Simulate database fetch
function fetch_users_from_db()
    println("Fetching from database...")
    return [
        (name = "Alice", id = 1),
        (name = "Bob", id = 2),
        (name = "Charlie", id = 3)
    ]
end

@component function lazy_data(; data_loader, cache_key = nothing)
    # Load data during render (blocking)
    data = if !isnothing(cache_key) && haskey(DATA_CACHE, cache_key)
        println("Using cached data for key: $cache_key")
        DATA_CACHE[cache_key]
    else
        result = data_loader()
        if !isnothing(cache_key)
            DATA_CACHE[cache_key] = result
        end
        result
    end

    @div {class = "data-container"} begin
        if isempty(data)
            @p {class = "empty"} "No data available"
        else
            @ul begin
                for item in data
                    @li "$(item.name) (ID: $(item.id))"
                end
            end
        end
    end
end

@deftag macro lazy_data end

# First render - fetches data
println("First render:")
html1 = @render @lazy_data {
    cache_key = "users",
    data_loader = () -> fetch_users_from_db()
}

<div class="data-container">
  <ul>
    <li>Alice (ID: 1)</li>
    <li>Bob (ID: 2)</li>
    <li>Charlie (ID: 3)</li>
  </ul>
</div>

# Second render - uses cache
println("\nSecond render:")
html2 = @render @lazy_data {
    cache_key = "users",
    data_loader = () -> fetch_users_from_db()
}

<div class="data-container">
  <ul>
    <li>Alice (ID: 1)</li>
    <li>Bob (ID: 2)</li>
    <li>Charlie (ID: 3)</li>
  </ul>
</div>