From be4394f4c0e0eeb9407822dc492f0bc0bb9255db Mon Sep 17 00:00:00 2001 From: 19Gerhard85 Date: Wed, 18 Dec 2024 08:36:31 +0100 Subject: [PATCH 1/3] [Feature] StimulusBundle Form Extension --- .../src/Form/Extension/FormTypeExtension.php | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 src/StimulusBundle/src/Form/Extension/FormTypeExtension.php diff --git a/src/StimulusBundle/src/Form/Extension/FormTypeExtension.php b/src/StimulusBundle/src/Form/Extension/FormTypeExtension.php new file mode 100644 index 00000000000..f528a05b9a9 --- /dev/null +++ b/src/StimulusBundle/src/Form/Extension/FormTypeExtension.php @@ -0,0 +1,136 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\Form\Extension; + +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\UX\StimulusBundle\Dto\StimulusAttributes; +use Twig\Environment; +use Twig\Loader\ArrayLoader; + +class FormTypeExtension extends AbstractTypeExtension +{ + private StimulusAttributes $stimulusAttributes; + + public static function getExtendedTypes(): iterable + { + return [FormType::class]; + } + + public function buildView(FormView $view, FormInterface $form, array $options): void + { + if ( + null === $options['stimulus_controller'] + && null === $options['stimulus_target'] + && null === $options['stimulus_action'] + ) { + return; + } + + $this->stimulusAttributes = new StimulusAttributes(new Environment(new ArrayLoader())); + + if (true === \array_key_exists('stimulus_controller', $options)) { + $this->handleController($options['stimulus_controller']); + } + + if (true === \array_key_exists('stimulus_target', $options)) { + $this->handleTarget($options['stimulus_target']); + } + + if (true === \array_key_exists('stimulus_action', $options)) { + $this->handleAction($options['stimulus_action']); + } + + $attributes = array_merge($view->vars['attr'], $this->stimulusAttributes->toArray()); + + $view->vars['attr'] = $attributes; + } + + private function handleController(string|array $controllers): void + { + if (\is_string($controllers)) { + $controllers = [$controllcers]; + } + + foreach ($controllers as $controllerName => $controller) { + if (\is_string($controller)) { // 'stimulus_controller' => ['controllerName1', 'controllerName2'] + $this->stimulusAttributes->addController($controller); + } elseif (\is_array($controller)) { // 'stimulus_controller' => ['controllerName' => ['values' => ['key' => 'value'], 'classes' => ['key' => 'value'], 'targets' => ['otherControllerName' => '.targetName']]] + $this->stimulusAttributes->addController((string) $controllerName, $controller['values'] ?? [], $controller['classes'] ?? [], $controller['outlets'] ?? []); + } + } + } + + private function handleTarget(array $targets): void + { + foreach ($targets as $controllerName => $target) { + $this->stimulusAttributes->addTarget($controllerName, \is_array($target) ? implode(' ', $target) : $target); + } + } + + private function handleAction(string|array $actions): void + { + // 'stimulus_action' => 'controllerName#actionName' + // 'stimulus_action' => 'eventName->controllerName#actionName' + if (\is_string($actions) && str_contains($actions, '#')) { + $eventName = null; + + if (str_contains($actions, '->')) { + [$eventName, $rest] = explode('->', $actions, 2); + } else { + $rest = $actions; + } + + [$controllerName, $actionName] = explode('#', $rest, 2); + + $this->stimulusAttributes->addAction($controllerName, $actionName, $eventName); + + return; + } + + foreach ($actions as $controllerName => $action) { + if (\is_string($action)) { // 'stimulus_action' => ['controllerName' => 'actionName'] + $this->stimulusAttributes->addAction($controllerName, $action); + } elseif (\is_array($action)) { + foreach ($action as $eventName => $actionName) { + if (\is_string($actionName)) { // 'stimulus_action' => ['controllerName' => ['eventName' => 'actionName']] + $this->stimulusAttributes->addAction($controllerName, $actionName, $eventName); + } elseif (\is_array($actionName)) { // 'stimulus_action' => ['controllerName' => ['eventName' => ['actionName' => ['key' => 'value']]]] + foreach ($actionName as $index => $params) { + $this->stimulusAttributes->addAction($controllerName, $index, $eventName, $params); + } + } + } + } + } + } + + public function configureOptions(OptionsResolver $resolver): void + { + parent::configureOptions($resolver); + + $resolver->setDefaults([ + 'stimulus_action' => null, + 'stimulus_controller' => null, + 'stimulus_target' => null, + ]); + + $resolver->setAllowedTypes('stimulus_action', ['string', 'array', 'null']); + $resolver->setAllowedTypes('stimulus_controller', ['string', 'array', 'null']); + $resolver->setAllowedTypes('stimulus_target', ['string', 'array', 'null']); + } +} From 92e2a5e956156235091096b521f92c1cdb4d6b1f Mon Sep 17 00:00:00 2001 From: 19Gerhard85 Date: Thu, 19 Dec 2024 08:43:50 +0100 Subject: [PATCH 2/3] Added support for row_attr and choice_attr --- .../src/Form/Extension/FormTypeExtension.php | 70 +++++++++++++------ 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/src/StimulusBundle/src/Form/Extension/FormTypeExtension.php b/src/StimulusBundle/src/Form/Extension/FormTypeExtension.php index f528a05b9a9..00e9579160e 100644 --- a/src/StimulusBundle/src/Form/Extension/FormTypeExtension.php +++ b/src/StimulusBundle/src/Form/Extension/FormTypeExtension.php @@ -1,7 +1,5 @@ stimulusAttributes = new StimulusAttributes(new Environment(new ArrayLoader())); - $this->stimulusAttributes = new StimulusAttributes(new Environment(new ArrayLoader())); + if (isset($options['stimulus_controller'])) { + $this->handleController($options['stimulus_controller']); + } - if (true === \array_key_exists('stimulus_controller', $options)) { - $this->handleController($options['stimulus_controller']); - } + if (isset($options['stimulus_target'])) { + $this->handleTarget($options['stimulus_target']); + } - if (true === \array_key_exists('stimulus_target', $options)) { - $this->handleTarget($options['stimulus_target']); - } + if (isset($options['stimulus_action'])) { + $this->handleAction($options['stimulus_action']); + } - if (true === \array_key_exists('stimulus_action', $options)) { - $this->handleAction($options['stimulus_action']); + $attributes = array_merge($view->vars['attr'], $this->stimulusAttributes->toArray()); + $view->vars['attr'] = $attributes; } - $attributes = array_merge($view->vars['attr'], $this->stimulusAttributes->toArray()); + foreach (['row_attr', 'choice_attr'] as $index) { + if ( + isset($options[$index]) + && ( + isset($options[$index]['stimulus_controller']) + || isset($options[$index]['stimulus_target']) + || isset($options[$index]['stimulus_action']) + ) + ) { + $this->stimulusAttributes = new StimulusAttributes(new Environment(new ArrayLoader())); + + if (isset($options[$index]['stimulus_controller'])) { + $this->handleController($options[$index]['stimulus_controller']); + unset($options[$index]['stimulus_controller']); + } + + if (isset($options[$index]['stimulus_target'])) { + $this->handleTarget($options[$index]['stimulus_target']); + unset($options[$index]['stimulus_target']); + } - $view->vars['attr'] = $attributes; + if (isset($options[$index]['stimulus_action'])) { + $this->handleAction($options[$index]['stimulus_action']); + unset($options[$index]['stimulus_action']); + } + + $attributes = array_merge($options[$index], $this->stimulusAttributes->toArray()); + $view->vars[$index] = $attributes; + } + } } private function handleController(string|array $controllers): void { if (\is_string($controllers)) { - $controllers = [$controllcers]; + $controllers = [$controllers]; } foreach ($controllers as $controllerName => $controller) { @@ -129,8 +155,8 @@ public function configureOptions(OptionsResolver $resolver): void 'stimulus_target' => null, ]); - $resolver->setAllowedTypes('stimulus_action', ['string', 'array', 'null']); - $resolver->setAllowedTypes('stimulus_controller', ['string', 'array', 'null']); - $resolver->setAllowedTypes('stimulus_target', ['string', 'array', 'null']); + $resolver->setAllowedTypes('stimulus_action', ['string', 'array', 'callable', 'null']); + $resolver->setAllowedTypes('stimulus_controller', ['string', 'array', 'callable', 'null']); + $resolver->setAllowedTypes('stimulus_target', ['string', 'array', 'callable', 'null']); } } From 65eaaa6591efa109f4f3b547da588fb46d5b2e16 Mon Sep 17 00:00:00 2001 From: 19Gerhard85 Date: Wed, 29 Jan 2025 11:33:11 +0100 Subject: [PATCH 3/3] AttributeBuilder --- .../src/Builder/StimulusAttributeBuilder.php | 162 ++++++++++++++++++ .../src/Form/Extension/FormTypeExtension.php | 162 ------------------ .../src/Helper/StimulusSyntaxHelper.php | 42 +++++ 3 files changed, 204 insertions(+), 162 deletions(-) create mode 100644 src/StimulusBundle/src/Builder/StimulusAttributeBuilder.php delete mode 100644 src/StimulusBundle/src/Form/Extension/FormTypeExtension.php create mode 100644 src/StimulusBundle/src/Helper/StimulusSyntaxHelper.php diff --git a/src/StimulusBundle/src/Builder/StimulusAttributeBuilder.php b/src/StimulusBundle/src/Builder/StimulusAttributeBuilder.php new file mode 100644 index 00000000000..24553b174dc --- /dev/null +++ b/src/StimulusBundle/src/Builder/StimulusAttributeBuilder.php @@ -0,0 +1,162 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\StimulusBundle\Builder; + +use Symfony\UX\StimulusBundle\Helper\StimulusSyntaxHelper; + +class StimulusAttributeBuilder +{ + private string $controllerName; + private array $actions = []; + private array $values = []; + private array $classes = []; + private array $targets = []; + private array $outlets = []; + private array $attributes = []; + + public static function controller(string $controllerName): self + { + $builder = new self(); + $builder->controllerName = $controllerName; + + return $builder; + } + + public function action(string $actionName, ?string $eventName = null, array $parameters = []): self + { + $this->actions[] = [ + 'actionName' => $actionName, + 'eventName' => $eventName, + 'parameters' => $parameters, + ]; + + return $this; + } + + public function value(string $key, mixed $value): self + { + $this->values[$key] = $value; + + return $this; + } + + public function class(string $key, mixed $value): self + { + $this->classes[$key] = $value; + + return $this; + } + + public function target(string $key, mixed $value): self + { + $this->targets[$key] = $value; + + return $this; + } + + public function outlet(string $identifier, string $outlet, mixed $selector): self + { + $this->outlets[] = [ + 'identifier' => $identifier, + 'outlet' => $outlet, + 'selector' => $selector, + ]; + + return $this; + } + + public function build(): array + { + $syntaxHelper = new StimulusSyntaxHelper(); + $controllerName = $syntaxHelper->normalizeControllerName($this->controllerName); + + $this->attributes['data-controller'] = $controllerName; + + // Actions + $this->attributes = array_merge( + $this->attributes, + ...array_map(function (array $actionData) use ($controllerName): array { + $actionName = htmlspecialchars($actionData['actionName']); + $eventName = $actionData['eventName']; + + $action = \sprintf('%s#%s', $controllerName, $actionName); + if (null !== $eventName) { + $action = \sprintf('%s->%s', htmlspecialchars($eventName), $action); + } + + return ['data-action' => $action]; + }, $this->actions) + ); + + // Action Parameters + $this->attributes = array_merge( + $this->attributes, + ...array_map( + function (array $actionData) use ($controllerName, $syntaxHelper): array { + $parameters = []; + + foreach ($actionData['parameters'] as $name => $value) { + $key = $syntaxHelper->normalizeKeyName($name); + $value = $syntaxHelper->getFormattedValue($value); + + $parameters[\sprintf( + 'data-%s-%s-param', + $controllerName, + $key + )] = htmlspecialchars($value); + } + + return $parameters; + }, + $this->actions + ) + ); + + // Values + foreach ($this->values as $key => $value) { + if (null === $value) { + continue; + } + + $key = $syntaxHelper->normalizeKeyName($key); + $value = $syntaxHelper->getFormattedValue($value); + + $this->attributes[\sprintf('data-%s-%s-value', $controllerName, $key)] = $value; + } + + // Classes + foreach ($this->classes as $key => $class) { + $key = $syntaxHelper->normalizeKeyName($key); + + $this->attributes[\sprintf('data-%s-%s-class', $controllerName, $key)] = $class; + } + + // Outlets + foreach ($this->outlets as $outletItem) { + $identifier = $syntaxHelper->normalizeControllerName($outletItem['identifier']); + $outlet = $syntaxHelper->normalizeKeyName($outletItem['outlet']); + + $this->attributes[\sprintf('data-%s-%s-outlet', $identifier, $outlet)] = htmlspecialchars( + $outletItem['selector'] + ); + } + + // Targets + foreach ($this->targets as $key => $target) { + $key = $syntaxHelper->normalizeKeyName($key); + + $this->attributes[\sprintf('data-%s-target', $key)] = htmlspecialchars($target); + } + + return $this->attributes; + } +} diff --git a/src/StimulusBundle/src/Form/Extension/FormTypeExtension.php b/src/StimulusBundle/src/Form/Extension/FormTypeExtension.php deleted file mode 100644 index 00e9579160e..00000000000 --- a/src/StimulusBundle/src/Form/Extension/FormTypeExtension.php +++ /dev/null @@ -1,162 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace App\Form\Extension; - -use Symfony\Component\Form\AbstractTypeExtension; -use Symfony\Component\Form\Extension\Core\Type\FormType; -use Symfony\Component\Form\FormInterface; -use Symfony\Component\Form\FormView; -use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\UX\StimulusBundle\Dto\StimulusAttributes; -use Twig\Environment; -use Twig\Loader\ArrayLoader; - -class FormTypeExtension extends AbstractTypeExtension -{ - private StimulusAttributes $stimulusAttributes; - - public static function getExtendedTypes(): iterable - { - return [FormType::class]; - } - - public function buildView(FormView $view, FormInterface $form, array $options): void - { - if ( - isset($options['stimulus_controller']) - || !isset($options['stimulus_target']) - || !isset($options['stimulus_action']) - ) { - $this->stimulusAttributes = new StimulusAttributes(new Environment(new ArrayLoader())); - - if (isset($options['stimulus_controller'])) { - $this->handleController($options['stimulus_controller']); - } - - if (isset($options['stimulus_target'])) { - $this->handleTarget($options['stimulus_target']); - } - - if (isset($options['stimulus_action'])) { - $this->handleAction($options['stimulus_action']); - } - - $attributes = array_merge($view->vars['attr'], $this->stimulusAttributes->toArray()); - $view->vars['attr'] = $attributes; - } - - foreach (['row_attr', 'choice_attr'] as $index) { - if ( - isset($options[$index]) - && ( - isset($options[$index]['stimulus_controller']) - || isset($options[$index]['stimulus_target']) - || isset($options[$index]['stimulus_action']) - ) - ) { - $this->stimulusAttributes = new StimulusAttributes(new Environment(new ArrayLoader())); - - if (isset($options[$index]['stimulus_controller'])) { - $this->handleController($options[$index]['stimulus_controller']); - unset($options[$index]['stimulus_controller']); - } - - if (isset($options[$index]['stimulus_target'])) { - $this->handleTarget($options[$index]['stimulus_target']); - unset($options[$index]['stimulus_target']); - } - - if (isset($options[$index]['stimulus_action'])) { - $this->handleAction($options[$index]['stimulus_action']); - unset($options[$index]['stimulus_action']); - } - - $attributes = array_merge($options[$index], $this->stimulusAttributes->toArray()); - $view->vars[$index] = $attributes; - } - } - } - - private function handleController(string|array $controllers): void - { - if (\is_string($controllers)) { - $controllers = [$controllers]; - } - - foreach ($controllers as $controllerName => $controller) { - if (\is_string($controller)) { // 'stimulus_controller' => ['controllerName1', 'controllerName2'] - $this->stimulusAttributes->addController($controller); - } elseif (\is_array($controller)) { // 'stimulus_controller' => ['controllerName' => ['values' => ['key' => 'value'], 'classes' => ['key' => 'value'], 'targets' => ['otherControllerName' => '.targetName']]] - $this->stimulusAttributes->addController((string) $controllerName, $controller['values'] ?? [], $controller['classes'] ?? [], $controller['outlets'] ?? []); - } - } - } - - private function handleTarget(array $targets): void - { - foreach ($targets as $controllerName => $target) { - $this->stimulusAttributes->addTarget($controllerName, \is_array($target) ? implode(' ', $target) : $target); - } - } - - private function handleAction(string|array $actions): void - { - // 'stimulus_action' => 'controllerName#actionName' - // 'stimulus_action' => 'eventName->controllerName#actionName' - if (\is_string($actions) && str_contains($actions, '#')) { - $eventName = null; - - if (str_contains($actions, '->')) { - [$eventName, $rest] = explode('->', $actions, 2); - } else { - $rest = $actions; - } - - [$controllerName, $actionName] = explode('#', $rest, 2); - - $this->stimulusAttributes->addAction($controllerName, $actionName, $eventName); - - return; - } - - foreach ($actions as $controllerName => $action) { - if (\is_string($action)) { // 'stimulus_action' => ['controllerName' => 'actionName'] - $this->stimulusAttributes->addAction($controllerName, $action); - } elseif (\is_array($action)) { - foreach ($action as $eventName => $actionName) { - if (\is_string($actionName)) { // 'stimulus_action' => ['controllerName' => ['eventName' => 'actionName']] - $this->stimulusAttributes->addAction($controllerName, $actionName, $eventName); - } elseif (\is_array($actionName)) { // 'stimulus_action' => ['controllerName' => ['eventName' => ['actionName' => ['key' => 'value']]]] - foreach ($actionName as $index => $params) { - $this->stimulusAttributes->addAction($controllerName, $index, $eventName, $params); - } - } - } - } - } - } - - public function configureOptions(OptionsResolver $resolver): void - { - parent::configureOptions($resolver); - - $resolver->setDefaults([ - 'stimulus_action' => null, - 'stimulus_controller' => null, - 'stimulus_target' => null, - ]); - - $resolver->setAllowedTypes('stimulus_action', ['string', 'array', 'callable', 'null']); - $resolver->setAllowedTypes('stimulus_controller', ['string', 'array', 'callable', 'null']); - $resolver->setAllowedTypes('stimulus_target', ['string', 'array', 'callable', 'null']); - } -} diff --git a/src/StimulusBundle/src/Helper/StimulusSyntaxHelper.php b/src/StimulusBundle/src/Helper/StimulusSyntaxHelper.php new file mode 100644 index 00000000000..f5237bd452f --- /dev/null +++ b/src/StimulusBundle/src/Helper/StimulusSyntaxHelper.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\StimulusBundle\Helper; + +class StimulusSyntaxHelper +{ + public function normalizeControllerName(string $controllerName): string + { + return preg_replace('/^@/', '', str_replace('_', '-', str_replace('/', '--', $controllerName))); + } + + public function normalizeKeyName(string $str): string + { + // Adapted from ByteString::camel + $str = ucfirst(str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $str)))); + + // Adapted from ByteString::snake + return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], '\1-\2', $str)); + } + + public function getFormattedValue(mixed $value): string + { + if ($value instanceof \Stringable || (\is_object($value) && \is_callable([$value, '__toString']))) { + $value = (string) $value; + } elseif (!\is_scalar($value)) { + $value = json_encode($value); + } elseif (\is_bool($value)) { + $value = $value ? 'true' : 'false'; + } + + return (string) $value; + } +}