From e5b3896f771b718730368d4eb9d0c5de36252b77 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Thu, 18 Jan 2024 16:16:13 -0500 Subject: [PATCH] feat(twig): Allow nested attributes --- src/TwigComponent/CHANGELOG.md | 4 + src/TwigComponent/doc/index.rst | 57 ++++++++++++++ src/TwigComponent/src/ComponentAttributes.php | 21 ++++- .../components/JustAttributes.html.twig | 1 + .../components/NestedAttributes.html.twig | 7 ++ .../Integration/ComponentExtensionTest.php | 78 +++++++++++++++++++ .../tests/Unit/ComponentAttributesTest.php | 14 ++++ 7 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 src/TwigComponent/tests/Fixtures/templates/components/JustAttributes.html.twig create mode 100644 src/TwigComponent/tests/Fixtures/templates/components/NestedAttributes.html.twig diff --git a/src/TwigComponent/CHANGELOG.md b/src/TwigComponent/CHANGELOG.md index f9fd2eeeb65..02b46078539 100644 --- a/src/TwigComponent/CHANGELOG.md +++ b/src/TwigComponent/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## 2.17.0 + +- Add nested attribute support #1405 + ## 2.16.0 - Introduce CVA to style TwigComponent #1416 diff --git a/src/TwigComponent/doc/index.rst b/src/TwigComponent/doc/index.rst index e58dc4970cc..712b173090d 100644 --- a/src/TwigComponent/doc/index.rst +++ b/src/TwigComponent/doc/index.rst @@ -1058,6 +1058,63 @@ Exclude specific attributes: My Component! +Nested Attributes +~~~~~~~~~~~~~~~~~ + +.. versionadded:: 2.17 + + The Nested Attributes feature was added in TwigComponents 2.17. + +You can have attributes that aren't meant to be used on the *root* element +but one of its *descendants*. This is useful for, say, a dialog component where +you want to allow customizing the attributes of the dialog's content, title, +and footer. Here's an example of this: + +.. code-block:: html+twig + + {# templates/components/Dialog.html.twig #} + + + {% block title %}Default Title{% endblock %} + + + {% block content %}{% endblock %} + + + {% block footer %}Default Footer{% endblock %} + + + + {# render #} + + Some content + + + {# output #} +
+
+ Default Title +
+
+ Some content +
+
+ Default Footer +
+
+ +The nesting is recursive so you could potentially do something like this: + +.. code-block:: html+twig + + + Component with Complex Variants (CVA) ------------------------------------- diff --git a/src/TwigComponent/src/ComponentAttributes.php b/src/TwigComponent/src/ComponentAttributes.php index 08d19fd56cd..ea351bd38a7 100644 --- a/src/TwigComponent/src/ComponentAttributes.php +++ b/src/TwigComponent/src/ComponentAttributes.php @@ -19,8 +19,10 @@ * * @immutable */ -final class ComponentAttributes implements \IteratorAggregate, \Countable +final class ComponentAttributes implements \Stringable, \IteratorAggregate, \Countable { + private const NESTED_REGEX = '#^([\w-]+):(.+)$#'; + /** @var array */ private array $rendered = []; @@ -39,6 +41,10 @@ public function __toString(): string fn (string $key) => !isset($this->rendered[$key]) ), function (string $carry, string $key) { + if (preg_match(self::NESTED_REGEX, $key)) { + return $carry; + } + $value = $this->attributes[$key]; if ($value instanceof \Stringable) { @@ -196,6 +202,19 @@ public function remove($key): self return new self($attributes); } + public function nested(string $namespace): self + { + $attributes = []; + + foreach ($this->attributes as $key => $value) { + if (preg_match(self::NESTED_REGEX, $key, $matches) && $namespace === $matches[1]) { + $attributes[$matches[2]] = $value; + } + } + + return new self($attributes); + } + public function getIterator(): \Traversable { return new \ArrayIterator($this->attributes); diff --git a/src/TwigComponent/tests/Fixtures/templates/components/JustAttributes.html.twig b/src/TwigComponent/tests/Fixtures/templates/components/JustAttributes.html.twig new file mode 100644 index 00000000000..b245f294419 --- /dev/null +++ b/src/TwigComponent/tests/Fixtures/templates/components/JustAttributes.html.twig @@ -0,0 +1 @@ + diff --git a/src/TwigComponent/tests/Fixtures/templates/components/NestedAttributes.html.twig b/src/TwigComponent/tests/Fixtures/templates/components/NestedAttributes.html.twig new file mode 100644 index 00000000000..6397b9fa6d6 --- /dev/null +++ b/src/TwigComponent/tests/Fixtures/templates/components/NestedAttributes.html.twig @@ -0,0 +1,7 @@ + + + + {{ component('JustAttributes', [...attributes.nested('inner')]) }} + + + diff --git a/src/TwigComponent/tests/Integration/ComponentExtensionTest.php b/src/TwigComponent/tests/Integration/ComponentExtensionTest.php index a6933710e85..74e424882a4 100644 --- a/src/TwigComponent/tests/Integration/ComponentExtensionTest.php +++ b/src/TwigComponent/tests/Integration/ComponentExtensionTest.php @@ -263,6 +263,84 @@ public function testComponentWithClassMerge(): void $this->assertStringContainsString('class="alert alert-red alert-lg font-semibold rounded-md dark:bg-gray-600 flex p-4"', $output); } + public function testRenderingComponentWithNestedAttributes(): void + { + $output = $this->renderComponent('NestedAttributes'); + + $this->assertSame(<< +
+ +
+ + +
+ + HTML, + trim($output) + ); + + $output = $this->renderComponent('NestedAttributes', [ + 'class' => 'foo', + 'title:class' => 'bar', + 'title:span:class' => 'baz', + ]); + + $this->assertSame(<< +
+ +
+ + +
+ + HTML, + trim($output) + ); + } + + public function testRenderingHtmlSyntaxComponentWithNestedAttributes(): void + { + $output = self::getContainer() + ->get(Environment::class) + ->createTemplate('') + ->render() + ; + + $this->assertSame(<< +
+ +
+ + +
+ + HTML, + trim($output) + ); + + $output = self::getContainer() + ->get(Environment::class) + ->createTemplate('') + ->render() + ; + + $this->assertSame(<< +
+ +
+ + +
+ + HTML, + trim($output) + ); + } + private function renderComponent(string $name, array $data = []): string { return self::getContainer()->get(Environment::class)->render('render_component.html.twig', [ diff --git a/src/TwigComponent/tests/Unit/ComponentAttributesTest.php b/src/TwigComponent/tests/Unit/ComponentAttributesTest.php index 22e587566c6..d983de8172b 100644 --- a/src/TwigComponent/tests/Unit/ComponentAttributesTest.php +++ b/src/TwigComponent/tests/Unit/ComponentAttributesTest.php @@ -244,4 +244,18 @@ public function testCanCheckIfAttributeExists(): void $this->assertTrue($attributes->has('foo')); } + + public function testNestedAttributes(): void + { + $attributes = new ComponentAttributes([ + 'class' => 'foo', + 'title:class' => 'bar', + 'title:span:class' => 'baz', + ]); + + $this->assertSame(' class="foo"', (string) $attributes); + $this->assertSame(' class="bar"', (string) $attributes->nested('title')); + $this->assertSame(' class="baz"', (string) $attributes->nested('title')->nested('span')); + $this->assertSame('', (string) $attributes->nested('invalid')); + } }