Components
Azu's component system provides a powerful way to build interactive, real-time UI components. With type safety, automatic updates, and efficient rendering, components make building dynamic web applications straightforward and maintainable.
What are Components?
Components in Azu are:
Interactive UI Elements: Reusable, stateful UI components
Real-time Updates: Automatic updates when state changes
Type Safe: Compile-time type safety for component state
Efficient: Minimal re-rendering and optimal performance
Testable: Easy to test and validate
Basic Component
class CounterComponent
include Azu::Component
@count = 0
def content
div class: "counter" do
h2 { text "Counter: #{@count}" }
button "Increment", onclick: "increment"
button "Decrement", onclick: "decrement"
button "Reset", onclick: "reset"
end
end
def on_event("increment", data)
@count += 1
update!
end
def on_event("decrement", data)
@count -= 1
update!
end
def on_event("reset", data)
@count = 0
update!
end
endComponent Lifecycle
Mounting
class LifecycleComponent
include Azu::Component
def initialize(@user : User)
@mounted_at = Time.utc
@update_count = 0
end
def content
div class: "lifecycle-component" do
h3 { text "Component Lifecycle" }
p { text "Mounted at: #{@mounted_at.to_rfc3339}" }
p { text "Update count: #{@update_count}" }
p { text "Age: #{age}" }
end
end
def on_mount
Log.info { "Component mounted: #{self.class.name}" }
@mounted_at = Time.utc
end
def on_update
@update_count += 1
Log.info { "Component updated: #{@update_count} times" }
end
def on_unmount
Log.info { "Component unmounted: #{self.class.name}" }
end
private def age
Time.utc - @mounted_at
end
endState Management
class StatefulComponent
include Azu::Component
def initialize(@initial_state : Hash(String, JSON::Any))
@state = @initial_state.dup
@listeners = [] of Proc(String, JSON::Any, Nil)
end
def content
div class: "stateful-component" do
h3 { text "Stateful Component" }
@state.each do |key, value|
div class: "state-item" do
span { text "#{key}: " }
span { text value.to_s }
button "Update", onclick: "update_#{key}"
end
end
end
end
def on_event("update_name", data)
@state["name"] = JSON::Any.new(data["value"].as_s)
notify_listeners("name", @state["name"])
update!
end
def on_event("update_email", data)
@state["email"] = JSON::Any.new(data["value"].as_s)
notify_listeners("email", @state["email"])
update!
end
def add_listener(&block : String, JSON::Any -> Nil)
@listeners << block
end
private def notify_listeners(key : String, value : JSON::Any)
@listeners.each do |listener|
listener.call(key, value)
end
end
endEvent Handling
Client Events
class EventComponent
include Azu::Component
def content
div class: "event-component" do
h3 { text "Event Handling" }
# Form events
form onsubmit: "handle_submit" do
input type: "text", name: "name", placeholder: "Enter name"
input type: "email", name: "email", placeholder: "Enter email"
button type: "submit", text: "Submit"
end
# Click events
button "Click me", onclick: "handle_click"
# Input events
input type: "text", oninput: "handle_input", placeholder: "Type something"
# Custom events
button "Custom Event", onclick: "trigger_custom"
end
end
def on_event("handle_submit", data)
name = data["name"]?.try(&.as_s) || ""
email = data["email"]?.try(&.as_s) || ""
# Process form submission
process_form(name, email)
end
def on_event("handle_click", data)
# Handle click event
Log.info { "Button clicked" }
update!
end
def on_event("handle_input", data)
value = data["value"]?.try(&.as_s) || ""
# Handle input change
Log.info { "Input changed: #{value}" }
end
def on_event("trigger_custom", data)
# Trigger custom event
trigger_event("custom_event", {message: "Custom event triggered"})
end
private def process_form(name : String, email : String)
# Process form data
Log.info { "Form submitted: #{name} - #{email}" }
end
endServer Events
class ServerEventComponent
include Azu::Component
def content
div class: "server-event-component" do
h3 { text "Server Events" }
p { text "Last server event: #{@last_server_event}" }
button "Request Server Event", onclick: "request_server_event"
end
end
def on_event("request_server_event", data)
# Request server event
spawn request_server_event
end
def on_server_event(event_type : String, data : JSON::Any)
case event_type
when "user_updated"
handle_user_update(data)
when "notification"
handle_notification(data)
when "error"
handle_error(data)
end
end
private def request_server_event
# Simulate server event
sleep 1.second
trigger_server_event("user_updated", {
user_id: 123,
name: "Updated User",
timestamp: Time.utc.to_rfc3339
})
end
private def handle_user_update(data : JSON::Any)
@last_server_event = "User updated: #{data["name"]}"
update!
end
private def handle_notification(data : JSON::Any)
@last_server_event = "Notification: #{data["message"]}"
update!
end
private def handle_error(data : JSON::Any)
@last_server_event = "Error: #{data["message"]}"
update!
end
endComponent Composition
Parent-Child Components
class ParentComponent
include Azu::Component
def initialize(@user : User)
@child_components = [] of ChildComponent
end
def content
div class: "parent-component" do
h2 { text "Parent Component" }
p { text "User: #{@user.name}" }
# Child components
@child_components.each do |child|
child.render
end
# Add child component button
button "Add Child", onclick: "add_child"
end
end
def on_event("add_child", data)
child = ChildComponent.new(@user)
@child_components << child
update!
end
def on_event("remove_child", data)
child_id = data["child_id"]?.try(&.as_i64)
@child_components.reject! { |child| child.id == child_id }
update!
end
end
class ChildComponent
include Azu::Component
def initialize(@user : User)
@created_at = Time.utc
end
def content
div class: "child-component" do
h3 { text "Child Component" }
p { text "Created at: #{@created_at.to_rfc3339}" }
p { text "User: #{@user.name}" }
button "Remove", onclick: "remove_self"
end
end
def on_event("remove_self", data)
# Notify parent to remove this component
trigger_event("remove_child", {child_id: @id})
end
endComponent Communication
class CommunicationComponent
include Azu::Component
def initialize(@message_bus : MessageBus)
@message_bus = @message_bus
@message_bus.subscribe("user_updated", method(:handle_user_update))
end
def content
div class: "communication-component" do
h3 { text "Component Communication" }
p { text "Last message: #{@last_message}" }
button "Send Message", onclick: "send_message"
end
end
def on_event("send_message", data)
message = data["message"]?.try(&.as_s) || "Hello from component!"
@message_bus.publish("component_message", {message: message})
end
def handle_user_update(data : JSON::Any)
@last_message = "User updated: #{data["name"]}"
update!
end
def on_unmount
@message_bus.unsubscribe("user_updated", method(:handle_user_update))
end
endComponent Testing
Unit Testing
require "spec"
describe CounterComponent do
it "initializes with zero count" do
component = CounterComponent.new
component.count.should eq(0)
end
it "increments count on increment event" do
component = CounterComponent.new
component.on_event("increment", {})
component.count.should eq(1)
end
it "decrements count on decrement event" do
component = CounterComponent.new
component.on_event("increment", {})
component.on_event("decrement", {})
component.count.should eq(0)
end
it "resets count on reset event" do
component = CounterComponent.new
component.on_event("increment", {})
component.on_event("increment", {})
component.on_event("reset", {})
component.count.should eq(0)
end
endIntegration Testing
describe "Component Integration" do
it "handles multiple events" do
component = CounterComponent.new
# Simulate multiple events
component.on_event("increment", {})
component.on_event("increment", {})
component.on_event("decrement", {})
component.count.should eq(1)
end
it "handles event with data" do
component = StatefulComponent.new({})
component.on_event("update_name", {value: "Alice"})
component.state["name"].should eq(JSON::Any.new("Alice"))
end
endPerformance Optimization
Lazy Rendering
class LazyComponent
include Azu::Component
def initialize(@items : Array(Item))
@visible_items = [] of Item
@page_size = 10
@current_page = 0
end
def content
div class: "lazy-component" do
h3 { text "Lazy Loading Component" }
# Render visible items
@visible_items.each do |item|
render_item(item)
end
# Load more button
if has_more_items?
button "Load More", onclick: "load_more"
end
end
end
def on_event("load_more", data)
load_next_page
update!
end
private def load_next_page
start_index = @current_page * @page_size
end_index = start_index + @page_size
new_items = @items[start_index...end_index]
@visible_items.concat(new_items)
@current_page += 1
end
private def has_more_items? : Bool
@current_page * @page_size < @items.size
end
private def render_item(item : Item)
div class: "item" do
h4 { text item.name }
p { text item.description }
end
end
endMemoization
class MemoizedComponent
include Azu::Component
def initialize(@data : Array(DataItem))
@memoized_results = {} of String => JSON::Any
end
def content
div class: "memoized-component" do
h3 { text "Memoized Component" }
# Use memoized results
@data.each do |item|
result = get_memoized_result(item)
render_item(item, result)
end
end
end
private def get_memoized_result(item : DataItem) : JSON::Any
cache_key = "#{item.id}_#{item.updated_at}"
if cached = @memoized_results[cache_key]?
cached
else
result = expensive_calculation(item)
@memoized_results[cache_key] = result
result
end
end
private def expensive_calculation(item : DataItem) : JSON::Any
# Expensive calculation
sleep 0.1.seconds
JSON::Any.new({
processed: true,
value: item.value * 2,
timestamp: Time.utc.to_rfc3339
})
end
endComponent State Management
Global State
class GlobalStateComponent
include Azu::Component
def initialize(@global_state : GlobalState)
@global_state.subscribe("user_updated", method(:handle_user_update))
end
def content
div class: "global-state-component" do
h3 { text "Global State Component" }
p { text "Current user: #{@global_state.current_user.name}" }
p { text "Theme: #{@global_state.theme}" }
button "Toggle Theme", onclick: "toggle_theme"
end
end
def on_event("toggle_theme", data)
new_theme = @global_state.theme == "light" ? "dark" : "light"
@global_state.set_theme(new_theme)
end
def handle_user_update(data : JSON::Any)
# Handle global state change
update!
end
endLocal State
class LocalStateComponent
include Azu::Component
def initialize
@local_state = {
"counter" => JSON::Any.new(0),
"message" => JSON::Any.new(""),
"is_loading" => JSON::Any.new(false)
}
end
def content
div class: "local-state-component" do
h3 { text "Local State Component" }
# Counter
p { text "Counter: #{@local_state["counter"]}" }
button "Increment", onclick: "increment"
# Message
input type: "text", value: @local_state["message"].as_s, oninput: "update_message"
# Loading state
if @local_state["is_loading"].as_bool
p { text "Loading..." }
else
button "Load Data", onclick: "load_data"
end
end
end
def on_event("increment", data)
current = @local_state["counter"].as_i
@local_state["counter"] = JSON::Any.new(current + 1)
update!
end
def on_event("update_message", data)
message = data["value"]?.try(&.as_s) || ""
@local_state["message"] = JSON::Any.new(message)
update!
end
def on_event("load_data", data)
@local_state["is_loading"] = JSON::Any.new(true)
update!
# Simulate async operation
spawn load_data_async
end
private def load_data_async
sleep 2.seconds
@local_state["is_loading"] = JSON::Any.new(false)
@local_state["message"] = JSON::Any.new("Data loaded!")
update!
end
endBest Practices
1. Keep Components Simple
# Good: Simple, focused component
class UserCardComponent
include Azu::Component
def initialize(@user : User)
end
def content
div class: "user-card" do
h3 { text @user.name }
p { text @user.email }
end
end
end
# Avoid: Complex component with multiple responsibilities
class ComplexComponent
include Azu::Component
def content
# User management
# Post management
# Comment management
# Notification management
# Too many responsibilities!
end
end2. Use Composition
# Good: Compose simple components
class UserProfileComponent
include Azu::Component
def content
div class: "user-profile" do
UserCardComponent.new(@user).render
UserPostsComponent.new(@user).render
UserCommentsComponent.new(@user).render
end
end
end3. Handle Errors Gracefully
class ErrorHandlingComponent
include Azu::Component
def content
div class: "error-handling-component" do
if @error
div class: "error" do
text "Error: #{@error}"
end
else
# Normal content
end
end
end
def on_event("risky_operation", data)
begin
perform_risky_operation(data)
rescue e
@error = e.message
update!
end
end
end4. Use Type Safety
class TypeSafeComponent
include Azu::Component
def initialize(@user : User, @settings : UserSettings)
# Type-safe initialization
end
def on_event("update_settings", data)
# Type-safe event handling
settings = UserSettings.from_json(data.to_json)
@settings = settings
update!
end
end5. Test Thoroughly
describe "Component" do
it "handles all events correctly" do
component = MyComponent.new
# Test all event types
component.on_event("event1", {})
component.on_event("event2", {})
component.on_event("event3", {})
# Assert expected state
end
endNext Steps
Now that you understand components:
Templates - Use components in templates
WebSockets - Add real-time features to components
Testing - Test your components
Performance - Optimize component performance
State Management - Advanced state management patterns
Components in Azu provide a powerful way to build interactive, real-time UI elements. With type safety, automatic updates, and efficient rendering, they make building dynamic web applications straightforward and maintainable.
Last updated
Was this helpful?
