Skip to content

Commit 8ddd75f

Browse files
committed
[TwigComponent] Document high-order components
1 parent dab74e2 commit 8ddd75f

File tree

5 files changed

+74
-0
lines changed

5 files changed

+74
-0
lines changed

src/TwigComponent/doc/index.rst

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,6 +1266,46 @@ The nesting is recursive so you could potentially do something like this:
12661266
row:widget:class="ui-form-widget"
12671267
/>
12681268

1269+
Higher-Order Components (Component Wrappers)
1270+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1271+
1272+
You can create a component that wraps another component while adding its own
1273+
default attributes. This is useful for creating component variants without
1274+
duplicating code.
1275+
1276+
For example, create a base ``Button`` component:
1277+
1278+
.. code-block:: html+twig
1279+
1280+
{# templates/components/Button.html.twig #}
1281+
<button{{ attributes.defaults({type: 'button', class: 'btn'}) }}>
1282+
{% block content %}{% endblock %}
1283+
</button>
1284+
1285+
Then create a ``Button:Primary`` variant that wraps it:
1286+
1287+
.. code-block:: html+twig
1288+
1289+
{# templates/components/Button/Primary.html.twig #}
1290+
<twig:Button {{ ...attributes.defaults({class: 'btn-primary'}) }}>
1291+
{{ block(outerBlocks.content) }}
1292+
</twig:Button>
1293+
1294+
Usage:
1295+
1296+
.. code-block:: html+twig
1297+
1298+
<twig:Button:Primary>Save</twig:Button:Primary>
1299+
{# <button type="button" class="btn btn-primary">Save</button> #}
1300+
1301+
<twig:Button:Primary class="btn-lg">Save</twig:Button:Primary>
1302+
{# <button type="button" class="btn btn-primary btn-lg">Save</button> #}
1303+
1304+
The key parts are:
1305+
1306+
- **Spread operator** ``{{ ...attributes.defaults() }}`` - passes attributes to wrapped component
1307+
- **``outerBlocks``** - forwards content blocks to the wrapped component
1308+
12691309
Component with Complex Variants (CVA)
12701310
-------------------------------------
12711311

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<button{{ attributes.defaults({type: 'button', class: 'btn'}) }}>
2+
{% block content %}{% endblock %}
3+
</button>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<twig:SimpleButton {{ ...attributes.defaults({class: 'btn-primary'}) }}>
2+
{{ block(outerBlocks.content) }}
3+
</twig:SimpleButton>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{# Test base SimpleButton component #}
2+
<twig:SimpleButton>Click me</twig:SimpleButton>
3+
4+
{# Test SimpleButton:Primary wrapper component #}
5+
<twig:SimpleButton:Primary>Save</twig:SimpleButton:Primary>
6+
7+
{# Test SimpleButton:Primary with custom class #}
8+
<twig:SimpleButton:Primary class="btn-lg">Save</twig:SimpleButton:Primary>
9+
10+
{# Test SimpleButton:Primary with other attributes #}
11+
<twig:SimpleButton:Primary data-action="delete">Delete</twig:SimpleButton:Primary>

src/TwigComponent/tests/Integration/ComponentExtensionTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,23 @@ public function testAnonymousComponentWithPropsOverwriteParentsProps()
488488
$this->assertStringNotContainsString('I am md', $output);
489489
}
490490

491+
public function testHigherOrderComponentWithAttributeDefaults()
492+
{
493+
$output = self::getContainer()->get(Environment::class)->render('higher_order_component.html.twig');
494+
495+
// Test base SimpleButton component
496+
$this->assertStringContainsString('<button type="button" class="btn">Click me</button>', $output);
497+
498+
// Test SimpleButton:Primary wrapper component - should merge btn and btn-primary classes
499+
$this->assertStringContainsString('<button type="button" class="btn btn-primary">Save</button>', $output);
500+
501+
// Test SimpleButton:Primary with custom class - should merge all three classes
502+
$this->assertStringContainsString('<button type="button" class="btn btn-primary btn-lg">Save</button>', $output);
503+
504+
// Test SimpleButton:Primary with data attribute
505+
$this->assertStringContainsString('<button type="button" class="btn btn-primary" data-action="delete">Delete</button>', $output);
506+
}
507+
491508
private function renderComponent(string $name, array $data = []): string
492509
{
493510
return self::getContainer()->get(Environment::class)->render('render_component.html.twig', [

0 commit comments

Comments
 (0)