diff --git a/core/lib/Drupal/Core/Template/TwigExtension.php b/core/lib/Drupal/Core/Template/TwigExtension.php index eb748c8dfe77fe3b400664b87c0aca1da27f2771..7ae9b8548d5449836b61f120dde0ce5d5e4ee913 100644 --- a/core/lib/Drupal/Core/Template/TwigExtension.php +++ b/core/lib/Drupal/Core/Template/TwigExtension.php @@ -140,6 +140,8 @@ public function getFilters() { // CSS class and ID filters. new TwigFilter('clean_class', '\Drupal\Component\Utility\Html::getClass'), new TwigFilter('clean_id', '\Drupal\Component\Utility\Html::getId'), + new TwigFilter('add_class', [$this, 'addClass']), + new TwigFilter('set_attribute', [$this, 'setAttribute']), // This filter will render a renderable array to use the string results. new TwigFilter('render', [$this, 'renderVar']), new TwigFilter('format_date', [$this->dateFormatter, 'format']), @@ -706,4 +708,56 @@ public function suggestThemeHook(?array $element, string|\Stringable $suggestion return $element; } + /** + * Adds a value into the class attributes of a given element. + * + * Assumes element is an array. + * + * @param array $element + * A render element. + * @param string[]|string ...$classes + * The class(es) to add to the element. Arguments can include string keys + * directly, or arrays of string keys. + * + * @return array + * The element with the given class(es) in attributes. + */ + public function addClass(array $element, ...$classes): array { + $attributes = new Attribute($element['#attributes'] ?? []); + $attributes->addClass(...$classes); + $element['#attributes'] = $attributes->toArray(); + + // Make sure element gets rendered again. + unset($element['#printed']); + + return $element; + } + + /** + * Sets an attribute on a given element. + * + * Assumes the element is an array. + * + * @param array $element + * A render element. + * @param string $name + * The attribute name. + * @param mixed $value + * (optional) The attribute value. + * + * @return array + * The element with the given sanitized attribute's value. + */ + public function setAttribute(array $element, string $name, mixed $value = NULL): array { + $element['#attributes'] = AttributeHelper::mergeCollections( + $element['#attributes'] ?? [], + new Attribute([$name => $value]) + ); + + // Make sure element gets rendered again. + unset($element['#printed']); + + return $element; + } + } diff --git a/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php b/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php index 880b9388206e1f18efd9c50795018771dfaf09f5..32e93c07196d82fdbc7745de715d71c921c3ab6e 100644 --- a/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php +++ b/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php @@ -4,8 +4,10 @@ // cspell:ignore mila +use Drupal\Component\Serialization\Json; use Drupal\Core\File\FileUrlGeneratorInterface; use Drupal\Core\GeneratedLink; +use Drupal\Core\Render\Markup; use Drupal\Core\Render\RenderableInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Template\Loader\StringLoader; @@ -438,7 +440,7 @@ public function testTwigAddSuggestionFilter($original_render_array, $suggestion, } /** - * A data provider for ::testTwigAddSuggestionFilter(). + * Provides data for ::testTwigAddSuggestionFilter(). * * @return \Iterator */ @@ -542,6 +544,130 @@ public function providerTestTwigAddSuggestionFilter(): \Iterator { ]; } + /** + * Tests Twig 'add_class' filter. + * + * @covers ::addClass + * @dataProvider providerTestTwigAddClass + */ + public function testTwigAddClass($element, $classes, $expected_result) { + $processed = $this->systemUnderTest->addClass($element, $classes); + $this->assertEquals($expected_result, $processed); + } + + /** + * Provides data for ::testTwigAddClass(). + * + * @return \Iterator + */ + public function providerTestTwigAddClass(): \Iterator { + yield 'should add a class on element' => [ + ['#type' => 'container'], + 'my-class', + ['#type' => 'container', '#attributes' => ['class' => ['my-class']]], + ]; + + yield 'should add a class from a array of string keys on element' => [ + ['#type' => 'container'], + ['my-class'], + ['#type' => 'container', '#attributes' => ['class' => ['my-class']]], + ]; + + yield 'should add a class from a Markup value' => [ + ['#type' => 'container'], + [Markup::create('my-class')], + ['#type' => 'container', '#attributes' => ['class' => ['my-class']]], + ]; + + yield '#printed should be removed after class(es) added' => [ + [ + '#markup' => 'This content is already is rendered', + '#printed' => TRUE, + ], + '', + [ + '#markup' => 'This content is already is rendered', + '#attributes' => [ + 'class' => [''], + ], + ], + ]; + } + + /** + * Tests Twig 'set_attribute' filter. + * + * @covers ::setAttribute + * @dataProvider providerTestTwigSetAttribute + */ + public function testTwigSetAttribute($element, $key, $value, $expected_result) { + $processed = $this->systemUnderTest->setAttribute($element, $key, $value); + $this->assertEquals($expected_result, $processed); + } + + /** + * A data provider for ::testTwigSetAttribute(). + * + * @return \Iterator + */ + public function providerTestTwigSetAttribute(): \Iterator { + yield 'should add attributes on element' => [ + ['#theme' => 'image'], + 'title', + 'Aloha', + [ + '#theme' => 'image', + '#attributes' => [ + 'title' => 'Aloha', + ], + ], + ]; + + yield 'should merge existing attributes on element' => [ + [ + '#theme' => 'image', + '#attributes' => [ + 'title' => 'Aloha', + ], + ], + 'title', + 'Bonjour', + [ + '#theme' => 'image', + '#attributes' => [ + 'title' => 'Bonjour', + ], + ], + ]; + + yield 'should add JSON attribute value correctly on element' => [ + ['#type' => 'container'], + 'data-slider', + Json::encode(['autoplay' => TRUE]), + [ + '#type' => 'container', + '#attributes' => [ + 'data-slider' => '{"autoplay":true}', + ], + ], + ]; + + yield '#printed should be removed after setting attribute' => [ + [ + '#markup' => 'This content is already is rendered', + '#printed' => TRUE, + ], + 'title', + NULL, + [ + '#markup' => 'This content is already is rendered', + '#attributes' => [ + 'title' => NULL, + ], + ], + ]; + } + } class TwigExtensionTestString {