Skip to content

Commit 84839d2

Browse files
committed
feat(twig): Allow nested attributes
1 parent a2418ef commit 84839d2

File tree

6 files changed

+138
-1
lines changed

6 files changed

+138
-1
lines changed

src/TwigComponent/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# CHANGELOG
22

3+
## Unreleased
4+
5+
- Added nested attribute support.
6+
37
## 2.13.0
48

59
- [BC BREAK] Add component metadata to `PreMountEvent` and `PostMountEvent`

src/TwigComponent/src/ComponentAttributes.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
*
2020
* @immutable
2121
*/
22-
final class ComponentAttributes
22+
final class ComponentAttributes implements \Stringable, \IteratorAggregate
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) {
@@ -157,4 +163,22 @@ public function remove($key): self
157163

158164
return new self($attributes);
159165
}
166+
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+
180+
public function getIterator(): \Traversable
181+
{
182+
return new \ArrayIterator($this->attributes);
183+
}
160184
}
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
@@ -191,4 +191,18 @@ public function testNullBehaviour(): void
191191
$this->assertSame(['disabled' => null], $attributes->all());
192192
$this->assertSame(' disabled', (string) $attributes);
193193
}
194+
195+
public function testNestedAttributes(): void
196+
{
197+
$attributes = new ComponentAttributes([
198+
'class' => 'foo',
199+
'title:class' => 'bar',
200+
'title:span:class' => 'baz',
201+
]);
202+
203+
$this->assertSame(' class="foo"', (string) $attributes);
204+
$this->assertSame(' class="bar"', (string) $attributes->nested('title'));
205+
$this->assertSame(' class="baz"', (string) $attributes->nested('title')->nested('span'));
206+
$this->assertSame('', (string) $attributes->nested('invalid'));
207+
}
194208
}

0 commit comments

Comments
 (0)