Skip to content

[TwigComponent] Add documentation about passing blocks to embedded components #985

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jul 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 122 additions & 5 deletions src/TwigComponent/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -765,13 +765,15 @@ 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
-------------------
.. _embedded-components:

Passing Blocks to Components
----------------------------

.. tip::

Embedded components (i.e. components with blocks) can be written in a more
readable way by using the `Component HTML Syntax`_.
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
Expand Down Expand Up @@ -830,7 +832,122 @@ The ``with`` data is what's mounted on the component object.

.. note::

Embedded components *cannot* currently be used with LiveComponents.
The ``{% component %}`` syntax *cannot* currently be used with LiveComponents.

Inheritance & Forwarding "Outer Blocks"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. versionadded:: 2.10

The ``outerBlocks`` variable was added in 2.10.

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:

.. code-block:: html+twig

{% extends 'base.html.twig' %}

{% block call_to_action %}<strong>Attention! Free Puppies!</strong>{% endblock %}

{% block body %}
{% component Alert %}
{% block content %}{{ block(outerBlocks.call_to_action) }}{% endblock %}
{% endcomponent %}
{% endblock %}

The ``outerBlocks`` variable becomes specially useful with nested components. For example,
imagine we want to create a ``SuccessAlert`` component that's usable like this:

.. code-block:: html+twig

{# templates/some_page.html.twig #}
{% component SuccessAlert %}
{% content %}We will successfully <em>forward</em> this block content!{% endblock %}
{% endcomponent %}

But we already have a generic ``Alert`` component, and we want to re-use it:

.. code-block:: html+twig

{# templates/Alert.html.twig #}
<div 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``:

.. code-block:: twig

{# templates/SuccessAlert.html.twig #}
{% component Alert with { type: 'success' } %}
{% block content %}{{ blocks(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' } %}
{{ this.someFunction }} {# this refers to Alert! #}

{{ type }} {# references a "type" prop from Alert #}
{% 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' } %}
Hello {{ name }}
{% 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
---------------------
Expand Down
7 changes: 0 additions & 7 deletions src/TwigComponent/src/BlockStack.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,6 @@ public function convert(array $blocks, int $targetEmbeddedTemplateIndex): array
// identifies the block definition.
$hostEmbeddedTemplateIndex = $this->findHostEmbeddedTemplateIndex();

if (0 === $hostEmbeddedTemplateIndex) {
// If there is no embedded template index, that means we're in a normal template.
// It wouldn't make sense to make these available as outer blocks,
// since the block is already printed in place.
continue;
}

// Change the name of outer blocks to something unique so blocks of nested components aren't overridden,
// which otherwise might cause a recursion loop when nesting components.
$newName = self::OUTER_BLOCK_PREFIX.$blockName.'_'.mt_rand();
Expand Down
22 changes: 22 additions & 0 deletions src/TwigComponent/tests/Fixtures/Component/BasicComponent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\TwigComponent\Tests\Fixtures\Component;

use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;

/**
* @author Bart Vanderstukken <bart.vanderstukken@gmail.com>
*/
#[AsTwigComponent]
final class BasicComponent
{
}
1 change: 1 addition & 0 deletions src/TwigComponent/tests/Fixtures/templates/base.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% block body %}{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div>
{% block content %}{% endblock %}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% extends 'base.html.twig' %}

{% block foo %}Hello world!{% endblock %}

{% block body %}
<twig:BasicComponent>
{{ block(outerBlocks.foo) }}
</twig:BasicComponent>
{% endblock %}
10 changes: 10 additions & 0 deletions src/TwigComponent/tests/Integration/EmbeddedComponentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,16 @@ public function testBlockCanBeUsedWithinNestedViaTheOuterBlocks(): void
);
}

/**
* Rule 5 bis: A block inside an extending template can be use inside a component in that template and is NOT rendered in the original location.
*/
public function testBlockCanBeUsedViaTheOuterBlocks(): void
{
$output = self::getContainer()->get(Environment::class)->render('embedded_component_blocks_outer_blocks_extended_template.html.twig');
$this->assertStringContainsStringIgnoringIndentation('<div>Hello world!</div>', $output);
$this->assertStringNotContainsString("Hello world!\n<div", $output);
}

/**
* Rule 8: Defining a block for a component overrides any default content that block has in the component's template.
* This also means that when passing block down that you will lose that default content.
Expand Down