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 isx-
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:
-
app/templates/components/s-button.sugar.php -
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:bindfor component props, not HTML attributes - Keep a single root element in component templates for proper attribute merging
-
Use
s:slotoutlets 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 dynamics:componentwhen 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, nots-uc)