Skip to main content

Components

Components are the building blocks of LUAT applications. They encapsulate reusable UI elements and can accept props and render children.

Basic Components

Creating a Component

A component is simply a .luat file that exports a render function:

<!-- Button.luat -->
<script>
local variant = props.variant or "primary"
local size = props.size or "medium"
local classes = "btn btn-" .. variant .. " btn-" .. size
</script>

<button class={classes} type={props.type or "button"}>
{@render props.children?.()}
</button>

Using Components

Import components in the script block and use them like HTML elements:

<!-- Page.luat -->
<script>
local Button = require("components/Button")
</script>

<div class="page">
<Button variant="primary" size="large">
Click me!
</Button>

<Button variant="secondary">
Cancel
</Button>
</div>

Try it live - Click the tabs to see both files:

Loading Luat...

Props

Passing Props

Props are passed as attributes on the component tag:

<script>
local Card = require("components/Card")
</script>

<Card
title="User Profile"
subtitle="Manage your account"
image="/avatar.jpg"
isActive={true}
/>

Spreading Props

You can use the spread operator (...) to pass all properties from a table to a component:

<script>
local Card = require("components/Card")
local cardProps = {
title = "User Profile",
subtitle = "Manage your account",
image = "/avatar.jpg",
isActive = true
}
</script>

<Card {...cardProps} />

This is particularly useful when:

  • Forwarding props from a parent component
  • Applying common properties to multiple components
  • Building reusable component configurations

You can combine multiple spreads and override specific properties:

<script>
local baseProps = { size = "medium", variant = "default" }
local themeProps = { variant = "primary", outlined = true }
</script>

<!-- Later spreads override earlier ones, and direct props override all spreads -->
<Button {...baseProps} {...themeProps} size="large" />

See Template Syntax: Props Spread Operator for more details.

Accessing Props

Inside the component, access props via the global props table:

<!-- Card.luat -->
<script>
local title = props.title
local subtitle = props.subtitle or ""
local image = props.image
local isActive = props.isActive or false
</script>

