Skip to content

Commit 534b9f5

Browse files
committed
feat(twig): Allow nested attributes
1 parent 3f7f811 commit 534b9f5

File tree

6 files changed

+130
-1
lines changed

6 files changed

+130
-1
lines changed

src/TwigComponent/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased
44

55
- Make `ComponentAttributes` traversable/countable.
6+
- Added nested attribute support.
67

78
## 2.13.0
89

src/TwigComponent/src/ComponentAttributes.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
*
2020
* @immutable
2121
*/
22-
final class ComponentAttributes implements \IteratorAggregate, \Countable
22+
final class ComponentAttributes implements \Stringable, \IteratorAggregate, \Countable
2323
{
24+
private const NESTED_REGEX = '#^([\w-]+):(.+)$#';
25+
2426
/**
2527
* @param array<string, string|bool> $attributes
2628
*/
@@ -33,6 +35,10 @@ public function __toString(): string
3335
return array_reduce(
3436
array_keys($this->attributes),
3537
function (string $carry, string $key) {
38+
if (preg_match(self::NESTED_REGEX, $key)) {
39+
return $carry;
40+
}
41+
3642
$value = $this->attributes[$key];
3743

3844
if (!\is_scalar($value) && null !== $value) {
@@ -158,6 +164,19 @@ public function remove($key): self
158164
return new self($attributes);
159165
}
160166

167+
public function nested(string $namespace): self
168+
{
169+
$attributes = [];
170+
171+
foreach ($this->attributes as $key => $value) {
172+
if (preg_match(self::NESTED_REGEX, $key, $matches) && $namespace === $matches[1]) {
173+
$attributes[$matches[2]] = $value;
174+
}
175+
}
176+
177+
return new self($attributes);
178+
}
179+
161180
public function getIterator(): \Traversable
162181
{
163182
return new \ArrayIterator($this->attributes);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div{{ attributes }}/>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<main{{ attributes }}>
2+
<div{{ attributes.nested('title') }}>
3+
<span{{ attributes.nested('title').nested('span') }}>
4+
{{ component('JustAttributes', [...attributes.nested('inner')]) }}
5+
</span>
6+
</div>
7+
</main>

src/TwigComponent/tests/Integration/ComponentExtensionTest.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\UX\TwigComponent\Tests\Fixtures\User;
1616
use Twig\Environment;
1717
use Twig\Error\RuntimeError;
18+
use Twig\Token;
1819

1920
/**
2021
* @author Kevin Bond <kevinbond@gmail.com>
@@ -216,6 +217,92 @@ public function testComponentPropsWithTrailingComma(): void
216217
$this->assertStringContainsString('Hello FOO, 123, and 456', $output);
217218
}
218219

220+
public function testRenderingComponentWithNestedAttributes(): void
221+
{
222+
if (!\defined(Token::class.'::SPREAD_TYPE')) {
223+
$this->markTestSkipped('Twig 3.7+ is required.');
224+
}
225+
226+
$output = $this->renderComponent('NestedAttributes');
227+
228+
$this->assertSame(<<<HTML
229+
<main>
230+
<div>
231+
<span>
232+
<div/>
233+
234+
</span>
235+
</div>
236+
</main>
237+
HTML,
238+
trim($output)
239+
);
240+
241+
$output = $this->renderComponent('NestedAttributes', [
242+
'class' => 'foo',
243+
'title:class' => 'bar',
244+
'title:span:class' => 'baz',
245+
]);
246+
247+
$this->assertSame(<<<HTML
248+
<main class="foo">
249+
<div class="bar">
250+
<span class="baz">
251+
<div/>
252+
253+
</span>
254+
</div>
255+
</main>
256+
HTML,
257+
trim($output)
258+
);
259+
}
260+
261+
public function testRenderingHtmlSyntaxComponentWithNestedAttributes(): void
262+
{
263+
if (!\defined(Token::class.'::SPREAD_TYPE')) {
264+
$this->markTestSkipped('Twig 3.7+ is required.');
265+
}
266+
267+
$output = self::getContainer()
268+
->get(Environment::class)
269+
->createTemplate('<twig:NestedAttributes />')
270+
->render()
271+
;
272+
273+
$this->assertSame(<<<HTML
274+
<main>
275+
<div>
276+
<span>
277+
<div/>
278+
279+
</span>
280+
</div>
281+
</main>
282+
HTML,
283+
trim($output)
284+
);
285+
286+
$output = self::getContainer()
287+
->get(Environment::class)
288+
->createTemplate('<twig:NestedAttributes class="foo" title:class="bar" title:span:class="baz" inner:class="foo" />')
289+
->render()
290+
;
291+
292+
$this->assertSame(<<<HTML
293+
<main class="foo">
294+
<div class="bar">
295+
<span class="baz">
296+
<div class="foo"/>
297+
298+
</span>
299+
</div>
300+
</main>
301+
HTML,
302+
trim($output)
303+
);
304+
}
305+
219306
private function renderComponent(string $name, array $data = []): string
220307
{
221308
return self::getContainer()->get(Environment::class)->render('render_component.html.twig', [

src/TwigComponent/tests/Unit/ComponentAttributesTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,4 +199,18 @@ public function testIsTraversableAndCountable(): void
199199
$this->assertSame($attributes->all(), iterator_to_array($attributes));
200200
$this->assertCount(1, $attributes);
201201
}
202+
203+
public function testNestedAttributes(): void
204+
{
205+
$attributes = new ComponentAttributes([
206+
'class' => 'foo',
207+
'title:class' => 'bar',
208+
'title:span:class' => 'baz',
209+
]);
210+
211+
$this->assertSame(' class="foo"', (string) $attributes);
212+
$this->assertSame(' class="bar"', (string) $attributes->nested('title'));
213+
$this->assertSame(' class="baz"', (string) $attributes->nested('title')->nested('span'));
214+
$this->assertSame('', (string) $attributes->nested('invalid'));
215+
}
202216
}

0 commit comments

Comments
 (0)