Skip to content

Commit 4bda8ed

Browse files
committed
feat(twig): Allow nested attributes
1 parent a2418ef commit 4bda8ed

File tree

6 files changed

+129
-1
lines changed

6 files changed

+129
-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: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,84 @@ public function testComponentPropsWithTrailingComma(): void
216216
$this->assertStringContainsString('Hello FOO, 123, and 456', $output);
217217
}
218218

219+
public function testRenderingComponentWithNestedAttributes(): void
220+
{
221+
$output = $this->renderComponent('NestedAttributes');
222+
223+
$this->assertSame(<<<HTML
224+
<main>
225+
<div>
226+
<span>
227+
<div/>
228+
229+
</span>
230+
</div>
231+
</main>
232+
HTML,
233+
trim($output)
234+
);
235+
236+
$output = $this->renderComponent('NestedAttributes', [
237+
'class' => 'foo',
238+
'title:class' => 'bar',
239+
'title:span:class' => 'baz',
240+
]);
241+
242+
$this->assertSame(<<<HTML
243+
<main class="foo">
244+
<div class="bar">
245+
<span class="baz">
246+
<div/>
247+
248+
</span>
249+
</div>
250+
</main>
251+
HTML,
252+
trim($output)
253+
);
254+
}
255+
256+
public function testRenderingHtmlSyntaxComponentWithNestedAttributes(): void
257+
{
258+
$output = self::getContainer()
259+
->get(Environment::class)
260+
->createTemplate('<twig:NestedAttributes />')
261+
->render()
262+
;
263+
264+
$this->assertSame(<<<HTML
265+
<main>
266+
<div>
267+
<span>
268+
<div/>
269+
270+
</span>
271+
</div>
272+
</main>
273+
HTML,
274+
trim($output)
275+
);
276+
277+
$output = self::getContainer()
278+
->get(Environment::class)
279+
->createTemplate('<twig:NestedAttributes class="foo" title:class="bar" title:span:class="baz" inner:class="foo" />')
280+
->render()
281+
;
282+
283+
$this->assertSame(<<<HTML
284+
<main class="foo">
285+
<div class="bar">
286+
<span class="baz">
287+
<div class="foo"/>
288+
289+
</span>
290+
</div>
291+
</main>
292+
HTML,
293+
trim($output)
294+
);
295+
}
296+
219297
private function renderComponent(string $name, array $data = []): string
220298
{
221299
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)