From 3b4530f16ee4fb955ffd0985cc6e5d1a0edbe3e4 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Sat, 23 Sep 2023 20:15:24 -0400 Subject: [PATCH] [TwigComponent] Updating & reorganizing Twig component docs --- src/TwigComponent/doc/index.rst | 1323 ++++++++++++++++--------------- 1 file changed, 671 insertions(+), 652 deletions(-) diff --git a/src/TwigComponent/doc/index.rst b/src/TwigComponent/doc/index.rst index 6ad33aa9a84..329f08d7388 100644 --- a/src/TwigComponent/doc/index.rst +++ b/src/TwigComponent/doc/index.rst @@ -7,8 +7,8 @@ making it easier to render and re-use small template "units" - like an Every component consists of (1) a class:: - // src/Components/Alert.php - namespace App\Components; + // src/Twig/Components/Alert.php + namespace App\Twig\Components; use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; @@ -19,7 +19,7 @@ Every component consists of (1) a class:: public string $message; } -And (2) a corresponding template: +And (2) a template: .. code-block:: html+twig @@ -34,6 +34,8 @@ Done! Now render it wherever you want: {{ component('Alert', { message: 'Hello Twig Components!' }) }} + + Enjoy your new component! .. image:: images/alert-example.png @@ -56,18 +58,28 @@ Let's get this thing installed! Run: $ composer require symfony/ux-twig-component -That's it! We're ready to go! +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: + +.. _default_config: -Creating a Basic Component --------------------------- +``` +# config/packages/twig_component.yaml +twig_component: + defaults: + # default component template directory + 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 always to create a -component that has an ``AsTwigComponent`` class attribute. Let's start -as simple as possible:: +or error messages across our site. Step 1 is to create a component class +and give it the ``AsTwigComponent`` attribute:: - // src/Components/Alert.php - namespace App\Components; + // src/Twig/Components/Alert.php + namespace App\Twig\Components; use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; @@ -76,7 +88,12 @@ as simple as possible:: { } -Step 2 is to create a template for this component. By default, templates +This class can technically live anywhere, but in practice, you'll put it +somewhere under the namespace configured in :ref:`config/packages/twig_component.yaml `. +This helps TwigComponent :ref:`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: @@ -87,7 +104,7 @@ live in ``templates/components/{component_name}.html.twig``, where Success! You've created a Twig component! -This isn't very interesting yet… since the message is hardcoded into the +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: @@ -95,36 +112,56 @@ any other Twig template: {{ component('Alert') }} -Done! You've just rendered your first Twig Component! Take a moment to -fist pump - then come back! +Done! You've just rendered your first Twig Component! You can see it +and any other components by running: + +.. code-block:: terminal + + $ php bin/console debug:twig-component --dir=bar + +Take a moment to fist pump - then come back! + +.. _naming: Naming Your Component ---------------------- +~~~~~~~~~~~~~~~~~~~~~ .. versionadded:: 2.8 Before 2.8, passing a name to ``AsTwigComponent`` was required. Now, the name is optional and defaults to the class name. -The name of your component is the class name by default. But you can -customize it by passing an argument to ``AsTwigComponent``:: +To give your component a name, TwigComponent looks at the namespace(s) +configured in :ref:`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\\Twig\\Components\\Button\\Primary`` ``Button:Primary`` +========================================= ================== + +The ``:`` character is used in the name instead of ``\``. See +:ref:`Configuration ` for more info. + +Instead of letting TwigComponent choose a name, you can also set on yourself:: #[AsTwigComponent('alert')] class Alert { } -Passing Data into your Component --------------------------------- +Passing Data (Props) into your Component +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Good start: but this isn't very interesting yet! To make our ``Alert`` -component reusable, we need to make the message and type -(e.g. ``success``, ``danger``, etc) configurable. To do that, create a +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: .. code-block:: diff - // src/Components/Alert.php + // src/Twig/Components/Alert.php // ... #[AsTwigComponent] @@ -141,11 +178,6 @@ 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: -.. versionadded:: 2.1 - - The ability to reference local variables in the template (e.g. ``message``) was added in TwigComponents 2.1. - Previously, all data needed to be referenced through ``this`` (e.g. ``this.message``). - .. code-block:: html+twig
@@ -156,7 +188,7 @@ Use them to render the two new properties:
How can we populate the ``message`` and ``type`` properties? By passing -them as a 2nd argument to the ``component()`` function when rendering: +them as "props" via the a 2nd argument to ``component()``: .. code-block:: twig @@ -184,39 +216,207 @@ called instead of setting the property directly. // ... } -Customize the Twig Template -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Passing & Rendering Attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you pass extra props that are *not* settable on your component class, +those can be rendered as attributes: + +.. code-block:: twig + + {{ 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: + +.. code-block:: html+twig + +
+ {{ message }} +
+ +See :ref:`Component Attributes ` to learn more. + +Component Template Path +~~~~~~~~~~~~~~~~~~~~~~~ + +If you're using the :ref:`default config `, the template +name will be: ``templates/components/{component_name}.html.twig``, where +``{component_name}`` matches the component *name*. -You can customize the template used to render the component by passing it -as the second argument to the ``AsTwigComponent`` attribute: +=================== ================================================== +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.s + +You can control the template used via the ``AsTwigComponent`` attribute: .. code-block:: diff - // src/Components/Alert.php + // src/Twig/Components/Alert.php // ... - #[AsTwigComponent] + #[AsTwigComponent(template: 'my/custom/template.html.twig')] class Alert - { - // ... - } -Twig Template Namespaces -~~~~~~~~~~~~~~~~~~~~~~~~ +You can also configure the default template directory for an entire +namespace. See :ref:`Configuration `. + +Component HTML Syntax +~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 2.8 + + This syntax was been introduced in 2.8 and is still experimental. + +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: + +.. code-block:: html+twig + + + +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: + +.. code-block:: html+twig + + + + // equal to + + + // pass object, array, or anything you imagine + + +Don't forget that you can mix and match props with attributes that you +want to render on the root element: + +.. code-block:: html+twig + + + +To pass an array of attributes, use `{{...}}` spread operator syntax. +This requires Twig 3.7.0 or higher: + +.. code-block:: html+twig + + + +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? + +.. code-block:: html+twig + + + I'm writing HTML right here! + + +We can! When you add content between the the ```` open and +close tag, it's passed to your component template as the block called +``content``. You can render it like any normal block: + +.. code-block:: html+twig + +
+ {% block content %}{% endblock %} +
+ +You can even give the block default content. See +:ref:`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``:: + + // 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``: + +.. code-block:: html+twig + + {# templates/components/FeaturedProducts.html.twig #} +
+

Featured Products

+ + {% for product in this.products %} + ... + {% endfor %} +
+ +And because this component doesn't have any public properties that we +need to populate, you can render it with: + +.. code-block:: twig + + + +.. 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 +------------- -You can use a ``:`` in your component's name to indicate a namespace. The default -template will replace the ``:`` with ``/``. For example, a component with the name -``form:input`` will look for a template in ``templates/components/form/input.html.twig``. +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 ~~~~~~~~~~~~~~~~~~ -If, for some reason, you don't want an option to the ``component()`` -function to be set directly onto a property, you can, instead, create a -``mount()`` method in your component:: +For more control over how your "props" are handled, you can create a method +called ``mount()``:: - // src/Components/Alert.php + // src/Twig/Components/Alert.php // ... #[AsTwigComponent] @@ -233,21 +433,22 @@ function to be set directly onto a property, you can, instead, create a // ... } -The ``mount()`` method is called just one time immediately after your +The ``mount()`` method is called just one time: immediately after your component is instantiated. Because the method has an ``$isSuccess`` -argument, we can pass an ``isSuccess`` option when rendering the -component: +argument, if we pass an ``isSuccess`` prop when rendering, it will be +passed to ``mount()``. .. code-block:: twig - {{ component('alert', { - isSuccess: false, - message: 'Danger Will Robinson!' - }) }} + -If an option name matches an argument name in ``mount()``, the option is -passed as that argument and the component system will *not* try to set -it directly on a property. +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 ~~~~~~~~~~~~~ @@ -255,7 +456,7 @@ PreMount Hook If you need to modify/validate data before it's *mounted* on the component use a ``PreMount`` hook:: - // src/Components/Alert.php + // src/Twig/Components/Alert.php use Symfony\UX\TwigComponent\Attribute\PreMount; // ... @@ -281,6 +482,8 @@ component use a ``PreMount`` hook:: // ... } +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 @@ -297,7 +500,7 @@ PostMount Hook After a component is instantiated and its data mounted, you can run extra code via the ``PostMount`` hook:: - // src/Components/Alert.php + // src/Twig/Components/Alert.php use Symfony\UX\TwigComponent\Attribute\PostMount; // ... @@ -305,7 +508,7 @@ code via the ``PostMount`` hook:: class Alert { #[PostMount] - public function postMount(): array + public function postMount(): void { if (str_contains($this->message, 'danger')) { $this->type = 'danger'; @@ -316,10 +519,11 @@ code via the ``PostMount`` hook:: 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. because they don't correspond to any property. You can handle these props, -remove them from the ``$data`` and return the array:: +(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:: - // src/Components/Alert.php + // src/Twig/Components/Alert.php #[AsTwigComponent] class Alert { @@ -350,210 +554,266 @@ remove them from the ``$data`` and return the array:: the order in which they're called, use the ``priority`` attribute parameter: ``PostMount(priority: 10)`` (higher called earlier). -ExposeInTemplate Attribute -~~~~~~~~~~~~~~~~~~~~~~~~~~ +Anonymous Components +-------------------- -.. versionadded:: 2.1 +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: - The ``ExposeInTemplate`` attribute was added in TwigComponents 2.1. +.. code-block:: html+twig -.. versionadded:: 2.3 + {# templates/components/Button/Primary.html.twig #} + - The ``ExposeInTemplate`` attribute can now be used on public methods. +The name for this component will be ``Button:Primary`` because of +the subdirectory: -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:: +.. code-block:: html+twig - // ... - use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate; + {# index.html.twig #} + ... +
+ Click Me! +
- #[AsTwigComponent] - class Alert - { - #[ExposeInTemplate] - private string $message; // available as `{{ message }}` in the template + {# renders as: #} + - #[ExposeInTemplate('alert_type')] - private string $type = 'success'; // available as `{{ alert_type }}` in the template +Like normal, you can pass extra attributes that will be rendered on the element: - #[ExposeInTemplate(name: 'ico', getter: 'fetchIcon')] - private string $icon = 'ico-warning'; // available as `{{ ico }}` in the template using `fetchIcon()` as the getter +.. code-block:: html+twig - /** - * Required to access $this->message - */ - public function getMessage(): string - { - return $this->message; - } + {# index.html.twig #} + ... +
+ Click Me! +
- /** - * Required to access $this->type - */ - public function getType(): string - { - return $this->type; - } + {# renders as: #} + - /** - * Required to access $this->icon - */ - public function fetchIcon(): string - { - return $this->icon; - } +You can also pass a variable (prop) into your template: - #[ExposeInTemplate] - public function getActions(): array // available as `{{ actions }}` in the template - { - // ... - } +.. code-block:: html+twig - #[ExposeInTemplate('dismissable')] - public function canBeDismissed(): bool // available as `{{ dismissable }}` in the template - { - // ... - } + {# index.html.twig #} + ... +
+ Click Me! +
- // ... - } +To tell the system that ``icon`` and ``type`` are props and not attributes, use the +``{% props %}`` tag at the top of your template. -.. note:: +.. code-block:: html+twig - When using ``ExposeInTemplate`` on a method the value is fetched eagerly - before rendering. + {# templates/components/Button.html.twig #} + {% props icon = null, type = 'primary' %} -Fetching Services ------------------ + -Let's create a more complex example: a "featured products" component. -You *could* choose to pass an array of Product objects into the -``component()`` function and set those on a ``$products`` property. But -instead, let's allow the component to do the work of executing the -query. +.. _embedded-components: -How? Components are *services*, which means autowiring works like -normal. This example assumes you have a ``Product`` Doctrine entity and -``ProductRepository``:: +Passing HTML to Components Via Blocks +------------------------------------- - // src/Components/FeaturedProducts.php - namespace App\Components; +Props aren't the only way you can pass something to your component. You can +also pass content: - use App\Repository\ProductRepository; - use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; +.. code-block:: html+twig - #[AsTwigComponent] - class FeaturedProducts - { - private ProductRepository $productRepository; + +
Congratulations! You've won a free puppy!
+
- public function __construct(ProductRepository $productRepository) - { - $this->productRepository = $productRepository; - } +In your component template, this becomes a block named ``content``: - public function getProducts(): array - { - // an example method that returns an array of Products - return $this->productRepository->findFeatured(); - } - } +.. code-block:: html+twig -In the template, the ``getProducts()`` method can be accessed via -``this.products``: +
+ {% block content %} + // the content will appear in here + {% endblock %} +
+ +You can also add more, named blocks: .. code-block:: html+twig - {# templates/components/FeaturedProducts.html.twig #} -
-

Featured Products

+ +
Congrats on winning a free puppy!
- {% for product in this.products %} - ... - {% endfor %} -
+ + {{ parent() }} {# render the default content if needed #} + + +
-And because this component doesn't have any public properties that we -need to populate, you can render it with: +Render these in the normal way. + +.. code-block:: html+twig + +
+ {% block content %}{% endblock %} + {% block footer %} +
Default Footer content
+ {% endblock %} +
+ +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: + +.. code-block:: html+twig + + {% component Alert with {type: 'success'} %} + {% block content %}
Congrats!
{% endblock %} + {% block footer %}... footer content{% endblock %} + {% endcomponent %} + +Context / Variables Inside of Blocks +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The content inside of the ```` 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 ````, the ``this`` variable represents +the component you're *now* rendering *and* you have access to all of *that* +component's variables: .. code-block:: twig - {{ component('FeaturedProducts') }} + {# templates/components/SuccessAlert.html.twig #} + {{ this.someFunction }} {# this === SuccessAlert #} -.. note:: + + {{ this.someFunction }} {# this === Alert #} - 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. + {{ type }} {# references a "type" prop from Alert #} + -Computed Properties -~~~~~~~~~~~~~~~~~~~ +Conveniently, in addition to the variables from the ``Alert`` component, you +*also* have access to whatever variables are available in the original template: -.. versionadded:: 2.1 +.. code-block:: twig - Computed Properties were added in TwigComponents 2.1. + {# templates/components/SuccessAlert.html.twig #} -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``. + {% set name = 'Fabien' %} + + Hello {{ name }} + -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`_. +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``. -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. +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: -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. +.. code-block:: twig + + {# 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: + +.. code-block:: twig + + {# templates/some_page.html.twig #} + + + I can override the alert_message block and access the {{ message }} too! + + + +Inheritance & Forwarding "Outer Blocks" +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 2.10` + + The ``outerBlocks`` variable was added in 2.10. + +The content inside a ```` 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: .. code-block:: html+twig - {# templates/components/FeaturedProducts.html.twig #} -
-

Featured Products

+ {% extends 'base.html.twig' %} - {% for product in computed.products %} - ... - {% endfor %} + {% block call_to_action %}Attention! Free Puppies!{% endblock %} - ... - {% for product in computed.products %} {# use cache, does not result in a second query #} - ... - {% endfor %} + {% block body %} + + {# block('call_to_action') #} would not work #} + + {{ block(outerBlocks.call_to_action) }} + + {% endblock %} + +The ``outerBlocks`` variable becomes especially useful with nested components. +For example, imagine we want to create a ``SuccessAlert`` component: + +.. code-block:: html+twig + + {# templates/some_page.html.twig #} + + We will successfully forward this block content! + + +We already have a generic ``Alert`` component, so let's re-use it: + +.. code-block:: html+twig + + {# templates/components/Alert.html.twig #} +
+ {% block content %}{% endblock %}
-.. note:: +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``: - Computed methods only work for component methods with no required - arguments. +.. code-block:: twig -Component Attributes --------------------- + {# templates/components/SuccessAlert.html.twig #} + + {% component Alert with { type: 'success' } %} + {{ block(outerBlocks.content) }} + -.. versionadded:: 2.1 +By passing the original ``content`` block into the `content` block of ``Alert``, +this will work perfectly. - Component attributes were added in TwigComponents 2.1. +.. _attributes: -A common need for components is to configure/render attributes for the -root node. Attributes are any data passed to ``component()`` that cannot be -mounted on the component itself. This extra data is added to a -``ComponentAttributes`` that is available as ``attributes`` in your -component's template. +Component Attributes +-------------------- -To use, in your component's template, render the ``attributes`` variable in -the root element: +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'ss available as ``attributes`` in your +component's template: .. code-block:: html+twig @@ -566,7 +826,7 @@ When rendering the component, you can pass an array of html attributes to add: .. code-block:: html+twig - {{ component('MyComponent', { class: 'foo', style: 'color:red' }) }} + {# renders as: #}
@@ -577,11 +837,11 @@ Set an attribute's value to ``true`` to render just the attribute name: .. code-block:: html+twig - {# templates/components/MyComponent.html.twig #} + {# templates/components/Input.html.twig #} {# render component #} - {{ component('MyComponent', { type: 'text', value: '', autofocus: true }) }} + {# renders as: #} @@ -590,17 +850,21 @@ Set an attribute's value to ``false`` to exclude the attribute: .. code-block:: html+twig - {# templates/components/MyComponent.html.twig #} + {# templates/components/Input.html.twig #} {# render component #} - {{ component('MyComponent', { type: 'text', value: '', autofocus: false }) }} + {# renders as: #} To add a custom `Stimulus controller`_ to your root component element: +.. code-block:: html+twig + +
+ .. versionadded:: 2.9 The ability to use ``stimulus_controller()`` with ``attributes.defaults()`` @@ -608,10 +872,6 @@ To add a custom `Stimulus controller`_ to your root component element: Previously, ``stimulus_controller()`` was passed to an ``attributes.add()`` method. -.. code-block:: html+twig - -
- .. note:: You can adjust the attributes variable exposed in your template:: @@ -686,437 +946,264 @@ Exclude specific attributes: My Component!
-PreRenderEvent --------------- - -.. versionadded:: 2.1 - - The ``PreRenderEvent`` was added in TwigComponents 2.1. +Test Helpers +------------ -Subscribing to the ``PreRenderEvent`` gives the ability to modify -the twig template and twig variables before components are rendered:: +You can test how your component is mounted and rendered using the +``InteractsWithTwigComponents`` trait:: - use Symfony\Component\EventDispatcher\EventSubscriberInterface; - use Symfony\UX\TwigComponent\Event\PreRenderEvent; + use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; + use Symfony\UX\TwigComponent\Test\InteractsWithTwigComponents; - class HookIntoTwigPreRenderSubscriber implements EventSubscriberInterface + class MyComponentTest extends KernelTestCase { - public function onPreRender(PreRenderEvent $event): void + use InteractsWithTwigComponents; + + public function testComponentMount(): 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 + $component = $this->mountTwigComponent( + name: 'MyComponent', // can also use FQCN (MyComponent::class) + data: ['foo' => 'bar'], + ); - $event->setTemplate('some_other_template.html.twig'); // change the template used + $this->assertInstanceOf(MyComponent::class, $component); + $this->assertSame('bar', $component->foo); + } - // manipulate the variables: - $variables = $event->getVariables(); - $variables['custom'] = 'value'; + public function testComponentRenders(): void + { + $rendered = $this->renderTwigComponent( + name: 'MyComponent', // can also use FQCN (MyComponent::class) + data: ['foo' => 'bar'], + ); - $event->setVariables($variables); // {{ custom }} will be available in your template + $this->assertStringContainsString('bar', $rendered); + + // use the crawler + $this->assertCount(5, $rendered->crawler('ul li')); } - public static function getSubscribedEvents(): array + public function testEmbeddedComponentRenders(): void { - return [PreRenderEvent::class => 'onPreRender']; + $rendered = $this->renderTwigComponent( + name: 'MyComponent', // can also use FQCN (MyComponent::class) + data: ['foo' => 'bar'], + content: '
My content
', // "content" (default) block + blocks: [ + 'header' => '
My header
', + 'menu' => $this->renderTwigComponent('Menu'), // can embed other components + ], + ); + + $this->assertStringContainsString('bar', $rendered); } } -PostRenderEvent ---------------- +.. note:: -.. versionadded:: 2.5 + The ``InteractsWithTwigComponents`` trait can only be used in tests that extend + ``Symfony\Bundle\FrameworkBundle\Test\KernelTestCase``. - The ``PostRenderEvent`` was added in TwigComponents 2.5. +Special Component Variables +--------------------------- -The ``PostRenderEvent`` is called after a component has finished -rendering and contains the ``MountedComponent`` that was just -rendered. +By default, your template will have access to the following variables: -PreCreateForRenderEvent ------------------------ +* ``this`` +* ``attributes`` +* ... and all public properties on your component -.. versionadded:: 2.5 +There are also a few other special ways you can control the variables. - The ``PreCreateForRenderEvent`` was added in TwigComponents 2.5. +ExposeInTemplate Attribute +~~~~~~~~~~~~~~~~~~~~~~~~~~ -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. +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:: -PreMountEvent and PostMountEvent --------------------------------- + // ... + use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate; -.. versionadded:: 2.1 + #[AsTwigComponent] + class Alert + { + #[ExposeInTemplate] + private string $message; // available as `{{ message }}` in the template - The ``PreMountEvent`` and ``PostMountEvent`` ere added in TwigComponents 2.5. + #[ExposeInTemplate('alert_type')] + private string $type = 'success'; // available as `{{ alert_type }}` in the template -To run code just before or after a component's data is mounted, you can -listen to ``PreMountEvent`` or ``PostMountEvent``. + #[ExposeInTemplate(name: 'ico', getter: 'fetchIcon')] + private string $icon = 'ico-warning'; // available as `{{ ico }}` in the template using `fetchIcon()` as the getter -Nested Components ------------------ - -It's totally possible to nest one component into another. When you do -this, there's nothing special to know: both components render -independently. 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`_. - -.. _embedded-components: - -Passing Blocks to Components ----------------------------- - -.. tip:: - - The `Component HTML Syntax`_ allows you to pass blocks to components in an - even more readable way. - -You can write your component's Twig template with blocks that can be overridden -when rendering using the ``{% component %}`` syntax. These blocks can be thought of as -*slots* which you may be familiar with from Vue. The ``component`` tag is very -similar to Twig's native `embed tag`_. - -Consider a data table component. You pass it headers and rows but can expose -blocks for the cells and an optional footer: - -.. code-block:: html+twig - - {# templates/components/DataTable.html.twig #} - - - - - {% for header in this.headers %} - - {% endfor %} - - - - {% for row in this.data %} - - {% for cell in row %} - - {% endfor %} - - {% endfor %} - -
- {{ header }} -
- {{ cell }} -
- {% block footer %}{% endblock %} -
- -When rendering, you can override the ``th_class``, ``td_class``, and ``footer`` blocks. -The ``with`` data is what's mounted on the component object. - -.. code-block:: html+twig - - {# templates/some_page.html.twig #} - {% component DataTable with {headers: ['key', 'value'], data: [[1, 2], [3, 4]]} %} - {% block th_class %}{{ parent() }} text-bold{% endblock %} - - {% block td_class %}{{ parent() }} text-italic{% endblock %} + /** + * Required to access $this->message + */ + public function getMessage(): string + { + return $this->message; + } - {% block footer %} - - {% endblock %} - {% endcomponent %} + /** + * Required to access $this->type + */ + public function getType(): string + { + return $this->type; + } -.. versionadded:: 2.11 + /** + * Required to access $this->icon + */ + public function fetchIcon(): string + { + return $this->icon; + } - The ``{% component %}`` syntax can also be used with LiveComponents since 2.11. - However, there are some caveats related to the context between parent and child - components during re-rending. Read `Passing Blocks to Live Components`_. + #[ExposeInTemplate] + public function getActions(): array // available as `{{ actions }}` in the template + { + // ... + } -Inheritance & Forwarding "Outer Blocks" -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + #[ExposeInTemplate('dismissable')] + public function canBeDismissed(): bool // available as `{{ dismissable }}` in the template + { + // ... + } -.. versionadded:: 2.10 + // ... + } - The ``outerBlocks`` variable was added in 2.10. +.. note:: -The content inside a ``{% 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 inside the ``{% component %}`` tag. -However, a special ``outerBlocks`` variable is added as a way to refer to those blocks: + When using ``ExposeInTemplate`` on a method the value is fetched eagerly + before rendering. -.. code-block:: html+twig +Computed Properties +~~~~~~~~~~~~~~~~~~~ - {% extends 'base.html.twig' %} +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``. - {% block call_to_action %}Attention! Free Puppies!{% endblock %} +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`_. - {% block body %} - {% component Alert %} - {% block content %}{{ block(outerBlocks.call_to_action) }}{% endblock %} - {% endcomponent %} - {% endblock %} +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. -The ``outerBlocks`` variable becomes specially useful with nested components. For example, -imagine we want to create a ``SuccessAlert`` component that's usable like this: +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. .. code-block:: html+twig - {# templates/some_page.html.twig #} - {% component SuccessAlert %} - {% block content %}We will successfully forward this block content!{% endblock %} - {% endcomponent %} - -But we already have a generic ``Alert`` component, and we want to re-use it: + {# templates/components/FeaturedProducts.html.twig #} +
+

Featured Products

-.. code-block:: html+twig + {% for product in computed.products %} + ... + {% endfor %} - {# templates/Alert.html.twig #} -
- {% block content %}{% endblock %} + ... + {% for product in computed.products %} {# use cache, does not result in a second query #} + ... + {% endfor %}
-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``: - -.. code-block:: twig - - {# templates/SuccessAlert.html.twig #} - {% component Alert with { type: 'success' } %} - {% block content %}{{ block(outerBlocks.content) }}{% endblock %} - {% endcomponent %} - -Note that to pass a block multiple components down, each component needs to pass it. - -Context / Variables Inside of Blocks -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The content inside of the ``{% component ... %}`` should be viewed as living in its own, -independent template, which extends the component's template. This has a few interesting consequences. - -First, once you're inside of ``{% component ... %}``, the ``this`` variable represents -the component you're now rendering *and* you have access to all of that component's variables: - -.. code-block:: twig - - {# templates/SuccessAlert.html.twig #} - {{ this.someFunction }} {# this refers to SuccessAlert #} - - {% component Alert with { type: 'success' } %} - {% block content %} - {{ this.someFunction }} {# this refers to Alert! #} - - {{ type }} {# references a "type" prop from Alert #} - {% endblock %} - {% endcomponent %} - -Conveniently, in addition to the variables from the ``Alert`` component, you still have -access to whatever variables are available in the original template: - -.. code-block:: twig - - {# templates/SuccessAlert.html.twig #} - {% set name = 'Fabien' %} - {% component Alert with { type: 'success' } %} - {% block content %} - Hello {{ name }} - {% endblock %} - {% endcomponent %} - -Note that ALL variables from upper components (e.g. ``SuccessAlert``) are available to lower -components (e.g. ``Alert``). However, because variables are merged, variables with the same name -are overridden by lower components (that's also why ``this`` refers to the embedded, or -"current" component). - -The most interesting thing is that the content inside of ``{% component ... %}`` is -executed 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: - -.. code-block:: twig - - {# templates/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: - -.. code-block:: twig - - {# templates/some_page.html.twig #} - {% component SuccessAlert %} - {% block alert_message %} - I can override the alert_message block and access the {{ message }} too! - {% endblock %} - {% endcomponent %} - -Component HTML Syntax ---------------------- - -.. versionadded:: 2.8 +.. note:: - This syntax was been introduced in 2.8 and is still experimental: it may change in the future. + Computed methods only work for component methods with no required + arguments. -Twig Components come with an HTML-like syntax to ease the readability of your template: +Events +------ -.. code-block:: html+twig +Twig Components dispatches various events throughout the lifecycle +of instantiating, mounting and rendering a component. - - // or use a self-closing tag - +PreRenderEvent +~~~~~~~~~~~~~~ -Passing Props as HTML Attributes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Subscribing to the ``PreRenderEvent`` gives the ability to modify +the twig template and twig variables before components are rendered:: -Passing props is done with HTML attributes. For example if you have this component:: + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\UX\TwigComponent\Event\PreRenderEvent; - #[AsTwigComponent] - class Alert + class HookIntoTwigPreRenderSubscriber implements EventSubscriberInterface { - public string $message = ''; - public bool $withActions = false; - public string $type = 'success'; - } - -You can pass the ``message``, ``withActions`` or ``type`` props as attributes: - -.. code-block:: html+twig - - // withActions will be set to true - - -To pass in a dynamic value, prefix the attribute with ``:`` or use the -normal ``{{ }}`` syntax: - -.. code-block:: html+twig - - - - // equal to - - - // and pass object, or table, or anything you imagine - - -To forward attributes to another component, use `{{...}}` spread operator syntax. -This requires Twig 3.7.0 or higher: - -.. code-block:: html+twig - - - -.. _passing-blocks: - -Passing Content (Blocks) to Components -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can also pass content directly to your component: - -.. code-block:: html+twig - - -
Congratulations! You've won a free puppy!
-
- -In your component template, this becomes a block named ``content``: - -.. code-block:: html+twig - -
- {% block content %} - // the content will appear in here - {% endblock %} -
- -In addition to the default block, you can also add named blocks: - -.. code-block:: html+twig - - -
Congrats on winning a free puppy!
- - - - -
- -And in your component template you can access your embedded block - -.. code-block:: html+twig - -
- {% block content %}{% endblock %} - {% block footer %}{% endblock %} -
- -Anonymous Components --------------------- - -Sometimes a component is simple enough that it doesn't have any complex logic or injected services. -In this case, you can skip the class and only create the template. The component name is determined -by the location of the template (see `Twig Template Namespaces`_): - -.. code-block:: html+twig + 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 - {# templates/components/Button/Primary.html.twig #} - + $event->setTemplate('some_other_template.html.twig'); // change the template used -Then use your component with ``:`` to navigate through sub-directories (if there are any): + // manipulate the variables: + $variables = $event->getVariables(); + $variables['custom'] = 'value'; -.. code-block:: html+twig + $event->setVariables($variables); // {{ custom }} will be available in your template + } - {# index.html.twig #} - ... -
- Click Me! -
+ public static function getSubscribedEvents(): array + { + return [PreRenderEvent::class => 'onPreRender']; + } + } - {# renders as: #} - +PostRenderEvent +~~~~~~~~~~~~~~~ -Like normal, you can pass extra attributes that will be rendered on the element: +.. versionadded:: 2.5 -.. code-block:: html+twig + The ``PostRenderEvent`` was added in TwigComponents 2.5. - {# index.html.twig #} - ... -
- Click Me! -
+The ``PostRenderEvent`` is called after a component has finished +rendering and contains the ``MountedComponent`` that was just +rendered. - {# renders as: #} - +PreCreateForRenderEvent +~~~~~~~~~~~~~~~~~~~~~~~ -You can also pass a variable (prop) into your template: +.. versionadded:: 2.5 -.. code-block:: html+twig + The ``PreCreateForRenderEvent`` was added in TwigComponents 2.5. - {# index.html.twig #} - ... -
- Click Me! -
+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. -To tell the system that ``icon`` and ``type`` are props and not attributes, use the ``{% props %}`` tag at the top of your template. +PreMountEvent and PostMountEvent +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. code-block:: html+twig +To run code just before or after a component's data is mounted, you can +listen to ``PreMountEvent`` or ``PostMountEvent``. - {# templates/components/Button.html.twig #} - {% props icon = null, type = 'primary' %} +Nested Components +----------------- - +It's totally possible to nest one component into another. When you do +this, there's nothing special to know: both components render +independently. 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`_. Debugging Components -------------------- @@ -1140,23 +1227,13 @@ who live in ``templates/components``: | foo:Anonymous | Anonymous component | components/foo/Anonymous.html.twig | | +---------------+-----------------------------+------------------------------------+------+ -.. tip:: - - The Live column show you which component is a LiveComponent. - -If you have some components who doesn't live in ``templates/components``, -but in ``templates/bar`` for example you can pass an option: +If you have some components that don't live in ``templates/components/``, +but in ``templates/bar`` for example, you can pass an option: .. code-block:: terminal $ php bin/console debug:twig-component --dir=bar - +----------------+-------------------------------+------------------------------+------+ - | Component | Class | Template | Live | - +----------------+-------------------------------+------------------------------+------+ - | OtherDirectory | App\Components\OtherDirectory | bar/OtherDirectory.html.twig | | - +----------------+-------------------------------+------------------------------+------+ - And the name of some component to this argument to print the component details: @@ -1177,64 +1254,6 @@ component details: | | int $min = 10 | +---------------------------------------------------+-----------------------------------+ -Test Helpers ------------- - -You can test how your component is mounted and rendered using the -``InteractsWithTwigComponents`` trait:: - - 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('ul li')); - } - - public function testEmbeddedComponentRenders(): void - { - $rendered = $this->renderTwigComponent( - name: 'MyComponent', // can also use FQCN (MyComponent::class) - data: ['foo' => 'bar'], - content: '
My content
', // "content" (default) block - blocks: [ - 'header' => '
My header
', - '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\Bundle\FrameworkBundle\Test\KernelTestCase``. - Contributing ------------