Components #

Components are one of the most powerful features of the Sugar templating engine. They let you package markup into reusable, self-contained building blocks with clear interfaces — props for data, slots for content injection, and automatic attribute merging. Combined with tag swapping and attribute merging on slot outlets, components give you fine-grained control over how caller and component markup combine, making it easy to build flexible, composable UI systems.

Components are opt-in. Enable the component extension first, then Sugar resolves component templates from configured component paths and renders them with props and slots.

Setup #

Before using components, you need to enable the component extension on your engine builder.

Basic Setup #

Register the component extension during engine configuration:

use Sugar\Core\Config\SugarConfig;
use Sugar\Core\Engine;
use Sugar\Core\Loader\FileTemplateLoader;
use Sugar\Extension\Component\ComponentExtension;

$engine = Engine::builder()
    ->withTemplateLoader(new FileTemplateLoader(
        templatePaths: __DIR__ . '/templates',
    ))
    ->withExtension(new ComponentExtension()) // [!code focus]
    ->build();

The component extension automatically discovers components in the configured components/ directory within all registered template namespaces.

Customizing Component Directories #

By default, components are loaded from a components/ directory within each namespace. You can customize this:

->withExtension(new ComponentExtension(['components', 'ui/components', 'shared']))

This searches for components in multiple subdirectories within each template namespace, in priority order. The first match is used.

Quick Start #

A component is just a template file that you call with a custom tag. Here’s the simplest possible component:

<button class="btn" type="button">
    <?= $slot ?>
</button>
<s-button>Click Me</s-button>
<button class="btn" type="button">Click Me</button>

In this example, <s-button> maps to components/s-button.sugar.php, and the inner content (Click Me) is injected into <?= $slot ?>.

Component Basics #

Filename Patterns #

Component files must use the element prefix and end with .sugar.php:

{prefix}-{name}.sugar.php

Examples:

  • s-button.sugar.php<s-button>
  • s-user-card.sugar.php<s-user-card>
  • x-alert.sugar.php<x-alert> when the prefix is x-

The fragment element filename (for example, s-template.sugar.php, x-template.sugar.php, or your configured fragment name) is reserved and not treated as a component.

File Organization #

Components live in a components/ directory within your template paths:

templates/
├── pages/
│   ├── home.sugar.php
│   └── profile.sugar.php
├── layouts/
│   └── base.sugar.php
└── components/ // [!code focus]
    ├── s-button.sugar.php // [!code focus]
    ├── s-card.sugar.php // [!code focus]
    └── s-alert.sugar.php // [!code focus]

Working with Slots #

Slots are placeholders in your component template where content from the usage site is inserted. Think of a slot as “content that flows from the calling template into the component template”.

Default Slots #

When you place content inside a component tag without any s:slot attribute, that content becomes the default slot:

<button class="btn" type="button">
    <?= $slot ?>
</button>
<s-button>Click Me</s-button>
<button class="btn" type="button">Click Me</button>

Named Slots #

Use s:slot="name" on elements in your usage markup to send content to specific named slots. The caller’s element tag replaces the outlet’s tag, and attributes from both sides are merged — class values are concatenated, other attributes are overridden by the caller:

<s-card>
    <h3 s:slot="title" class="text-lg">User Profile</h3>
    <p>Main content here</p>
    <nav s:slot="actions" class="flex gap-2">
        <a href="/edit">Edit</a>
    </nav>
</s-card>
<article class="card">
    <h2 s:slot="title" class="card-title">Untitled</h2>
    <div class="card-body"><?= $slot ?></div>
    <div s:slot="actions" class="card-actions"></div>
</article>
<article class="card">
    <h3 class="card-title text-lg">User Profile</h3>
    <div class="card-body">
        <p>Main content here</p>
    </div>
    <nav class="card-actions flex gap-2">
        <a href="/edit">Edit</a>
    </nav>
</article>

In this example: - The caller’s <h3> replaces the outlet’s <h2> (tag swapping) - Class values are merged: card-title from the outlet + text-lg from the caller - The caller’s <nav> replaces the outlet’s <div>, preserving the outlet’s card-actions class

Slot Outlets #

In your component template, use s:slot attributes on elements to define where slot content should be rendered. When a caller provides content for a slot, the outlet element is replaced with the caller’s element — the caller’s tag and attributes take over, merged with the outlet’s attributes.

  • s:slot (no value) defines the outlet for the default slot
  • s:slot="name" defines the outlet for a named slot
  • Child content inside outlet elements serves as fallback when no content is provided
<div class="modal">
    <h2 s:slot="header" class="modal-title">Default Title</h2>
    <main s:slot>
        <p>Default body content</p>
    </main>
    <footer s:slot="footer"></footer>
</div>
<s-modal>
    <p>Custom body content</p>
</s-modal>
<div class="modal">
    <h2 class="modal-title">Default Title</h2>
    <main>
        <p>Custom body content</p>
    </main>
    <footer></footer>
</div>

Tag Swapping and Attribute Merging #

When a caller provides content for a named slot using an element with s:slot, the caller’s tag replaces the outlet’s tag, and attributes are merged:

  • Class values are concatenated (outlet classes first, then caller classes)
  • Other attributes with the same name are overridden by the caller
  • Unique attributes from both sides are included
<h2 s:slot="header" class="card-title" id="default-id">Default</h2>
<h3 s:slot="header" class="text-lg" id="custom-id" data-role="heading">My Title</h3>
<h3 class="card-title text-lg" id="custom-id" data-role="heading">My Title</h3>

This gives callers full control over the semantic tag while preserving the component’s styling defaults.

Fragment Outlets #

