Markup DSL
Azu's Markup DSL allows you to build HTML components directly in Crystal code with type safety, component composition, and real-time event handling. It provides a clean, readable syntax for generating HTML without template files.
Overview
The Markup DSL provides:
Type-safe HTML generation with compile-time validation
Component composition for reusable UI elements
Real-time event handling for interactive components
Clean, readable syntax that mirrors HTML structure
Performance optimization with minimal allocations
Basic Usage
Simple HTML Generation
class WelcomeComponent < Azu::Component
def content
div class: "welcome" do
h1 "Welcome to Azu!"
p "Build fast, type-safe web applications with Crystal."
button class: "btn btn-primary", onclick: "showFeatures()" do
text "Learn More"
end
end
end
end
Component with Data
class UserCardComponent < Azu::Component
def initialize(@user : User)
end
def content
div class: "user-card", id: "user-#{@user.id}" do
img src: @user.avatar_url, alt: @user.name, class: "avatar"
div class: "user-info" do
h3 @user.name
p @user.email
span class: "role #{@user.role}" do
text @user.role.capitalize
end
end
div class: "actions" do
button class: "btn btn-sm", onclick: "editUser(#{@user.id})" do
text "Edit"
end
button class: "btn btn-sm btn-danger", onclick: "deleteUser(#{@user.id})" do
text "Delete"
end
end
end
end
end
HTML Elements
Basic Elements
def content
# Headings
h1 "Main Title"
h2 "Subtitle"
h3 "Section Title"
# Paragraphs
p "This is a paragraph."
p class: "highlight" do
text "This is a highlighted paragraph."
end
# Links
a href: "/users", class: "nav-link" do
text "View Users"
end
# Images
img src: "/images/logo.png", alt: "Logo", class: "logo"
# Lists
ul class: "menu" do
li "Home"
li "About"
li "Contact"
end
# Tables
table class: "data-table" do
thead do
tr do
th "Name"
th "Email"
th "Role"
end
end
tbody do
users.each do |user|
tr do
td user.name
td user.email
td user.role
end
end
end
end
end
Forms
def content
form method: "POST", action: "/users", class: "user-form" do
div class: "form-group" do
label "Name", for: "name"
input type: "text", id: "name", name: "name", value: @user.name, required: true
end
div class: "form-group" do
label "Email", for: "email"
input type: "email", id: "email", name: "email", value: @user.email, required: true
end
div class: "form-group" do
label "Role", for: "role"
select id: "role", name: "role" do
option value: "user", selected: @user.role == "user" do
text "User"
end
option value: "admin", selected: @user.role == "admin" do
text "Administrator"
end
end
end
div class: "form-actions" do
button type: "submit", class: "btn btn-primary" do
text "Save User"
end
a href: "/users", class: "btn btn-secondary" do
text "Cancel"
end
end
end
end
Component Composition
Reusable Components
class ButtonComponent < Azu::Component
def initialize(@text : String, @variant : String = "primary", @size : String = "md")
end
def content
button class: "btn btn-#{@variant} btn-#{@size}" do
text @text
end
end
end
class ModalComponent < Azu::Component
def initialize(@title : String, @id : String)
end
def content
div class: "modal", id: @id do
div class: "modal-dialog" do
div class: "modal-content" do
div class: "modal-header" do
h5 class: "modal-title" do
text @title
end
button type: "button", class: "btn-close", onclick: "closeModal('#{@id}')"
end
div class: "modal-body" do
yield
end
end
end
end
end
end
Using Components
class UserListComponent < Azu::Component
def content
div class: "user-list" do
users.each do |user|
UserCardComponent.new(user).render
end
div class: "actions" do
ButtonComponent.new("Add User", "success").render
ButtonComponent.new("Export", "secondary").render
end
end
end
end
Real-time Components
Live Components with Events
class CounterComponent < Azu::Component
def initialize(@initial_count : Int32 = 0)
@count = @initial_count
end
def content
div class: "counter" do
h3 "Counter"
span id: "count", class: "count" do
text @count.to_s
end
div class: "controls" do
button class: "btn btn-sm", onclick: "increment()" do
text "Increment"
end
button class: "btn btn-sm", onclick: "decrement()" do
text "Decrement"
end
button class: "btn btn-sm btn-secondary", onclick: "reset()" do
text "Reset"
end
end
end
end
def on_event("increment", data)
@count += 1
update_element "count", @count.to_s
end
def on_event("decrement", data)
@count -= 1
update_element "count", @count.to_s
end
def on_event("reset", data)
@count = @initial_count
update_element "count", @count.to_s
end
end
Form Components with Validation
class UserFormComponent < Azu::Component
def initialize(@user : User? = nil)
@errors = {} of String => String
end
def content
form method: "POST", action: "/users", class: "user-form" do
div class: "form-group" do
label "Name", for: "name"
input type: "text", id: "name", name: "name", value: @user.try(&.name), required: true
if error = @errors["name"]?
span class: "error" do
text error
end
end
end
div class: "form-group" do
label "Email", for: "email"
input type: "email", id: "email", name: "email", value: @user.try(&.email), required: true
if error = @errors["email"]?
span class: "error" do
text error
end
end
end
div class: "form-actions" do
button type: "submit", class: "btn btn-primary" do
text @user ? "Update User" : "Create User"
end
end
end
end
def on_event("validation_error", data)
@errors = data["errors"].as(Hash(String, String))
render_errors
end
private def render_errors
@errors.each do |field, message|
update_element "#{field}-error", message
end
end
end
Conditional Rendering
If Statements
def content
div class: "user-profile" do
h1 @user.name
if @user.admin?
div class: "admin-badge" do
text "Administrator"
end
end
if @user.avatar_url
img src: @user.avatar_url, alt: @user.name, class: "avatar"
else
div class: "avatar-placeholder" do
text @user.name[0].upcase
end
end
unless @user.posts.empty?
div class: "recent-posts" do
h3 "Recent Posts"
@user.posts.each do |post|
div class: "post" do
h4 post.title
p post.excerpt
end
end
end
end
end
end
Case Statements
def content
div class: "status-indicator" do
case @user.status
when "active"
span class: "status active" do
text "Active"
end
when "inactive"
span class: "status inactive" do
text "Inactive"
end
when "suspended"
span class: "status suspended" do
text "Suspended"
end
else
span class: "status unknown" do
text "Unknown"
end
end
end
end
Loops and Iteration
Basic Loops
def content
div class: "user-list" do
users.each do |user|
div class: "user-item" do
h3 user.name
p user.email
end
end
end
end
Loops with Index
def content
table class: "data-table" do
thead do
tr do
th "#"
th "Name"
th "Email"
th "Actions"
end
end
tbody do
users.each_with_index do |user, index|
tr class: index.even? ? "even" : "odd" do
td (index + 1).to_s
td user.name
td user.email
td do
button class: "btn btn-sm", onclick: "editUser(#{user.id})" do
text "Edit"
end
end
end
end
end
end
end
Conditional Loops
def content
div class: "posts" do
if posts.any?
posts.each do |post|
article class: "post" do
h2 post.title
p post.excerpt
time post.created_at.to_s("%Y-%m-%d")
end
end
else
div class: "no-posts" do
text "No posts found."
end
end
end
end
Attributes and Properties
Dynamic Attributes
def content
# Basic attributes
div class: "container", id: "main-content" do
text "Content"
end
# Conditional attributes
div class: "alert #{@alert_type}", role: "alert" do
text @message
end
# Data attributes
div data_user_id: @user.id, data_role: @user.role do
text @user.name
end
# Style attributes
div style: "background-color: #{@bg_color}; color: #{@text_color}" do
text "Styled content"
end
end
Boolean Attributes
def content
# Checkbox with checked state
input type: "checkbox", checked: @user.active?, name: "active"
# Disabled button
button disabled: @user.locked?, class: "btn" do
text "Edit"
end
# Required field
input type: "email", required: true, name: "email"
# Readonly field
input type: "text", readonly: true, value: @user.id
end
Event Handling
Client-side Events
def content
div class: "interactive-component" do
button onclick: "handleClick()", class: "btn" do
text "Click Me"
end
input type: "text", onchange: "handleChange(this.value)", placeholder: "Type something"
select onchange: "handleSelect(this.value)" do
option value: "option1" do
text "Option 1"
end
option value: "option2" do
text "Option 2"
end
end
end
end
Server-side Event Handling
class ChatComponent < Azu::Component
def content
div class: "chat" do
div id: "messages", class: "messages" do
@messages.each do |message|
div class: "message" do
span class: "user" do
text message.user
end
span class: "text" do
text message.text
end
end
end
end
form onsubmit: "sendMessage(event)" do
input type: "text", id: "message-input", placeholder: "Type a message..."
button type: "submit" do
text "Send"
end
end
end
end
def on_event("send_message", data)
message_text = data["text"].as(String)
user = data["user"].as(String)
# Add message to chat
@messages << Message.new(user, message_text)
# Update the messages container
update_element "messages" do
@messages.each do |message|
div class: "message" do
span class: "user" do
text message.user
end
span class: "text" do
text message.text
end
end
end
end
end
end
Performance Optimization
Lazy Loading
class LazyListComponent < Azu::Component
def initialize(@items : Array(Item), @page_size : Int32 = 20)
@current_page = 0
end
def content
div class: "lazy-list" do
div id: "items-container" do
render_items(@current_page)
end
if @items.size > (@current_page + 1) * @page_size
button onclick: "loadMore()", class: "btn btn-secondary" do
text "Load More"
end
end
end
end
def on_event("load_more", data)
@current_page += 1
new_items = render_items(@current_page)
append_element "items-container", new_items
end
private def render_items(page : Int32)
start_index = page * @page_size
end_index = Math.min(start_index + @page_size, @items.size)
@items[start_index...end_index].map do |item|
div class: "item" do
text item.name
end
end
end
end
Caching
class CachedComponent < Azu::Component
def content
cached_fragment "user-list", ttl: 300 do
div class: "user-list" do
users.each do |user|
UserCardComponent.new(user).render
end
end
end
end
end
Best Practices
1. Component Structure
Keep components focused and single-purpose
Use descriptive component names
Separate presentation logic from business logic
Use composition over inheritance
2. Performance
Minimize DOM updates
Use lazy loading for large lists
Cache expensive operations
Optimize event handlers
3. Maintainability
Use consistent naming conventions
Document complex components
Keep components small and focused
Use type-safe data binding
4. Accessibility
Include proper ARIA attributes
Use semantic HTML elements
Ensure keyboard navigation
Provide alternative text for images
Next Steps
Template Engine - Learn about Jinja2 templates
Hot Reloading - Development workflow
Real-time Components - WebSocket integration
Component Examples - Working examples
Ready to build components? Start with the basic examples above, then explore Real-time Components for interactive features.
Last updated
Was this helpful?