All skills
smnandre avatar

/twig-component

@6d7ef8e

Symfony UX TwigComponent for reusable UI building blocks -- server-rendered components with PHP classes and Twig templates. Use when creating buttons, cards, alerts, badges, navbars, or any reusable UI element with props, blocks/slots, computed properties, or anonymous (template-only) components. Code triggers: AsTwigComponent, #[AsTwigComponent], ExposeInTemplate, PreMount, PostMount, <twig:Alert />, <twig:Button>, component(), computed properties, anonymous component, HTML syntax. Also trigger when the user asks "how to create a reusable component", "how to make a component library", "how to pass props to a component", "how to use slots/blocks in a component", "how to build a design system in Symfony", "what is the HTML syntax for components", "how to create a component without a PHP class". Do NOT trigger for components that re-render dynamically on user input (use live-component), for JS behavior (use stimulus), or for page navigation (use turbo).

Use this Skill: https://skilld.dev/gh/smnandre/symfony-ux-skills/twig-component

This session only. Nothing lands on disk.

referencesapi.md

≈2k tokens on demand. Your agent reads this file only when SKILL.md points to it.

TwigComponent API Reference

AsTwigComponent Attribute

use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;

#[AsTwigComponent(
    name: 'Alert',                              // Optional, defaults to class name
    template: 'components/Alert.html.twig',    // Optional, defaults to components/{Name}.html.twig
    exposePublicProps: true,                   // Expose public props directly in template
)]
final class Alert
{
    // ...
}

Props

Public Properties

#[AsTwigComponent]
final class Button
{
    public string $label;                    // Required (no default)
    public string $variant = 'primary';      // Optional with default
    public ?string $icon = null;             // Nullable
    public bool $disabled = false;           // Boolean
    public array $options = [];              // Array
}

Anonymous Props

