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>

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>

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

Getting Context

Access shared context data using getContext():

<script>
local pageContext = getContext("pageContext")
local currentNode = getContext("currentNode")
local currentTheme = pageContext.currentTheme
</script>

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

Available Context

Common context variables include:

  • pageContext - Page-level data and configuration
  • currentNode - Current content node data
  • currentTheme - Active theme settings

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>

Component Factories

<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>

Slots (Named Children)

Pass specific content to named slots:

<!-- Card.luat -->
<div class="card">
<div class="card-header">
{@render props.header?.()}
</div>
<div class="card-body">
{@render props.children?.()}
</div>
<div class="card-footer">
{@render props.footer?.()}
</div>
</div>

Usage:

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

local function renderHeader()
return "<h2>Card Title</h2>"
end

local function renderFooter()
return "<button>Action</button>"
end
</script>

<Card header={renderHeader} footer={renderFooter}>
<p>This is the main card content.</p>
</Card>

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>