Components Guide
Components are the building blocks for creating reusable, maintainable templates in HypertextTemplates.jl. This guide covers everything from basic component creation to advanced patterns.
Basic Component Definition
Components are defined using the @component
macro applied to a function:
using HypertextTemplates
using HypertextTemplates.Elements
@component function greeting(; name = "World")
@div {class = "greeting"} begin
@h1 "Hello, " $name "!"
end
end
# Important: Define a macro for the component
@deftag macro greeting end
# Now you can use it
html = @render @greeting {name = "Julia"}
<div class="greeting"><h1>Hello, Julia!</h1></div>
# Also works with default value
html2 = @render @greeting
<div class="greeting"><h1>Hello, World!</h1></div>
Component Properties (Props)
Props are passed as keyword arguments to component functions:
Required Props
Omit default values to make props required:
using HypertextTemplates
using HypertextTemplates.Elements
@component function user_card(; username, email) # Both required
@div {class = "user-card"} begin
@h3 $username
@p $email
end
end
@deftag macro user_card end
# Must provide both props
html = @render @user_card {username = "julia_dev", email = "julia@example.com"}
<div class="user-card">
<h3>julia_dev</h3>
<p>julia@example.com</p>
</div>
Optional Props with Defaults
using HypertextTemplates
using HypertextTemplates.Elements
@component function my_button(;
text = "Click me",
type = "button",
variant = "primary",
disabled = false
)
classes = "btn btn-" * variant
@button {type, class = classes, disabled} $text
end
@deftag macro my_button end
# Use with defaults
@render @my_button {}
<button type="button" class="btn btn-primary">Click me</button>
# Override specific props
@render @my_button {text = "Submit", variant = "success"}
<button type="button" class="btn btn-success">Submit</button>
# With disabled state
@render @my_button {text = "Loading...", disabled = true}
<button type="button" class="btn btn-primary" disabled>Loading...</button>
Typed Props
Leverage Julia's type system for safer components:
using HypertextTemplates
using HypertextTemplates.Elements
@component function product_price(;
amount::Number,
currency::String = "USD",
decimal_places::Int = 2
)
formatted = round(amount, digits = decimal_places)
@span {class = "price"} begin
@span {class = "currency"} $currency
@span {class = "amount"} $formatted
end
end
@deftag macro product_price end
# Usage with different types
@render @product_price {amount = 29.99}
<span class="price"><span class="currency">USD</span><span class="amount"
>29.99</span></span>
@render @product_price {amount = 100, currency = "EUR", decimal_places = 0}
<span class="price"><span class="currency">EUR</span><span class="amount"
>100.0</span></span>
Slots System
Slots allow components to accept content from their parent, enabling flexible composition.
Default Slot
The simplest form - a single content area:
using HypertextTemplates
using HypertextTemplates.Elements
@component function card(; title)
@div {class = "card"} begin
@div {class = "card-header"} @h3 $title
@div {class = "card-body"} begin
@__slot__ # Default slot receives content
end
end
end
@deftag macro card end
# Usage with content
html = @render @card {title = "User Profile"} begin
@p "Name: Alice"
@p "Role: Developer"
end
<div class="card">
<div class="card-header"><h3>User Profile</h3></div>
<div class="card-body">
<p>Name: Alice</p>
<p>Role: Developer</p>
</div>
</div>
Named Slots
For more complex layouts with multiple content areas:
using HypertextTemplates
using HypertextTemplates.Elements
@component function layout()
@div {class = "layout"} begin
@header {class = "layout-header"} begin
@__slot__ header # Named slot: header
end
@article {class = "layout-main"} begin
@__slot__ # Default slot
end
@footer {class = "layout-footer"} begin
@__slot__ footer # Named slot: footer
end
end
end
@deftag macro layout end
# Usage with named slots
html = @render @layout begin
# Named slot content uses := syntax
header := begin
@h1 "My Application"
@nav begin
@a {href = "/"} "Home"
@a {href = "/about"} "About"
end
end
# Default slot content
@section begin
@h2 "Welcome"
@p "This is the main content area."
end
footer := @p "© 2024 My Company"
end
<div class="layout">
<header class="layout-header">
<h1>My Application</h1>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>
<article class="layout-main">
<section>
<h2>Welcome</h2>
<p>This is the main content area.</p>
</section>
</article>
<footer class="layout-footer"><p>© 2024 My Company</p></footer>
</div>
Conditional Slots
Slots can be conditionally rendered:
using HypertextTemplates
using HypertextTemplates.Elements
@component function message(; type = "info", dismissible = false)
@div {class = "message message-$type"} begin
@__slot__ # Message content
if dismissible
@button {class = "message-close"} begin
@__slot__ close_button # Optional slot
end
end
end
end
@deftag macro message end
# Without close button content
html1 = @render @message {type = "warning"} begin
@p "This is a warning"
end
<div class="message message-warning"><p>This is a warning</p></div>
# With custom close button
html2 = @render @message {type = "error", dismissible = true} begin
@p "An error occurred"
close_button := @span "×"
end
<div class="message message-error">
<p>An error occurred</p>
<button class="message-close"><span>×</span></button>
</div>
Slot Fallbacks
Provide default content when slots are empty:
using HypertextTemplates
using HypertextTemplates.Elements
@component function avatar(; src = nothing, alt = "")
@div {class = "avatar"} begin
if !isnothing(src)
@img {src, alt}
else
@div {class = "avatar-placeholder"} begin
# Show slot content (initials)
@__slot__
end
end
end
end
@deftag macro avatar end
# With image
html1 = @render @avatar {src = "/user.jpg", alt = "User"}
<div class="avatar"><img src="/user.jpg" alt="User"></div>
# With placeholder (slot content)
html2 = @render @avatar {alt = "John Doe"} begin
@span "JD" # Initials as fallback
end
<div class="avatar"><div class="avatar-placeholder"><span>JD</span></div></div>
Component Composition
Components can use other components, enabling powerful composition patterns:
Basic Composition
using HypertextTemplates
using HypertextTemplates.Elements
@component function nav_link(; href, active = false)
class = active ? "nav-link active" : "nav-link"
@a {href, class} @__slot__
end
@deftag macro nav_link end
@component function navbar(; links, current_path = "/")
@nav {class = "navbar"} begin
@ul begin
for link in links
@li begin
@nav_link {
href = link.href,
active = link.href == current_path
} begin
@text link.text
end
end
end
end
end
end
@deftag macro navbar end
# Usage
links = [
(href = "/", text = "Home"),
(href = "/about", text = "About"),
(href = "/contact", text = "Contact")
]
html = @render @navbar {links, current_path = "/about"}
<nav class="navbar">
<ul>
<li><a href="/" class="nav-link">Home</a></li>
<li><a href="/about" class="nav-link active">About</a></li>
<li><a href="/contact" class="nav-link">Contact</a></li>
</ul>
</nav>
Higher-Order Components
Create components that modify behavior of other components:
using HypertextTemplates
using HypertextTemplates.Elements
@component function with_tooltip(; tooltip, position = "top")
@div {
class = "tooltip-wrapper",
"data-tooltip" := tooltip,
"data-position" := position
} begin
@__slot__
end
end
@deftag macro with_tooltip end
# Wrap any content with tooltip
html = @render @with_tooltip {tooltip = "Click to submit"} begin
@button "Submit"
end
<div class="tooltip-wrapper" data-tooltip="Click to submit" data-position="top">
<button>Submit</button>
</div>
Component Arrays
Render collections of components:
using HypertextTemplates
using HypertextTemplates.Elements
@component function todo_item(; task, completed = false)
@li {class = completed ? "completed" : ""} begin
@input {type = "checkbox", checked = completed}
@span " " $task
end
end
@deftag macro todo_item end
@component function todo_list(; items)
@ul {class = "todo-list"} begin
for item in items
@todo_item {task = item.task, completed = item.completed}
end
end
end
@deftag macro todo_list end
# Example usage
items = [
(task = "Write documentation", completed = true),
(task = "Add tests", completed = false),
(task = "Deploy to production", completed = false)
]
html = @render @todo_list {items}
<ul class="todo-list">
<li class="completed">
<input type="checkbox" checked>
<span> Write documentation</span>
</li>
<li class="">
<input type="checkbox">
<span> Add tests</span>
</li>
<li class="">
<input type="checkbox">
<span> Deploy to production</span>
</li>
</ul>
Dynamic Components
Use the @<
macro to render components dynamically:
Component as Props
using HypertextTemplates
using HypertextTemplates.Elements
@component function flexible_layout(;
header_component = nothing,
main_component = article,
sidebar = true
)
@div {class = "layout"} begin
if !isnothing(header_component)
@<header_component
end
@<main_component {class = "main-content"} begin
@__slot__
end
if sidebar
@aside {class = "sidebar"} begin
@__slot__ sidebar
end
end
end
end
@deftag macro flexible_layout end
# Custom header component
@component function custom_header()
@header {class = "fancy-header"} begin
@h1 "My App"
end
end
# Usage
html = @render @flexible_layout {
header_component = custom_header,
main_component = section
} begin
@p "Main content here"
sidebar := @p "Sidebar content"
end
<div class="layout">
<header class="fancy-header"><h1>My App</h1></header>
<section class="main-content"><p>Main content here</p></section>
<aside class="sidebar"><p>Sidebar content</p></aside>
</div>
Conditional Component Selection
using HypertextTemplates
using HypertextTemplates.Elements
# Define icon components
@component function error_icon()
@span {class = "icon icon-error"} "❌"
end
@component function warning_icon()
@span {class = "icon icon-warning"} "⚠️"
end
@component function info_icon()
@span {class = "icon icon-info"} "ℹ️"
end
@component function alert(; type = "info", message)
# Select icon based on type
icon_component = if type == "error"
error_icon
elseif type == "warning"
warning_icon
else
info_icon
end
@div {class = "alert alert-$type"} begin
@<icon_component
@span " "
@span $message
end
end
@deftag macro alert end
# Test different alert types
@render @alert {type = "info", message = "This is information"}
Main.display_html(ans)
<div class="alert alert-info">
<span class="icon icon-info">ℹ️</span>
<span> </span>
<span>This is information</span>
</div>
@render @alert {type = "warning", message = "This is a warning"}
<div class="alert alert-warning">
<span class="icon icon-warning">⚠️</span>
<span> </span>
<span>This is a warning</span>
</div>
@render @alert {type = "error", message = "This is an error"}
<div class="alert alert-error">
<span class="icon icon-error">❌</span>
<span> </span>
<span>This is an error</span>
</div>
Module-Qualified Components
Components can be organized in modules and referenced with qualification:
using HypertextTemplates
using HypertextTemplates.Elements
module UI
using HypertextTemplates
using HypertextTemplates.Elements
@component function my_button(; variant = "primary")
@button {class = "ui-button ui-button-$variant"} @__slot__
end
@deftag macro my_button end
@component function card()
@div {class = "ui-card"} @__slot__
end
@deftag macro card end
end
# Usage with module qualification
html = @render @div begin
@UI.card begin
@h2 "Card Title"
@UI.my_button {variant = "secondary"} "Click me"
end
end
<div>
<div class="ui-card">
<h2>Card Title</h2>
<button class="ui-button ui-button-secondary">Click me</button>
</div>
</div>
Creating Component Macros
Use @deftag
to create macro shortcuts for components:
using HypertextTemplates
using HypertextTemplates.Elements
@component function badge(; text, variant = "primary", size = "normal")
size_class = size == "small" ? "badge-sm" : "badge-normal"
@span {
class = "badge badge-$variant $size_class"
} $text
end
# Create a macro for easier use
@deftag macro badge end
# Now can use as @badge instead of @<badge
html = @render @div begin
@h3 begin
@text "Products "
@badge {text = "New", variant = "success", size = "small"}
end
@p begin
@text "Status: "
@badge {text = "In Stock", variant = "primary"}
end
end
<div>
<h3>Products <span class="badge badge-success badge-sm">New</span></h3>
<p>Status: <span class="badge badge-primary badge-normal">In Stock</span></p>
</div>
Component Patterns
Container/Presenter Pattern
Separate logic from presentation:
using HypertextTemplates
using HypertextTemplates.Elements
# Presenter component (pure UI)
@component function user_list_view(; users)
@div {class = "user-list"} begin
for user in users
# Use data attributes for JavaScript interaction
@div {class = "user-item", "data-user-id" := user.id} begin
@img {src = user.avatar, alt = user.name, width = 32, height = 32}
@span " " $(user.name)
end
end
end
end
@deftag macro user_list_view end
# Example usage with mock data
users = [
(id = 1, name = "Alice Johnson", avatar = "/avatars/alice.jpg"),
(id = 2, name = "Bob Smith", avatar = "/avatars/bob.jpg"),
(id = 3, name = "Charlie Brown", avatar = "/avatars/charlie.jpg")
]
html = @render @user_list_view {users}
<div class="user-list">
<div class="user-item" data-user-id="1">
<img src="/avatars/alice.jpg" alt="Alice Johnson" width="32" height="32">
<span> Alice Johnson</span>
</div>
<div class="user-item" data-user-id="2">
<img src="/avatars/bob.jpg" alt="Bob Smith" width="32" height="32">
<span> Bob Smith</span>
</div>
<div class="user-item" data-user-id="3">
<img src="/avatars/charlie.jpg" alt="Charlie Brown" width="32" height="32">
<span> Charlie Brown</span>
</div>
</div>
Compound Components
Related components that work together:
using HypertextTemplates
using HypertextTemplates.Elements
module Tabs
using HypertextTemplates
using HypertextTemplates.Elements
@component function container(; active_tab = 1)
@div {class = "tabs", "data-active" := active_tab} begin
@__slot__
end
end
@deftag macro container end
@component function list()
@ul {class = "tab-list", role = "tablist"} begin
@__slot__
end
end
@deftag macro list end
@component function tab(; index, active = false)
@li {role = "presentation"} begin
@button {
role = "tab",
class = active ? "tab active" : "tab",
"aria-selected" := active
} begin
@__slot__
end
end
end
@deftag macro tab end
@component function panels()
@div {class = "tab-panels"} begin
@__slot__
end
end
@deftag macro panels end
@component function panel(; index, active = false)
@div {
role = "tabpanel",
class = active ? "panel active" : "panel",
hidden = !active
} begin
@__slot__
end
end
@deftag macro panel end
end
# Usage
html = @render @Tabs.container {active_tab = 2} begin
@Tabs.list begin
@Tabs.tab {index = 1, active = false} "Tab 1"
@Tabs.tab {index = 2, active = true} "Tab 2"
@Tabs.tab {index = 3, active = false} "Tab 3"
end
@Tabs.panels begin
@Tabs.panel {index = 1, active = false} begin
@p "Content for tab 1"
end
@Tabs.panel {index = 2, active = true} begin
@p "Content for tab 2"
end
@Tabs.panel {index = 3, active = false} begin
@p "Content for tab 3"
end
end
end
<div class="tabs" data-active="2">
<ul class="tab-list" role="tablist">
<li role="presentation"><button role="tab" class="tab">Tab 1</button></li>
<li role="presentation">
<button role="tab" class="tab active" aria-selected>Tab 2</button>
</li>
<li role="presentation"><button role="tab" class="tab">Tab 3</button></li>
</ul>
<div class="tab-panels">
<div role="tabpanel" class="panel" hidden><p>Content for tab 1</p></div>
<div role="tabpanel" class="panel active"><p>Content for tab 2</p></div>
<div role="tabpanel" class="panel" hidden><p>Content for tab 3</p></div>
</div>
</div>
Safe Rendering Pattern
While HypertextTemplates renders directly to IO (making traditional try-catch error boundaries impossible), you can implement safe rendering patterns:
using HypertextTemplates
using HypertextTemplates.Elements
# Helper function to safely access nested data
safe_get(obj, field, default="N/A") = try
getfield(obj, field)
catch
default
end
# Component that handles potentially missing data
@component function user_card(; user=nothing)
@div {class = "user-card"} begin
if user !== nothing
@h3 safe_get(user, :name, "Unknown User")
@p "Email: " safe_get(user, :email)
@p "Role: " safe_get(user, :role, "Guest")
else
@div {class = "empty-state"} begin
@p "No user data available"
end
end
end
end
@deftag macro user_card end
# Example with valid user
user = (name = "Alice", email = "alice@example.com", role = "Admin")
html1 = @render @user_card {user}
<div class="user-card">
<h3></h3>
<p>Email:</p>
<p>Role:</p>
</div>
# Example with missing user
html2 = @render @user_card {}
<div class="user-card">
<div class="empty-state"><p>No user data available</p></div>
</div>
# Example with partial data
partial_user = (name = "Bob") # Missing email and role
html3 = @render @user_card {user = partial_user}
<div class="user-card">
<h3></h3>
<p>Email:</p>
<p>Role:</p>
</div>
Best Practices
1. Keep Components Focused
Each component should have a single, clear purpose:
using HypertextTemplates
using HypertextTemplates.Elements
# Good: Focused components
@component function price_display(; amount, currency = "USD")
@span {class = "price"} $currency " " $amount
end
@deftag macro price_display end
@component function product_card(; product)
@div {class = "product"} begin
@h3 $(product.name)
@price_display {amount = product.price, currency = product.currency}
end
end
@deftag macro product_card end
# Example usage
product = (name = "Laptop", price = 999.99, currency = "USD")
html = @render @product_card {product}
<div class="product">
<h3>Laptop</h3>
<span class="price">USD 999.99</span>
</div>
2. Use Props for Configuration
Make components flexible through props:
using HypertextTemplates
using HypertextTemplates.Elements
@component function data_table(;
data,
columns,
striped = true,
hoverable = true,
bordered = false
)
classes = [
"table",
striped ? "table-striped" : nothing,
hoverable ? "table-hover" : nothing,
bordered ? "table-bordered" : nothing
] |> x -> filter(!isnothing, x) |> x -> join(x, " ")
@table {class = classes} begin
@thead begin
@tr begin
for col in columns
@th $col
end
end
end
@tbody begin
for row in data
@tr begin
for value in row
@td $value
end
end
end
end
end
end
@deftag macro data_table end
# Example usage
columns = ["Name", "Age", "City"]
data = [
["Alice", 25, "New York"],
["Bob", 30, "London"],
["Charlie", 35, "Tokyo"]
]
html = @render @data_table {data, columns, striped = true, bordered = true}
<table class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th>Name</th>
<th>Age</th>
<th>City</th>
</tr>
</thead>
<tbody>
<tr>
<td>Alice</td>
<td>25</td>
<td>New York</td>
</tr>
<tr>
<td>Bob</td>
<td>30</td>
<td>London</td>
</tr>
<tr>
<td>Charlie</td>
<td>35</td>
<td>Tokyo</td>
</tr>
</tbody>
</table>
3. Document Components
Add docstrings to components:
"""
@alert(; type, title, message, dismissible)
Display an alert message with optional dismiss button.
# Arguments
- `type::String = "info"`: Alert type (info, warning, error, success)
- `title::String`: Alert title
- `message::String`: Alert message body
- `dismissible::Bool = false`: Whether alert can be dismissed
"""
@component function alert(; type = "info", title, message, dismissible = false)
# Implementation
end
4. Consider Performance
For frequently rendered components, optimize:
using HypertextTemplates
using HypertextTemplates.Elements
# Precompute static values
const BUTTON_CLASSES = Dict(
:primary => "btn btn-primary",
:secondary => "btn btn-secondary",
:danger => "btn btn-danger"
)
@component function my_button(; variant = :primary)
@button {class = BUTTON_CLASSES[variant]} @__slot__
end
@deftag macro my_button end
# Example usage
@render @my_button {variant = :primary} "Click me"
<button class="btn btn-primary">Click me</button>
@render @my_button {variant = :danger} "Delete"
<button class="btn btn-danger">Delete</button>
5. Error Handling
Handle edge cases gracefully:
using HypertextTemplates
using HypertextTemplates.Elements
@component function safe_image(; src, alt = "", fallback = "/placeholder.png")
image_src = isempty(src) ? fallback : src
@img {src = image_src, alt, onerror = "this.src='$fallback'"}
end
@deftag macro safe_image end
# Example usage
@render @safe_image {src = "/user.jpg", alt = "User avatar"}
<img
src="/user.jpg"
alt="User avatar"
onerror="this.src='/placeholder.png'"
>
@render @safe_image {src = "", alt = "User avatar"}
<img
src="/placeholder.png"
alt="User avatar"
onerror="this.src='/placeholder.png'"
>
Context: Avoiding Prop Drilling
For cross-cutting concerns like themes, authentication, or localization, passing props through every component level becomes cumbersome. The context system provides a cleaner solution:
using HypertextTemplates
using HypertextTemplates.Elements
@deftag macro app_without_context end
@deftag macro navbar end
@deftag macro user_menu end
@deftag macro navbar_ctx end
@deftag macro user_menu_ctx end
# Without context - props passed through every level
@component function app_without_context(; user)
@navbar {user} # Pass to navbar
end
@component function navbar(; user)
@user_menu {user} # Pass to user menu
end
@component function user_menu(; user)
@span "Welcome, $(user.name)!"
end
# With context - cleaner and more maintainable
@component function app_with_context(; user)
@context {current_user = user} begin
@navbar_ctx # No need to pass user
end
end
@component function navbar_ctx()
@user_menu_ctx # No need to pass user
end
@component function user_menu_ctx()
user = @get_context(:current_user)
@span "Welcome, $(user.name)!"
end
user_menu_ctx (generic function with 1 method)
Context is particularly useful for:
- Theme systems - Colors, styles, dark/light mode
- Authentication - Current user, permissions
- Localization - Language, date/number formats
- Feature flags - Enable/disable features globally
For detailed context usage and patterns, see the Context System section in the Advanced Features guide.
Summary
Components combine reusability, composition through slots, type safety, flexible rendering, and context support to create maintainable templates. Use these patterns to structure your applications effectively.