Twig Components
Twig components give you the power to bind an object to a template, making it easier to render and re-use small template "units" - like an "alert", markup for a modal, or a category sidebar:
Every component consists of (1) a class:
1 2 3 4 5 6 7 8 9 10 11
// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
class Alert
{
public string $type = 'success';
public string $message;
}
And (2) a template:
1 2 3 4
{# templates/components/Alert.html.twig #}
<div class="alert alert-{{ type }}">
{{ message }}
</div>
Done! Now render it wherever you want:
1 2 3
{{ component('Alert', {message: 'Hello Twig Components!'}) }}
<twig:Alert message="Or use the fun HTML syntax!" />
Enjoy your new component!
Example of the Alert ComponentThis brings the familiar "component" system from client-side frameworks into Symfony. Combine this with Live Components, to create an interactive frontend with automatic, Ajax-powered rendering.
Want a demo? Check out https://ux.symfony.com/twig-component#demo
Installation
Let's get this thing installed! Run:
1
$ composer require symfony/ux-twig-component
That's it! We're ready to go! If you're not using Symfony Flex, add a config file to control the template directory for your components:
1 2 3 4 5 6
# config/packages/twig_component.yaml
twig_component:
anonymous_template_directory: 'components/'
defaults:
# Namespace & directory for components
App\Twig\Components\: 'components/'
Component Basics
Let's create a reusable "alert" element that we can use to show success
or error messages across our site. Step 1 is to create a component class
and give it the AsTwigComponent
attribute:
1 2 3 4 5 6 7 8 9
// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
class Alert
{
}
This class can technically live anywhere, but in practice, you'll put it somewhere under the namespace configured in config/packages/twig_component.yaml. This helps TwigComponent name your component and know where its template lives.
Step 2 is to create the template. By default, templates
live in templates/components/{component_name}.html.twig
, where
{component_name}
matches the class name of the component:
1 2 3 4
{# templates/components/Alert.html.twig #}
<div class="alert alert-success">
Success! You've created a Twig component!
</div>
This isn't very interesting yet… since the message is hardcoded in the template. But it's enough! Celebrate by rendering your component from any other Twig template:
1
{{ component('Alert') }}
Done! You've just rendered your first Twig Component! You can see it and any other components by running:
1
$ php bin/console debug:twig-component
Take a moment to fist pump - then come back!
Naming Your Component
2.8
Before 2.8, passing a name to AsTwigComponent
was required. Now, the
name is optional and defaults to the class name.
To give your component a name, TwigComponent looks at the namespace(s)
configured in twig_component.yaml and finds the
first match. If your have the recommended App\Twig\Components\
, then:
Component Class | Component Name |
---|---|
App\Twig\Components\Alert |
Alert |
App |
Button:Primary |
The :
character is used in the name instead of \
. See
Configuration for more info.
Instead of letting TwigComponent choose a name, you can also set one yourself:
1 2 3 4
#[AsTwigComponent('alert')]
class Alert
{
}
Passing Data (Props) into your Component
To make our Alert
component reusable, we need the message and type
(e.g. success
, danger
, etc) to be configurable. To do that, create a
public property for each:
1 2 3 4 5 6 7 8 9 10 11 12
// src/Twig/Components/Alert.php
// ...
#[AsTwigComponent]
class Alert
{
+ public string $message;
+ public string $type = 'success';
// ...
}
In the template, the Alert
instance is available via
the this
variable and public properties are available directly.
Use them to render the two new properties:
1 2 3 4 5 6
<div class="alert alert-{{ type }}">
{{ message }}
{# Same as above, but using "this", which is the component object #}
{{ this.message }}
</div>
How can we populate the message
and type
properties? By passing
them as "props" via the a 2nd argument to component()
:
1 2 3 4 5 6
{{ component('Alert', {message: 'Successfully created!'}) }}
{{ component('Alert', {
type: 'danger',
message: 'Danger Will Robinson!'
}) }}
Behind the scenes, a new Alert
will be instantiated and the
message
key (and type
if passed) will be set onto the
$message
property of the object. Then, the component is rendered! If
a property has a setter method (e.g. setMessage()
), that will be
called instead of setting the property directly.
Note
You can disable exposing public properties for a component. When disabled,
this.property
must be used:
1 2 3 4 5
#[AsTwigComponent(exposePublicProps: false)]
class Alert
{
// ...
}
Passing & Rendering Attributes
If you pass extra props that are not settable on your component class, those can be rendered as attributes:
1 2 3 4
{{ component('Alert', {
id: 'custom-alert-id',
message: 'Danger Will Robinson!'
}) }}
To render the attributes, use the special attributes
variable that's
available in every component template:
1 2 3
<div {{ attributes.defaults({class: 'alert alert-' ~ type}) }}>
{{ message }}
</div>
See Component Attributes to learn more.
Component Template Path
If you're using the default config, the template
name will be: templates/components/{component_name}.html.twig
, where
{component_name}
matches the component name.
Component Name | Template Path |
---|---|
Alert |
templates/components/Alert.html.twig |
Button:Primary |
templates/components/Button/Primary.html.twig |
Any :
in the name are changed to subdirectories.
You can control the template used via the AsTwigComponent
attribute:
1 2 3 4 5 6
// src/Twig/Components/Alert.php
// ...
- #[AsTwigComponent]
+ #[AsTwigComponent(template: 'my/custom/template.html.twig')]
class Alert
You can also configure the default template directory for an entire namespace. See Configuration.
Component HTML Syntax
So far so good! To make it really nice to work with Twig Components, it comes with an HTML-like syntax where props are passed as attributes:
1
<twig:Alert message="This is really cool!" withCloseButton />
This would pass a message
and withCloseButton
(true
) props
to the Alert
component and render it! If an attribute is dynamic,
prefix the attribute with :
or use the normal {{ }}
syntax:
1 2 3 4 5 6 7
<twig:Alert message="hello!" :user="user.id" />
// equal to
<twig:Alert message="hello!" user="{{ user.id }}" />
// pass object, array, or anything you imagine
<twig:Alert :foo="{col: ['foo', 'oof']}" />
Boolean props are converted using PHP's type juggling rules. The
string "false"
is converted to the boolean true
.
To pass the boolean false
, you can pass a Twig expression{{ false }}
or use the dynamic syntax (with the :
prefix):
1 2 3 4 5 6 7 8
{# ❌ the string 'false' is converted to the boolean 'true' #}
<twig:Alert message="..." withCloseButton="false" />
{# ✅ use the 'false' boolean value #}
<twig:Alert message="..." withCloseButton="{{ false }}" />
{# ✅ use the dynamic syntax #}
<twig:Alert message="..." :withCloseButton="false" />
Don't forget that you can mix and match props with attributes that you want to render on the root element:
1
<twig:Alert message="hello!" id="custom-alert-id" />
To pass an array of attributes, use {{...}}
spread operator syntax.
This requires Twig 3.7.0 or higher:
1
<twig:Alert {{ ...myAttributes }} />
We'll use the HTML syntax for the rest of the guide.
Passing HTML to Components
Instead of passing a message
prop to the Alert
component, what if we
could do this?
1 2 3
<twig:Alert>
I'm writing <strong>HTML</strong> right here!
</twig:Alert>
We can! When you add content between the <twig:Alert>
open and
close tag, it's passed to your component template as the block called
content
. You can render it like any normal block:
1 2 3
<div {{ attributes.defaults({class: 'alert alert-' ~ type}) }}>
{% block content %}{% endblock %}
</div>
You can even give the block default content. See Passing HTML to Components via Block for more info.
Fetching Services
Let's create a more complex example: a "featured products" component.
You could choose to pass an array of Product objects to the component
and set those on a $products
property. But instead, let's let the
component to do the work of executing the query.
How? Components are services, which means autowiring works like
normal. This example assumes you have a Product
Doctrine entity and
ProductRepository
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// src/Twig/Components/FeaturedProducts.php
namespace App\Twig\Components;
use App\Repository\ProductRepository;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
class FeaturedProducts
{
private ProductRepository $productRepository;
public function __construct(ProductRepository $productRepository)
{
$this->productRepository = $productRepository;
}
public function getProducts(): array
{
// an example method that returns an array of Products
return $this->productRepository->findFeatured();
}
}
In the template, the getProducts()
method can be accessed via
this.products
:
1 2 3 4 5 6 7 8
{# templates/components/FeaturedProducts.html.twig #}
<div>
<h3>Featured Products</h3>
{% for product in this.products %}
...
{% endfor %}
</div>
And because this component doesn't have any public properties that we need to populate, you can render it with:
1
<twig:FeaturedProducts />
Note
Because components are services, normal dependency injection can be used.
However, each component service is registered with shared: false
. That
means that you can safely render the same component multiple times with
different data because each component will be an independent instance.
Mounting Data
Most of the time, you will create public properties and then pass values to those as "props" when rendering. But there are several hooks in case you need to do something more complex.
The mount() Method
For more control over how your "props" are handled, you can create a method
called mount()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// src/Twig/Components/Alert.php
// ...
#[AsTwigComponent]
class Alert
{
public string $message;
public string $type = 'success';
public function mount(bool $isSuccess = true)
{
$this->type = $isSuccess ? 'success' : 'danger';
}
// ...
}
The mount()
method is called just one time: immediately after your
component is instantiated. Because the method has an $isSuccess
argument, if we pass an isSuccess
prop when rendering, it will be
passed to mount()
.
1 2 3 4
<twig:Alert
isSuccess="{{ false }}"
message="Danger Will Robinson!"
/>
If a prop name (e.g. isSuccess
) matches an argument name in mount()
,
the prop will be passed as that argument and the component system will
not try to set it directly on a property or use it for the component
attributes
.
PreMount Hook
If you need to modify/validate data before it's mounted on the
component use a PreMount
hook:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
// src/Twig/Components/Alert.php
use Symfony\UX\TwigComponent\Attribute\PreMount;
// ...
#[AsTwigComponent]
class Alert
{
public string $message;
public string $type = 'success';
#[PreMount]
public function preMount(array $data): array
{
// validate data
$resolver = new OptionsResolver();
$resolver->setIgnoreUndefined(true);
$resolver->setDefaults(['type' => 'success']);
$resolver->setAllowedValues('type', ['success', 'danger']);
$resolver->setRequired('message');
$resolver->setAllowedTypes('message', 'string');
return $resolver->resolve($data) + $data;
}
// ...
}
Note
In its default configuration, the OptionsResolver treats all props.
However, if more props are passed than the options defined in the OptionsResolver,
an error will be prompted, indicating that one or more options do not exist.
To avoid this, use the ignoreUndefined()
method with true
.
See ignore not defined options for more info:
1
$resolver->setIgnoreUndefined(true);
The major drawback of this configuration is that the OptionsResolver will remove every non-defined option when resolving data. To maintain props that have not been defined within the OptionsResolver, combine the data from the hook with the resolved data:
1
return $resolver->resolve($data) + $data;
The data returned from preMount()
will be used as the props for mounting.
Note
If your component has multiple PreMount
hooks, and you'd like to control
the order in which they're called, use the priority
attribute parameter:
PreMount(priority: 10)
(higher called earlier).
PostMount Hook
2.1
The PostMount
hook was added in TwigComponents 2.1.
After a component is instantiated and its data mounted, you can run extra
code via the PostMount
hook:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// src/Twig/Components/Alert.php
use Symfony\UX\TwigComponent\Attribute\PostMount;
// ...
#[AsTwigComponent]
class Alert
{
#[PostMount]
public function postMount(): void
{
if (str_contains($this->message, 'danger')) {
$this->type = 'danger';
}
}
// ...
}
A PostMount
method can also receive an array $data
argument, which
will contain any props passed to the component that have not yet been processed,
(i.e. they don't correspond to any property and weren't an argument to the
mount()
method). You can handle these props, remove them from the $data
and return the array:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// src/Twig/Components/Alert.php
#[AsTwigComponent]
class Alert
{
public string $message;
public string $type = 'success';
#[PostMount]
public function processAutoChooseType(array $data): array
{
if ($data['autoChooseType'] ?? false) {
if (str_contains($this->message, 'danger')) {
$this->type = 'danger';
}
// remove the autoChooseType prop from the data array
unset($data['autoChooseType']);
}
// any remaining data will become attributes on the component
return $data;
}
// ...
}
Note
If your component has multiple PostMount
hooks, and you'd like to control
the order in which they're called, use the priority
attribute parameter:
PostMount(priority: 10)
(higher called earlier).
Anonymous Components
Sometimes a component is simple enough that it doesn't need a PHP class. In this case, you can skip the class and only create the template. The component name is determined by the location of the template:
1 2 3 4
{# templates/components/Button/Primary.html.twig #}
<button {{ attributes.defaults({class: 'primary'}) }}>
{% block content %}{% endblock %}
</button>
The name for this component will be Button:Primary
because of
the subdirectory:
1 2 3 4 5 6 7 8
{# index.html.twig #}
...
<div>
<twig:Button:Primary>Click Me!</twig:Button:Primary>
</div>
{# renders as: #}
<button class="primary">Click Me!</button>
Like normal, you can pass extra attributes that will be rendered on the element:
1 2 3 4 5 6 7 8
{# index.html.twig #}
...
<div>
<twig:Button:Primary type="button" name="foo">Click Me!</twig:Button:Primary>
</div>
{# renders as: #}
<button class="primary" type="button" name="foo">Click Me!</button>
You can also pass a variable (prop) into your template:
1 2 3 4 5
{# index.html.twig #}
...
<div>
<twig:Button icon="fa-plus" type="primary" role="button">Click Me!</twig:Button>
</div>
To tell the system that icon
and type
are props and not attributes, use the
{% props %}
tag at the top of your template.
1 2 3 4 5 6 7 8 9
{# templates/components/Button.html.twig #}
{% props icon = null, type = 'primary' %}
<button {{ attributes.defaults({class: 'btn btn-'~type}) }}>
{% block content %}{% endblock %}
{% if icon %}
<span class="fa-solid fa-{{ icon }}"></span>
{% endif %}
</button>
Passing HTML to Components Via Blocks
Props aren't the only way you can pass something to your component. You can also pass content:
1 2 3
<twig:Alert type="success">
<div>Congratulations! You've won a free puppy!</div>
</twig:Alert>
In your component template, this becomes a block named content
:
1 2 3 4 5
<div class="alert alert-{{ type }}">
{% block content %}
// the content will appear in here
{% endblock %}
</div>
You can also add more, named blocks:
1 2 3 4 5 6
<div class="alert alert-{{ type }}">
{% block content %}{% endblock %}
{% block footer %}
<div>Default Footer content</div>
{% endblock %}
</div>
Render these in the normal way.
1 2 3 4 5 6 7 8
<twig:Alert type="success">
<div>Congrats on winning a free puppy!</div>
<twig:block name="footer">
{{ parent() }} {# render the default content if needed #}
<button class="btn btn-primary">Claim your prize</button>
</twig:block>
</twig:Alert>
Passing content into your template can also be done with LiveComponents though there are some caveats to know related to variable scope. See Passing Blocks to Live Components.
There is also a non-HTML syntax that can be used:
1 2 3 4
{% component Alert with {type: 'success'} %}
{% block content %}<div>Congrats!</div>{% endblock %}
{% block footer %}... footer content{% endblock %}
{% endcomponent %}
Context / Variables Inside of Blocks
The content inside of the <twig:{Component}>
should be viewed as living in its own,
independent template, which extends the component's template. This has a few interesting
consequences.
First, inside of <twig:{Component}>
, the this
variable represents
the component you're now rendering and you have access to all of that
component's variables:
1 2 3 4 5 6 7 8
{# templates/components/SuccessAlert.html.twig #}
{{ this.someFunction }} {# this === SuccessAlert #}
<twig:Alert type="success">
{{ this.someFunction }} {# this === Alert #}
{{ type }} {# references a "type" prop from Alert #}
</twig:Alert>
Conveniently, in addition to the variables from the Alert
component, you
also have access to whatever variables are available in the original template:
1 2 3 4 5
{# templates/components/SuccessAlert.html.twig #}
{% set name = 'Fabien' %}
<twig:Alert type="success">
Hello {{ name }}
</twig:Alert>
ALL variables from the upper component (e.g. SuccessAlert
) are available
inside the content of the lower component (e.g. Alert
). However, because variables
are merged, any variables with the same name are overridden by the lower component
(e.g. Alert
). That's why this
refers to the embedded, or "current" component
Alert
.
There is also one special superpower when passing content to a component: your code executes as if it is "copy-and-pasted" into the block of the target template. This means you can access variables from the block you're overriding! For example:
1 2 3 4 5 6
{# templates/component/SuccessAlert.html.twig #}
{% for message in messages %}
{% block alert_message %}
A default {{ message }}
{% endblock %}
{% endfor %}
When overriding the alert_message
block, you have access to the message
variable:
1 2 3 4 5 6
{# templates/some_page.html.twig #}
<twig:SuccessAlert>
<twig:block name="alert_message">
I can override the alert_message block and access the {{ message }} too!
</twig:block>
</twig:SuccessAlert>
2.13
The ability to refer to the scope of higher components via the outerScope
variable was added in 2.13.
As mentioned before, variables from lower components are merged with those from upper components. When you need
access to some properties or functions from higher components, that can be done via the outerScope...
variable:
1 2 3 4 5 6 7 8 9 10 11 12
{# templates/SuccessAlert.html.twig #}
{% set name = 'Fabien' %}
{% set message = 'Hello' %}
{% component Alert with {type: 'success', name: 'Bart'} %}
Hello {{ name }} {# Hello Bart #}
{{ message }} {{ outerScope.name }} {# Hello Fabien #}
{{ outerScope.this.someFunction }} {# this refers to SuccessAlert #}
{{ outerScope.this.someProp }} {# references a "someProp" prop from SuccessAlert #}
{% endcomponent %}
You can keep referring to components higher up as well. Just add another outerScope
.
Remember though that the outerScope
reference only starts once you're INSIDE the (embedded) component.
1 2 3 4 5 6 7 8 9 10 11
{# templates/FancyProfileCard.html.twig #}
{% component Card %}
{% block header %}
{% component Alert with {message: outerScope.this.someProp} %} {# not yet INSIDE the Alert template #}
{% block content %}
{{ message }} {# same value as below, indirectly refers to FancyProfileCard::someProp #}
{{ outerScope.outerScope.this.someProp }} {# directly refers to FancyProfileCard::someProp #}
{% endblock %}
{% endcomponent %}
{% endblock %}
{% endcomponent %}
Inheritance & Forwarding "Outer Blocks"
2.10
The outerBlocks
variable was added in 2.10.
The content inside a <twig:
component tag should be viewed as living in
its own, independent template, which extends the component's template. This means that
any blocks that live in the "outer" template are not available. However, you
can access these via a special outerBlocks
variable:
1 2 3 4 5 6 7 8 9 10 11 12 13
{% extends 'base.html.twig' %}
{% block call_to_action %}<strong>Attention! Free Puppies!</strong>{% endblock %}
{% block body %}
<twig:Alert>
{# this would NOT work... #}
{{ block('call_to_action') }}
{# ...but this works! #}
{{ block(outerBlocks.call_to_action) }}
</twig:Alert>
{% endblock %}
The outerBlocks
variable becomes especially useful with nested components.
For example, imagine we want to create a SuccessAlert
component:
1 2 3 4
{# templates/some_page.html.twig #}
<twig:SuccessAlert>
We will successfully <em>forward</em> this block content!
<twig:SuccessAlert>
We already have a generic Alert
component, so let's re-use it:
1 2 3 4
{# templates/components/Alert.html.twig #}
<div {{ attributes.defaults({class: 'alert alert-'~type}) }}">
{% block content %}{% endblock %}
</div>
To do this, the SuccessAlert
component can grab the content
block
that's passed to it via the outerBlocks
variable and forward it into Alert
:
1 2 3 4
{# templates/components/SuccessAlert.html.twig #}
<twig:Alert type="success">
{{ block(outerBlocks.content) }}
</twig:Alert>
By passing the original content
block into the content
block of Alert
,
this will work perfectly.
Component Attributes
A common need for components is to configure/render attributes for the
root node. Attributes are any props that are passed when rendering that
cannot be mounted on the component itself. This extra data is added to a
ComponentAttributes
object that's available as attributes
in your
component's template:
1 2 3 4
{# templates/components/MyComponent.html.twig #}
<div {{ attributes }}>
My Component!
</div>
When rendering the component, you can pass an array of html attributes to add:
1 2 3 4 5 6
<twig:MyComponent class="foo" style="color: red" />
{# renders as: #}
<div class="foo" style="color:red">
My Component!
</div>
Set an attribute's value to true
to render just the attribute name:
1 2 3 4 5 6 7 8
{# templates/components/Input.html.twig #}
<input {{ attributes }}/>
{# render component #}
<twig:Input type="text" value="" :autofocus="true" />
{# renders as: #}
<input type="text" value="" autofocus/>
Set an attribute's value to false
to exclude the attribute:
1 2 3 4 5 6 7 8
{# templates/components/Input.html.twig #}
<input {{ attributes }}/>
{# render component #}
<twig:Input type="text" value="" :autofocus="false" />
{# renders as: #}
<input type="text" value=""/>
To add a custom Stimulus controller to your root component element:
1
<div {{ attributes.defaults(stimulus_controller('my-controller', {someValue: 'foo'})) }}>
2.9
The ability to use stimulus_controller()
with attributes.defaults()
was added in TwigComponents 2.9 and requires symfony/stimulus-bundle
.
Previously, stimulus_controller()
was passed to an attributes.add()
method.
Note
You can adjust the attributes variable exposed in your template:
1 2 3 4 5
#[AsTwigComponent(attributesVar: '_attributes')]
class Alert
{
// ...
}
Defaults & Merging
In your component template, you can set defaults that are merged with
passed attributes. The passed attributes override the default with
the exception of class. For class
, the defaults are prepended:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
{# templates/components/MyComponent.html.twig #}
<button {{ attributes.defaults({class: 'bar', type: 'button'}) }}>Save</button>
{# render component #}
{{ component('MyComponent', {style: 'color:red'}) }}
{# renders as: #}
<button class="bar" type="button" style="color:red">Save</button>
{# render component #}
{{ component('MyComponent', {class: 'foo', type: 'submit'}) }}
{# renders as: #}
<button class="bar foo" type="submit">Save</button>
Render
2.15
The ability to render attributes was added in TwigComponents 2.15.
You can take full control over the attributes that are rendered by using the
render()
method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
{# templates/components/MyComponent.html.twig #}
<div
style="{{ attributes.render('style') }} display:block;"
{{ attributes }} {# be sure to always render the remaining attributes! #}
>
My Component!
</div>
{# render component #}
{{ component('MyComponent', {style: 'color:red;'}) }}
{# renders as: #}
<div style="color:red; display:block;">
My Component!
</div>
Caution
There are a few important things to know about using render()
:
You need to be sure to call your
render()
methods before calling{{ attributes }}
or some attributes could be rendered twice. For instance:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
{# templates/components/MyComponent.html.twig #} <div {{ attributes }} {# called before style is rendered #} style="{{ attributes.render('style') }} display:block;" > My Component! </div> {# render component #} {{ component('MyComponent', {style: 'color:red;'}) }} {# renders as: #} <div style="color:red;" style="color:red; display:block;"> {# style is rendered twice! #} My Component! </div>
If you add an attribute without calling
render()
, it will be rendered twice. For instance:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
{# templates/components/MyComponent.html.twig #} <div style="display:block;" {# not calling attributes.render('style') #} {{ attributes }} > My Component! </div> {# render component #} {{ component('MyComponent', {style: 'color:red;'}) }} {# renders as: #} <div style="display:block;" style="color:red;"> {# style is rendered twice! #} My Component! </div>
Only
Extract specific attributes and discard the rest:
1 2 3 4 5 6 7 8 9 10 11 12
{# render component #}
{{ component('MyComponent', {class: 'foo', style: 'color:red'}) }}
{# templates/components/MyComponent.html.twig #}
<div {{ attributes.only('class') }}>
My Component!
</div>
{# renders as: #}
<div class="foo">
My Component!
</div>
Without
Exclude specific attributes:
1 2 3 4 5 6 7 8 9 10 11 12
{# render component #}
{{ component('MyComponent', {class: 'foo', style: 'color:red'}) }}
{# templates/components/MyComponent.html.twig #}
<div {{ attributes.without('class') }}>
My Component!
</div>
{# renders as: #}
<div style="color:red">
My Component!
</div>
Nested Attributes
2.17
The Nested Attributes feature was added in TwigComponents 2.17.
You can have attributes that aren't meant to be used on the root element but one of its descendants. This is useful for, say, a dialog component where you want to allow customizing the attributes of the dialog's content, title, and footer. Here's an example of this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
{# templates/components/Dialog.html.twig #}
<div {{ attributes }}>
<div {{ attributes.nested('title') }}>
{% block title %}Default Title{% endblock %}
</div>
<div {{ attributes.nested('body') }}>
{% block content %}{% endblock %}
</div>
<div {{ attributes.nested('footer') }}>
{% block footer %}Default Footer{% endblock %}
</div>
</div>
{# render #}
<twig:Dialog class="foo" title:class="bar" body:class="baz" footer:class="qux">
Some content
</twig:MyDialog>
{# output #}
<div class="foo">
<div class="bar">
Default Title
</div>
<div class="baz">
Some content
</div>
<div class="qux">
Default Footer
</div>
</div>
The nesting is recursive so you could potentially do something like this:
1 2 3 4 5 6 7
<twig:Form
:form="form"
class="ui-form"
row:class="ui-form-row"
row:label:class="ui-form-label"
row:widget:class="ui-form-widget"
/>
Component with Complex Variants (CVA)
2.16
The cva
function was added in TwigComponents 2.16.
2.20
The cva
function was deprecated in TwigComponents 2.20, and will be removed in 3.0.
The function is now provided by the twig/html-extra:^3.12
package under the name html_cva.
CVA (Class Variant Authority) is a concept from the JavaScript world and used
by the well-known shadcn/ui.
CVA allows you to display a component with different variants (color, size, etc.),
to create highly reusable and customizable components. This is powered by a cva()
Twig
function where you define base
classes that should always be present and then different
variants
and the corresponding classes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
{# templates/components/Alert.html.twig #}
{% props color = 'blue', size = 'md' %}
{% set alert = cva({
base: 'alert',
variants: {
color: {
blue: 'bg-blue',
red: 'bg-red',
green: 'bg-green',
},
size: {
sm: 'text-sm',
md: 'text-md',
lg: 'text-lg',
}
}
}) %}
<div class="{{ alert.apply({color, size}, attributes.render('class')) }}">
{% block content %}{% endblock %}
</div>
Then use the color
and size
variants to select the classes needed:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
{# index.html.twig #}
<twig:Alert color="red" size="lg">
<div>My content</div>
</twig:Alert>
// class="alert bg-red text-lg"
<twig:Alert color="green" size="sm">
<div>My content</div>
</twig:Alert>
// class="alert bg-green text-sm"
<twig:Alert color="red" class="flex items-center justify-center">
<div>My content</div>
</twig:Alert>
// class="alert bg-red text-md flex items-center justify-center"
CVA and Tailwind CSS
CVA work perfectly with Tailwind CSS. The only drawback is that you can have class conflicts.
To "merge" conflicting classes together and keep only the ones you need, use the
tailwind_merge()
method from tales-from-a-dev/twig-tailwind-extra
with the cva()
function:
1
$ composer require tales-from-a-dev/twig-tailwind-extra
1 2 3 4 5 6 7 8 9 10
{# templates/components/Alert.html.twig #}
{% props color = 'blue', size = 'md' %}
{% set alert = cva({
// ...
}) %}
<div class="{{ alert.apply({color, size}, attributes.render('class'))|tailwind_merge }}">
{% block content %}{% endblock %}
</div>
Compound Variants
You can define compound variants. A compound variant is a variant that applies when multiple other variant conditions are met.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
{# templates/components/Alert.html.twig #}
{% props color = 'blue', size = 'md' %}
{% set alert = cva({
base: 'alert',
variants: {
color: {
blue: 'bg-blue',
red: 'bg-red',
green: 'bg-green',
},
size: {
sm: 'text-sm',
md: 'text-md',
lg: 'text-lg',
}
},
compoundVariants: [{
// if color = red AND size = (md or lg), add the `font-bold` class
color: ['red'],
size: ['md', 'lg'],
class: 'font-bold'
}]
}) %}
<div class="{{ alert.apply({color, size}) }}">
{% block content %}{% endblock %}
</div>
{# index.html.twig #}
<twig:Alert color="red" size="lg">
<div>My content</div>
</twig:Alert>
// class="alert bg-red text-lg font-bold"
<twig:Alert color="green" size="sm">
<div>My content</div>
</twig:Alert>
// class="alert bg-green text-sm"
<twig:Alert color="red" size="md">
<div>My content</div>
</twig:Alert>
// class="alert bg-green text-lg font-bold"
Default Variants
If no variants match, you can define a default set of classes to apply:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
{# templates/components/Alert.html.twig #}
{% props color = 'blue', size = 'md' %}
{% set alert = cva({
base: 'alert',
variants: {
color: {
blue: 'bg-blue',
red: 'bg-red',
green: 'bg-green',
},
size: {
sm: 'text-sm',
md: 'text-md',
lg: 'text-lg',
},
rounded: {
sm: 'rounded-sm',
md: 'rounded-md',
lg: 'rounded-lg',
}
},
defaultVariants: {
rounded: 'md',
}
}) %}
<div class="{{ alert.apply({color, size}) }}">
{% block content %}{% endblock %}
</div>
{# index.html.twig #}
<twig:Alert color="red" size="lg">
<div>My content</div>
</twig:Alert>
// class="alert bg-red text-lg font-bold rounded-md"
Test Helpers
You can test how your component is mounted and rendered using the
InteractsWithTwigComponents
trait:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\UX\TwigComponent\Test\InteractsWithTwigComponents;
class MyComponentTest extends KernelTestCase
{
use InteractsWithTwigComponents;
public function testComponentMount(): void
{
$component = $this->mountTwigComponent(
name: 'MyComponent', // can also use FQCN (MyComponent::class)
data: ['foo' => 'bar'],
);
$this->assertInstanceOf(MyComponent::class, $component);
$this->assertSame('bar', $component->foo);
}
public function testComponentRenders(): void
{
$rendered = $this->renderTwigComponent(
name: 'MyComponent', // can also use FQCN (MyComponent::class)
data: ['foo' => 'bar'],
);
$this->assertStringContainsString('bar', $rendered);
// use the crawler
$this->assertCount(5, $rendered->crawler()->filter('ul li'));
}
public function testEmbeddedComponentRenders(): void
{
$rendered = $this->renderTwigComponent(
name: 'MyComponent', // can also use FQCN (MyComponent::class)
data: ['foo' => 'bar'],
content: '<div>My content</div>', // "content" (default) block
blocks: [
'header' => '<div>My header</div>',
'menu' => $this->renderTwigComponent('Menu'), // can embed other components
],
);
$this->assertStringContainsString('bar', $rendered);
}
}
Note
The InteractsWithTwigComponents
trait can only be used in tests that extend
Symfony
.
Special Component Variables
By default, your template will have access to the following variables:
this
attributes
- ... and all public properties on your component
There are also a few other special ways you can control the variables.
ExposeInTemplate Attribute
All public component properties are available directly in your component
template. You can use the ExposeInTemplate
attribute to expose
private/protected properties and public methods directly in a component
template (someProp
vs this.someProp
, someMethod
vs this.someMethod
).
Properties must be accessible (have a getter). Methods cannot have
required parameters:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
// ...
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
#[AsTwigComponent]
class Alert
{
#[ExposeInTemplate]
private string $message; // available as `{{ message }}` in the template
#[ExposeInTemplate('alert_type')]
private string $type = 'success'; // available as `{{ alert_type }}` in the template
#[ExposeInTemplate(name: 'ico', getter: 'fetchIcon')]
private string $icon = 'ico-warning'; // available as `{{ ico }}` in the template using `fetchIcon()` as the getter
/**
* Required to access $this->message
*/
public function getMessage(): string
{
return $this->message;
}
/**
* Required to access $this->type
*/
public function getType(): string
{
return $this->type;
}
/**
* Required to access $this->icon
*/
public function fetchIcon(): string
{
return $this->icon;
}
#[ExposeInTemplate]
public function getActions(): array // available as `{{ actions }}` in the template
{
// ...
}
#[ExposeInTemplate('dismissable')]
public function canBeDismissed(): bool // available as `{{ dismissable }}` in the template
{
// ...
}
// ...
}
Note
When using ExposeInTemplate
on a method the value is fetched eagerly
before rendering.
Computed Properties
In the previous example, instead of querying for the featured products
immediately (e.g. in __construct()
), we created a getProducts()
method and called that from the template via this.products
.
This was done because, as a general rule, you should make your
components as lazy as possible and store only the information you need
on its properties (this also helps if you convert your component to a
live component later). With this setup, the query is only executed if and
when the getProducts()
method is actually called. This is very similar
to the idea of "computed properties" in frameworks like Vue.
But there's no magic with the getProducts()
method: if you call
this.products
multiple times in your template, the query would be
executed multiple times.
To make your getProducts()
method act like a true computed property,
call computed.products
in your template. computed
is a proxy
that wraps your component and caches the return of methods. If they
are called additional times, the cached value is used.
1 2 3 4 5 6 7 8 9 10 11 12 13
{# templates/components/FeaturedProducts.html.twig #}
<div>
<h3>Featured Products</h3>
{% for product in computed.products %}
...
{% endfor %}
...
{% for product in computed.products %} {# use cache, does not result in a second query #}
...
{% endfor %}
</div>
Note
Computed methods only work for component methods with no required arguments.
Events
Twig Components dispatches various events throughout the lifecycle of instantiating, mounting and rendering a component.
PreRenderEvent
Subscribing to the PreRenderEvent
gives the ability to modify
the twig template and twig variables before components are rendered:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\UX\TwigComponent\Event\PreRenderEvent;
class HookIntoTwigPreRenderSubscriber implements EventSubscriberInterface
{
public function onPreRender(PreRenderEvent $event): void
{
$event->getComponent(); // the component object
$event->getTemplate(); // the twig template name that will be rendered
$event->getVariables(); // the variables that will be available in the template
$event->setTemplate('some_other_template.html.twig'); // change the template used
// manipulate the variables:
$variables = $event->getVariables();
$variables['custom'] = 'value';
$event->setVariables($variables); // {{ custom }} will be available in your template
}
public static function getSubscribedEvents(): array
{
return [PreRenderEvent::class => 'onPreRender'];
}
}
PostRenderEvent
2.5
The PostRenderEvent
was added in TwigComponents 2.5.
The PostRenderEvent
is called after a component has finished
rendering and contains the MountedComponent
that was just
rendered.
PreCreateForRenderEvent
2.5
The PreCreateForRenderEvent
was added in TwigComponents 2.5.
Subscribing to the PreCreateForRenderEvent
gives the ability to be
notified before a component object is created or hydrated, at the
very start of the rendering process. You have access to the component
name, input props and can interrupt the process by setting HTML. This
event is not triggered during a re-render.
PreMountEvent and PostMountEvent
To run code just before or after a component's data is mounted, you can
listen to PreMountEvent
or PostMountEvent
.
Nested Components
It's totally possible to include components as part of the contents of another component. When you do this, all components render independently. The only caveat is that you cannot mix the Twig syntax and the HTML syntax when using nested components:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
{# ❌ this won't work because it mixes different syntaxes #}
<twig:Card>
{# ... #}
{% block footer %}
<twig:Button:Primary :isBlock="true">Edit</twig:Button:Primary>
{% endblock %}
</twig:Card>
{# ✅ this works because it only uses the HTML syntax #}
<twig:Card>
{# ... #}
<twig:block name="footer">
<twig:Button:Primary :isBlock="true">Edit</twig:Button:Primary>
</twig:block>
</twig:Card>
{# ✅ this also works because it only uses the Twig syntax #}
{% component Card %}
{# ... #}
{% block footer %}
{% component 'Button:Primary' with {isBlock: true} %}
{% block content %}Edit{% endblock %}
{% endcomponent %}
{% endblock %}
{% endcomponent %}
If you're using Live Components, then there are some guidelines related to how the re-rendering of parent and child components works. Read Live Nested Components.
Configuration
To see the full list of configuration options, run:
1
$ php bin/console config:dump twig_component
The most important configuration is the defaults
key, which allows
you to define options for different namespaces of your components. This
controls how components are named and where their templates live:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
# config/packages/twig_component.yaml
twig_component:
defaults:
# short form: components under this namespace:
# - name will be the class name that comes after the prefix
# App\Twig\Components\Alert => Alert
# App\Twig\Components\Button\Primary => Button:Primary
# - templates will live in "components/"
# Alert => templates/components/Alert.html.twig
# Button:Primary => templates/components/Button/Primary.html.twig
App\Twig\Components\: components/
# long form
App\Pizza\Components\:
template_directory: components/pizza
# component names will have an extra "Pizza:" prefix
# App\Pizza\Components\Alert => Pizza:Alert
# App\Pizza\Components\Button\Primary => Pizza:Button:Primary
name_prefix: Pizza
If a component class matches multiple namespaces, the first matched will be used.
3rd-Party Bundle
The flexibility of Twig Components is extended even further when integrated with third-party bundles, allowing developers to seamlessly include pre-built components into their projects.
Anonymous Components
2.20
The bundle convention for Anonymous components was added in TwigComponents 2.20.
Using a component from a third-party bundle is just as straightforward as using one from your own application. Once the bundle is installed and configured, you can reference its components directly within your Twig templates:
1 2 3
<twig:Shadcn:Button type="primary">
Click me
</twig:Shadcn:Button>
Here, the component name is composed of the bundle's Twig namespace Shadcn
, followed
by a colon, and then the component path Button.
Note
You can discover the Twig namespace of every registered bundle by inspecting the
bin/console debug:twig
command.
The component must be located in the bundle's templates/components/
directory. For
example, the component referenced as <twig:Shadcn:Button>
should have its template
file at templates/components/Button.html.twig
within the Shadcn bundle.
Debugging Components
As your application grows, you'll eventually have a lot of components.
This command will help you to debug some components issues.
First, the debug:twig-component command lists all your application components
that live in templates/components/
:
1 2 3 4 5 6 7 8 9 10 11 12
$ php bin/console debug:twig-component
+---------------+-----------------------------+------------------------------------+------+
| Component | Class | Template | Type |
+---------------+-----------------------------+------------------------------------+------+
| Coucou | App\Components\Alert | components/Coucou.html.twig | |
| RandomNumber | App\Components\RandomNumber | components/RandomNumber.html.twig | Live |
| Test | App\Components\foo\Test | components/foo/Test.html.twig | |
| Button | | components/Button.html.twig | Anon |
| foo:Anonymous | | components/foo/Anonymous.html.twig | Anon |
| Acme:Button | | @Acme/components/Button.html.twig | Anon |
+---------------+-----------------------------+------------------------------------+------+
Pass the name of some component as an argument to print its details:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
$ php bin/console debug:twig-component RandomNumber
+---------------------------------------------------+-----------------------------------+
| Property | Value |
+---------------------------------------------------+-----------------------------------+
| Component | RandomNumber |
| Class | App\Components\RandomNumber |
| Template | components/RandomNumber.html.twig |
| Type | Live |
+---------------------------------------------------+-----------------------------------+
| Properties (type / name / default value if exist) | string $name = toto |
| | string $type = test |
| Live Properties | int $max = 1000 |
| | int $min = 10 |
+---------------------------------------------------+-----------------------------------+
Contributing
Interested in contributing? Visit the main source for this repository: https://github.com/symfony/ux/tree/main/src/TwigComponent.
Backward Compatibility promise
This bundle aims at following the same Backward Compatibility promise as the Symfony framework: https://symfony.com/doc/current/contributing/code/bc.html