<div class="card {isActive and 'active' or ''}">
{#if image}
<img src={image} alt={title} class="card-image">
{/if}

<div class="card-content">
<h3 class="card-title">{title}</h3>
{#if subtitle}
<p class="card-subtitle">{subtitle}</p>
{/if}
</div>
</div>

Default Props

Handle missing props with default values:

<script>
local variant = props.variant or "default"
local size = props.size or "medium"
local disabled = props.disabled or false
local children = props.children
</script>

Children

Rendering Children

Use {@render props.children?.()} to render content passed between component tags:

<!-- Modal.luat -->
<script>
local title = props.title
local isOpen = props.isOpen or false
</script>

{#if isOpen}
<div class="modal-overlay">
<div class="modal">
<div class="modal-header">
<h2>{title}</h2>
<button class="modal-close">×</button>
</div>
<div class="modal-body">
{@render props.children?.()}
</div>
</div>
</div>
{/if}

Using Components with Children

Pass content between the opening and closing tags:

<script>
local Modal = require("components/Modal")
</script>

<Modal title="Confirm Action" isOpen={showModal}>
<p>Are you sure you want to delete this item?</p>
<div class="modal-actions">
<button>Cancel</button>
<button class="danger">Delete</button>
</div>
</Modal>

Try it live - A simple Alert component with children:

Loading Luat...

Component Composition

Nested Components

Components can use other components:

<!-- UserCard.luat -->
<script>
local Card = require("components/Card")
local Button = require("components/Button")
local Avatar = require("components/Avatar")
</script>

<Card title={props.user.name} subtitle={props.user.role}>
<div class="user-card-content">
<Avatar src={props.user.avatar} size="large" />

<div class="user-info">
<p>Email: {props.user.email}</p>
<p>Joined: {props.user.createdAt}</p>
</div>

<div class="user-actions">
<Button variant="primary">Edit</Button>
<Button variant="secondary">View Profile</Button>
</div>
</div>
</Card>

Layout Components

Create reusable layout components:

<!-- Page.luat -->
<script>
local AppBar = require("components/AppBar")
local Footer = require("components/Footer")
local currentNode = getContext("currentNode")
local pageContext = getContext("pageContext")
</script>

<html>
<head>
<title>{currentNode.properties.title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div class="app">
<AppBar />

<main class="main-content">
{@render props.children?.()}
</main>

<Footer />
</div>
</body>
</html>

Component Context

Context provides a way to pass data through the component tree without having to pass props manually at every level. This is useful for sharing data like themes, user information, or application state that many components need access to.

Setting Context

Use setContext(key, value) to make data available to all child components:

<script>
-- Set context values that children can access
setContext("theme", "dark")
setContext("user", { name = "Alice", role = "admin" })
setContext("config", { language = "en", currency = "USD" })
</script>

<ChildComponent />

Getting Context

Use getContext(key) to retrieve context values set by parent components:

<script>
-- Access context from any parent component
local theme = getContext("theme")
local user = getContext("user")
local config = getContext("config")
</script>

<div class={"app theme-" .. (theme or "light")}>
Welcome, {user and user.name or "Guest"}!
</div>

How Context Works

Context in Luat uses a stack-based scoping system:

  1. Each component creates a new scope when it renders
  2. setContext() writes to the current component's scope
  3. getContext() searches from the current scope up through all parent scopes
  4. When a component finishes rendering, its scope is removed

This means:

  • Child components can access context set by any ancestor
  • Sibling components cannot see each other's context
  • Context values can be "overridden" by setting the same key in a child component

Context Flow Example

Here's a practical example showing context flowing through three levels of components. The App sets theme and user context, Dashboard adds navigation context, and UserCard (three levels deep) accesses all of them:

Loading Luat...

Context vs Props

Use props when:

  • Data is specific to one component
  • Parent needs to customize child behavior
  • You want explicit data flow

Use context when:

  • Many components at different levels need the same data
  • Passing props through intermediate components is cumbersome ("prop drilling")
  • Data is "global" within a subtree (theme, user, locale)
<!-- Props: explicit, local -->
<Button variant="primary" size="large">Click</Button>

<!-- Context: implicit, shared -->
<script>
setContext("theme", { variant = "primary", size = "large" })
</script>
<ThemedButton>Click</ThemedButton> <!-- reads theme from context -->

Framework-Provided Context

When using Luat with a framework like Wunderframe, certain context values are automatically available:

  • pageContext - Page-level data and configuration
  • currentNode - Current content node data
  • currentTheme - Active theme settings
<script>
local pageContext = getContext("pageContext")
local currentNode = getContext("currentNode")
</script>

<div data-theme={pageContext.currentTheme}>
<h1>{currentNode.properties.title}</h1>
</div>

Component Types

Module Exports

Components can export metadata using module scripts:

<!-- Hero.luat -->
<script module>
local type = "content:hero"
exports.type = type
</script>

<script>
local title = props.title
local subtitle = props.subtitle
local image = props.image
</script>

<div class="hero" style="background-image: url('{image}')">
<div class="hero-content">
<h1>{title}</h1>
<p>{subtitle}</p>
</div>
</div>

Advanced Patterns

Conditional Component Rendering

<script>
local Button = require("components/Button")
local Link = require("components/Link")

local ComponentToRender = props.href and Link or Button
</script>

<ComponentToRender href={props.href} onClick={props.onClick}>
{props.label}
</ComponentToRender>

Dynamic Block Rendering

A powerful pattern for CMS-style content is rendering different components based on block types. Here's how to render a blog post with various content blocks:

Loading Luat...

This pattern is useful for:

  • CMS content - Render different block types (text, images, videos, embeds)
  • Page builders - Dynamic layouts with configurable sections
  • Form builders - Different input types based on field configuration

Component Factories

Create reusable HTML generators using factory functions. These return HTML strings that are rendered with {@html}:

<script>
local function createIcon(name)
return function(iconProps)
return string.format('<i class="icon icon-%s %s"></i>',
name, iconProps.class or "")
end
end

local HomeIcon = createIcon("home")
local UserIcon = createIcon("user")
</script>

<nav>
<a href="/home">{@html HomeIcon({class = "nav-icon"})}</a>
<a href="/profile">{@html UserIcon({class = "nav-icon"})}</a>
</nav>

Note: This pattern uses string-based HTML generation, not true component rendering. The factory function returns an HTML string, and {@html} outputs it unescaped. For reusable UI elements with children support, use regular components instead.

main.luat
Loading Luat...

Multiple Content Sections

Components can accept multiple render functions as props to create flexible layouts with header, body, and footer sections:

<!-- Card.luat -->
<script>
local title = props.title
</script>

<div class="card">
{#if title}
<div class="card-header">
<h2>{title}</h2>
</div>
{/if}
<div class="card-body">
{@render props.children?.()}
</div>
{#if props.footer}
<div class="card-footer">
{@render props.footer()}
</div>
{/if}
</div>

Usage with title prop and footer render function:

<script>
local Card = require("components/Card")
</script>

<Card title="User Profile" footer={function(__write)
__write("<button>Save</button>")
end}>
<p>This is the main card content.</p>
</Card>
Loading Luat...

Best Practices

1. Keep components focused

Each component should have a single responsibility:

<!-- Good: Focused button component -->
<!-- Button.luat -->
<button class="btn btn-{props.variant}" disabled={props.disabled}>
{@render props.children?.()}
</button>

2. Use descriptive prop names

<!-- Good -->
<UserCard
user={userData}
showActions={true}
onEdit={handleEdit}
/>

<!-- Avoid -->
<UserCard
data={userData}
flag={true}
callback={handleEdit}
/>

3. Provide sensible defaults

<script>
local variant = props.variant or "primary"
local size = props.size or "medium"
local disabled = props.disabled or false
</script>

4. Document complex components

<!-- 
UserProfileCard.luat

Props:
- user (object): User data with name, email, avatar
- editable (boolean): Whether to show edit controls
- onSave (function): Callback when profile is saved
- onCancel (function): Callback when editing is cancelled
-->
<script>
local user = props.user
local editable = props.editable or false
</script>