Use a fragment element (s-template) as an outlet when you want slot content to be injected without a wrapper element:

<div class="sidebar">
    <s-template s:slot="nav">
        <p>Default navigation</p>
    </s-template>
</div>
<s-sidebar>
    <s-template s:slot="nav">
        <ul><li>Link 1</li><li>Link 2</li></ul>
    </s-template>
</s-sidebar>
<div class="sidebar">
    <ul><li>Link 1</li><li>Link 2</li></ul>
</div>

Fragment outlets are useful when the component shouldn’t impose any wrapper markup around the slot content.

Use element outlets (e.g., <h2 s:slot="title">) when the component needs a structural element with default styling. Use fragment outlets (<s-template s:slot="name">) when slot content should be injected as-is without a wrapper.

Props and Data #

Components receive props as variables using the s:bind directive. This lets you pass dynamic data into your components.

Passing Props with s:bind #

Use s:bind with an array to map keys to component variables:

<s-card s:bind="['title' => 'Well done!', 'elevated' => true]">
    Your changes have been saved.
</s-card>
<s-card s:bind="$cardProps">Your changes have been saved.</s-card>

Inside components/s-card.sugar.php, array keys become:

  • $title = 'Well done!'
  • $elevated = true
  • $slot = 'Your changes have been saved.'

Default Values #

Define default values at the top of your component template using the null coalescing operator:

<?php
$title ??= 'Untitled';
$elevated ??= false;
?>

<article class="card" s:class="['card--elevated' => $elevated]">
    <h3><?= $title ?></h3>
    <?= $slot ?>
</article>
<s-card s:bind="['title' => 'Profile', 'elevated' => true]">
    <p>Profile content here</p>
</s-card>

Only props passed through s:bind become component variables. Regular HTML attributes (like class, id, data-*) are merged onto the root element instead.

Attribute Merging #

Attributes that are not consumed as props through s:bind are automatically merged onto the component’s root element:

<?php
$title ??= 'Untitled';
?>

<article class="card">
    <h3><?= $title ?></h3>
    <?= $slot ?>
</article>
<s-card
    s:bind="['title' => 'Profile']"
    class="shadow-lg"
    id="profile-card"
    @click="handleClick"
    x-data="{ open: false }"
>
    Profile content here
</s-card>
<article class="card shadow-lg" id="profile-card" @click="handleClick" x-data="{ open: false }">
    <h3>Profile</h3>
    Profile content here
</article>

The attributes (class, id, @click, x-data) are applied to the root <article> element in the component template. This makes components work seamlessly with CSS frameworks, Alpine.js, and other attribute-based libraries.

Pass data values as props via s:bind, and styling/behavior attributes as regular HTML attributes.

Advanced Usage #

Dynamic Component Invocation #

Use the s:component directive when the component name must be determined at runtime:

<div s:component="button">Click Me</div>
<div s:component="$componentName">Click Me</div>
<s-template s:component="alert" s:bind="['type' => 'info']">Hello</s-template>

When to use what:

Mode Example Component name known Typical use
Normal component tag <s-button>...</s-button> Compile time Most cases (preferred when name is fixed)
Dynamic component directive <div s:component="$componentName">...</div> Runtime Feature flags, role-based UI, configurable widgets

Both support slots and s:bind props, but normal tags provide clearer templates when the component name is known up front.

Prefer normal component tags (<s-button>) for clearer templates. Use s:component only when the component name must be dynamic at runtime.

Multi-Namespace Component Loading #

When working with plugins or multiple template namespaces, components are resolved across all registered namespaces in registration order.

For example, with this setup:

$loader = new FileTemplateLoader([__DIR__ . '/app/templates']);
$loader->registerNamespace(
    'plugin-auth',
    new TemplateNamespaceDefinition([__DIR__ . '/plugins/auth/templates'])
);

$engine = Engine::builder()
    ->withTemplateLoader($loader)
    ->withExtension(new ComponentExtension(['components']))
    ->build();

The <s-button> component is resolved in this order:

  1. app/templates/components/s-button.sugar.php
  2. plugins/auth/templates/components/s-button.sugar.php

The first match found is used. This allows plugins and namespaces to share the same component hierarchy while letting app components take priority.

Multi-Namespace File Structure #

When your application uses plugins or shared packages, each namespace can have its own component directory:

app/
└── templates/
    ├── pages/
    ├── layouts/
    └── components/
        ├── s-button.sugar.php (priority: #1)
        ├── s-card.sugar.php
        └── s-dashboard.sugar.php

plugins/
└── auth/
    └── templates/
        └── components/
            ├── s-button.sugar.php (overridden by @app)
            ├── s-login-form.sugar.php (priority: #2)
            └── s-auth-modal.sugar.php

packages/
└── shared-ui/
    └── templates/
        └── components/
            ├── s-alert.sugar.php (priority: #3)
            └── s-tooltip.sugar.php

With this structure: - <s-button> loads from app/templates/components/ (first registered) - <s-login-form> loads from plugins/auth/templates/components/ (second registered) - <s-alert> loads from packages/shared-ui/templates/components/ (third registered)

By default, @app is registered first and therefore has priority over plugin namespaces during component lookup. Additional namespaces are searched after @app in registration order.

Best Practices #

  • Use s:bind for component props, not HTML attributes
  • Keep a single root element in component templates for proper attribute merging
  • Use s:slot outlets to properly position default and named slot content
  • Provide fallback content for optional slots to handle cases where callers don’t provide content
  • Prefer normal component tags (<s-button>) over dynamic s:component when the component name is known
  • Define prop defaults at the top of your component using ??= for better readability
  • Name components clearly with descriptive, hyphenated names (e.g., s-user-card, not s-uc)