From c022b08d0b081be5272ac3a956e7b998656acf78 Mon Sep 17 00:00:00 2001 From: Hugo Alliaume Date: Tue, 27 Apr 2021 12:03:41 +0200 Subject: [PATCH] feat(twig): implements stimulus_action() and stimulus_target() Twig functions, close #119 --- src/Twig/StimulusTwigExtension.php | 101 ++++++++++++++++++++++++- tests/IntegrationTest.php | 116 +++++++++++++++++++++++++++++ 2 files changed, 216 insertions(+), 1 deletion(-) diff --git a/src/Twig/StimulusTwigExtension.php b/src/Twig/StimulusTwigExtension.php index 545d7a00..c7f82daf 100644 --- a/src/Twig/StimulusTwigExtension.php +++ b/src/Twig/StimulusTwigExtension.php @@ -19,6 +19,8 @@ public function getFunctions() { return [ new TwigFunction('stimulus_controller', [$this, 'renderStimulusController'], ['needs_environment' => true, 'is_safe' => ['html_attr']]), + new TwigFunction('stimulus_action', [$this, 'renderStimulusAction'], ['needs_environment' => true, 'is_safe' => ['html_attr']]), + new TwigFunction('stimulus_target', [$this, 'renderStimulusTarget'], ['needs_environment' => true, 'is_safe' => ['html_attr']]), ]; } @@ -27,7 +29,7 @@ public function getFunctions() * as keys set to their "values". Or this * can be a string controller name and data * is passed as the 2nd argument. - * @param array $controllerValues Array of data if a string is passed to the first argument. + * @param array $controllerValues Array of data if a string is passed to the 1st argument. * @return string * @throws \Twig\Error\RuntimeError */ @@ -73,6 +75,103 @@ public function renderStimulusController(Environment $env, $dataOrControllerName return rtrim('data-controller="'.implode(' ', $controllers).'" '.implode(' ', $values)); } + /** + * @param string|array $dataOrControllerName This can either be a map of controller names + * as keys set to their "actions" and "events". + * Or this can be a string controller name and + * action and event are passed as the 2nd and 3rd arguments. + * @param string|null $actionName The action to trigger if a string is passed to the 1st argument. Optional. + * @param string|null $eventName The event to listen to trigger if a string is passed to the 1st argument. Optional. + * + * @return string + * @throws \Twig\Error\RuntimeError + */ + public function renderStimulusAction(Environment $env, $dataOrControllerName, string $actionName = null, string $eventName = null): string + { + if (is_string($dataOrControllerName)) { + $data = [$dataOrControllerName => $eventName === null ? [[$actionName]] : [[$eventName => $actionName]]]; + } else { + if ($actionName || $eventName) { + throw new \InvalidArgumentException('You cannot pass a string to the second or third argument while passing an array to the first argument of stimulus_action(): check the documentation.'); + } + + $data = $dataOrControllerName; + + if (!$data) { + return ''; + } + } + + $actions = []; + + foreach ($data as $controllerName => $controllerActions) { + $controllerName = twig_escape_filter($env, $this->normalizeControllerName($controllerName), 'html_attr'); + + if (is_string($controllerActions)) { + $controllerActions = [[$controllerActions]]; + } + + foreach ($controllerActions as $possibleEventName => $controllerAction) { + if (is_string($possibleEventName) && is_string($controllerAction)) { + $controllerAction = [$possibleEventName => $controllerAction]; + } else if (is_string($controllerAction)) { + $controllerAction = [$controllerAction]; + } + + foreach ($controllerAction as $eventName => $actionName) { + $action = $controllerName.'#'.twig_escape_filter($env, $actionName, 'html_attr'); + + if (is_string($eventName)) { + $action = $eventName.'->'.$action; + } + + $actions[] = $action; + } + } + } + + return 'data-action="'.implode(' ', $actions).'"'; + } + + /** + * @param string|array $dataOrControllerName This can either be a map of controller names + * as keys set to their "targets". Or this can + * be a string controller name and targets are + * passed as the 2nd argument. + * @param string|null $targetNames The space-separated list of target names if a string is passed to the 1st argument. Optional. + * + * @return string + * @throws \Twig\Error\RuntimeError + */ + public function renderStimulusTarget(Environment $env, $dataOrControllerName, string $targetNames = null): string + { + if (is_string($dataOrControllerName)) { + $data = [$dataOrControllerName => $targetNames]; + } else { + if ($targetNames) { + throw new \InvalidArgumentException('You cannot pass a string to the second argument while passing an array to the first argument of stimulus_target(): check the documentation.'); + } + + $data = $dataOrControllerName; + + if (!$data) { + return ''; + } + } + + $targets = []; + + foreach ($data as $controllerName => $targetNames) { + $controllerName = twig_escape_filter($env, $this->normalizeControllerName($controllerName), 'html_attr'); + + $targets['data-'.$controllerName.'-target'] = twig_escape_filter($env, $targetNames, 'html_attr'); + } + + return implode(' ', array_map(function(string $attribute, string $value) { + return $attribute.'="'.$value.'"'; + }, array_keys($targets), $targets)); + } + /** * Normalize a Stimulus controller name into its HTML equivalent (no special character and / becomes --). * diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index ecb4bf1f..c540dec0 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -288,6 +288,122 @@ public function testRenderStimulusController($dataOrControllerName, array $contr $this->assertSame($expected, $extension->renderStimulusController($twig, $dataOrControllerName, $controllerValues)); } + public function provideRenderStimulusAction() + { + yield 'with default event' => [ + 'dataOrControllerName' => 'my-controller', + 'actionName' => 'onClick', + 'eventName' => null, + 'expected' => 'data-action="my-controller#onClick"', + ]; + + yield 'with custom event' => [ + 'dataOrControllerName' => 'my-controller', + 'actionName' => 'onClick', + 'eventName' => 'click', + 'expected' => 'data-action="click->my-controller#onClick"', + ]; + + yield 'multiple actions, with default event' => [ + 'dataOrControllerName' => [ + 'my-controller' => 'onClick', + 'my-second-controller' => ['onClick', 'onSomethingElse'], + 'foo/bar-controller' => 'onClick' + ], + 'actionName' => null, + 'eventName' => null, + 'expected' => 'data-action="my-controller#onClick my-second-controller#onClick my-second-controller#onSomethingElse foo--bar-controller#onClick"', + ]; + + yield 'multiple actions, with custom event' => [ + 'dataOrControllerName' => [ + 'my-controller' => ['click' => 'onClick'], + 'my-second-controller' => [['click' => 'onClick'], ['change' => 'onSomethingElse']], + 'resize-controller' => ['resize@window' => 'onWindowResize'], + 'foo/bar-controller' => ['click' => 'onClick'] + ], + 'actionName' => null, + 'eventName' => null, + 'expected' => 'data-action="click->my-controller#onClick click->my-second-controller#onClick change->my-second-controller#onSomethingElse resize@window->resize-controller#onWindowResize click->foo--bar-controller#onClick"', + ]; + + yield 'multiple actions, with default and custom event' => [ + 'dataOrControllerName' => [ + 'my-controller' => ['click' => 'onClick'], + 'my-second-controller' => ['onClick', ['click' => 'onAnotherClick'], ['change' => 'onSomethingElse']], + 'resize-controller' => ['resize@window' => 'onWindowResize'], + 'foo/bar-controller' => ['click' => 'onClick'] + ], + 'actionName' => null, + 'eventName' => null, + 'expected' => 'data-action="click->my-controller#onClick my-second-controller#onClick click->my-second-controller#onAnotherClick change->my-second-controller#onSomethingElse resize@window->resize-controller#onWindowResize click->foo--bar-controller#onClick"', + ]; + + yield 'normalize-name, with default event' => [ + 'dataOrControllerName' => '@symfony/ux-dropzone/dropzone', + 'actionName' => 'onClick', + 'eventName' => null, + 'expected' => 'data-action="symfony--ux-dropzone--dropzone#onClick"', + ]; + + yield 'normalize-name, with custom event' => [ + 'dataOrControllerName' => '@symfony/ux-dropzone/dropzone', + 'actionName' => 'onClick', + 'eventName' => 'click', + 'expected' => 'data-action="click->symfony--ux-dropzone--dropzone#onClick"', + ]; + } + + /** + * @dataProvider provideRenderStimulusAction + */ + public function testRenderStimulusAction($dataOrControllerName, ?string $actionName, ?string $eventName, string $expected) + { + $kernel = new WebpackEncoreIntegrationTestKernel(true); + $kernel->boot(); + $twig = $this->getTwigEnvironmentFromBootedKernel($kernel); + + $extension = new StimulusTwigExtension(); + $this->assertSame($expected, $extension->renderStimulusAction($twig, $dataOrControllerName, $actionName, $eventName)); + } + + public function provideRenderStimulusTarget() + { + yield 'simple' => [ + 'dataOrControllerName' => 'my-controller', + 'targetName' => 'myTarget', + 'expected' => 'data-my-controller-target="myTarget"', + ]; + + yield 'normalize-name' => [ + 'dataOrControllerName' => '@symfony/ux-dropzone/dropzone', + 'targetName' => 'myTarget', + 'expected' => 'data-symfony--ux-dropzone--dropzone-target="myTarget"', + ]; + + yield 'multiple' => [ + 'dataOrControllerName' => [ + 'my-controller' => 'myTarget', + '@symfony/ux-dropzone/dropzone' => 'anotherTarget fooTarget', + ], + 'targetName' => null, + 'expected' => 'data-my-controller-target="myTarget" data-symfony--ux-dropzone--dropzone-target="anotherTarget fooTarget"', + ]; + } + + /** + * @dataProvider provideRenderStimulusTarget + */ + public function testRenderStimulusTarget($dataOrControllerName, ?string $targetName, string $expected) + { + $kernel = new WebpackEncoreIntegrationTestKernel(true); + $kernel->boot(); + $twig = $this->getTwigEnvironmentFromBootedKernel($kernel); + + $extension = new StimulusTwigExtension(); + $this->assertSame($expected, $extension->renderStimulusTarget($twig, $dataOrControllerName, $targetName)); + } + private function getContainerFromBootedKernel(WebpackEncoreIntegrationTestKernel $kernel) { if ($kernel::VERSION_ID >= 40100) {