{# templates/components/Button.html.twig #}
{% props 
    label,
    variant = 'primary',
    icon = null,
    disabled = false
%}

Accessing Props in Template

{# Direct access (exposePublicProps: true, default) #}
{{ label }}
{{ variant }}

{# Via this #}
{{ this.label }}
{{ this.variant }}

mount() Method

Called when component is created. Receives all passed props.

#[AsTwigComponent]
final class UserAvatar
{
    public User $user;
    public int $size;
    public string $initials;
    
    public function mount(User $user, int $size = 40): void
    {
        $this->user = $user;
        $this->size = $size;
        $this->initials = $this->computeInitials($user);
    }
    
    private function computeInitials(User $user): string
    {
        // ...
    }
}

Lifecycle Hooks

PreMount

Modify data before mounting:

use Symfony\UX\TwigComponent\Attribute\PreMount;

#[AsTwigComponent]
final class Table
{
    #[PreMount]
    public function preMount(array $data): array
    {
        // Normalize incoming data
        $data['page'] = max(1, $data['page'] ?? 1);
        return $data;
    }
}

With priority (higher = earlier):

#[PreMount(priority: 10)]
public function first(array $data): array { }

#[PreMount(priority: 0)]
public function second(array $data): array { }

PostMount

Run code after all props are set:

use Symfony\UX\TwigComponent\Attribute\PostMount;

#[AsTwigComponent]
final class Chart
{
    public array $data;
    private array $processedData;
    
    #[PostMount]
    public function postMount(): void
    {
        $this->processedData = $this->processData($this->data);
    }
}

Computed Properties

Methods starting with get or is/has become template properties:

#[AsTwigComponent]
final class Invoice
{
    public array $items;
    
    // Access via: this.total
    public function getTotal(): float
    {
        return array_sum(array_column($this->items, 'price'));
    }
    
    // Access via: this.empty
    public function isEmpty(): bool
    {
        return count($this->items) === 0;
    }
    
    // Access via: this.items (boolean check)
    public function hasItems(): bool
    {
        return !$this->isEmpty();
    }
}
{% if this.hasItems %}
    Total: {{ this.total }}
{% else %}
    No items
{% endif %}

ExposeInTemplate

Expose private properties or rename public ones:

use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;

#[AsTwigComponent]
final class Stats
{
    #[ExposeInTemplate]
    private int $count;
    
    #[ExposeInTemplate(name: 'total')]
    public int $totalCount;
    
    #[ExposeInTemplate(getter: 'getFormattedCount')]
    private int $rawCount;
    
    public function getFormattedCount(): string
    {
        return number_format($this->rawCount);
    }
}

Attributes Object

In Template

{# Render all attributes #}
<div {{ attributes }}>

{# With defaults (merged) #}
<div {{ attributes.defaults({class: 'card', role: 'article'}) }}>

{# Only render if class attribute present #}
<div class="{{ attributes.render('class')|default('default-class') }}">

{# Exclude specific attributes #}
<div {{ attributes.without('class', 'id') }}>

{# Only specific attributes #}
<div {{ attributes.only('id', 'data-*') }}>

{# Check attribute existence #}
{% if attributes.has('disabled') %}
    <span>Disabled</span>
{% endif %}

Stimulus Integration

<div {{ attributes.defaults({
    'data-controller': 'my-controller',
    'data-my-controller-url-value': url
}) }}>

Blocks

Definition

{# templates/components/Card.html.twig #}
<div class="card">
    <header class="card-header">
        {% block header %}{% endblock %}
    </header>
    <div class="card-body">
        {% block content %}Default content{% endblock %}
    </div>
    <footer class="card-footer">
        {% block footer %}{% endblock %}
    </footer>
</div>

Usage with twig:block

<twig:Card>
    <twig:block name="header">
        <h3>Title</h3>
    </twig:block>
    
    <twig:block name="content">
        <p>Body text</p>
    </twig:block>
    
    <twig:block name="footer">
        <button>Save</button>
    </twig:block>
</twig:Card>

Default Block (content)

Content not in a named block goes to content:

<twig:Card>
    <twig:block name="header">Title</twig:block>
    
    {# This goes to content block #}
    <p>Card body content here</p>
</twig:Card>

Outer Blocks

Access blocks from parent context:

{# Component template #}
{% block sidebar %}
    {% if outerBlocks.sidebar is defined %}
        {{ outerBlocks.sidebar }}
    {% else %}
        Default sidebar
    {% endif %}
{% endblock %}

Configuration

# config/packages/twig_component.yaml
twig_component:
    # Directory for anonymous components
    anonymous_template_directory: 'components/'
    
    # Map namespaces to template directories
    defaults:
        App\Twig\Components\: 'components/'
        App\Twig\Components\Form\: 'components/form/'
        
    # For components outside src/
    # App\Component\: '../component/'

Custom Template Location

#[AsTwigComponent(template: 'my/custom/path/Alert.html.twig')]
final class Alert { }

Custom Component Name

#[AsTwigComponent('UI:Alert')]
final class Alert { }
<twig:UI:Alert />

Events

PreRenderEvent

use Symfony\UX\TwigComponent\Event\PreRenderEvent;

class ComponentSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [PreRenderEvent::class => 'onPreRender'];
    }
    
    public function onPreRender(PreRenderEvent $event): void
    {
        $component = $event->getComponent();
        $template = $event->getTemplate();
        $variables = $event->getVariables();
        
        // Modify before render
        $event->setVariables([...$variables, 'extra' => 'data']);
    }
}

PostRenderEvent

use Symfony\UX\TwigComponent\Event\PostRenderEvent;

public function onPostRender(PostRenderEvent $event): void
{
    $html = $event->getRenderedString();
    $mountedComponent = $event->getMountedComponent();
}

Debugging

# List all components
php bin/console debug:twig-component

# Show specific component
php bin/console debug:twig-component Alert

Rendering Programmatically

use Symfony\UX\TwigComponent\ComponentRendererInterface;

class SomeService
{
    public function __construct(
        private readonly ComponentRendererInterface $renderer,
    ) {}
    
    public function renderAlert(): string
    {
        return $this->renderer->createAndRender('Alert', [
            'type' => 'success',
            'message' => 'Done!',
        ]);
    }
}

Source: SKILL.md on GitHub

No alerts15d4 checks · Risk SAFE
  • Gen Agent Trust Hub15d

    This skill provides comprehensive documentation and implementation patterns for the Symfony UX TwigComponent library. It contains legitimate code snippets, API references, and configuration guides. No malicious behavior or security risks were identified.

  • Socket15d

    No alerts

  • Snyk15d

    Risk: LOW · No issues

  • Runlayer7mo

    3 files scanned · No issues

Signed by skilld at 6d7ef8e. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 4 months ago
Other metadata
metadata
{
  "author": "Simon Andre",
  "email": "smn.andre@gmail.com",
  "url": "https://smnandre.dev",
  "version": "1.2.0"
}

README badge

README badge for smnandre/symfony-ux-skills/twig-component