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>