From 6a3b1d41e670fb0ced18fde7c6b565163e74d2b5 Mon Sep 17 00:00:00 2001 From: jmsche Date: Mon, 18 Jul 2022 12:43:03 +0200 Subject: [PATCH] Handle Stimulus CSS Classes --- README.md | 26 +++++++++++++++++++++++++- src/Dto/StimulusControllersDto.php | 23 ++++++++++++++++++----- src/Twig/StimulusTwigExtension.php | 17 +++++++++-------- tests/IntegrationTest.php | 24 ++++++++++++++++++++---- 4 files changed, 72 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index b084b46c..3300cf2f 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,8 @@ class ScriptNonceSubscriber implements EventSubscriberInterface ### stimulus_controller This bundle also ships with a special `stimulus_controller()` Twig function -that can be used to render [Stimulus Controllers & Values](https://stimulus.hotwired.dev/reference/values). +that can be used to render [Stimulus Controllers & Values](https://stimulus.hotwired.dev/reference/values) +and [CSS Classes](https://stimulus.hotwired.dev/reference/css-classes). See [stimulus-bridge](https://github.com/symfony/stimulus-bridge) for more details. For example: @@ -224,6 +225,29 @@ For example: ``` +If you want to set CSS classes: + +```twig +
+ Hello +
+ + +
+ Hello +
+ + +
+ Hello +
+``` + Any non-scalar values (like `data: [1, 2, 3, 4]`) are JSON-encoded. And all values are properly escaped (the string `[` is an escaped `[` character, so the attribute is really `[1,2,3,4]`). diff --git a/src/Dto/StimulusControllersDto.php b/src/Dto/StimulusControllersDto.php index 096e5cc7..5cdde862 100644 --- a/src/Dto/StimulusControllersDto.php +++ b/src/Dto/StimulusControllersDto.php @@ -15,8 +15,9 @@ final class StimulusControllersDto extends AbstractStimulusDto { private $controllers = []; private $values = []; + private $classes = []; - public function addController(string $controllerName, array $controllerValues = []): void + public function addController(string $controllerName, array $controllerValues = [], array $controllerClasses = []): void { $controllerName = $this->getFormattedControllerName($controllerName); $this->controllers[] = $controllerName; @@ -31,6 +32,12 @@ public function addController(string $controllerName, array $controllerValues = $this->values['data-'.$controllerName.'-'.$key.'-value'] = $value; } + + foreach ($controllerClasses as $key => $class) { + $key = $this->escapeAsHtmlAttr($this->normalizeKeyName($key)); + + $this->values['data-'.$controllerName.'-'.$key.'-class'] = $class; + } } public function __toString(): string @@ -39,9 +46,15 @@ public function __toString(): string return ''; } - return rtrim('data-controller="'.implode(' ', $this->controllers).'" '.implode(' ', array_map(static function (string $attribute, string $value): string { - return $attribute.'="'.$value.'"'; - }, array_keys($this->values), $this->values))); + return rtrim( + 'data-controller="'.implode(' ', $this->controllers).'" '. + implode(' ', array_map(static function (string $attribute, string $value): string { + return $attribute.'="'.$value.'"'; + }, array_keys($this->values), $this->values)).' '. + implode(' ', array_map(static function (string $attribute, string $value): string { + return $attribute.'="'.$value.'"'; + }, array_keys($this->classes), $this->classes)) + ); } public function toArray(): array @@ -52,7 +65,7 @@ public function toArray(): array return [ 'data-controller' => implode(' ', $this->controllers), - ] + $this->values; + ] + $this->values + $this->classes; } /** diff --git a/src/Twig/StimulusTwigExtension.php b/src/Twig/StimulusTwigExtension.php index 4545dac7..68509867 100644 --- a/src/Twig/StimulusTwigExtension.php +++ b/src/Twig/StimulusTwigExtension.php @@ -38,18 +38,19 @@ public function getFilters(): array } /** - * @param string $controllerName the Stimulus controller name - * @param array $controllerValues array of controller values + * @param string $controllerName the Stimulus controller name + * @param array $controllerValues array of controller values + * @param array $controllerClasses array of controller CSS classes */ - public function renderStimulusController(Environment $env, $controllerName, array $controllerValues = []): StimulusControllersDto + public function renderStimulusController(Environment $env, $controllerName, array $controllerValues = [], array $controllerClasses = []): StimulusControllersDto { $dto = new StimulusControllersDto($env); if (\is_array($controllerName)) { trigger_deprecation('symfony/webpack-encore-bundle', 'v1.15.0', 'Passing an array as first argument of stimulus_controller() is deprecated.'); - if ($controllerValues) { - throw new \InvalidArgumentException('You cannot pass an array to the first and second argument of stimulus_controller(): check the documentation.'); + if ($controllerValues || $controllerClasses) { + throw new \InvalidArgumentException('You cannot pass an array to the first and second/third argument of stimulus_controller(): check the documentation.'); } $data = $controllerName; @@ -61,7 +62,7 @@ public function renderStimulusController(Environment $env, $controllerName, arra return $dto; } - $dto->addController($controllerName, $controllerValues); + $dto->addController($controllerName, $controllerValues, $controllerClasses); return $dto; } @@ -107,9 +108,9 @@ public function renderStimulusAction(Environment $env, $controllerName, string $ return $dto; } - public function appendStimulusController(StimulusControllersDto $dto, string $controllerName, array $controllerValues = []): StimulusControllersDto + public function appendStimulusController(StimulusControllersDto $dto, string $controllerName, array $controllerValues = [], array $controllerClasses = []): StimulusControllersDto { - $dto->addController($controllerName, $controllerValues); + $dto->addController($controllerName, $controllerValues, $controllerClasses); return $dto; } diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 95548ce0..eb1c9fcb 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -209,13 +209,17 @@ public function provideRenderStimulusController() 'controllerValues' => [ 'my"Key"' => true, ], - 'expectedString' => 'data-controller="symfony--ux-dropzone--dropzone" data-symfony--ux-dropzone--dropzone-my-key-value="true"', - 'expectedArray' => ['data-controller' => 'symfony--ux-dropzone--dropzone', 'data-symfony--ux-dropzone--dropzone-my-key-value' => 'true'], + 'controllerClasses' => [ + 'second"Key"' => 'loading', + ], + 'expectedString' => 'data-controller="symfony--ux-dropzone--dropzone" data-symfony--ux-dropzone--dropzone-my-key-value="true" data-symfony--ux-dropzone--dropzone-second-key-class="loading"', + 'expectedArray' => ['data-controller' => 'symfony--ux-dropzone--dropzone', 'data-symfony--ux-dropzone--dropzone-my-key-value' => 'true', 'data-symfony--ux-dropzone--dropzone-second-key-class' => 'loading'], ]; yield 'short-single-controller-no-data' => [ 'dataOrControllerName' => 'my-controller', 'controllerValues' => [], + 'controllerClasses' => [], 'expectedString' => 'data-controller="my-controller"', 'expectedArray' => ['data-controller' => 'my-controller'], ]; @@ -223,6 +227,7 @@ public function provideRenderStimulusController() yield 'short-single-controller-with-data' => [ 'dataOrControllerName' => 'my-controller', 'controllerValues' => ['myValue' => 'scalar-value'], + 'controllerClasses' => [], 'expectedString' => 'data-controller="my-controller" data-my-controller-my-value-value="scalar-value"', 'expectedArray' => ['data-controller' => 'my-controller', 'data-my-controller-my-value-value' => 'scalar-value'], ]; @@ -230,6 +235,7 @@ public function provideRenderStimulusController() yield 'false-attribute-value-renders-false' => [ 'dataOrControllerName' => 'false-controller', 'controllerValues' => ['isEnabled' => false], + 'controllerClasses' => [], 'expectedString' => 'data-controller="false-controller" data-false-controller-is-enabled-value="false"', 'expectedArray' => ['data-controller' => 'false-controller', 'data-false-controller-is-enabled-value' => 'false'], ]; @@ -237,6 +243,7 @@ public function provideRenderStimulusController() yield 'true-attribute-value-renders-true' => [ 'dataOrControllerName' => 'true-controller', 'controllerValues' => ['isEnabled' => true], + 'controllerClasses' => [], 'expectedString' => 'data-controller="true-controller" data-true-controller-is-enabled-value="true"', 'expectedArray' => ['data-controller' => 'true-controller', 'data-true-controller-is-enabled-value' => 'true'], ]; @@ -244,22 +251,31 @@ public function provideRenderStimulusController() yield 'null-attribute-value-does-not-render' => [ 'dataOrControllerName' => 'null-controller', 'controllerValues' => ['firstName' => null], + 'controllerClasses' => [], 'expectedString' => 'data-controller="null-controller"', 'expectedArray' => ['data-controller' => 'null-controller'], ]; + + yield 'short-single-controller-no-data-with-class' => [ + 'dataOrControllerName' => 'my-controller', + 'controllerValues' => [], + 'controllerClasses' => ['loading' => 'spinner'], + 'expectedString' => 'data-controller="my-controller" data-my-controller-loading-class="spinner"', + 'expectedArray' => ['data-controller' => 'my-controller', 'data-my-controller-loading-class' => 'spinner'], + ]; } /** * @dataProvider provideRenderStimulusController */ - public function testRenderStimulusController($dataOrControllerName, array $controllerValues, string $expectedString, array $expectedArray) + public function testRenderStimulusController($dataOrControllerName, array $controllerValues, array $controllerClasses, string $expectedString, array $expectedArray) { $kernel = new WebpackEncoreIntegrationTestKernel(true); $kernel->boot(); $twig = $this->getTwigEnvironmentFromBootedKernel($kernel); $extension = new StimulusTwigExtension(); - $dto = $extension->renderStimulusController($twig, $dataOrControllerName, $controllerValues); + $dto = $extension->renderStimulusController($twig, $dataOrControllerName, $controllerValues, $controllerClasses); $this->assertSame($expectedString, (string) $dto); $this->assertSame($expectedArray, $dto->toArray()); }