Skip to content

Commit 36cbe0b

Browse files
committed
Support passing blocks to nested embedded components
1 parent 94a1d30 commit 36cbe0b

File tree

10 files changed

+158
-4
lines changed

10 files changed

+158
-4
lines changed

src/TwigComponent/src/Twig/ComponentNode.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,7 @@ public function compile(Compiler $compiler): void
4848
;
4949

5050
$this->addGetTemplate($compiler);
51-
52-
$compiler->raw('->display($props);');
51+
$compiler->raw('->display($props, $blocks);');
5352
$compiler->raw("\n");
5453
}
5554
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\TwigComponent\Tests\Fixtures\Component;
13+
14+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
15+
16+
/**
17+
* @author Bart Vanderstukken <bart.vanderstukken@gmail.com>
18+
*/
19+
#[AsTwigComponent]
20+
final class DivComponent
21+
{
22+
public string $divComponentName = 'foo';
23+
24+
public function someFunction(): string
25+
{
26+
return 'calling DivComponent';
27+
}
28+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\TwigComponent\Tests\Fixtures\Component;
13+
14+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
15+
16+
/**
17+
* @author Bart Vanderstukken <bart.vanderstukken@gmail.com>
18+
*/
19+
#[AsTwigComponent]
20+
final class DivComponentWrapper
21+
{
22+
public string $divComponentWrapperName = 'bar';
23+
24+
public function someFunction(): string
25+
{
26+
return 'calling DivComponentWrapper';
27+
}
28+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\TwigComponent\Tests\Fixtures\Component;
13+
14+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
15+
16+
/**
17+
* @author Bart Vanderstukken <bart.vanderstukken@gmail.com>
18+
*/
19+
#[AsTwigComponent]
20+
final class GenericElement
21+
{
22+
public string $element;
23+
public string $id = 'symfonyIsAwesome';
24+
25+
public function someFunction(): string
26+
{
27+
return 'calling GenericElement';
28+
}
29+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<twig:GenericElement element="div" class="divComponent">
2+
{{ parent() }} It is accessible via the {{'{{'}} parent() {{'}}'}} function.
3+
Of course you can add some extra content from the DivComponent component.
4+
5+
Did you know I can access the embedded template's context?
6+
Yeah, the Generic Element's property "id" is "{{ id }}".
7+
And the result of a function via `this` works too: {{ this.someFunction }}.
8+
9+
I can also access DivComponent's properties just like those of Generic Element. I know DivComponent's name is {{ divComponentName }}.
10+
11+
But note that I can not access the scope of the template embedding this component.
12+
I can't tell you what "name={{ name ?? '' }}" should be, since this content is only used when you're NOT using embedding components
13+
(aka a self closing twig tag, or a {{'{{'}} component {{'}}'}}'.
14+
</twig:GenericElement>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<twig:DivComponent>
2+
We can even do something crazy...
3+
</twig:DivComponent>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<{{ element }}{{ attributes }}>
2+
{%- block content -%}
3+
The Generic Element could have some default content, although it does not make sense in this example.
4+
{%- endblock -%}
5+
</{{ element }}>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{% set name = 'Fabien' %}
2+
3+
<twig:DivComponent>Hello {{ name }}!</twig:DivComponent>
4+
5+
<twig:DivComponent/>
6+
7+
<twig:DivComponentWrapper/>
8+
9+
<twig:DivComponentWrapper>
10+
And pass the content along and along and along.
11+
Btw even I can access the final block's context. I know that the id is {{ id }} as well!
12+
And of course since DivComponentWrapper's content is rendered inside of a DivComponent element, I also know DivComponent's properties. Its name is {{ divComponentName }}!
13+
14+
Apart from the obvious access to DivComponentWrapper's properties, like its property "name": {{ divComponentWrapperName }}.
15+
16+
The less obvious thing is that even at this level "this" refers to the component where the content block is used, i.e. the Generic Element.
17+
Therefore, functions through this will be {{ this.someFunction }}.
18+
</twig:DivComponentWrapper>

src/TwigComponent/tests/Integration/ComponentExtensionTest.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,36 @@ public function testCanRenderEmbeddedComponent(): void
151151
$this->assertStringContainsString('custom td (1)', $output);
152152
}
153153

154+
public function testCanPassBlocksToChildEmbeddedComponent(): void
155+
{
156+
$output = self::getContainer()->get(Environment::class)->render('embedded_component_passthrough_blocks.html.twig');
157+
158+
// variable available from outside context + content overriding default content two levels deep (aka passthrough)
159+
$this->assertStringContainsString('<div class="divComponent">Hello Fabien!</div>', $output);
160+
// usage of parent function
161+
$this->assertStringContainsString('The Generic Element could have some default content, although it does not make sense in this example. It is accessible via the {{ parent() }} function.', $output);
162+
// access to embedded component's properties from within overriding content block
163+
$this->assertStringContainsString('Yeah, the Generic Element\'s property "id" is "symfonyIsAwesome"', $output);
164+
// access to embedded component's functions from within overriding content block
165+
$this->assertStringContainsString('And the result of a function via `this` works too: calling GenericElement.', $output);
166+
// access to component's properties from within an embedded component
167+
$this->assertStringContainsString('I can also access DivComponent\'s properties just like those of Generic Element. I know DivComponent\'s name is foo.', $output);
168+
// access to component's properties from within an embedded component
169+
$this->assertStringContainsString("But note that I can not access the scope of the template embedding this component.\n I can't tell you what \"name=\" should be, since this content is only used when you're NOT using embedding components\n (aka a self closing twig tag, or a {{ component }}", $output);
170+
// wrapping component overriding content block with another default block
171+
$this->assertStringContainsString('We can even do something crazy...', $output);
172+
// wrapping component overriding content block three levels deep (aka passthrough)
173+
$this->assertStringContainsString('And pass the content along and along and along.', $output);
174+
// access to embedded component's properties from within overriding content block even at this level
175+
$this->assertStringContainsString('Btw even I can access the final block\'s context. I know that the id is symfonyIsAwesome as well!', $output);
176+
// access to second embedded component's properties from within overriding block
177+
$this->assertStringContainsString('And of course since DivComponentWrapper\'s content is rendered inside of a DivComponent element, I also know DivComponent\'s properties. Its name is foo!', $output);
178+
// access to first component's properties from within overriding block
179+
$this->assertStringContainsString('Apart from the obvious access to DivComponentWrapper\'s properties, like its property "name": bar.', $output);
180+
// this refers to the Component where the block is eventually shown
181+
$this->assertStringContainsString("The less obvious thing is that even at this level \"this\" refers to the component where the content block is used, i.e. the Generic Element.\n Therefore, functions through this will be calling GenericElement.", $output);
182+
}
183+
154184
public function testComponentWithNamespace(): void
155185
{
156186
$output = $this->renderComponent('foo:bar:baz');

src/TwigComponent/tests/Integration/ComponentFactoryTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,15 +151,15 @@ public function testCanGetMetadataForSameComponentWithDifferentName(): void
151151
public function testCannotGetConfigByNameForNonRegisteredComponent(): void
152152
{
153153
$this->expectException(\InvalidArgumentException::class);
154-
$this->expectExceptionMessage('Unknown component "invalid". The registered components are: component_a');
154+
$this->expectExceptionMessageMatches('/^Unknown component "invalid"\. The registered components are:.* component_a/');
155155

156156
$this->factory()->metadataFor('invalid');
157157
}
158158

159159
public function testCannotGetInvalidComponent(): void
160160
{
161161
$this->expectException(\InvalidArgumentException::class);
162-
$this->expectExceptionMessage('Unknown component "invalid". The registered components are: component_a');
162+
$this->expectExceptionMessageMatches('/^Unknown component "invalid"\. The registered components are:.* component_a/');
163163

164164
$this->factory()->get('invalid');
165165
}

0 commit comments

Comments
 (0)