From 33ef6ae4eec662743c824aedea7c2e8a145c626c Mon Sep 17 00:00:00 2001 From: Matheo Daninos Date: Mon, 15 May 2023 11:01:47 +0200 Subject: [PATCH 01/13] [TwigComponent] new twig_component tag and slot --- src/TwigComponent/src/ComponentFactory.php | 10 + .../TwigComponentExtension.php | 5 + src/TwigComponent/src/Twig/AttributeBag.php | 157 +++++++++++++++ .../src/Twig/ComponentExtension.php | 5 +- src/TwigComponent/src/Twig/ComponentSlot.php | 53 ++++++ src/TwigComponent/src/Twig/SlotNode.php | 54 ++++++ .../src/Twig/SlotTokenParser.php | 88 +++++++++ .../src/Twig/TwigComponentNode.php | 179 ++++++++++++++++++ .../src/Twig/TwigComponentTokenParser.php | 96 ++++++++++ 9 files changed, 646 insertions(+), 1 deletion(-) create mode 100644 src/TwigComponent/src/Twig/AttributeBag.php create mode 100644 src/TwigComponent/src/Twig/ComponentSlot.php create mode 100644 src/TwigComponent/src/Twig/SlotNode.php create mode 100644 src/TwigComponent/src/Twig/SlotTokenParser.php create mode 100644 src/TwigComponent/src/Twig/TwigComponentNode.php create mode 100644 src/TwigComponent/src/Twig/TwigComponentTokenParser.php diff --git a/src/TwigComponent/src/ComponentFactory.php b/src/TwigComponent/src/ComponentFactory.php index f45ebc3f5a7..3cf10e6e040 100644 --- a/src/TwigComponent/src/ComponentFactory.php +++ b/src/TwigComponent/src/ComponentFactory.php @@ -36,6 +36,7 @@ public function __construct( ) { } + // should be deprecated, and use metadataForTwigComponent instead public function metadataFor(string $name): ComponentMetadata { if (!$config = $this->config[$name] ?? null) { @@ -45,6 +46,15 @@ public function metadataFor(string $name): ComponentMetadata return new ComponentMetadata($config); } + public function metadataForTwigComponent(string $name): ?ComponentMetadata + { + if (!$config = $this->config[$name] ?? null) { + return null; + } + + return new ComponentMetadata($config); + } + /** * Creates the component and "mounts" it with the passed data. */ diff --git a/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php b/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php index 6a3820c5752..7d29187545f 100644 --- a/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php +++ b/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php @@ -11,6 +11,7 @@ namespace Symfony\UX\TwigComponent\DependencyInjection; +use Psr\Container\ContainerInterface; use Symfony\Component\DependencyInjection\Argument\AbstractArgument; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -74,6 +75,10 @@ class_exists(AbstractArgument::class) ? new AbstractArgument(sprintf('Added in % ->addTag('twig.extension') ->addTag('container.service_subscriber', ['key' => ComponentRenderer::class, 'id' => 'ux.twig_component.component_renderer']) ->addTag('container.service_subscriber', ['key' => ComponentFactory::class, 'id' => 'ux.twig_component.component_factory']) + ->setArguments([ + new Reference(ContainerInterface::class), + new Reference('twig') + ]) ; $container->register('ux.twig_component.twig.lexer', ComponentLexer::class); diff --git a/src/TwigComponent/src/Twig/AttributeBag.php b/src/TwigComponent/src/Twig/AttributeBag.php new file mode 100644 index 00000000000..48dc25c81d0 --- /dev/null +++ b/src/TwigComponent/src/Twig/AttributeBag.php @@ -0,0 +1,157 @@ +attributes = $attributes; + + if (array_key_exists('attributes', $this->attributes) && $this->attributes['attributes'] instanceof ComponentAttributeBag) { + $parentAttributes = $this->attributes['attributes']; + unset($this->attributes['attributes']); + $this->attributes = $this->merge($parentAttributes->getAttributes())->getAttributes(); + } + } + + public function first($default = null): mixed + { + return $this->getIterator()->current() ?? $default; + } + + public function get($key, $default = ''): mixed + { + return $this->attributes[$key] ?? $default; + } + + public function has($key): bool + { + return array_key_exists($key, $this->attributes); + } + + public function only($keys): self + { + if (is_null($keys)) { + $values = $this->attributes; + } else { + $keys = is_array($keys) ? $keys : [$keys]; + + $values = array_filter( + $this->attributes, + function ($key) use ($keys) { + return in_array($key, $keys); + }, + ARRAY_FILTER_USE_KEY + ); + } + + return new static($values); + } + + public function except($keys): self + { + if (is_null($keys)) { + $values = $this->attributes; + } else { + $keys = is_array($keys) ? $keys : [$keys]; + + $values = array_filter( + $this->attributes, + function ($key) use ($keys) { + return ! in_array($key, $keys); + }, + ARRAY_FILTER_USE_KEY + ); + } + + return new static($values); + } + + public function merge(array $attributeDefaults = []): self + { + $attributes = $this->getAttributes(); + + foreach ($attributeDefaults as $key => $value) { + if (! array_key_exists($key, $attributes)) { + $attributes[$key] = ''; + } + } + + foreach ($attributes as $key => $value) { + $attributes[$key] = trim($value . ' ' . ($attributeDefaults[$key] ?? '')); + } + + return new static($attributes); + } + + public function class($defaultClass = ''): self + { + return $this->merge(['class' => $defaultClass]); + } + + public function getAttributes(): mixed + { + return $this->attributes; + } + + public function setAttributes(array $attributes): void + { + if (isset($attributes['attributes']) && + $attributes['attributes'] instanceof self) { + $parentBag = $attributes['attributes']; + + unset($attributes['attributes']); + + $attributes = $parentBag->merge($attributes, $escape = false)->getAttributes(); + } + + $this->attributes = $attributes; + } + + public function offsetExists($offset): bool + { + return isset($this->attributes[$offset]); + } + + public function offsetGet($offset): mixed + { + return $this->get($offset); + } + + public function offsetSet($offset, $value): void + { + $this->attributes[$offset] = $value; + } + + public function offsetUnset($offset): void + { + unset($this->attributes[$offset]); + } + + public function getIterator(): \ArrayIterator + { + return new \ArrayIterator($this->attributes); + } + + public function __toString(): string + { + $string = ''; + + foreach ($this->attributes as $key => $value) { + if ($value === false || is_null($value)) { + continue; + } + + if ($value === true) { + $value = $key; + } + + $string .= ' ' . $key . '="' . str_replace('"', '\\"', trim($value)) . '"'; + } + + return trim($string); + } +} \ No newline at end of file diff --git a/src/TwigComponent/src/Twig/ComponentExtension.php b/src/TwigComponent/src/Twig/ComponentExtension.php index a9afea75102..a763552760d 100644 --- a/src/TwigComponent/src/Twig/ComponentExtension.php +++ b/src/TwigComponent/src/Twig/ComponentExtension.php @@ -15,6 +15,7 @@ use Symfony\Contracts\Service\ServiceSubscriberInterface; use Symfony\UX\TwigComponent\ComponentFactory; use Symfony\UX\TwigComponent\ComponentRenderer; +use Twig\Environment; use Twig\Error\RuntimeError; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; @@ -26,7 +27,7 @@ */ final class ComponentExtension extends AbstractExtension implements ServiceSubscriberInterface { - public function __construct(private ContainerInterface $container) + public function __construct(private ContainerInterface $container, private Environment $environment) { } @@ -49,6 +50,8 @@ public function getTokenParsers(): array { return [ new ComponentTokenParser(fn () => $this->container->get(ComponentFactory::class)), + new TwigComponentTokenParser(fn () => $this->container->get(ComponentFactory::class), $this->environment), + new SlotTokenParser() ]; } diff --git a/src/TwigComponent/src/Twig/ComponentSlot.php b/src/TwigComponent/src/Twig/ComponentSlot.php new file mode 100644 index 00000000000..a1a96868d42 --- /dev/null +++ b/src/TwigComponent/src/Twig/ComponentSlot.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\TwigComponent\Twig; + +/** + * @author Mathéo Daninos + * + * @internal + */ +class ComponentSlot +{ + public AttributeBag $attributes; + + protected string $contents; + + public function __construct(string $contents = '', array $attributes = []) + { + $this->contents = $contents; + + $this->withAttributes($attributes); + } + + public function withAttributes(array $attributes): self + { + $this->attributes = new AttributeBag($attributes); + + return $this; + } + + public function toHtml(): string + { + return $this->contents; + } + + public function isEmpty(): bool + { + return $this->contents === ''; + } + + public function __toString() + { + return $this->toHtml(); + } +} \ No newline at end of file diff --git a/src/TwigComponent/src/Twig/SlotNode.php b/src/TwigComponent/src/Twig/SlotNode.php new file mode 100644 index 00000000000..234c77eeac1 --- /dev/null +++ b/src/TwigComponent/src/Twig/SlotNode.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\TwigComponent\Twig; + +use Twig\Compiler; +use Twig\Node\Expression\AbstractExpression; +use Twig\Node\Node; +use Twig\Node\NodeOutputInterface; + +/** + * @author Mathéo Daninos + * + * @internal + */ +final class SlotNode extends Node implements NodeOutputInterface +{ + public function __construct($name, $body, ?AbstractExpression $variables, int $lineno = 0) + { + parent::__construct(['body' => $body], ['name' => $name], $lineno, null); + + if ($variables) { + $this->setNode('variables', $variables); + } + } + + public function compile(Compiler $compiler): void + { + $name = $this->getAttribute('name'); + + $compiler + ->write('ob_start();') + ->subcompile($this->getNode('body')) + ->write('$body = ob_get_clean();' . PHP_EOL) + ->write("\$slots['$name'] = new " . ComponentSlot::class . "(\$body, "); + ; + + if ($this->hasNode('variables')) { + $compiler->subcompile($this->getNode('variables')); + } else { + $compiler->raw('[]'); + } + + $compiler->write(");"); + } +} \ No newline at end of file diff --git a/src/TwigComponent/src/Twig/SlotTokenParser.php b/src/TwigComponent/src/Twig/SlotTokenParser.php new file mode 100644 index 00000000000..b4ed3a9e6e7 --- /dev/null +++ b/src/TwigComponent/src/Twig/SlotTokenParser.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\TwigComponent\Twig; + +use Twig\Node\Expression\AbstractExpression; +use Twig\Node\Expression\ConstantExpression; +use Twig\Node\Expression\NameExpression; +use Twig\Token; +use Twig\TokenParser\AbstractTokenParser; + +/** + * @author Mathéo Daninos + * + * @internal + */ +class SlotTokenParser extends AbstractTokenParser +{ + public function parse(Token $token) + { + $parent = $this->parser->getExpressionParser()->parseExpression(); + + $name = $this->slotName($parent); + $variables = $this->parseArguments(); + + $slot = $this->parser->subparse([$this, 'decideBlockEnd'], true); + + $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); + + return new SlotNode($name, $slot, $variables, $token->getLine()); + } + + protected function parseArguments(): ?AbstractExpression + { + $stream = $this->parser->getStream(); + + $variables = null; + if ($stream->nextIf(/* Token::NAME_TYPE */5, 'with')) { + $variables = $this->parser->getExpressionParser()->parseExpression(); + } + + $stream->expect(/* Token::BLOCK_END_TYPE */3); + + return $variables; + } + + public function parseSlotName(): string + { + $stream = $this->parser->getStream(); + + if ($this->parser->getCurrentToken()->getType() != /** Token::NAME_TYPE */ 5) { + throw new \Exception('First token must be a name type'); + } + + return $stream->next()->getValue(); + } + + public function decideBlockEnd(Token $token): bool + { + return $token->test('endslot'); + } + + public function getTag(): string + { + return 'slot'; + } + + private function slotName(AbstractExpression $expression): string + { + if ($expression instanceof ConstantExpression) { // using {% component 'name' %} + return $expression->getAttribute('value'); + } + + if ($expression instanceof NameExpression) { // using {% component name %} + return $expression->getAttribute('name'); + } + + throw new \LogicException('Could not parse twig component name.'); + } +} \ No newline at end of file diff --git a/src/TwigComponent/src/Twig/TwigComponentNode.php b/src/TwigComponent/src/Twig/TwigComponentNode.php new file mode 100644 index 00000000000..19fdde26527 --- /dev/null +++ b/src/TwigComponent/src/Twig/TwigComponentNode.php @@ -0,0 +1,179 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\TwigComponent\Twig; + +use Symfony\UX\TwigComponent\ComponentFactory; +use Symfony\UX\TwigComponent\ComponentMetadata; +use Twig\Compiler; +use Twig\Environment; +use Twig\Node\Expression\AbstractExpression; +use Twig\Node\Expression\ConstantExpression; +use Twig\Node\IncludeNode; +use Twig\Node\Node; + +/** + * @author Mathéo Daninos + * + * @internal + */ +class TwigComponentNode extends IncludeNode +{ + private Environment $environment; + + /** + * @param callable():ComponentFactory $factory + */ + public function __construct(string $componentName, Node $slot, ?AbstractExpression $variables, int $lineno, callable $factory, Environment $environment) + { + parent::__construct(new ConstantExpression('not_used', $lineno), $variables, false, false, $lineno, null); + $this->setAttribute('componentName', $componentName); + $this->setAttribute('componentMetadata', $factory()->metadataFor($componentName)); + $this->setNode('slot', $slot); + $this->environment = $environment; + } + public function compile(Compiler $compiler): void + { + $compiler->addDebugInfo($this); + + $template = $compiler->getVarName(); + + $compiler->write(sprintf("$%s = ", $template)); + + $this->addGetTemplate($compiler); + + $compiler + ->write(sprintf("if ($%s) {\n", $template)) + ->write('$slotsStack = $slotsStack ?? [];' . PHP_EOL) + ->write('$slotsStack[] = $slots ?? [];' . PHP_EOL) + ->write('$slots = [];' . PHP_EOL) + ; + + if ($this->getAttribute('componentMetadata') instanceof ComponentMetadata) { + $this->addComponentProps($compiler); + } + + $compiler + ->write("ob_start();" . PHP_EOL) + ->subcompile($this->getNode('slot')) + ->write('$slot = ob_get_clean();' . PHP_EOL) + ->write(sprintf('$%s->display(', $template)); + + $this->addTemplateArguments($compiler); + + $compiler + ->raw(");\n") + ->write('$slots = array_pop($slotsStack);' . PHP_EOL) + ->write("}\n") + ; + } + + protected function addGetTemplate(Compiler $compiler) + { + $compiler + ->raw('$this->loadTemplate(' . PHP_EOL) + ->indent(1) + ->write('') + ->repr($this->getTemplatePath()) + ->raw(', ' . PHP_EOL) + ->write('') + ->repr($this->getTemplatePath()) + ->raw(', ' . PHP_EOL) + ->write('') + ->repr($this->getTemplateLine()) + ->indent(-1) + ->raw(PHP_EOL . ');' . PHP_EOL . PHP_EOL); + } + + protected function addTemplateArguments(Compiler $compiler) + { + $compiler + ->indent(1) + ->write("\n") + ->write("array_merge(\n") + ->write('$slots,' . PHP_EOL) + ; + + if ($this->getAttribute('componentMetadata') instanceof ComponentMetadata) { + $compiler->write('$props,' . PHP_EOL); + } + + $compiler + ->write('$context,[') + ->write("'slot' => new " . ComponentSlot::class . " (\$slot),\n") + ->write("'attributes' => new " . AttributeBag::class . "("); + + if ($this->hasNode('variables')) { + $compiler->subcompile($this->getNode('variables')); + } else { + $compiler->raw('[]'); + } + + $compiler->write(")\n") + ->indent(-1) + ->write("],"); + + if ($this->hasNode('variables')) { + $compiler->subcompile($this->getNode('variables')); + } else { + $compiler->raw('[]'); + } + + $compiler->write(")\n"); + } + + private function getTemplatePath(): string + { + $name = $this->getAttribute('componentName'); + + $loader = $this->environment->getLoader(); + $componentPath = rtrim(str_replace('.', '/', $name)); + + /** @var ComponentMetadata $componentMetadata */ + if (($componentMetadata = $this->getAttribute('componentMetadata')) !== null) { + return $componentMetadata->getTemplate(); + } + + if ($loader->exists($componentPath)) { + return $componentPath; + } + + if ($loader->exists($componentPath . '.html.twig')) { + return $componentPath . '.html.twig'; + } + + if ($loader->exists('components/' . $componentPath)) { + return 'components/' . $componentPath; + } + + if ($loader->exists('/components/' . $componentPath . '.html.twig')) { + return '/components/' . $componentPath . '.html.twig'; + } + + throw new \LogicException("No template found for: {$name}"); + } + + private function addComponentProps(Compiler $compiler) + { + $compiler + ->raw('$props = $this->extensions[') + ->string(ComponentExtension::class) + ->raw(']->embeddedContext(') + ->string($this->getAttribute('componentName')) + ->raw(', ') + ->raw('twig_to_array(') + ->subcompile($this->getNode('variables')) + ->raw('), ') + ->raw('$context') + ->raw(");\n") + ; + } +} \ No newline at end of file diff --git a/src/TwigComponent/src/Twig/TwigComponentTokenParser.php b/src/TwigComponent/src/Twig/TwigComponentTokenParser.php new file mode 100644 index 00000000000..e83b67e1e80 --- /dev/null +++ b/src/TwigComponent/src/Twig/TwigComponentTokenParser.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\TwigComponent\Twig; + +use Symfony\UX\TwigComponent\ComponentFactory; +use Twig\Environment; +use Twig\Node\Expression\AbstractExpression; +use Twig\Node\Expression\ConstantExpression; +use Twig\Node\Expression\NameExpression; +use Twig\Node\Node; +use Twig\Token; +use Twig\TokenParser\AbstractTokenParser; + +/** + * @author Mathéo Daninos + * + * @internal + */ +final class TwigComponentTokenParser extends AbstractTokenParser +{ + /** @var ComponentFactory|callable():ComponentFactory */ + private $factory; + + private Environment $environment; + public function __construct( + $factory, + Environment $environment + ) { + $this->factory = $factory; + $this->environment = $environment; + } + + public function parse(Token $token): Node + { + $parent = $this->parser->getExpressionParser()->parseExpression(); + $name = $this->componentName($parent); + [$variables, $only] = $this->parseArguments(); + $slot = $this->parser->subparse([$this, 'decideBlockEnd'], true); + $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); + + return new TwigComponentNode($name, $slot, $variables, $token->getLine(), $this->factory, $this->environment); + } + + public function getTag(): string + { + return 'twig_component'; + } + + public function decideBlockEnd(Token $token): bool + { + return $token->test('end_twig_component'); + } + + private function componentName(AbstractExpression $expression): string + { + if ($expression instanceof ConstantExpression) { // using {% component 'name' %} + return $expression->getAttribute('value'); + } + + if ($expression instanceof NameExpression) { // using {% component name %} + return $expression->getAttribute('name'); + } + + throw new \LogicException('Could not parse twig component name.'); + } + + private function parseArguments(): array + { + $stream = $this->parser->getStream(); + + $variables = null; + + if ($stream->nextIf(Token::NAME_TYPE, 'with')) { + $variables = $this->parser->getExpressionParser()->parseExpression(); + } + + $only = false; + + if ($stream->nextIf(Token::NAME_TYPE, 'only')) { + $only = true; + } + + $stream->expect(Token::BLOCK_END_TYPE); + + return [$variables, $only]; + } +} \ No newline at end of file From bfa351da13f2bc64a06ab9d999bddddbeded4789 Mon Sep 17 00:00:00 2001 From: Matheo Daninos Date: Mon, 15 May 2023 11:47:23 +0200 Subject: [PATCH 02/13] apply fixer --- .../TwigComponentExtension.php | 2 +- src/TwigComponent/src/Twig/AttributeBag.php | 32 ++++++------- .../src/Twig/ComponentExtension.php | 2 +- src/TwigComponent/src/Twig/ComponentSlot.php | 4 +- src/TwigComponent/src/Twig/SlotNode.php | 9 ++-- .../src/Twig/SlotTokenParser.php | 8 ++-- .../src/Twig/TwigComponentNode.php | 47 ++++++++++--------- .../src/Twig/TwigComponentTokenParser.php | 3 +- 8 files changed, 54 insertions(+), 53 deletions(-) diff --git a/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php b/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php index 7d29187545f..6ed3a48267d 100644 --- a/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php +++ b/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php @@ -77,7 +77,7 @@ class_exists(AbstractArgument::class) ? new AbstractArgument(sprintf('Added in % ->addTag('container.service_subscriber', ['key' => ComponentFactory::class, 'id' => 'ux.twig_component.component_factory']) ->setArguments([ new Reference(ContainerInterface::class), - new Reference('twig') + new Reference('twig'), ]) ; diff --git a/src/TwigComponent/src/Twig/AttributeBag.php b/src/TwigComponent/src/Twig/AttributeBag.php index 48dc25c81d0..aa54bda4ec4 100644 --- a/src/TwigComponent/src/Twig/AttributeBag.php +++ b/src/TwigComponent/src/Twig/AttributeBag.php @@ -10,7 +10,7 @@ public function __construct(array $attributes = []) { $this->attributes = $attributes; - if (array_key_exists('attributes', $this->attributes) && $this->attributes['attributes'] instanceof ComponentAttributeBag) { + if (\array_key_exists('attributes', $this->attributes) && $this->attributes['attributes'] instanceof ComponentAttributeBag) { $parentAttributes = $this->attributes['attributes']; unset($this->attributes['attributes']); $this->attributes = $this->merge($parentAttributes->getAttributes())->getAttributes(); @@ -29,22 +29,22 @@ public function get($key, $default = ''): mixed public function has($key): bool { - return array_key_exists($key, $this->attributes); + return \array_key_exists($key, $this->attributes); } public function only($keys): self { - if (is_null($keys)) { + if (null === $keys) { $values = $this->attributes; } else { - $keys = is_array($keys) ? $keys : [$keys]; + $keys = \is_array($keys) ? $keys : [$keys]; $values = array_filter( $this->attributes, function ($key) use ($keys) { - return in_array($key, $keys); + return \in_array($key, $keys); }, - ARRAY_FILTER_USE_KEY + \ARRAY_FILTER_USE_KEY ); } @@ -53,17 +53,17 @@ function ($key) use ($keys) { public function except($keys): self { - if (is_null($keys)) { + if (null === $keys) { $values = $this->attributes; } else { - $keys = is_array($keys) ? $keys : [$keys]; + $keys = \is_array($keys) ? $keys : [$keys]; $values = array_filter( $this->attributes, function ($key) use ($keys) { - return ! in_array($key, $keys); + return !\in_array($key, $keys); }, - ARRAY_FILTER_USE_KEY + \ARRAY_FILTER_USE_KEY ); } @@ -75,13 +75,13 @@ public function merge(array $attributeDefaults = []): self $attributes = $this->getAttributes(); foreach ($attributeDefaults as $key => $value) { - if (! array_key_exists($key, $attributes)) { + if (!\array_key_exists($key, $attributes)) { $attributes[$key] = ''; } } foreach ($attributes as $key => $value) { - $attributes[$key] = trim($value . ' ' . ($attributeDefaults[$key] ?? '')); + $attributes[$key] = trim($value.' '.($attributeDefaults[$key] ?? '')); } return new static($attributes); @@ -141,17 +141,17 @@ public function __toString(): string $string = ''; foreach ($this->attributes as $key => $value) { - if ($value === false || is_null($value)) { + if (false === $value || null === $value) { continue; } - if ($value === true) { + if (true === $value) { $value = $key; } - $string .= ' ' . $key . '="' . str_replace('"', '\\"', trim($value)) . '"'; + $string .= ' '.$key.'="'.str_replace('"', '\\"', trim($value)).'"'; } return trim($string); } -} \ No newline at end of file +} diff --git a/src/TwigComponent/src/Twig/ComponentExtension.php b/src/TwigComponent/src/Twig/ComponentExtension.php index a763552760d..63bc65bff82 100644 --- a/src/TwigComponent/src/Twig/ComponentExtension.php +++ b/src/TwigComponent/src/Twig/ComponentExtension.php @@ -51,7 +51,7 @@ public function getTokenParsers(): array return [ new ComponentTokenParser(fn () => $this->container->get(ComponentFactory::class)), new TwigComponentTokenParser(fn () => $this->container->get(ComponentFactory::class), $this->environment), - new SlotTokenParser() + new SlotTokenParser(), ]; } diff --git a/src/TwigComponent/src/Twig/ComponentSlot.php b/src/TwigComponent/src/Twig/ComponentSlot.php index a1a96868d42..28e8e6c68ef 100644 --- a/src/TwigComponent/src/Twig/ComponentSlot.php +++ b/src/TwigComponent/src/Twig/ComponentSlot.php @@ -43,11 +43,11 @@ public function toHtml(): string public function isEmpty(): bool { - return $this->contents === ''; + return '' === $this->contents; } public function __toString() { return $this->toHtml(); } -} \ No newline at end of file +} diff --git a/src/TwigComponent/src/Twig/SlotNode.php b/src/TwigComponent/src/Twig/SlotNode.php index 234c77eeac1..de0d5769337 100644 --- a/src/TwigComponent/src/Twig/SlotNode.php +++ b/src/TwigComponent/src/Twig/SlotNode.php @@ -39,9 +39,8 @@ public function compile(Compiler $compiler): void $compiler ->write('ob_start();') ->subcompile($this->getNode('body')) - ->write('$body = ob_get_clean();' . PHP_EOL) - ->write("\$slots['$name'] = new " . ComponentSlot::class . "(\$body, "); - ; + ->write('$body = ob_get_clean();'.\PHP_EOL) + ->write("\$slots['$name'] = new ".ComponentSlot::class.'($body, '); if ($this->hasNode('variables')) { $compiler->subcompile($this->getNode('variables')); @@ -49,6 +48,6 @@ public function compile(Compiler $compiler): void $compiler->raw('[]'); } - $compiler->write(");"); + $compiler->write(');'); } -} \ No newline at end of file +} diff --git a/src/TwigComponent/src/Twig/SlotTokenParser.php b/src/TwigComponent/src/Twig/SlotTokenParser.php index b4ed3a9e6e7..a744536994f 100644 --- a/src/TwigComponent/src/Twig/SlotTokenParser.php +++ b/src/TwigComponent/src/Twig/SlotTokenParser.php @@ -43,11 +43,11 @@ protected function parseArguments(): ?AbstractExpression $stream = $this->parser->getStream(); $variables = null; - if ($stream->nextIf(/* Token::NAME_TYPE */5, 'with')) { + if ($stream->nextIf(/* Token::NAME_TYPE */ 5, 'with')) { $variables = $this->parser->getExpressionParser()->parseExpression(); } - $stream->expect(/* Token::BLOCK_END_TYPE */3); + $stream->expect(/* Token::BLOCK_END_TYPE */ 3); return $variables; } @@ -56,7 +56,7 @@ public function parseSlotName(): string { $stream = $this->parser->getStream(); - if ($this->parser->getCurrentToken()->getType() != /** Token::NAME_TYPE */ 5) { + if (5 != /* Token::NAME_TYPE */ $this->parser->getCurrentToken()->getType()) { throw new \Exception('First token must be a name type'); } @@ -85,4 +85,4 @@ private function slotName(AbstractExpression $expression): string throw new \LogicException('Could not parse twig component name.'); } -} \ No newline at end of file +} diff --git a/src/TwigComponent/src/Twig/TwigComponentNode.php b/src/TwigComponent/src/Twig/TwigComponentNode.php index 19fdde26527..16d5ffd5069 100644 --- a/src/TwigComponent/src/Twig/TwigComponentNode.php +++ b/src/TwigComponent/src/Twig/TwigComponentNode.php @@ -40,21 +40,22 @@ public function __construct(string $componentName, Node $slot, ?AbstractExpressi $this->setNode('slot', $slot); $this->environment = $environment; } + public function compile(Compiler $compiler): void { $compiler->addDebugInfo($this); $template = $compiler->getVarName(); - $compiler->write(sprintf("$%s = ", $template)); + $compiler->write(sprintf('$%s = ', $template)); $this->addGetTemplate($compiler); $compiler ->write(sprintf("if ($%s) {\n", $template)) - ->write('$slotsStack = $slotsStack ?? [];' . PHP_EOL) - ->write('$slotsStack[] = $slots ?? [];' . PHP_EOL) - ->write('$slots = [];' . PHP_EOL) + ->write('$slotsStack = $slotsStack ?? [];'.\PHP_EOL) + ->write('$slotsStack[] = $slots ?? [];'.\PHP_EOL) + ->write('$slots = [];'.\PHP_EOL) ; if ($this->getAttribute('componentMetadata') instanceof ComponentMetadata) { @@ -62,16 +63,16 @@ public function compile(Compiler $compiler): void } $compiler - ->write("ob_start();" . PHP_EOL) + ->write('ob_start();'.\PHP_EOL) ->subcompile($this->getNode('slot')) - ->write('$slot = ob_get_clean();' . PHP_EOL) + ->write('$slot = ob_get_clean();'.\PHP_EOL) ->write(sprintf('$%s->display(', $template)); $this->addTemplateArguments($compiler); $compiler ->raw(");\n") - ->write('$slots = array_pop($slotsStack);' . PHP_EOL) + ->write('$slots = array_pop($slotsStack);'.\PHP_EOL) ->write("}\n") ; } @@ -79,18 +80,18 @@ public function compile(Compiler $compiler): void protected function addGetTemplate(Compiler $compiler) { $compiler - ->raw('$this->loadTemplate(' . PHP_EOL) + ->raw('$this->loadTemplate('.\PHP_EOL) ->indent(1) ->write('') ->repr($this->getTemplatePath()) - ->raw(', ' . PHP_EOL) + ->raw(', '.\PHP_EOL) ->write('') ->repr($this->getTemplatePath()) - ->raw(', ' . PHP_EOL) + ->raw(', '.\PHP_EOL) ->write('') ->repr($this->getTemplateLine()) ->indent(-1) - ->raw(PHP_EOL . ');' . PHP_EOL . PHP_EOL); + ->raw(\PHP_EOL.');'.\PHP_EOL.\PHP_EOL); } protected function addTemplateArguments(Compiler $compiler) @@ -99,17 +100,17 @@ protected function addTemplateArguments(Compiler $compiler) ->indent(1) ->write("\n") ->write("array_merge(\n") - ->write('$slots,' . PHP_EOL) + ->write('$slots,'.\PHP_EOL) ; if ($this->getAttribute('componentMetadata') instanceof ComponentMetadata) { - $compiler->write('$props,' . PHP_EOL); + $compiler->write('$props,'.\PHP_EOL); } $compiler ->write('$context,[') - ->write("'slot' => new " . ComponentSlot::class . " (\$slot),\n") - ->write("'attributes' => new " . AttributeBag::class . "("); + ->write("'slot' => new ".ComponentSlot::class." (\$slot),\n") + ->write("'attributes' => new ".AttributeBag::class.'('); if ($this->hasNode('variables')) { $compiler->subcompile($this->getNode('variables')); @@ -119,7 +120,7 @@ protected function addTemplateArguments(Compiler $compiler) $compiler->write(")\n") ->indent(-1) - ->write("],"); + ->write('],'); if ($this->hasNode('variables')) { $compiler->subcompile($this->getNode('variables')); @@ -146,16 +147,16 @@ private function getTemplatePath(): string return $componentPath; } - if ($loader->exists($componentPath . '.html.twig')) { - return $componentPath . '.html.twig'; + if ($loader->exists($componentPath.'.html.twig')) { + return $componentPath.'.html.twig'; } - if ($loader->exists('components/' . $componentPath)) { - return 'components/' . $componentPath; + if ($loader->exists('components/'.$componentPath)) { + return 'components/'.$componentPath; } - if ($loader->exists('/components/' . $componentPath . '.html.twig')) { - return '/components/' . $componentPath . '.html.twig'; + if ($loader->exists('/components/'.$componentPath.'.html.twig')) { + return '/components/'.$componentPath.'.html.twig'; } throw new \LogicException("No template found for: {$name}"); @@ -176,4 +177,4 @@ private function addComponentProps(Compiler $compiler) ->raw(");\n") ; } -} \ No newline at end of file +} diff --git a/src/TwigComponent/src/Twig/TwigComponentTokenParser.php b/src/TwigComponent/src/Twig/TwigComponentTokenParser.php index e83b67e1e80..1e281454ca6 100644 --- a/src/TwigComponent/src/Twig/TwigComponentTokenParser.php +++ b/src/TwigComponent/src/Twig/TwigComponentTokenParser.php @@ -31,6 +31,7 @@ final class TwigComponentTokenParser extends AbstractTokenParser private $factory; private Environment $environment; + public function __construct( $factory, Environment $environment @@ -93,4 +94,4 @@ private function parseArguments(): array return [$variables, $only]; } -} \ No newline at end of file +} From 18a7cd7dfcd28eacc3fd0d6062a2fbe527fc1043 Mon Sep 17 00:00:00 2001 From: Matheo Daninos Date: Mon, 15 May 2023 11:51:43 +0200 Subject: [PATCH 03/13] add return type --- src/TwigComponent/src/Twig/SlotTokenParser.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/TwigComponent/src/Twig/SlotTokenParser.php b/src/TwigComponent/src/Twig/SlotTokenParser.php index a744536994f..4118df47d5c 100644 --- a/src/TwigComponent/src/Twig/SlotTokenParser.php +++ b/src/TwigComponent/src/Twig/SlotTokenParser.php @@ -14,6 +14,7 @@ use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\NameExpression; +use Twig\Node\Node; use Twig\Token; use Twig\TokenParser\AbstractTokenParser; @@ -24,7 +25,7 @@ */ class SlotTokenParser extends AbstractTokenParser { - public function parse(Token $token) + public function parse(Token $token): Node { $parent = $this->parser->getExpressionParser()->parseExpression(); From 30d48af77c10994502e5eedb612462b400f581c8 Mon Sep 17 00:00:00 2001 From: Matheo Daninos Date: Thu, 18 May 2023 15:45:00 +0200 Subject: [PATCH 04/13] add tests and some fix --- .../src/Twig/TwigComponentNode.php | 18 ++++- .../tests/Fixtures/Component/Alarm.php | 10 +++ .../templates/components/Alarm.html.twig | 6 ++ .../templates/components/Button.html.twig | 3 + .../templates/components/Sub/Hello.html.twig | 1 + .../templates/render_twig_component.html.twig | 3 + ...r_static_component_in_sub_folder.html.twig | 3 + .../slot/render_static_components.html.twig | 3 + .../slot/render_with_attributes.html.twig | 3 + .../slot/render_with_custom_slot.html.twig | 6 ++ .../slot/render_with_default_slot.html.twig | 3 + .../slot/use_attribute_variables.html.twig | 3 + .../tags/embedded_component.html.twig | 8 +- .../Integration/ComponentExtensionTest.php | 19 +++++ .../Integration/ComponentFactoryTest.php | 2 - .../tests/Integration/ComponentLexerTest.php | 48 ------------ .../TwigComponentExtensionTest.php | 77 +++++++++++++++++++ .../tests/Unit/TwigPreLexerTest.php | 12 +-- 18 files changed, 159 insertions(+), 69 deletions(-) create mode 100644 src/TwigComponent/tests/Fixtures/Component/Alarm.php create mode 100644 src/TwigComponent/tests/Fixtures/templates/components/Alarm.html.twig create mode 100644 src/TwigComponent/tests/Fixtures/templates/components/Button.html.twig create mode 100644 src/TwigComponent/tests/Fixtures/templates/components/Sub/Hello.html.twig create mode 100644 src/TwigComponent/tests/Fixtures/templates/render_twig_component.html.twig create mode 100644 src/TwigComponent/tests/Fixtures/templates/slot/render_static_component_in_sub_folder.html.twig create mode 100644 src/TwigComponent/tests/Fixtures/templates/slot/render_static_components.html.twig create mode 100644 src/TwigComponent/tests/Fixtures/templates/slot/render_with_attributes.html.twig create mode 100644 src/TwigComponent/tests/Fixtures/templates/slot/render_with_custom_slot.html.twig create mode 100644 src/TwigComponent/tests/Fixtures/templates/slot/render_with_default_slot.html.twig create mode 100644 src/TwigComponent/tests/Fixtures/templates/slot/use_attribute_variables.html.twig delete mode 100644 src/TwigComponent/tests/Integration/ComponentLexerTest.php create mode 100644 src/TwigComponent/tests/Integration/TwigComponentExtensionTest.php diff --git a/src/TwigComponent/src/Twig/TwigComponentNode.php b/src/TwigComponent/src/Twig/TwigComponentNode.php index 16d5ffd5069..c272c04c1b8 100644 --- a/src/TwigComponent/src/Twig/TwigComponentNode.php +++ b/src/TwigComponent/src/Twig/TwigComponentNode.php @@ -36,7 +36,7 @@ public function __construct(string $componentName, Node $slot, ?AbstractExpressi { parent::__construct(new ConstantExpression('not_used', $lineno), $variables, false, false, $lineno, null); $this->setAttribute('componentName', $componentName); - $this->setAttribute('componentMetadata', $factory()->metadataFor($componentName)); + $this->setAttribute('componentMetadata', $factory()->metadataForTwigComponent($componentName)); $this->setNode('slot', $slot); $this->environment = $environment; } @@ -170,9 +170,19 @@ private function addComponentProps(Compiler $compiler) ->raw(']->embeddedContext(') ->string($this->getAttribute('componentName')) ->raw(', ') - ->raw('twig_to_array(') - ->subcompile($this->getNode('variables')) - ->raw('), ') + ; + + if ($this->hasNode('variables')) { + $compiler + ->raw('twig_to_array(') + ->subcompile($this->getNode('variables')) + ->raw('), ') + ; + } else { + $compiler->raw('[], '); + } + + $compiler ->raw('$context') ->raw(");\n") ; diff --git a/src/TwigComponent/tests/Fixtures/Component/Alarm.php b/src/TwigComponent/tests/Fixtures/Component/Alarm.php new file mode 100644 index 00000000000..66589ff593c --- /dev/null +++ b/src/TwigComponent/tests/Fixtures/Component/Alarm.php @@ -0,0 +1,10 @@ + +
+ {{ slot }} +
+ {{ footer|default('')|raw }} + \ No newline at end of file diff --git a/src/TwigComponent/tests/Fixtures/templates/components/Button.html.twig b/src/TwigComponent/tests/Fixtures/templates/components/Button.html.twig new file mode 100644 index 00000000000..48358080b1c --- /dev/null +++ b/src/TwigComponent/tests/Fixtures/templates/components/Button.html.twig @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/src/TwigComponent/tests/Fixtures/templates/components/Sub/Hello.html.twig b/src/TwigComponent/tests/Fixtures/templates/components/Sub/Hello.html.twig new file mode 100644 index 00000000000..57d4ecac7a0 --- /dev/null +++ b/src/TwigComponent/tests/Fixtures/templates/components/Sub/Hello.html.twig @@ -0,0 +1 @@ +

Hello from a sub folder

\ No newline at end of file diff --git a/src/TwigComponent/tests/Fixtures/templates/render_twig_component.html.twig b/src/TwigComponent/tests/Fixtures/templates/render_twig_component.html.twig new file mode 100644 index 00000000000..ce06fe31da0 --- /dev/null +++ b/src/TwigComponent/tests/Fixtures/templates/render_twig_component.html.twig @@ -0,0 +1,3 @@ +{% twig_component name with {{ data }} %} +

foo

+{% end_twig_component %} \ No newline at end of file diff --git a/src/TwigComponent/tests/Fixtures/templates/slot/render_static_component_in_sub_folder.html.twig b/src/TwigComponent/tests/Fixtures/templates/slot/render_static_component_in_sub_folder.html.twig new file mode 100644 index 00000000000..5e28b3a9323 --- /dev/null +++ b/src/TwigComponent/tests/Fixtures/templates/slot/render_static_component_in_sub_folder.html.twig @@ -0,0 +1,3 @@ + +

Hello

+
\ No newline at end of file diff --git a/src/TwigComponent/tests/Fixtures/templates/slot/render_static_components.html.twig b/src/TwigComponent/tests/Fixtures/templates/slot/render_static_components.html.twig new file mode 100644 index 00000000000..0444c0847dc --- /dev/null +++ b/src/TwigComponent/tests/Fixtures/templates/slot/render_static_components.html.twig @@ -0,0 +1,3 @@ + + Submit! + \ No newline at end of file diff --git a/src/TwigComponent/tests/Fixtures/templates/slot/render_with_attributes.html.twig b/src/TwigComponent/tests/Fixtures/templates/slot/render_with_attributes.html.twig new file mode 100644 index 00000000000..970f5387061 --- /dev/null +++ b/src/TwigComponent/tests/Fixtures/templates/slot/render_with_attributes.html.twig @@ -0,0 +1,3 @@ + + You have a new message! + \ No newline at end of file diff --git a/src/TwigComponent/tests/Fixtures/templates/slot/render_with_custom_slot.html.twig b/src/TwigComponent/tests/Fixtures/templates/slot/render_with_custom_slot.html.twig new file mode 100644 index 00000000000..ad18e220032 --- /dev/null +++ b/src/TwigComponent/tests/Fixtures/templates/slot/render_with_custom_slot.html.twig @@ -0,0 +1,6 @@ + + You have a new message! + +

from @fapbot

+
+
\ No newline at end of file diff --git a/src/TwigComponent/tests/Fixtures/templates/slot/render_with_default_slot.html.twig b/src/TwigComponent/tests/Fixtures/templates/slot/render_with_default_slot.html.twig new file mode 100644 index 00000000000..853ac339b5d --- /dev/null +++ b/src/TwigComponent/tests/Fixtures/templates/slot/render_with_default_slot.html.twig @@ -0,0 +1,3 @@ + + You have a new message! + \ No newline at end of file diff --git a/src/TwigComponent/tests/Fixtures/templates/slot/use_attribute_variables.html.twig b/src/TwigComponent/tests/Fixtures/templates/slot/use_attribute_variables.html.twig new file mode 100644 index 00000000000..1245914c818 --- /dev/null +++ b/src/TwigComponent/tests/Fixtures/templates/slot/use_attribute_variables.html.twig @@ -0,0 +1,3 @@ + + Submit! + \ No newline at end of file diff --git a/src/TwigComponent/tests/Fixtures/templates/tags/embedded_component.html.twig b/src/TwigComponent/tests/Fixtures/templates/tags/embedded_component.html.twig index fcce917f9db..fc6cd5e1a12 100644 --- a/src/TwigComponent/tests/Fixtures/templates/tags/embedded_component.html.twig +++ b/src/TwigComponent/tests/Fixtures/templates/tags/embedded_component.html.twig @@ -1,8 +1,8 @@ - custom th ({{ parent() }}) - custom td ({{ parent() }}) + custom th + custom td - + My footer - + \ No newline at end of file diff --git a/src/TwigComponent/tests/Integration/ComponentExtensionTest.php b/src/TwigComponent/tests/Integration/ComponentExtensionTest.php index ec38581ebf7..91d3d3f7334 100644 --- a/src/TwigComponent/tests/Integration/ComponentExtensionTest.php +++ b/src/TwigComponent/tests/Integration/ComponentExtensionTest.php @@ -158,6 +158,17 @@ public function testComponentWithNamespace(): void $this->assertStringContainsString('Content...', $output); } + public function testTwigComponent(): void + { + $output = $this->renderComponent('component_a', [ + 'propA' => 'prop a value', + 'propB' => 'prop b value', + ]); + + $this->assertStringContainsString('propA: prop a value', $output); + $this->assertStringContainsString('propB: prop b value', $output); + } + private function renderComponent(string $name, array $data = []): string { return self::getContainer()->get(Environment::class)->render('render_component.html.twig', [ @@ -165,4 +176,12 @@ private function renderComponent(string $name, array $data = []): string 'data' => $data, ]); } + + private function renderTwigComponent(string $name, array $data = []): string + { + return self::getContainer()->get(Environment::class)->render('render_twig_component.html.twig', [ + 'name' => $name, + 'data' => $data, + ]); + } } diff --git a/src/TwigComponent/tests/Integration/ComponentFactoryTest.php b/src/TwigComponent/tests/Integration/ComponentFactoryTest.php index 9c6af84a320..37e8e87a22b 100644 --- a/src/TwigComponent/tests/Integration/ComponentFactoryTest.php +++ b/src/TwigComponent/tests/Integration/ComponentFactoryTest.php @@ -151,7 +151,6 @@ public function testCanGetMetadataForSameComponentWithDifferentName(): void public function testCannotGetConfigByNameForNonRegisteredComponent(): void { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Unknown component "invalid". The registered components are: component_a'); $this->factory()->metadataFor('invalid'); } @@ -159,7 +158,6 @@ public function testCannotGetConfigByNameForNonRegisteredComponent(): void public function testCannotGetInvalidComponent(): void { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Unknown component "invalid". The registered components are: component_a'); $this->factory()->get('invalid'); } diff --git a/src/TwigComponent/tests/Integration/ComponentLexerTest.php b/src/TwigComponent/tests/Integration/ComponentLexerTest.php deleted file mode 100644 index 7ed6c6c15c7..00000000000 --- a/src/TwigComponent/tests/Integration/ComponentLexerTest.php +++ /dev/null @@ -1,48 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\TwigComponent\Tests\Integration; - -use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Twig\Environment; - -/** - * @author Mathèo Daninos - * - * @internal - */ -class ComponentLexerTest extends KernelTestCase -{ - public function testComponentSyntaxOpenTags(): void - { - $output = self::getContainer()->get(Environment::class)->render('tags/open_tag.html.twig'); - - $this->assertStringContainsString('propA: 1', $output); - $this->assertStringContainsString('propB: hello', $output); - } - - public function testComponentSyntaxSelfCloseTags(): void - { - $output = self::getContainer()->get(Environment::class)->render('tags/self_close_tag.html.twig'); - - $this->assertStringContainsString('propA: 1', $output); - $this->assertStringContainsString('propB: hello', $output); - } - - public function testComponentSyntaxCanRenderEmbeddedComponent(): void - { - $output = self::getContainer()->get(Environment::class)->render('tags/embedded_component.html.twig'); - - $this->assertStringContainsString('data table', $output); - $this->assertStringContainsString('custom th (key)', $output); - $this->assertStringContainsString('custom td (1)', $output); - } -} diff --git a/src/TwigComponent/tests/Integration/TwigComponentExtensionTest.php b/src/TwigComponent/tests/Integration/TwigComponentExtensionTest.php new file mode 100644 index 00000000000..8ebd05ba708 --- /dev/null +++ b/src/TwigComponent/tests/Integration/TwigComponentExtensionTest.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\TwigComponent\Tests\Integration; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Twig\Environment; + +/** + * @author Mathèo Daninos + * + * @internal + */ +class TwigComponentExtensionTest extends KernelTestCase +{ + public function testComponentSyntaxOpenTags(): void + { + $output = self::getContainer()->get(Environment::class)->render('tags/open_tag.html.twig'); + + $this->assertStringContainsString('propA: 1', $output); + $this->assertStringContainsString('propB: hello', $output); + } + + public function testRenderTwigComponentWithSlot(): void + { + $output = self::getContainer()->get(Environment::class)->render('slot/render_with_default_slot.html.twig'); + + $this->assertStringContainsString('You have a new message!', $output); + } + + public function testRenderTwigComponentWithAttributes(): void + { + $output = self::getContainer()->get(Environment::class)->render('slot/render_with_attributes.html.twig'); + + $this->assertStringContainsString('You have a new message!', $output); + $this->assertStringContainsString('background: red', $output); + } + + public function testRenderTwigComponentWithCustomSlot(): void + { + $output = self::getContainer()->get(Environment::class)->render('slot/render_with_custom_slot.html.twig'); + + $this->assertStringContainsString('You have a new message!', $output); + $this->assertStringContainsString('background: red', $output); + $this->assertStringContainsString('from @fapbot', $output); + } + + public function testRenderStaticTwigComponent(): void + { + $output = self::getContainer()->get(Environment::class)->render('slot/render_static_components.html.twig'); + + $this->assertStringContainsString('Submit!', $output); + } + + public function testRenderStaticTwigComponentWithAttributes(): void + { + $output = self::getContainer()->get(Environment::class)->render('slot/use_attribute_variables.html.twig'); + + $this->assertStringContainsString('Submit!', $output); + $this->assertStringContainsString('class="btn-primary btn"', $output); + } + + public function testRenderStaticComponentInSubFolder(): void + { + $output = self::getContainer()->get(Environment::class)->render('slot/render_static_component_in_sub_folder.html.twig'); + + $this->assertStringContainsString('Hello from a sub folder', $output); + } +} diff --git a/src/TwigComponent/tests/Unit/TwigPreLexerTest.php b/src/TwigComponent/tests/Unit/TwigPreLexerTest.php index e8b368a6b75..3e1f118084c 100644 --- a/src/TwigComponent/tests/Unit/TwigPreLexerTest.php +++ b/src/TwigComponent/tests/Unit/TwigPreLexerTest.php @@ -62,21 +62,11 @@ public function getLexTests(): iterable 'Hello {% block foo_block %}Foo{% endblock %}{{ component(\'foo\') }}{% block bar_block %}Bar{% endblock %}', ]; - yield 'component_with_component_inside_block' => [ + yield 'component_with_embedded_component_inside_block' => [ '', '{% component \'foo\' %}{% block foo_block %}{{ component(\'bar\') }}{% endblock %}{% endcomponent %}', ]; - yield 'component_with_embedded_component_inside_block' => [ - '', - '{% component \'foo\' %}{% block foo_block %}{% component \'bar\' %}{% block content %}{{ component(\'baz\') }}{% endblock %}{% endcomponent %}{% endblock %}{% endcomponent %}', - ]; - - yield 'component_with_embedded_component' => [ - 'foo_content', - '{% component \'foo\' %}{% block content %}foo_content{% component \'bar\' %}{% block content %}{{ component(\'baz\') }}{% endblock %}{% endcomponent %}{% endblock %}{% endcomponent %}', - ]; - yield 'attribute_with_no_value' => [ '', '{{ component(\'foo\', { bar: true }) }}', From 4ebc9d8bb083e645186898e9f8c8b710879ff4a4 Mon Sep 17 00:00:00 2001 From: Matheo Daninos Date: Thu, 18 May 2023 15:55:45 +0200 Subject: [PATCH 05/13] add thanks @giorgiopogliani --- src/TwigComponent/src/Twig/AttributeBag.php | 15 +++++++++++++++ src/TwigComponent/src/Twig/ComponentSlot.php | 3 +++ src/TwigComponent/src/Twig/SlotNode.php | 3 +++ src/TwigComponent/src/Twig/SlotTokenParser.php | 3 +++ src/TwigComponent/src/Twig/TwigComponentNode.php | 3 +++ .../src/Twig/TwigComponentTokenParser.php | 3 +++ 6 files changed, 30 insertions(+) diff --git a/src/TwigComponent/src/Twig/AttributeBag.php b/src/TwigComponent/src/Twig/AttributeBag.php index aa54bda4ec4..07a41fb0d97 100644 --- a/src/TwigComponent/src/Twig/AttributeBag.php +++ b/src/TwigComponent/src/Twig/AttributeBag.php @@ -1,7 +1,22 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\UX\TwigComponent\Twig; +/** + * thanks to @giorgiopogliani! + * This file is inspired by: https://github.com/giorgiopogliani/twig-components + * + * @author Mathéo Daninos + */ class AttributeBag implements \ArrayAccess, \IteratorAggregate { protected $attributes = []; diff --git a/src/TwigComponent/src/Twig/ComponentSlot.php b/src/TwigComponent/src/Twig/ComponentSlot.php index 28e8e6c68ef..731bcb24049 100644 --- a/src/TwigComponent/src/Twig/ComponentSlot.php +++ b/src/TwigComponent/src/Twig/ComponentSlot.php @@ -12,6 +12,9 @@ namespace Symfony\UX\TwigComponent\Twig; /** + * thanks to @giorgiopogliani! + * This file is inspired by: https://github.com/giorgiopogliani/twig-components + * * @author Mathéo Daninos * * @internal diff --git a/src/TwigComponent/src/Twig/SlotNode.php b/src/TwigComponent/src/Twig/SlotNode.php index de0d5769337..575cf406692 100644 --- a/src/TwigComponent/src/Twig/SlotNode.php +++ b/src/TwigComponent/src/Twig/SlotNode.php @@ -17,6 +17,9 @@ use Twig\Node\NodeOutputInterface; /** + * thanks to @giorgiopogliani! + * This file is inspired by: https://github.com/giorgiopogliani/twig-components + * * @author Mathéo Daninos * * @internal diff --git a/src/TwigComponent/src/Twig/SlotTokenParser.php b/src/TwigComponent/src/Twig/SlotTokenParser.php index 4118df47d5c..5cb7108752b 100644 --- a/src/TwigComponent/src/Twig/SlotTokenParser.php +++ b/src/TwigComponent/src/Twig/SlotTokenParser.php @@ -19,6 +19,9 @@ use Twig\TokenParser\AbstractTokenParser; /** + * thanks to @giorgiopogliani! + * This file is inspired by: https://github.com/giorgiopogliani/twig-components + * * @author Mathéo Daninos * * @internal diff --git a/src/TwigComponent/src/Twig/TwigComponentNode.php b/src/TwigComponent/src/Twig/TwigComponentNode.php index c272c04c1b8..f66a479067e 100644 --- a/src/TwigComponent/src/Twig/TwigComponentNode.php +++ b/src/TwigComponent/src/Twig/TwigComponentNode.php @@ -21,6 +21,9 @@ use Twig\Node\Node; /** + * thanks to @giorgiopogliani! + * This file is inspired by: https://github.com/giorgiopogliani/twig-components + * * @author Mathéo Daninos * * @internal diff --git a/src/TwigComponent/src/Twig/TwigComponentTokenParser.php b/src/TwigComponent/src/Twig/TwigComponentTokenParser.php index 1e281454ca6..d6316196528 100644 --- a/src/TwigComponent/src/Twig/TwigComponentTokenParser.php +++ b/src/TwigComponent/src/Twig/TwigComponentTokenParser.php @@ -21,6 +21,9 @@ use Twig\TokenParser\AbstractTokenParser; /** + * thanks to @giorgiopogliani! + * This file is inspired by: https://github.com/giorgiopogliani/twig-components + * * @author Mathéo Daninos * * @internal From ac7589f8db534bee5f89f6e584ee0975c8f6b089 Mon Sep 17 00:00:00 2001 From: Matheo Daninos Date: Thu, 18 May 2023 17:59:38 +0200 Subject: [PATCH 06/13] fix --- src/TwigComponent/src/Twig/AttributeBag.php | 2 +- src/TwigComponent/src/Twig/ComponentSlot.php | 2 +- src/TwigComponent/src/Twig/SlotNode.php | 2 +- src/TwigComponent/src/Twig/SlotTokenParser.php | 2 +- src/TwigComponent/src/Twig/TwigComponentNode.php | 2 +- src/TwigComponent/src/Twig/TwigComponentTokenParser.php | 2 +- src/TwigComponent/tests/Fixtures/Component/Alarm.php | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/TwigComponent/src/Twig/AttributeBag.php b/src/TwigComponent/src/Twig/AttributeBag.php index 07a41fb0d97..9d9473fe05a 100644 --- a/src/TwigComponent/src/Twig/AttributeBag.php +++ b/src/TwigComponent/src/Twig/AttributeBag.php @@ -13,7 +13,7 @@ /** * thanks to @giorgiopogliani! - * This file is inspired by: https://github.com/giorgiopogliani/twig-components + * This file is inspired by: https://github.com/giorgiopogliani/twig-components. * * @author Mathéo Daninos */ diff --git a/src/TwigComponent/src/Twig/ComponentSlot.php b/src/TwigComponent/src/Twig/ComponentSlot.php index 731bcb24049..669275a82c6 100644 --- a/src/TwigComponent/src/Twig/ComponentSlot.php +++ b/src/TwigComponent/src/Twig/ComponentSlot.php @@ -13,7 +13,7 @@ /** * thanks to @giorgiopogliani! - * This file is inspired by: https://github.com/giorgiopogliani/twig-components + * This file is inspired by: https://github.com/giorgiopogliani/twig-components. * * @author Mathéo Daninos * diff --git a/src/TwigComponent/src/Twig/SlotNode.php b/src/TwigComponent/src/Twig/SlotNode.php index 575cf406692..ed448ba450a 100644 --- a/src/TwigComponent/src/Twig/SlotNode.php +++ b/src/TwigComponent/src/Twig/SlotNode.php @@ -18,7 +18,7 @@ /** * thanks to @giorgiopogliani! - * This file is inspired by: https://github.com/giorgiopogliani/twig-components + * This file is inspired by: https://github.com/giorgiopogliani/twig-components. * * @author Mathéo Daninos * diff --git a/src/TwigComponent/src/Twig/SlotTokenParser.php b/src/TwigComponent/src/Twig/SlotTokenParser.php index 5cb7108752b..d43c34f3755 100644 --- a/src/TwigComponent/src/Twig/SlotTokenParser.php +++ b/src/TwigComponent/src/Twig/SlotTokenParser.php @@ -20,7 +20,7 @@ /** * thanks to @giorgiopogliani! - * This file is inspired by: https://github.com/giorgiopogliani/twig-components + * This file is inspired by: https://github.com/giorgiopogliani/twig-components. * * @author Mathéo Daninos * diff --git a/src/TwigComponent/src/Twig/TwigComponentNode.php b/src/TwigComponent/src/Twig/TwigComponentNode.php index f66a479067e..1fe2162aa19 100644 --- a/src/TwigComponent/src/Twig/TwigComponentNode.php +++ b/src/TwigComponent/src/Twig/TwigComponentNode.php @@ -22,7 +22,7 @@ /** * thanks to @giorgiopogliani! - * This file is inspired by: https://github.com/giorgiopogliani/twig-components + * This file is inspired by: https://github.com/giorgiopogliani/twig-components. * * @author Mathéo Daninos * diff --git a/src/TwigComponent/src/Twig/TwigComponentTokenParser.php b/src/TwigComponent/src/Twig/TwigComponentTokenParser.php index d6316196528..c6dd7eba611 100644 --- a/src/TwigComponent/src/Twig/TwigComponentTokenParser.php +++ b/src/TwigComponent/src/Twig/TwigComponentTokenParser.php @@ -22,7 +22,7 @@ /** * thanks to @giorgiopogliani! - * This file is inspired by: https://github.com/giorgiopogliani/twig-components + * This file is inspired by: https://github.com/giorgiopogliani/twig-components. * * @author Mathéo Daninos * diff --git a/src/TwigComponent/tests/Fixtures/Component/Alarm.php b/src/TwigComponent/tests/Fixtures/Component/Alarm.php index 66589ff593c..20546f57fd8 100644 --- a/src/TwigComponent/tests/Fixtures/Component/Alarm.php +++ b/src/TwigComponent/tests/Fixtures/Component/Alarm.php @@ -4,7 +4,7 @@ use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; -#[AsTwigComponent('Alarm', template: 'components/AAlarm.html.twig')] +#[AsTwigComponent('Alarm', template: 'components/Alarm.html.twig')] class Alarm { } \ No newline at end of file From e57305978a4a11e425e05c56b3ef7982d177a401 Mon Sep 17 00:00:00 2001 From: matheo Date: Fri, 19 May 2023 17:46:44 +0200 Subject: [PATCH 07/13] add more test --- .../templates/components/Button.html.twig | 4 +--- .../templates/components/DangerButton.html.twig | 1 + .../slot/pass_default_slot_to_child.html.twig | 1 + .../slot/render_nested_component.html.twig | 8 ++++++++ .../Integration/TwigComponentExtensionTest.php | 15 +++++++++++++++ 5 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 src/TwigComponent/tests/Fixtures/templates/components/DangerButton.html.twig create mode 100644 src/TwigComponent/tests/Fixtures/templates/slot/pass_default_slot_to_child.html.twig create mode 100644 src/TwigComponent/tests/Fixtures/templates/slot/render_nested_component.html.twig diff --git a/src/TwigComponent/tests/Fixtures/templates/components/Button.html.twig b/src/TwigComponent/tests/Fixtures/templates/components/Button.html.twig index 48358080b1c..d17c380531b 100644 --- a/src/TwigComponent/tests/Fixtures/templates/components/Button.html.twig +++ b/src/TwigComponent/tests/Fixtures/templates/components/Button.html.twig @@ -1,3 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/TwigComponent/tests/Fixtures/templates/components/DangerButton.html.twig b/src/TwigComponent/tests/Fixtures/templates/components/DangerButton.html.twig new file mode 100644 index 00000000000..77dd9c1ee84 --- /dev/null +++ b/src/TwigComponent/tests/Fixtures/templates/components/DangerButton.html.twig @@ -0,0 +1 @@ +
{{ slot }}

Danger Zone

\ No newline at end of file diff --git a/src/TwigComponent/tests/Fixtures/templates/slot/pass_default_slot_to_child.html.twig b/src/TwigComponent/tests/Fixtures/templates/slot/pass_default_slot_to_child.html.twig new file mode 100644 index 00000000000..28d599b656b --- /dev/null +++ b/src/TwigComponent/tests/Fixtures/templates/slot/pass_default_slot_to_child.html.twig @@ -0,0 +1 @@ +Delete User \ No newline at end of file diff --git a/src/TwigComponent/tests/Fixtures/templates/slot/render_nested_component.html.twig b/src/TwigComponent/tests/Fixtures/templates/slot/render_nested_component.html.twig new file mode 100644 index 00000000000..56d3ab55d6a --- /dev/null +++ b/src/TwigComponent/tests/Fixtures/templates/slot/render_nested_component.html.twig @@ -0,0 +1,8 @@ + + You have a new message from @fabot + + + Go to the message + + + \ No newline at end of file diff --git a/src/TwigComponent/tests/Integration/TwigComponentExtensionTest.php b/src/TwigComponent/tests/Integration/TwigComponentExtensionTest.php index 8ebd05ba708..6f4f80b0062 100644 --- a/src/TwigComponent/tests/Integration/TwigComponentExtensionTest.php +++ b/src/TwigComponent/tests/Integration/TwigComponentExtensionTest.php @@ -74,4 +74,19 @@ public function testRenderStaticComponentInSubFolder(): void $this->assertStringContainsString('Hello from a sub folder', $output); } + + public function testRenderNestedComponents(): void + { + $output = self::getContainer()->get(Environment::class)->render('slot/render_nested_component.html.twig'); + + $this->assertStringContainsString('You have a new message from @fabot', $output); + $this->assertStringContainsString('Go to the message', $output); + } + + public function testPassDefaultSlotToChildComponents(): void + { + $output = self::getContainer()->get(Environment::class)->render('slot/pass_default_slot_to_child.html.twig'); + + $this->assertStringContainsString('', $output); + } } From c74271b9653d5ecd37a395541cbe19d2febab7d0 Mon Sep 17 00:00:00 2001 From: Matheo Daninos Date: Tue, 6 Jun 2023 11:55:09 +0200 Subject: [PATCH 08/13] mixing slots and blocks --- src/TwigComponent/src/Twig/AttributeBag.php | 11 ++ src/TwigComponent/src/Twig/ComponentSlot.php | 11 ++ .../src/Twig/TwigComponentNode.php | 88 +++------------ .../src/Twig/TwigComponentTokenParser.php | 106 ++++++++++++++++-- .../tests/Fixtures/Component/Table.php | 4 +- .../templates/slot/render_block.html.twig | 6 + .../Integration/ComponentExtensionTest.php | 8 -- .../TwigComponentExtensionTest.php | 9 ++ 8 files changed, 149 insertions(+), 94 deletions(-) create mode 100644 src/TwigComponent/tests/Fixtures/templates/slot/render_block.html.twig diff --git a/src/TwigComponent/src/Twig/AttributeBag.php b/src/TwigComponent/src/Twig/AttributeBag.php index 9d9473fe05a..56e4f9e8b4f 100644 --- a/src/TwigComponent/src/Twig/AttributeBag.php +++ b/src/TwigComponent/src/Twig/AttributeBag.php @@ -164,6 +164,17 @@ public function __toString(): string $value = $key; } + if (\is_array($value)) { + $convertedArray = '['; + foreach ($value as $key => $item) { + $convertedArray .= $key.'=>'.$item.','; + } + + $convertedArray = rtrim($convertedArray, ','); + $convertedArray .= ']'; + $value = $convertedArray; + } + $string .= ' '.$key.'="'.str_replace('"', '\\"', trim($value)).'"'; } diff --git a/src/TwigComponent/src/Twig/ComponentSlot.php b/src/TwigComponent/src/Twig/ComponentSlot.php index 669275a82c6..9406862556b 100644 --- a/src/TwigComponent/src/Twig/ComponentSlot.php +++ b/src/TwigComponent/src/Twig/ComponentSlot.php @@ -39,6 +39,17 @@ public function withAttributes(array $attributes): self return $this; } + public function withContext(array $contexts): void + { + $content = $this->contents; + + foreach ($contexts as $key => $value) { + $content = str_replace("", $value, $content); + } + + $this->contents = $content; + } + public function toHtml(): string { return $this->contents; diff --git a/src/TwigComponent/src/Twig/TwigComponentNode.php b/src/TwigComponent/src/Twig/TwigComponentNode.php index 1fe2162aa19..d778d453e12 100644 --- a/src/TwigComponent/src/Twig/TwigComponentNode.php +++ b/src/TwigComponent/src/Twig/TwigComponentNode.php @@ -11,13 +11,10 @@ namespace Symfony\UX\TwigComponent\Twig; -use Symfony\UX\TwigComponent\ComponentFactory; use Symfony\UX\TwigComponent\ComponentMetadata; use Twig\Compiler; -use Twig\Environment; +use Twig\Node\EmbedNode; use Twig\Node\Expression\AbstractExpression; -use Twig\Node\Expression\ConstantExpression; -use Twig\Node\IncludeNode; use Twig\Node\Node; /** @@ -28,20 +25,16 @@ * * @internal */ -class TwigComponentNode extends IncludeNode +class TwigComponentNode extends EmbedNode { - private Environment $environment; - - /** - * @param callable():ComponentFactory $factory - */ - public function __construct(string $componentName, Node $slot, ?AbstractExpression $variables, int $lineno, callable $factory, Environment $environment) + public function __construct(string $componentName, string $template, ?AbstractExpression $variables, int $lineno, $index, $tag, bool $only, Node $slot, ?ComponentMetadata $componentMetadata) { - parent::__construct(new ConstantExpression('not_used', $lineno), $variables, false, false, $lineno, null); - $this->setAttribute('componentName', $componentName); - $this->setAttribute('componentMetadata', $factory()->metadataForTwigComponent($componentName)); + parent::__construct($template, $index, $variables, $only, false, $lineno, $tag); + + $this->setAttribute('component', $componentName); + $this->setAttribute('componentMetadata', $componentMetadata); + $this->setNode('slot', $slot); - $this->environment = $environment; } public function compile(Compiler $compiler): void @@ -49,12 +42,11 @@ public function compile(Compiler $compiler): void $compiler->addDebugInfo($this); $template = $compiler->getVarName(); - $compiler->write(sprintf('$%s = ', $template)); - $this->addGetTemplate($compiler); $compiler + ->raw(';') ->write(sprintf("if ($%s) {\n", $template)) ->write('$slotsStack = $slotsStack ?? [];'.\PHP_EOL) ->write('$slotsStack[] = $slots ?? [];'.\PHP_EOL) @@ -69,32 +61,13 @@ public function compile(Compiler $compiler): void ->write('ob_start();'.\PHP_EOL) ->subcompile($this->getNode('slot')) ->write('$slot = ob_get_clean();'.\PHP_EOL) - ->write(sprintf('$%s->display(', $template)); - - $this->addTemplateArguments($compiler); - - $compiler - ->raw(");\n") - ->write('$slots = array_pop($slotsStack);'.\PHP_EOL) - ->write("}\n") ; - } - protected function addGetTemplate(Compiler $compiler) - { - $compiler - ->raw('$this->loadTemplate('.\PHP_EOL) - ->indent(1) - ->write('') - ->repr($this->getTemplatePath()) - ->raw(', '.\PHP_EOL) - ->write('') - ->repr($this->getTemplatePath()) - ->raw(', '.\PHP_EOL) - ->write('') - ->repr($this->getTemplateLine()) - ->indent(-1) - ->raw(\PHP_EOL.');'.\PHP_EOL.\PHP_EOL); + $compiler->raw(sprintf('$%s->display(', $template)); + + $this->addTemplateArguments($compiler); + $compiler->raw(");\n"); + $compiler->write("}\n"); } protected function addTemplateArguments(Compiler $compiler) @@ -134,44 +107,13 @@ protected function addTemplateArguments(Compiler $compiler) $compiler->write(")\n"); } - private function getTemplatePath(): string - { - $name = $this->getAttribute('componentName'); - - $loader = $this->environment->getLoader(); - $componentPath = rtrim(str_replace('.', '/', $name)); - - /** @var ComponentMetadata $componentMetadata */ - if (($componentMetadata = $this->getAttribute('componentMetadata')) !== null) { - return $componentMetadata->getTemplate(); - } - - if ($loader->exists($componentPath)) { - return $componentPath; - } - - if ($loader->exists($componentPath.'.html.twig')) { - return $componentPath.'.html.twig'; - } - - if ($loader->exists('components/'.$componentPath)) { - return 'components/'.$componentPath; - } - - if ($loader->exists('/components/'.$componentPath.'.html.twig')) { - return '/components/'.$componentPath.'.html.twig'; - } - - throw new \LogicException("No template found for: {$name}"); - } - private function addComponentProps(Compiler $compiler) { $compiler ->raw('$props = $this->extensions[') ->string(ComponentExtension::class) ->raw(']->embeddedContext(') - ->string($this->getAttribute('componentName')) + ->string($this->getAttribute('component')) ->raw(', ') ; diff --git a/src/TwigComponent/src/Twig/TwigComponentTokenParser.php b/src/TwigComponent/src/Twig/TwigComponentTokenParser.php index c6dd7eba611..63f7aee357b 100644 --- a/src/TwigComponent/src/Twig/TwigComponentTokenParser.php +++ b/src/TwigComponent/src/Twig/TwigComponentTokenParser.php @@ -14,8 +14,10 @@ use Symfony\UX\TwigComponent\ComponentFactory; use Twig\Environment; use Twig\Node\Expression\AbstractExpression; +use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\NameExpression; +use Twig\Node\ModuleNode; use Twig\Node\Node; use Twig\Token; use Twig\TokenParser\AbstractTokenParser; @@ -35,8 +37,11 @@ final class TwigComponentTokenParser extends AbstractTokenParser private Environment $environment; + /** + * @param callable():ComponentFactory $factory + */ public function __construct( - $factory, + callable $factory, Environment $environment ) { $this->factory = $factory; @@ -45,13 +50,51 @@ public function __construct( public function parse(Token $token): Node { + $stream = $this->parser->getStream(); $parent = $this->parser->getExpressionParser()->parseExpression(); - $name = $this->componentName($parent); + $componentName = $this->componentName($parent); + $componentMetadata = $this->factory()->metadataForTwigComponent($componentName); + [$variables, $only] = $this->parseArguments(); - $slot = $this->parser->subparse([$this, 'decideBlockEnd'], true); - $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); - return new TwigComponentNode($name, $slot, $variables, $token->getLine(), $this->factory, $this->environment); + if (null === $variables) { + $variables = new ArrayExpression([], $parent->getTemplateLine()); + } + + $parentToken = new Token(Token::STRING_TYPE, $this->getTemplatePath($componentName), $token->getLine()); + $fakeParentToken = new Token(Token::STRING_TYPE, '__parent__', $token->getLine()); + + // inject a fake parent to make the parent() function work + $stream->injectTokens([ + new Token(Token::BLOCK_START_TYPE, '', $token->getLine()), + new Token(Token::NAME_TYPE, 'extends', $token->getLine()), + $parentToken, + new Token(Token::BLOCK_END_TYPE, '', $token->getLine()), + ]); + + $module = $this->parser->parse($stream, fn (Token $token) => $token->test("end_{$this->getTag()}"), true); + + $slot = $this->getSlotFromBlockContent($module); + + // override the parent with the correct one + if ($fakeParentToken === $parentToken) { + $module->setNode('parent', $parent); + } + + $this->parser->embedTemplate($module); + + $stream->expect(Token::BLOCK_END_TYPE); + + return new TwigComponentNode( + $componentName, + $module->getTemplateName(), + $variables, $token->getLine(), + $module->getAttribute('index'), + $this->getTag(), + $only, + $slot, + $componentMetadata + ); } public function getTag(): string @@ -59,11 +102,6 @@ public function getTag(): string return 'twig_component'; } - public function decideBlockEnd(Token $token): bool - { - return $token->test('end_twig_component'); - } - private function componentName(AbstractExpression $expression): string { if ($expression instanceof ConstantExpression) { // using {% component 'name' %} @@ -74,7 +112,16 @@ private function componentName(AbstractExpression $expression): string return $expression->getAttribute('name'); } - throw new \LogicException('Could not parse twig component name.'); + throw new \LogicException('Could not parse component name.'); + } + + private function factory(): ComponentFactory + { + if (\is_callable($this->factory)) { + $this->factory = ($this->factory)(); + } + + return $this->factory; } private function parseArguments(): array @@ -97,4 +144,41 @@ private function parseArguments(): array return [$variables, $only]; } + + private function getTemplatePath(string $name): string + { + $loader = $this->environment->getLoader(); + $componentPath = rtrim(str_replace('.', '/', $name)); + + if (($componentMetadata = $this->factory->metadataForTwigComponent($name)) !== null) { + return $componentMetadata->getTemplate(); + } + + if ($loader->exists($componentPath)) { + return $componentPath; + } + + if ($loader->exists($componentPath.'.html.twig')) { + return $componentPath.'.html.twig'; + } + + if ($loader->exists('components/'.$componentPath)) { + return 'components/'.$componentPath; + } + + if ($loader->exists('/components/'.$componentPath.'.html.twig')) { + return '/components/'.$componentPath.'.html.twig'; + } + + throw new \LogicException("No template found for: {$name}"); + } + + private function getSlotFromBlockContent(ModuleNode $module): Node + { + if ($module->getNode('blocks')->hasNode('content')) { + return $module->getNode('blocks')->getNode('content')->getNode(0)->getNode('body'); + } + + return new Node(); + } } diff --git a/src/TwigComponent/tests/Fixtures/Component/Table.php b/src/TwigComponent/tests/Fixtures/Component/Table.php index cfdc151182f..930a2117ebd 100644 --- a/src/TwigComponent/tests/Fixtures/Component/Table.php +++ b/src/TwigComponent/tests/Fixtures/Component/Table.php @@ -8,6 +8,6 @@ final class Table { public ?string $caption = null; - public array $headers; - public array $data; + public array $headers = ['key', 'value']; + public array $data = [[1, 2], [3, 4]]; } diff --git a/src/TwigComponent/tests/Fixtures/templates/slot/render_block.html.twig b/src/TwigComponent/tests/Fixtures/templates/slot/render_block.html.twig new file mode 100644 index 00000000000..895da91da9a --- /dev/null +++ b/src/TwigComponent/tests/Fixtures/templates/slot/render_block.html.twig @@ -0,0 +1,6 @@ + + {% block th %}custom th ({{ parent() }}){% endblock %} + {% block td %}custom td ({{ parent() }}){% endblock %} + + {% block footer %}My footer{% endblock %} + \ No newline at end of file diff --git a/src/TwigComponent/tests/Integration/ComponentExtensionTest.php b/src/TwigComponent/tests/Integration/ComponentExtensionTest.php index 91d3d3f7334..234ed433c82 100644 --- a/src/TwigComponent/tests/Integration/ComponentExtensionTest.php +++ b/src/TwigComponent/tests/Integration/ComponentExtensionTest.php @@ -176,12 +176,4 @@ private function renderComponent(string $name, array $data = []): string 'data' => $data, ]); } - - private function renderTwigComponent(string $name, array $data = []): string - { - return self::getContainer()->get(Environment::class)->render('render_twig_component.html.twig', [ - 'name' => $name, - 'data' => $data, - ]); - } } diff --git a/src/TwigComponent/tests/Integration/TwigComponentExtensionTest.php b/src/TwigComponent/tests/Integration/TwigComponentExtensionTest.php index 6f4f80b0062..7a1b2427091 100644 --- a/src/TwigComponent/tests/Integration/TwigComponentExtensionTest.php +++ b/src/TwigComponent/tests/Integration/TwigComponentExtensionTest.php @@ -89,4 +89,13 @@ public function testPassDefaultSlotToChildComponents(): void $this->assertStringContainsString('', $output); } + + public function testCanRenderEmbeddedComponent(): void + { + $output = self::getContainer()->get(Environment::class)->render('slot/render_block.html.twig'); + + $this->assertStringContainsString('data table', $output); + $this->assertStringContainsString('custom th (key)', $output); + $this->assertStringContainsString('custom td (1)', $output); + } } From 71ae7ad899f9abe6ee37a3b4fa8a28967dcdfa86 Mon Sep 17 00:00:00 2001 From: Matheo Daninos Date: Tue, 6 Jun 2023 22:51:02 +0200 Subject: [PATCH 09/13] small fix on test --- src/TwigComponent/src/Twig/AttributeBag.php | 11 ----------- src/TwigComponent/tests/Fixtures/Component/Table.php | 4 ++-- .../Fixtures/templates/components/table.html.twig | 2 +- .../Fixtures/templates/slot/render_block.html.twig | 2 +- 4 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/TwigComponent/src/Twig/AttributeBag.php b/src/TwigComponent/src/Twig/AttributeBag.php index 56e4f9e8b4f..9d9473fe05a 100644 --- a/src/TwigComponent/src/Twig/AttributeBag.php +++ b/src/TwigComponent/src/Twig/AttributeBag.php @@ -164,17 +164,6 @@ public function __toString(): string $value = $key; } - if (\is_array($value)) { - $convertedArray = '['; - foreach ($value as $key => $item) { - $convertedArray .= $key.'=>'.$item.','; - } - - $convertedArray = rtrim($convertedArray, ','); - $convertedArray .= ']'; - $value = $convertedArray; - } - $string .= ' '.$key.'="'.str_replace('"', '\\"', trim($value)).'"'; } diff --git a/src/TwigComponent/tests/Fixtures/Component/Table.php b/src/TwigComponent/tests/Fixtures/Component/Table.php index 930a2117ebd..cfdc151182f 100644 --- a/src/TwigComponent/tests/Fixtures/Component/Table.php +++ b/src/TwigComponent/tests/Fixtures/Component/Table.php @@ -8,6 +8,6 @@ final class Table { public ?string $caption = null; - public array $headers = ['key', 'value']; - public array $data = [[1, 2], [3, 4]]; + public array $headers; + public array $data; } diff --git a/src/TwigComponent/tests/Fixtures/templates/components/table.html.twig b/src/TwigComponent/tests/Fixtures/templates/components/table.html.twig index cb8126c41cc..303160e4110 100644 --- a/src/TwigComponent/tests/Fixtures/templates/components/table.html.twig +++ b/src/TwigComponent/tests/Fixtures/templates/components/table.html.twig @@ -1,4 +1,4 @@ - + {% if this.caption %} {% endif %} diff --git a/src/TwigComponent/tests/Fixtures/templates/slot/render_block.html.twig b/src/TwigComponent/tests/Fixtures/templates/slot/render_block.html.twig index 895da91da9a..41a3e6e45ce 100644 --- a/src/TwigComponent/tests/Fixtures/templates/slot/render_block.html.twig +++ b/src/TwigComponent/tests/Fixtures/templates/slot/render_block.html.twig @@ -1,4 +1,4 @@ - + {% block th %}custom th ({{ parent() }}){% endblock %} {% block td %}custom td ({{ parent() }}){% endblock %} From c6312aedc8ab8b4854d37db0756b19956663e680 Mon Sep 17 00:00:00 2001 From: Matheo Daninos Date: Fri, 9 Jun 2023 12:39:08 +0200 Subject: [PATCH 10/13] add slots variables --- src/TwigComponent/src/Twig/SlotNode.php | 2 +- .../src/Twig/TwigComponentNode.php | 22 ++--- src/TwigComponent/src/Twig/TwigPreLexer.php | 88 ++++++++++++++++++- .../templates/components/Alarm.html.twig | 7 +- .../templates/components/Button.html.twig | 2 +- .../components/DangerButton.html.twig | 2 +- .../render_mix_of_slot_and_blocks.html.twig | 10 +++ .../TwigComponentExtensionTest.php | 9 ++ .../tests/Unit/TwigPreLexerTest.php | 74 ++++++++-------- 9 files changed, 160 insertions(+), 56 deletions(-) create mode 100644 src/TwigComponent/tests/Fixtures/templates/slot/render_mix_of_slot_and_blocks.html.twig diff --git a/src/TwigComponent/src/Twig/SlotNode.php b/src/TwigComponent/src/Twig/SlotNode.php index ed448ba450a..fa5e918aed4 100644 --- a/src/TwigComponent/src/Twig/SlotNode.php +++ b/src/TwigComponent/src/Twig/SlotNode.php @@ -43,7 +43,7 @@ public function compile(Compiler $compiler): void ->write('ob_start();') ->subcompile($this->getNode('body')) ->write('$body = ob_get_clean();'.\PHP_EOL) - ->write("\$slots['$name'] = new ".ComponentSlot::class.'($body, '); + ->write("\$slotsStack['$name'] = new ".ComponentSlot::class.'($body, '); if ($this->hasNode('variables')) { $compiler->subcompile($this->getNode('variables')); diff --git a/src/TwigComponent/src/Twig/TwigComponentNode.php b/src/TwigComponent/src/Twig/TwigComponentNode.php index d778d453e12..4b458cdc985 100644 --- a/src/TwigComponent/src/Twig/TwigComponentNode.php +++ b/src/TwigComponent/src/Twig/TwigComponentNode.php @@ -41,16 +41,8 @@ public function compile(Compiler $compiler): void { $compiler->addDebugInfo($this); - $template = $compiler->getVarName(); - $compiler->write(sprintf('$%s = ', $template)); - $this->addGetTemplate($compiler); - $compiler - ->raw(';') - ->write(sprintf("if ($%s) {\n", $template)) ->write('$slotsStack = $slotsStack ?? [];'.\PHP_EOL) - ->write('$slotsStack[] = $slots ?? [];'.\PHP_EOL) - ->write('$slots = [];'.\PHP_EOL) ; if ($this->getAttribute('componentMetadata') instanceof ComponentMetadata) { @@ -63,11 +55,15 @@ public function compile(Compiler $compiler): void ->write('$slot = ob_get_clean();'.\PHP_EOL) ; - $compiler->raw(sprintf('$%s->display(', $template)); + $compiler + ->write("\$slotsStack['content'] = new ".ComponentSlot::class." (\$slot);\n") + ; + + $this->addGetTemplate($compiler); + $compiler->raw('->display('); $this->addTemplateArguments($compiler); $compiler->raw(");\n"); - $compiler->write("}\n"); } protected function addTemplateArguments(Compiler $compiler) @@ -76,7 +72,6 @@ protected function addTemplateArguments(Compiler $compiler) ->indent(1) ->write("\n") ->write("array_merge(\n") - ->write('$slots,'.\PHP_EOL) ; if ($this->getAttribute('componentMetadata') instanceof ComponentMetadata) { @@ -85,8 +80,9 @@ protected function addTemplateArguments(Compiler $compiler) $compiler ->write('$context,[') - ->write("'slot' => new ".ComponentSlot::class." (\$slot),\n") - ->write("'attributes' => new ".AttributeBag::class.'('); + ->write("'slots' => \$slotsStack,") + ->write("'attributes' => new ".AttributeBag::class.'(') + ; if ($this->hasNode('variables')) { $compiler->subcompile($this->getNode('variables')); diff --git a/src/TwigComponent/src/Twig/TwigPreLexer.php b/src/TwigComponent/src/Twig/TwigPreLexer.php index ce44eb9ad62..172edd5859b 100644 --- a/src/TwigComponent/src/Twig/TwigPreLexer.php +++ b/src/TwigComponent/src/Twig/TwigPreLexer.php @@ -76,6 +76,12 @@ public function preLexComponents(string $input): string continue; } + if ('slot' === $componentName) { + $output .= $this->consumeSlot($componentName); + + continue; + } + // if we're already inside a component, // *and* we've just found a new component, then we should try to // open the default block @@ -95,9 +101,9 @@ public function preLexComponents(string $input): string // use the simpler component() format, so that the system doesn't think // this is an "embedded" component with blocks // see https://github.com/symfony/ux/issues/810 - $output .= "{{ component('{$componentName}'".($attributes ? ", { {$attributes} }" : '').') }}'; + $output .= "{% twig_component '{$componentName}'".($attributes ? " with { {$attributes} }" : '').' %}{% end_twig_component %}'; } else { - $output .= "{% component '{$componentName}'".($attributes ? " with { {$attributes} }" : '').' %}'; + $output .= "{% twig_component '{$componentName}'".($attributes ? " with { {$attributes} }" : '').' %}'; } continue; @@ -121,7 +127,7 @@ public function preLexComponents(string $input): string $output .= '{% endblock %}'; } - $output .= '{% endcomponent %}'; + $output .= '{% end_twig_component %}'; continue; } @@ -410,6 +416,82 @@ private function consumeUntilEndBlock(): string return substr($this->input, $start, $this->position - $start); } + private function consumeSlot(string $componentName): string + { + $attributes = $this->consumeAttributes($componentName); + $this->consume('>'); + + $slotName = ''; + foreach (explode(', ', $attributes) as $attr) { + [$key, $value] = explode(': ', $attr); + if ('name' === $key) { + $slotName = trim($value, "'"); + break; + } + } + + if (empty($slotName)) { + throw new SyntaxError('Expected block name.', $this->line); + } + + $output = "{% slot {$slotName} %}"; + + $closingTag = ''; + if (!$this->doesStringEventuallyExist($closingTag)) { + throw new SyntaxError("Expected closing tag '{$closingTag}' for block '{$slotName}'.", $this->line); + } + $slotContents = $this->consumeUntilEndSlot(); + + $subLexer = new self($this->line); + $output .= $subLexer->preLexComponents($slotContents); + + $this->consume($closingTag); + $output .= '{% endslot %}'; + + return $output; + } + + private function consumeUntilEndSlot(): string + { + $start = $this->position; + + $depth = 1; + while ($this->position < $this->length) { + if ('input, $this->position, 11)) { + if (1 === $depth) { + break; + } else { + --$depth; + } + } + + if ('{% endslot %}' === substr($this->input, $this->position, 13)) { + if (1 === $depth) { + // in this case, we want to advanced ALL the way beyond the endblock + $this->position += 14; + break; + } else { + --$depth; + } + } + + if ('input, $this->position, 10)) { + ++$depth; + } + + if ('{% slot' === substr($this->input, $this->position, 7)) { + ++$depth; + } + + if ("\n" === $this->input[$this->position]) { + ++$this->line; + } + ++$this->position; + } + + return substr($this->input, $start, $this->position - $start); + } + private function consumeAttributeValue(string $quote): string { $parts = []; diff --git a/src/TwigComponent/tests/Fixtures/templates/components/Alarm.html.twig b/src/TwigComponent/tests/Fixtures/templates/components/Alarm.html.twig index be4ef1cd80f..b2f347c5734 100644 --- a/src/TwigComponent/tests/Fixtures/templates/components/Alarm.html.twig +++ b/src/TwigComponent/tests/Fixtures/templates/components/Alarm.html.twig @@ -1,6 +1,9 @@
+ {% block header %} +

You have an alert!

+ {% endblock %}
- {{ slot }} + {{ slots.content }}
- {{ footer|default('')|raw }} + {{ slots.footer|default('')|raw }}
\ No newline at end of file diff --git a/src/TwigComponent/tests/Fixtures/templates/components/Button.html.twig b/src/TwigComponent/tests/Fixtures/templates/components/Button.html.twig index d17c380531b..47b03649419 100644 --- a/src/TwigComponent/tests/Fixtures/templates/components/Button.html.twig +++ b/src/TwigComponent/tests/Fixtures/templates/components/Button.html.twig @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/TwigComponent/tests/Fixtures/templates/components/DangerButton.html.twig b/src/TwigComponent/tests/Fixtures/templates/components/DangerButton.html.twig index 77dd9c1ee84..3539c91a1cd 100644 --- a/src/TwigComponent/tests/Fixtures/templates/components/DangerButton.html.twig +++ b/src/TwigComponent/tests/Fixtures/templates/components/DangerButton.html.twig @@ -1 +1 @@ -
{{ slot }}

Danger Zone

\ No newline at end of file +
{{ slots.content }}

Danger Zone

\ No newline at end of file diff --git a/src/TwigComponent/tests/Fixtures/templates/slot/render_mix_of_slot_and_blocks.html.twig b/src/TwigComponent/tests/Fixtures/templates/slot/render_mix_of_slot_and_blocks.html.twig new file mode 100644 index 00000000000..fb8dd2098cf --- /dev/null +++ b/src/TwigComponent/tests/Fixtures/templates/slot/render_mix_of_slot_and_blocks.html.twig @@ -0,0 +1,10 @@ + + {% block header %} + {{ parent() }} +

A new message

+ {% endblock %} +

Hey!

+ + from @bob + +
\ No newline at end of file diff --git a/src/TwigComponent/tests/Integration/TwigComponentExtensionTest.php b/src/TwigComponent/tests/Integration/TwigComponentExtensionTest.php index 7a1b2427091..fdf932cc4fa 100644 --- a/src/TwigComponent/tests/Integration/TwigComponentExtensionTest.php +++ b/src/TwigComponent/tests/Integration/TwigComponentExtensionTest.php @@ -98,4 +98,13 @@ public function testCanRenderEmbeddedComponent(): void $this->assertStringContainsString('custom th (key)', $output); $this->assertStringContainsString('custom td (1)', $output); } + + public function testCanRenderMixOfBlockAndSlot(): void + { + $output = self::getContainer()->get(Environment::class)->render('slot/render_mix_of_slot_and_blocks.html.twig'); + + $this->assertStringContainsString('

You have an alert!

', $output); + $this->assertStringContainsString('Hey!', $output); + $this->assertStringContainsString('from @bob', $output); + } } diff --git a/src/TwigComponent/tests/Unit/TwigPreLexerTest.php b/src/TwigComponent/tests/Unit/TwigPreLexerTest.php index 3e1f118084c..9d3afc72d7a 100644 --- a/src/TwigComponent/tests/Unit/TwigPreLexerTest.php +++ b/src/TwigComponent/tests/Unit/TwigPreLexerTest.php @@ -29,98 +29,98 @@ public function getLexTests(): iterable { yield 'simple_component' => [ '', - '{{ component(\'foo\') }}', + '{% twig_component \'foo\' %}{% end_twig_component %}', ]; yield 'component_with_attributes' => [ '', - "{{ component('foo', { bar: 'baz', with_quotes: 'It\'s with quotes' }) }}", + "{% twig_component 'foo' with { bar: 'baz', with_quotes: 'It\'s with quotes' } %}{% end_twig_component %}", ]; yield 'component_with_dynamic_attributes' => [ '', - '{{ component(\'foo\', { dynamic: (dynamicVar), otherDynamic: anotherVar }) }}', + '{% twig_component \'foo\' with { dynamic: (dynamicVar), otherDynamic: anotherVar } %}{% end_twig_component %}', ]; yield 'component_with_closing_tag' => [ '', - '{% component \'foo\' %}{% endcomponent %}', + '{% twig_component \'foo\' %}{% end_twig_component %}', ]; yield 'component_with_block' => [ 'Foo', - '{% component \'foo\' %}{% block foo_block %}Foo{% endblock %}{% endcomponent %}', + '{% twig_component \'foo\' %}{% block foo_block %}Foo{% endblock %}{% end_twig_component %}', ]; yield 'component_with_traditional_block' => [ '{% block foo_block %}Foo{% endblock %}', - '{% component \'foo\' %}{% block foo_block %}Foo{% endblock %}{% endcomponent %}', + '{% twig_component \'foo\' %}{% block foo_block %}Foo{% endblock %}{% end_twig_component %}', ]; yield 'traditional_blocks_around_component_do_not_confuse' => [ 'Hello {% block foo_block %}Foo{% endblock %}{% block bar_block %}Bar{% endblock %}', - 'Hello {% block foo_block %}Foo{% endblock %}{{ component(\'foo\') }}{% block bar_block %}Bar{% endblock %}', + 'Hello {% block foo_block %}Foo{% endblock %}{% twig_component \'foo\' %}{% end_twig_component %}{% block bar_block %}Bar{% endblock %}', ]; yield 'component_with_embedded_component_inside_block' => [ '', - '{% component \'foo\' %}{% block foo_block %}{{ component(\'bar\') }}{% endblock %}{% endcomponent %}', + '{% twig_component \'foo\' %}{% block foo_block %}{% twig_component \'bar\' %}{% end_twig_component %}{% endblock %}{% end_twig_component %}', ]; yield 'attribute_with_no_value' => [ '', - '{{ component(\'foo\', { bar: true }) }}', + '{% twig_component \'foo\' with { bar: true } %}{% end_twig_component %}', ]; yield 'attribute_with_no_value_and_no_attributes' => [ '', - '{{ component(\'foo\') }}', + '{% twig_component \'foo\' %}{% end_twig_component %}', ]; yield 'component_with_default_block_content' => [ 'Foo', - '{% component \'foo\' %}{% block content %}Foo{% endblock %}{% endcomponent %}', + '{% twig_component \'foo\' %}{% block content %}Foo{% endblock %}{% end_twig_component %}', ]; yield 'component_with_default_block_that_holds_a_component_and_multi_blocks' => [ 'Foo Other block', - '{% component \'foo\' %}{% block content %}Foo {{ component(\'bar\') }}{% endblock %}{% block other_block %}Other block{% endblock %}{% endcomponent %}', + '{% twig_component \'foo\' %}{% block content %}Foo {% twig_component \'bar\' %}{% end_twig_component %}{% endblock %}{% block other_block %}Other block{% endblock %}{% end_twig_component %}', ]; yield 'component_with_character_:_on_his_name' => [ '', - '{% component \'foo:bar\' %}{% endcomponent %}', + '{% twig_component \'foo:bar\' %}{% end_twig_component %}', ]; yield 'component_with_character_@_on_his_name' => [ '', - '{% component \'@foo\' %}{% endcomponent %}', + '{% twig_component \'@foo\' %}{% end_twig_component %}', ]; yield 'component_with_character_-_on_his_name' => [ '', - '{% component \'foo-bar\' %}{% endcomponent %}', + '{% twig_component \'foo-bar\' %}{% end_twig_component %}', ]; yield 'component_with_character_._on_his_name' => [ '', - '{% component \'foo.bar\' %}{% endcomponent %}', + '{% twig_component \'foo.bar\' %}{% end_twig_component %}', ]; yield 'nested_component_2_levels' => [ 'Hello World!', - '{% component \'foo\' %}{% block child %}{% component \'bar\' %}{% block message %}Hello World!{% endblock %}{% endcomponent %}{% endblock %}{% endcomponent %}', + '{% twig_component \'foo\' %}{% block child %}{% twig_component \'bar\' %}{% block message %}Hello World!{% endblock %}{% end_twig_component %}{% endblock %}{% end_twig_component %}', ]; yield 'component_with_mixture_of_string_and_twig_in_argument' => [ '', - "{{ component('foo', { text: 'Hello '~(name)~'!' }) }}", + "{% twig_component 'foo' with { text: 'Hello '~(name)~'!' } %}{% end_twig_component %}", ]; yield 'component_with_mixture_of_dynamic_twig_from_start' => [ '', - "{{ component('foo', { text: (name)~' is my name'~(ending~'!!') }) }}", + "{% twig_component 'foo' with { text: (name)~' is my name'~(ending~'!!') } %}{% end_twig_component %}", ]; yield 'dynamic_attribute_with_quotation_included' => [ '', - "{{ component('foo', { text: (\"hello!\") }) }}", + "{% twig_component 'foo' with { text: (\"hello!\") } %}{% end_twig_component %}", ]; yield 'component_with_mixture_of_string_and_twig_with_quote_in_argument' => [ '', - "{{ component('foo', { text: 'Hello '~(name)~', I\'m Theo!' }) }}", + "{% twig_component 'foo' with { text: 'Hello '~(name)~', I\'m Theo!' } %}{% end_twig_component %}", ]; yield 'component_where_entire_default_block_is_embedded_component' => [ << EOF, << [ @@ -141,9 +141,9 @@ public function getLexTests(): iterable EOF, << EOF, << [ '', - '{% component \'foobar\' with { \'data-action\': \'foo#bar\' } %}{% endcomponent %}', + '{% twig_component \'foobar\' with { \'data-action\': \'foo#bar\' } %}{% end_twig_component %}', ]; yield 'component_with_dashed_attribute_self_closing' => [ '', - '{{ component(\'foobar\', { \'data-action\': \'foo#bar\' }) }}', + '{% twig_component \'foobar\' with { \'data-action\': \'foo#bar\' } %}{% end_twig_component %}', ]; yield 'component_with_colon_attribute' => [ '', - '{% component \'foobar\' with { \'my:attribute\': \'yo\' } %}{% endcomponent %}', + '{% twig_component \'foobar\' with { \'my:attribute\': \'yo\' } %}{% end_twig_component %}', ]; yield 'component_with_truthy_attribute' => [ '', - '{% component \'foobar\' with { \'data-turbo-stream\': true } %}{% endcomponent %}', + '{% twig_component \'foobar\' with { \'data-turbo-stream\': true } %}{% end_twig_component %}', ]; yield 'ignore_twig_comment' => [ '{# #} ', - '{# #} {{ component(\'Alert\') }}', + '{# #} {% twig_component \'Alert\' %}{% end_twig_component %}', ]; yield 'file_ended_with_comments' => [ @@ -194,7 +194,11 @@ public function getLexTests(): iterable yield 'mixing_component_and_file_ended_with_comments' => [ ' {# #}', - '{{ component(\'Alert\') }} {# #}', + '{% twig_component \'Alert\' %}{% end_twig_component %} {# #}', + ]; + yield 'slot_inside_block_content' => [ + '{% block footer %}{{ user }}{% endblock %}

Hello

You have a new message
', + '{% twig_component \'Alert\' %}{% block footer %}{{ user }}{% endblock %}{% block content %}

Hello

{% slot message %}You have a new message{% endslot %}{% endblock %}{% end_twig_component %}', ]; } } From fdeb357a5eb74b9084bc3b86437c1fb2daa0a29d Mon Sep 17 00:00:00 2001 From: Matheo Daninos Date: Sun, 11 Jun 2023 14:54:47 +0200 Subject: [PATCH 11/13] use ComponentAttribute instead of AttributeBag --- src/TwigComponent/src/Twig/AttributeBag.php | 172 ------------------ src/TwigComponent/src/Twig/ComponentSlot.php | 6 +- .../src/Twig/TwigComponentNode.php | 36 ++-- .../templates/components/Button.html.twig | 2 +- .../TwigComponentExtensionTest.php | 4 +- 5 files changed, 23 insertions(+), 197 deletions(-) delete mode 100644 src/TwigComponent/src/Twig/AttributeBag.php diff --git a/src/TwigComponent/src/Twig/AttributeBag.php b/src/TwigComponent/src/Twig/AttributeBag.php deleted file mode 100644 index 9d9473fe05a..00000000000 --- a/src/TwigComponent/src/Twig/AttributeBag.php +++ /dev/null @@ -1,172 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\TwigComponent\Twig; - -/** - * thanks to @giorgiopogliani! - * This file is inspired by: https://github.com/giorgiopogliani/twig-components. - * - * @author Mathéo Daninos - */ -class AttributeBag implements \ArrayAccess, \IteratorAggregate -{ - protected $attributes = []; - - public function __construct(array $attributes = []) - { - $this->attributes = $attributes; - - if (\array_key_exists('attributes', $this->attributes) && $this->attributes['attributes'] instanceof ComponentAttributeBag) { - $parentAttributes = $this->attributes['attributes']; - unset($this->attributes['attributes']); - $this->attributes = $this->merge($parentAttributes->getAttributes())->getAttributes(); - } - } - - public function first($default = null): mixed - { - return $this->getIterator()->current() ?? $default; - } - - public function get($key, $default = ''): mixed - { - return $this->attributes[$key] ?? $default; - } - - public function has($key): bool - { - return \array_key_exists($key, $this->attributes); - } - - public function only($keys): self - { - if (null === $keys) { - $values = $this->attributes; - } else { - $keys = \is_array($keys) ? $keys : [$keys]; - - $values = array_filter( - $this->attributes, - function ($key) use ($keys) { - return \in_array($key, $keys); - }, - \ARRAY_FILTER_USE_KEY - ); - } - - return new static($values); - } - - public function except($keys): self - { - if (null === $keys) { - $values = $this->attributes; - } else { - $keys = \is_array($keys) ? $keys : [$keys]; - - $values = array_filter( - $this->attributes, - function ($key) use ($keys) { - return !\in_array($key, $keys); - }, - \ARRAY_FILTER_USE_KEY - ); - } - - return new static($values); - } - - public function merge(array $attributeDefaults = []): self - { - $attributes = $this->getAttributes(); - - foreach ($attributeDefaults as $key => $value) { - if (!\array_key_exists($key, $attributes)) { - $attributes[$key] = ''; - } - } - - foreach ($attributes as $key => $value) { - $attributes[$key] = trim($value.' '.($attributeDefaults[$key] ?? '')); - } - - return new static($attributes); - } - - public function class($defaultClass = ''): self - { - return $this->merge(['class' => $defaultClass]); - } - - public function getAttributes(): mixed - { - return $this->attributes; - } - - public function setAttributes(array $attributes): void - { - if (isset($attributes['attributes']) && - $attributes['attributes'] instanceof self) { - $parentBag = $attributes['attributes']; - - unset($attributes['attributes']); - - $attributes = $parentBag->merge($attributes, $escape = false)->getAttributes(); - } - - $this->attributes = $attributes; - } - - public function offsetExists($offset): bool - { - return isset($this->attributes[$offset]); - } - - public function offsetGet($offset): mixed - { - return $this->get($offset); - } - - public function offsetSet($offset, $value): void - { - $this->attributes[$offset] = $value; - } - - public function offsetUnset($offset): void - { - unset($this->attributes[$offset]); - } - - public function getIterator(): \ArrayIterator - { - return new \ArrayIterator($this->attributes); - } - - public function __toString(): string - { - $string = ''; - - foreach ($this->attributes as $key => $value) { - if (false === $value || null === $value) { - continue; - } - - if (true === $value) { - $value = $key; - } - - $string .= ' '.$key.'="'.str_replace('"', '\\"', trim($value)).'"'; - } - - return trim($string); - } -} diff --git a/src/TwigComponent/src/Twig/ComponentSlot.php b/src/TwigComponent/src/Twig/ComponentSlot.php index 9406862556b..c273d2f09f5 100644 --- a/src/TwigComponent/src/Twig/ComponentSlot.php +++ b/src/TwigComponent/src/Twig/ComponentSlot.php @@ -11,6 +11,8 @@ namespace Symfony\UX\TwigComponent\Twig; +use Symfony\UX\TwigComponent\ComponentAttributes; + /** * thanks to @giorgiopogliani! * This file is inspired by: https://github.com/giorgiopogliani/twig-components. @@ -21,7 +23,7 @@ */ class ComponentSlot { - public AttributeBag $attributes; + public ComponentAttributes $attributes; protected string $contents; @@ -34,7 +36,7 @@ public function __construct(string $contents = '', array $attributes = []) public function withAttributes(array $attributes): self { - $this->attributes = new AttributeBag($attributes); + $this->attributes = new ComponentAttributes($attributes); return $this; } diff --git a/src/TwigComponent/src/Twig/TwigComponentNode.php b/src/TwigComponent/src/Twig/TwigComponentNode.php index 4b458cdc985..3f760f9004f 100644 --- a/src/TwigComponent/src/Twig/TwigComponentNode.php +++ b/src/TwigComponent/src/Twig/TwigComponentNode.php @@ -11,6 +11,7 @@ namespace Symfony\UX\TwigComponent\Twig; +use Symfony\UX\TwigComponent\ComponentAttributes; use Symfony\UX\TwigComponent\ComponentMetadata; use Twig\Compiler; use Twig\Node\EmbedNode; @@ -81,16 +82,21 @@ protected function addTemplateArguments(Compiler $compiler) $compiler ->write('$context,[') ->write("'slots' => \$slotsStack,") - ->write("'attributes' => new ".AttributeBag::class.'(') ; - if ($this->hasNode('variables')) { - $compiler->subcompile($this->getNode('variables')); - } else { - $compiler->raw('[]'); + if (!$this->getAttribute('componentMetadata') instanceof ComponentMetadata) { + $compiler->write("'attributes' => new ".ComponentAttributes::class.'('); + + if ($this->hasNode('variables')) { + $compiler->subcompile($this->getNode('variables')); + } else { + $compiler->raw('[]'); + } + + $compiler->write(")\n"); } - $compiler->write(")\n") + $compiler ->indent(-1) ->write('],'); @@ -111,20 +117,10 @@ private function addComponentProps(Compiler $compiler) ->raw(']->embeddedContext(') ->string($this->getAttribute('component')) ->raw(', ') - ; - - if ($this->hasNode('variables')) { - $compiler - ->raw('twig_to_array(') - ->subcompile($this->getNode('variables')) - ->raw('), ') - ; - } else { - $compiler->raw('[], '); - } - - $compiler - ->raw('$context') + ->raw('twig_to_array(') + ->subcompile($this->getNode('variables')) + ->raw('), ') + ->raw($this->getAttribute('only') ? '[]' : '$context') ->raw(");\n") ; } diff --git a/src/TwigComponent/tests/Fixtures/templates/components/Button.html.twig b/src/TwigComponent/tests/Fixtures/templates/components/Button.html.twig index 47b03649419..20bd70c77a9 100644 --- a/src/TwigComponent/tests/Fixtures/templates/components/Button.html.twig +++ b/src/TwigComponent/tests/Fixtures/templates/components/Button.html.twig @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/TwigComponent/tests/Integration/TwigComponentExtensionTest.php b/src/TwigComponent/tests/Integration/TwigComponentExtensionTest.php index fdf932cc4fa..e6521911042 100644 --- a/src/TwigComponent/tests/Integration/TwigComponentExtensionTest.php +++ b/src/TwigComponent/tests/Integration/TwigComponentExtensionTest.php @@ -65,7 +65,7 @@ public function testRenderStaticTwigComponentWithAttributes(): void $output = self::getContainer()->get(Environment::class)->render('slot/use_attribute_variables.html.twig'); $this->assertStringContainsString('Submit!', $output); - $this->assertStringContainsString('class="btn-primary btn"', $output); + $this->assertStringContainsString('class="btn btn-primary"', $output); } public function testRenderStaticComponentInSubFolder(): void @@ -87,7 +87,7 @@ public function testPassDefaultSlotToChildComponents(): void { $output = self::getContainer()->get(Environment::class)->render('slot/pass_default_slot_to_child.html.twig'); - $this->assertStringContainsString('', $output); + $this->assertStringContainsString('', $output); } public function testCanRenderEmbeddedComponent(): void From e3eab50feb0122769c7aa239c585e78eec89e5cf Mon Sep 17 00:00:00 2001 From: Matheo Daninos Date: Sun, 11 Jun 2023 15:27:41 +0200 Subject: [PATCH 12/13] remove twig_component tag and move slot logic into component tag --- .../src/Twig/ComponentExtension.php | 3 +- src/TwigComponent/src/Twig/ComponentNode.php | 80 +++++++- .../src/Twig/ComponentTokenParser.php | 52 ++++- .../src/Twig/TwigComponentNode.php | 127 ------------ .../src/Twig/TwigComponentTokenParser.php | 184 ------------------ src/TwigComponent/src/Twig/TwigPreLexer.php | 6 +- .../Integration/ComponentExtensionTest.php | 78 ++++++++ .../TwigComponentExtensionTest.php | 110 ----------- .../tests/Unit/TwigPreLexerTest.php | 72 +++---- 9 files changed, 240 insertions(+), 472 deletions(-) delete mode 100644 src/TwigComponent/src/Twig/TwigComponentNode.php delete mode 100644 src/TwigComponent/src/Twig/TwigComponentTokenParser.php delete mode 100644 src/TwigComponent/tests/Integration/TwigComponentExtensionTest.php diff --git a/src/TwigComponent/src/Twig/ComponentExtension.php b/src/TwigComponent/src/Twig/ComponentExtension.php index 63bc65bff82..18983a6df8a 100644 --- a/src/TwigComponent/src/Twig/ComponentExtension.php +++ b/src/TwigComponent/src/Twig/ComponentExtension.php @@ -49,8 +49,7 @@ public function getFunctions(): array public function getTokenParsers(): array { return [ - new ComponentTokenParser(fn () => $this->container->get(ComponentFactory::class)), - new TwigComponentTokenParser(fn () => $this->container->get(ComponentFactory::class), $this->environment), + new ComponentTokenParser(fn () => $this->container->get(ComponentFactory::class), $this->environment), new SlotTokenParser(), ]; } diff --git a/src/TwigComponent/src/Twig/ComponentNode.php b/src/TwigComponent/src/Twig/ComponentNode.php index b8259a228e8..4a9bc03f3cb 100644 --- a/src/TwigComponent/src/Twig/ComponentNode.php +++ b/src/TwigComponent/src/Twig/ComponentNode.php @@ -11,9 +11,12 @@ namespace Symfony\UX\TwigComponent\Twig; +use Symfony\UX\TwigComponent\ComponentAttributes; +use Symfony\UX\TwigComponent\ComponentMetadata; use Twig\Compiler; use Twig\Node\EmbedNode; use Twig\Node\Expression\AbstractExpression; +use Twig\Node\Node; /** * @author Fabien Potencier @@ -23,17 +26,87 @@ */ final class ComponentNode extends EmbedNode { - public function __construct(string $component, string $template, int $index, AbstractExpression $variables, bool $only, int $lineno, string $tag) + public function __construct(string $component, string $template, int $index, AbstractExpression $variables, bool $only, int $lineno, string $tag, Node $slot, ?ComponentMetadata $componentMetadata) { parent::__construct($template, $index, $variables, $only, false, $lineno, $tag); $this->setAttribute('component', $component); + $this->setAttribute('componentMetadata', $componentMetadata); + + $this->setNode('slot', $slot); } public function compile(Compiler $compiler): void { $compiler->addDebugInfo($this); + $compiler + ->write('$slotsStack = $slotsStack ?? [];'.\PHP_EOL) + ; + + if ($this->getAttribute('componentMetadata') instanceof ComponentMetadata) { + $this->addComponentProps($compiler); + } + + $compiler + ->write('ob_start();'.\PHP_EOL) + ->subcompile($this->getNode('slot')) + ->write('$slot = ob_get_clean();'.\PHP_EOL) + ->write("\$slotsStack['content'] = new ".ComponentSlot::class." (\$slot);\n") + ; + + $this->addGetTemplate($compiler); + + $compiler->raw('->display('); + + $this->addTemplateArguments($compiler); + $compiler->raw(");\n"); + } + + protected function addTemplateArguments(Compiler $compiler) + { + $compiler + ->indent(1) + ->write("\n") + ->write("array_merge(\n") + ; + + if ($this->getAttribute('componentMetadata') instanceof ComponentMetadata) { + $compiler->write('$props,'.\PHP_EOL); + } + + $compiler + ->write('$context,[') + ->write("'slots' => \$slotsStack,") + ; + + if (!$this->getAttribute('componentMetadata') instanceof ComponentMetadata) { + $compiler->write("'attributes' => new ".ComponentAttributes::class.'('); + + if ($this->hasNode('variables')) { + $compiler->subcompile($this->getNode('variables')); + } else { + $compiler->raw('[]'); + } + + $compiler->write(")\n"); + } + + $compiler + ->indent(-1) + ->write('],'); + + if ($this->hasNode('variables')) { + $compiler->subcompile($this->getNode('variables')); + } else { + $compiler->raw('[]'); + } + + $compiler->write(")\n"); + } + + private function addComponentProps(Compiler $compiler) + { $compiler ->raw('$props = $this->extensions[') ->string(ComponentExtension::class) @@ -46,10 +119,5 @@ public function compile(Compiler $compiler): void ->raw($this->getAttribute('only') ? '[]' : '$context') ->raw(");\n") ; - - $this->addGetTemplate($compiler); - - $compiler->raw('->display($props);'); - $compiler->raw("\n"); } } diff --git a/src/TwigComponent/src/Twig/ComponentTokenParser.php b/src/TwigComponent/src/Twig/ComponentTokenParser.php index 2e87337aa01..2113160168f 100644 --- a/src/TwigComponent/src/Twig/ComponentTokenParser.php +++ b/src/TwigComponent/src/Twig/ComponentTokenParser.php @@ -12,10 +12,12 @@ namespace Symfony\UX\TwigComponent\Twig; use Symfony\UX\TwigComponent\ComponentFactory; +use Twig\Environment; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\NameExpression; +use Twig\Node\ModuleNode; use Twig\Node\Node; use Twig\Token; use Twig\TokenParser\AbstractTokenParser; @@ -31,12 +33,15 @@ final class ComponentTokenParser extends AbstractTokenParser /** @var ComponentFactory|callable():ComponentFactory */ private $factory; + private Environment $environment; + /** * @param callable():ComponentFactory $factory */ - public function __construct(callable $factory) + public function __construct(callable $factory, Environment $environment) { $this->factory = $factory; + $this->environment = $environment; } public function parse(Token $token): Node @@ -44,7 +49,7 @@ public function parse(Token $token): Node $stream = $this->parser->getStream(); $parent = $this->parser->getExpressionParser()->parseExpression(); $componentName = $this->componentName($parent); - $componentMetadata = $this->factory()->metadataFor($componentName); + $componentMetadata = $this->factory()->metadataForTwigComponent($componentName); [$variables, $only] = $this->parseArguments(); @@ -52,7 +57,7 @@ public function parse(Token $token): Node $variables = new ArrayExpression([], $parent->getTemplateLine()); } - $parentToken = new Token(Token::STRING_TYPE, $componentMetadata->getTemplate(), $token->getLine()); + $parentToken = new Token(Token::STRING_TYPE, $this->getTemplatePath($componentName), $token->getLine()); $fakeParentToken = new Token(Token::STRING_TYPE, '__parent__', $token->getLine()); // inject a fake parent to make the parent() function work @@ -65,6 +70,8 @@ public function parse(Token $token): Node $module = $this->parser->parse($stream, fn (Token $token) => $token->test("end{$this->getTag()}"), true); + $slot = $this->getSlotFromBlockContent($module); + // override the parent with the correct one if ($fakeParentToken === $parentToken) { $module->setNode('parent', $parent); @@ -74,7 +81,7 @@ public function parse(Token $token): Node $stream->expect(Token::BLOCK_END_TYPE); - return new ComponentNode($componentName, $module->getTemplateName(), $module->getAttribute('index'), $variables, $only, $token->getLine(), $this->getTag()); + return new ComponentNode($componentName, $module->getTemplateName(), $module->getAttribute('index'), $variables, $only, $token->getLine(), $this->getTag(), $slot, $componentMetadata); } public function getTag(): string @@ -95,6 +102,34 @@ private function componentName(AbstractExpression $expression): string throw new \LogicException('Could not parse component name.'); } + private function getTemplatePath(string $name): string + { + $loader = $this->environment->getLoader(); + $componentPath = rtrim(str_replace('.', '/', $name)); + + if (($componentMetadata = $this->factory->metadataForTwigComponent($name)) !== null) { + return $componentMetadata->getTemplate(); + } + + if ($loader->exists($componentPath)) { + return $componentPath; + } + + if ($loader->exists($componentPath.'.html.twig')) { + return $componentPath.'.html.twig'; + } + + if ($loader->exists('components/'.$componentPath)) { + return 'components/'.$componentPath; + } + + if ($loader->exists('/components/'.$componentPath.'.html.twig')) { + return '/components/'.$componentPath.'.html.twig'; + } + + throw new \LogicException("No template found for: {$name}"); + } + private function factory(): ComponentFactory { if (\is_callable($this->factory)) { @@ -124,4 +159,13 @@ private function parseArguments(): array return [$variables, $only]; } + + private function getSlotFromBlockContent(ModuleNode $module): Node + { + if ($module->getNode('blocks')->hasNode('content')) { + return $module->getNode('blocks')->getNode('content')->getNode(0)->getNode('body'); + } + + return new Node(); + } } diff --git a/src/TwigComponent/src/Twig/TwigComponentNode.php b/src/TwigComponent/src/Twig/TwigComponentNode.php deleted file mode 100644 index 3f760f9004f..00000000000 --- a/src/TwigComponent/src/Twig/TwigComponentNode.php +++ /dev/null @@ -1,127 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\TwigComponent\Twig; - -use Symfony\UX\TwigComponent\ComponentAttributes; -use Symfony\UX\TwigComponent\ComponentMetadata; -use Twig\Compiler; -use Twig\Node\EmbedNode; -use Twig\Node\Expression\AbstractExpression; -use Twig\Node\Node; - -/** - * thanks to @giorgiopogliani! - * This file is inspired by: https://github.com/giorgiopogliani/twig-components. - * - * @author Mathéo Daninos - * - * @internal - */ -class TwigComponentNode extends EmbedNode -{ - public function __construct(string $componentName, string $template, ?AbstractExpression $variables, int $lineno, $index, $tag, bool $only, Node $slot, ?ComponentMetadata $componentMetadata) - { - parent::__construct($template, $index, $variables, $only, false, $lineno, $tag); - - $this->setAttribute('component', $componentName); - $this->setAttribute('componentMetadata', $componentMetadata); - - $this->setNode('slot', $slot); - } - - public function compile(Compiler $compiler): void - { - $compiler->addDebugInfo($this); - - $compiler - ->write('$slotsStack = $slotsStack ?? [];'.\PHP_EOL) - ; - - if ($this->getAttribute('componentMetadata') instanceof ComponentMetadata) { - $this->addComponentProps($compiler); - } - - $compiler - ->write('ob_start();'.\PHP_EOL) - ->subcompile($this->getNode('slot')) - ->write('$slot = ob_get_clean();'.\PHP_EOL) - ; - - $compiler - ->write("\$slotsStack['content'] = new ".ComponentSlot::class." (\$slot);\n") - ; - - $this->addGetTemplate($compiler); - $compiler->raw('->display('); - - $this->addTemplateArguments($compiler); - $compiler->raw(");\n"); - } - - protected function addTemplateArguments(Compiler $compiler) - { - $compiler - ->indent(1) - ->write("\n") - ->write("array_merge(\n") - ; - - if ($this->getAttribute('componentMetadata') instanceof ComponentMetadata) { - $compiler->write('$props,'.\PHP_EOL); - } - - $compiler - ->write('$context,[') - ->write("'slots' => \$slotsStack,") - ; - - if (!$this->getAttribute('componentMetadata') instanceof ComponentMetadata) { - $compiler->write("'attributes' => new ".ComponentAttributes::class.'('); - - if ($this->hasNode('variables')) { - $compiler->subcompile($this->getNode('variables')); - } else { - $compiler->raw('[]'); - } - - $compiler->write(")\n"); - } - - $compiler - ->indent(-1) - ->write('],'); - - if ($this->hasNode('variables')) { - $compiler->subcompile($this->getNode('variables')); - } else { - $compiler->raw('[]'); - } - - $compiler->write(")\n"); - } - - private function addComponentProps(Compiler $compiler) - { - $compiler - ->raw('$props = $this->extensions[') - ->string(ComponentExtension::class) - ->raw(']->embeddedContext(') - ->string($this->getAttribute('component')) - ->raw(', ') - ->raw('twig_to_array(') - ->subcompile($this->getNode('variables')) - ->raw('), ') - ->raw($this->getAttribute('only') ? '[]' : '$context') - ->raw(");\n") - ; - } -} diff --git a/src/TwigComponent/src/Twig/TwigComponentTokenParser.php b/src/TwigComponent/src/Twig/TwigComponentTokenParser.php deleted file mode 100644 index 63f7aee357b..00000000000 --- a/src/TwigComponent/src/Twig/TwigComponentTokenParser.php +++ /dev/null @@ -1,184 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\TwigComponent\Twig; - -use Symfony\UX\TwigComponent\ComponentFactory; -use Twig\Environment; -use Twig\Node\Expression\AbstractExpression; -use Twig\Node\Expression\ArrayExpression; -use Twig\Node\Expression\ConstantExpression; -use Twig\Node\Expression\NameExpression; -use Twig\Node\ModuleNode; -use Twig\Node\Node; -use Twig\Token; -use Twig\TokenParser\AbstractTokenParser; - -/** - * thanks to @giorgiopogliani! - * This file is inspired by: https://github.com/giorgiopogliani/twig-components. - * - * @author Mathéo Daninos - * - * @internal - */ -final class TwigComponentTokenParser extends AbstractTokenParser -{ - /** @var ComponentFactory|callable():ComponentFactory */ - private $factory; - - private Environment $environment; - - /** - * @param callable():ComponentFactory $factory - */ - public function __construct( - callable $factory, - Environment $environment - ) { - $this->factory = $factory; - $this->environment = $environment; - } - - public function parse(Token $token): Node - { - $stream = $this->parser->getStream(); - $parent = $this->parser->getExpressionParser()->parseExpression(); - $componentName = $this->componentName($parent); - $componentMetadata = $this->factory()->metadataForTwigComponent($componentName); - - [$variables, $only] = $this->parseArguments(); - - if (null === $variables) { - $variables = new ArrayExpression([], $parent->getTemplateLine()); - } - - $parentToken = new Token(Token::STRING_TYPE, $this->getTemplatePath($componentName), $token->getLine()); - $fakeParentToken = new Token(Token::STRING_TYPE, '__parent__', $token->getLine()); - - // inject a fake parent to make the parent() function work - $stream->injectTokens([ - new Token(Token::BLOCK_START_TYPE, '', $token->getLine()), - new Token(Token::NAME_TYPE, 'extends', $token->getLine()), - $parentToken, - new Token(Token::BLOCK_END_TYPE, '', $token->getLine()), - ]); - - $module = $this->parser->parse($stream, fn (Token $token) => $token->test("end_{$this->getTag()}"), true); - - $slot = $this->getSlotFromBlockContent($module); - - // override the parent with the correct one - if ($fakeParentToken === $parentToken) { - $module->setNode('parent', $parent); - } - - $this->parser->embedTemplate($module); - - $stream->expect(Token::BLOCK_END_TYPE); - - return new TwigComponentNode( - $componentName, - $module->getTemplateName(), - $variables, $token->getLine(), - $module->getAttribute('index'), - $this->getTag(), - $only, - $slot, - $componentMetadata - ); - } - - public function getTag(): string - { - return 'twig_component'; - } - - private function componentName(AbstractExpression $expression): string - { - if ($expression instanceof ConstantExpression) { // using {% component 'name' %} - return $expression->getAttribute('value'); - } - - if ($expression instanceof NameExpression) { // using {% component name %} - return $expression->getAttribute('name'); - } - - throw new \LogicException('Could not parse component name.'); - } - - private function factory(): ComponentFactory - { - if (\is_callable($this->factory)) { - $this->factory = ($this->factory)(); - } - - return $this->factory; - } - - private function parseArguments(): array - { - $stream = $this->parser->getStream(); - - $variables = null; - - if ($stream->nextIf(Token::NAME_TYPE, 'with')) { - $variables = $this->parser->getExpressionParser()->parseExpression(); - } - - $only = false; - - if ($stream->nextIf(Token::NAME_TYPE, 'only')) { - $only = true; - } - - $stream->expect(Token::BLOCK_END_TYPE); - - return [$variables, $only]; - } - - private function getTemplatePath(string $name): string - { - $loader = $this->environment->getLoader(); - $componentPath = rtrim(str_replace('.', '/', $name)); - - if (($componentMetadata = $this->factory->metadataForTwigComponent($name)) !== null) { - return $componentMetadata->getTemplate(); - } - - if ($loader->exists($componentPath)) { - return $componentPath; - } - - if ($loader->exists($componentPath.'.html.twig')) { - return $componentPath.'.html.twig'; - } - - if ($loader->exists('components/'.$componentPath)) { - return 'components/'.$componentPath; - } - - if ($loader->exists('/components/'.$componentPath.'.html.twig')) { - return '/components/'.$componentPath.'.html.twig'; - } - - throw new \LogicException("No template found for: {$name}"); - } - - private function getSlotFromBlockContent(ModuleNode $module): Node - { - if ($module->getNode('blocks')->hasNode('content')) { - return $module->getNode('blocks')->getNode('content')->getNode(0)->getNode('body'); - } - - return new Node(); - } -} diff --git a/src/TwigComponent/src/Twig/TwigPreLexer.php b/src/TwigComponent/src/Twig/TwigPreLexer.php index 172edd5859b..52eb7b34632 100644 --- a/src/TwigComponent/src/Twig/TwigPreLexer.php +++ b/src/TwigComponent/src/Twig/TwigPreLexer.php @@ -101,9 +101,9 @@ public function preLexComponents(string $input): string // use the simpler component() format, so that the system doesn't think // this is an "embedded" component with blocks // see https://github.com/symfony/ux/issues/810 - $output .= "{% twig_component '{$componentName}'".($attributes ? " with { {$attributes} }" : '').' %}{% end_twig_component %}'; + $output .= "{% component '{$componentName}'".($attributes ? " with { {$attributes} }" : '').' %}{% endcomponent %}'; } else { - $output .= "{% twig_component '{$componentName}'".($attributes ? " with { {$attributes} }" : '').' %}'; + $output .= "{% component '{$componentName}'".($attributes ? " with { {$attributes} }" : '').' %}'; } continue; @@ -127,7 +127,7 @@ public function preLexComponents(string $input): string $output .= '{% endblock %}'; } - $output .= '{% end_twig_component %}'; + $output .= '{% endcomponent %}'; continue; } diff --git a/src/TwigComponent/tests/Integration/ComponentExtensionTest.php b/src/TwigComponent/tests/Integration/ComponentExtensionTest.php index 234ed433c82..642c693b70d 100644 --- a/src/TwigComponent/tests/Integration/ComponentExtensionTest.php +++ b/src/TwigComponent/tests/Integration/ComponentExtensionTest.php @@ -169,6 +169,84 @@ public function testTwigComponent(): void $this->assertStringContainsString('propB: prop b value', $output); } + public function testComponentSyntaxOpenTags(): void + { + $output = self::getContainer()->get(Environment::class)->render('tags/open_tag.html.twig'); + + $this->assertStringContainsString('propA: 1', $output); + $this->assertStringContainsString('propB: hello', $output); + } + + public function testRenderTwigComponentWithSlot(): void + { + $output = self::getContainer()->get(Environment::class)->render('slot/render_with_default_slot.html.twig'); + + $this->assertStringContainsString('You have a new message!', $output); + } + + public function testRenderTwigComponentWithAttributes(): void + { + $output = self::getContainer()->get(Environment::class)->render('slot/render_with_attributes.html.twig'); + + $this->assertStringContainsString('You have a new message!', $output); + $this->assertStringContainsString('background: red', $output); + } + + public function testRenderTwigComponentWithCustomSlot(): void + { + $output = self::getContainer()->get(Environment::class)->render('slot/render_with_custom_slot.html.twig'); + + $this->assertStringContainsString('You have a new message!', $output); + $this->assertStringContainsString('background: red', $output); + $this->assertStringContainsString('from @fapbot', $output); + } + + public function testRenderStaticTwigComponent(): void + { + $output = self::getContainer()->get(Environment::class)->render('slot/render_static_components.html.twig'); + + $this->assertStringContainsString('Submit!', $output); + } + + public function testRenderStaticTwigComponentWithAttributes(): void + { + $output = self::getContainer()->get(Environment::class)->render('slot/use_attribute_variables.html.twig'); + + $this->assertStringContainsString('Submit!', $output); + $this->assertStringContainsString('class="btn btn-primary"', $output); + } + + public function testRenderStaticComponentInSubFolder(): void + { + $output = self::getContainer()->get(Environment::class)->render('slot/render_static_component_in_sub_folder.html.twig'); + + $this->assertStringContainsString('Hello from a sub folder', $output); + } + + public function testRenderNestedComponents(): void + { + $output = self::getContainer()->get(Environment::class)->render('slot/render_nested_component.html.twig'); + + $this->assertStringContainsString('You have a new message from @fabot', $output); + $this->assertStringContainsString('Go to the message', $output); + } + + public function testPassDefaultSlotToChildComponents(): void + { + $output = self::getContainer()->get(Environment::class)->render('slot/pass_default_slot_to_child.html.twig'); + + $this->assertStringContainsString('', $output); + } + + public function testCanRenderMixOfBlockAndSlot(): void + { + $output = self::getContainer()->get(Environment::class)->render('slot/render_mix_of_slot_and_blocks.html.twig'); + + $this->assertStringContainsString('

You have an alert!

', $output); + $this->assertStringContainsString('Hey!', $output); + $this->assertStringContainsString('from @bob', $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/Integration/TwigComponentExtensionTest.php b/src/TwigComponent/tests/Integration/TwigComponentExtensionTest.php deleted file mode 100644 index e6521911042..00000000000 --- a/src/TwigComponent/tests/Integration/TwigComponentExtensionTest.php +++ /dev/null @@ -1,110 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\TwigComponent\Tests\Integration; - -use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Twig\Environment; - -/** - * @author Mathèo Daninos - * - * @internal - */ -class TwigComponentExtensionTest extends KernelTestCase -{ - public function testComponentSyntaxOpenTags(): void - { - $output = self::getContainer()->get(Environment::class)->render('tags/open_tag.html.twig'); - - $this->assertStringContainsString('propA: 1', $output); - $this->assertStringContainsString('propB: hello', $output); - } - - public function testRenderTwigComponentWithSlot(): void - { - $output = self::getContainer()->get(Environment::class)->render('slot/render_with_default_slot.html.twig'); - - $this->assertStringContainsString('You have a new message!', $output); - } - - public function testRenderTwigComponentWithAttributes(): void - { - $output = self::getContainer()->get(Environment::class)->render('slot/render_with_attributes.html.twig'); - - $this->assertStringContainsString('You have a new message!', $output); - $this->assertStringContainsString('background: red', $output); - } - - public function testRenderTwigComponentWithCustomSlot(): void - { - $output = self::getContainer()->get(Environment::class)->render('slot/render_with_custom_slot.html.twig'); - - $this->assertStringContainsString('You have a new message!', $output); - $this->assertStringContainsString('background: red', $output); - $this->assertStringContainsString('from @fapbot', $output); - } - - public function testRenderStaticTwigComponent(): void - { - $output = self::getContainer()->get(Environment::class)->render('slot/render_static_components.html.twig'); - - $this->assertStringContainsString('Submit!', $output); - } - - public function testRenderStaticTwigComponentWithAttributes(): void - { - $output = self::getContainer()->get(Environment::class)->render('slot/use_attribute_variables.html.twig'); - - $this->assertStringContainsString('Submit!', $output); - $this->assertStringContainsString('class="btn btn-primary"', $output); - } - - public function testRenderStaticComponentInSubFolder(): void - { - $output = self::getContainer()->get(Environment::class)->render('slot/render_static_component_in_sub_folder.html.twig'); - - $this->assertStringContainsString('Hello from a sub folder', $output); - } - - public function testRenderNestedComponents(): void - { - $output = self::getContainer()->get(Environment::class)->render('slot/render_nested_component.html.twig'); - - $this->assertStringContainsString('You have a new message from @fabot', $output); - $this->assertStringContainsString('Go to the message', $output); - } - - public function testPassDefaultSlotToChildComponents(): void - { - $output = self::getContainer()->get(Environment::class)->render('slot/pass_default_slot_to_child.html.twig'); - - $this->assertStringContainsString('', $output); - } - - public function testCanRenderEmbeddedComponent(): void - { - $output = self::getContainer()->get(Environment::class)->render('slot/render_block.html.twig'); - - $this->assertStringContainsString('
', $output); - $this->assertStringContainsString('custom th (key)', $output); - $this->assertStringContainsString('custom td (1)', $output); - } - - public function testCanRenderMixOfBlockAndSlot(): void - { - $output = self::getContainer()->get(Environment::class)->render('slot/render_mix_of_slot_and_blocks.html.twig'); - - $this->assertStringContainsString('

You have an alert!

', $output); - $this->assertStringContainsString('Hey!', $output); - $this->assertStringContainsString('from @bob', $output); - } -} diff --git a/src/TwigComponent/tests/Unit/TwigPreLexerTest.php b/src/TwigComponent/tests/Unit/TwigPreLexerTest.php index 9d3afc72d7a..f76e48a82c1 100644 --- a/src/TwigComponent/tests/Unit/TwigPreLexerTest.php +++ b/src/TwigComponent/tests/Unit/TwigPreLexerTest.php @@ -29,98 +29,98 @@ public function getLexTests(): iterable { yield 'simple_component' => [ '', - '{% twig_component \'foo\' %}{% end_twig_component %}', + '{% component \'foo\' %}{% endcomponent %}', ]; yield 'component_with_attributes' => [ '', - "{% twig_component 'foo' with { bar: 'baz', with_quotes: 'It\'s with quotes' } %}{% end_twig_component %}", + "{% component 'foo' with { bar: 'baz', with_quotes: 'It\'s with quotes' } %}{% endcomponent %}", ]; yield 'component_with_dynamic_attributes' => [ '', - '{% twig_component \'foo\' with { dynamic: (dynamicVar), otherDynamic: anotherVar } %}{% end_twig_component %}', + '{% component \'foo\' with { dynamic: (dynamicVar), otherDynamic: anotherVar } %}{% endcomponent %}', ]; yield 'component_with_closing_tag' => [ '', - '{% twig_component \'foo\' %}{% end_twig_component %}', + '{% component \'foo\' %}{% endcomponent %}', ]; yield 'component_with_block' => [ 'Foo', - '{% twig_component \'foo\' %}{% block foo_block %}Foo{% endblock %}{% end_twig_component %}', + '{% component \'foo\' %}{% block foo_block %}Foo{% endblock %}{% endcomponent %}', ]; yield 'component_with_traditional_block' => [ '{% block foo_block %}Foo{% endblock %}', - '{% twig_component \'foo\' %}{% block foo_block %}Foo{% endblock %}{% end_twig_component %}', + '{% component \'foo\' %}{% block foo_block %}Foo{% endblock %}{% endcomponent %}', ]; yield 'traditional_blocks_around_component_do_not_confuse' => [ 'Hello {% block foo_block %}Foo{% endblock %}{% block bar_block %}Bar{% endblock %}', - 'Hello {% block foo_block %}Foo{% endblock %}{% twig_component \'foo\' %}{% end_twig_component %}{% block bar_block %}Bar{% endblock %}', + 'Hello {% block foo_block %}Foo{% endblock %}{% component \'foo\' %}{% endcomponent %}{% block bar_block %}Bar{% endblock %}', ]; yield 'component_with_embedded_component_inside_block' => [ '', - '{% twig_component \'foo\' %}{% block foo_block %}{% twig_component \'bar\' %}{% end_twig_component %}{% endblock %}{% end_twig_component %}', + '{% component \'foo\' %}{% block foo_block %}{% component \'bar\' %}{% endcomponent %}{% endblock %}{% endcomponent %}', ]; yield 'attribute_with_no_value' => [ '', - '{% twig_component \'foo\' with { bar: true } %}{% end_twig_component %}', + '{% component \'foo\' with { bar: true } %}{% endcomponent %}', ]; yield 'attribute_with_no_value_and_no_attributes' => [ '', - '{% twig_component \'foo\' %}{% end_twig_component %}', + '{% component \'foo\' %}{% endcomponent %}', ]; yield 'component_with_default_block_content' => [ 'Foo', - '{% twig_component \'foo\' %}{% block content %}Foo{% endblock %}{% end_twig_component %}', + '{% component \'foo\' %}{% block content %}Foo{% endblock %}{% endcomponent %}', ]; yield 'component_with_default_block_that_holds_a_component_and_multi_blocks' => [ 'Foo Other block', - '{% twig_component \'foo\' %}{% block content %}Foo {% twig_component \'bar\' %}{% end_twig_component %}{% endblock %}{% block other_block %}Other block{% endblock %}{% end_twig_component %}', + '{% component \'foo\' %}{% block content %}Foo {% component \'bar\' %}{% endcomponent %}{% endblock %}{% block other_block %}Other block{% endblock %}{% endcomponent %}', ]; yield 'component_with_character_:_on_his_name' => [ '', - '{% twig_component \'foo:bar\' %}{% end_twig_component %}', + '{% component \'foo:bar\' %}{% endcomponent %}', ]; yield 'component_with_character_@_on_his_name' => [ '', - '{% twig_component \'@foo\' %}{% end_twig_component %}', + '{% component \'@foo\' %}{% endcomponent %}', ]; yield 'component_with_character_-_on_his_name' => [ '', - '{% twig_component \'foo-bar\' %}{% end_twig_component %}', + '{% component \'foo-bar\' %}{% endcomponent %}', ]; yield 'component_with_character_._on_his_name' => [ '', - '{% twig_component \'foo.bar\' %}{% end_twig_component %}', + '{% component \'foo.bar\' %}{% endcomponent %}', ]; yield 'nested_component_2_levels' => [ 'Hello World!', - '{% twig_component \'foo\' %}{% block child %}{% twig_component \'bar\' %}{% block message %}Hello World!{% endblock %}{% end_twig_component %}{% endblock %}{% end_twig_component %}', + '{% component \'foo\' %}{% block child %}{% component \'bar\' %}{% block message %}Hello World!{% endblock %}{% endcomponent %}{% endblock %}{% endcomponent %}', ]; yield 'component_with_mixture_of_string_and_twig_in_argument' => [ '', - "{% twig_component 'foo' with { text: 'Hello '~(name)~'!' } %}{% end_twig_component %}", + "{% component 'foo' with { text: 'Hello '~(name)~'!' } %}{% endcomponent %}", ]; yield 'component_with_mixture_of_dynamic_twig_from_start' => [ '', - "{% twig_component 'foo' with { text: (name)~' is my name'~(ending~'!!') } %}{% end_twig_component %}", + "{% component 'foo' with { text: (name)~' is my name'~(ending~'!!') } %}{% endcomponent %}", ]; yield 'dynamic_attribute_with_quotation_included' => [ '', - "{% twig_component 'foo' with { text: (\"hello!\") } %}{% end_twig_component %}", + "{% component 'foo' with { text: (\"hello!\") } %}{% endcomponent %}", ]; yield 'component_with_mixture_of_string_and_twig_with_quote_in_argument' => [ '', - "{% twig_component 'foo' with { text: 'Hello '~(name)~', I\'m Theo!' } %}{% end_twig_component %}", + "{% component 'foo' with { text: 'Hello '~(name)~', I\'m Theo!' } %}{% endcomponent %}", ]; yield 'component_where_entire_default_block_is_embedded_component' => [ << EOF, << [ @@ -141,9 +141,9 @@ public function getLexTests(): iterable EOF, << EOF, << [ '', - '{% twig_component \'foobar\' with { \'data-action\': \'foo#bar\' } %}{% end_twig_component %}', + '{% component \'foobar\' with { \'data-action\': \'foo#bar\' } %}{% endcomponent %}', ]; yield 'component_with_dashed_attribute_self_closing' => [ '', - '{% twig_component \'foobar\' with { \'data-action\': \'foo#bar\' } %}{% end_twig_component %}', + '{% component \'foobar\' with { \'data-action\': \'foo#bar\' } %}{% endcomponent %}', ]; yield 'component_with_colon_attribute' => [ '', - '{% twig_component \'foobar\' with { \'my:attribute\': \'yo\' } %}{% end_twig_component %}', + '{% component \'foobar\' with { \'my:attribute\': \'yo\' } %}{% endcomponent %}', ]; yield 'component_with_truthy_attribute' => [ '', - '{% twig_component \'foobar\' with { \'data-turbo-stream\': true } %}{% end_twig_component %}', + '{% component \'foobar\' with { \'data-turbo-stream\': true } %}{% endcomponent %}', ]; yield 'ignore_twig_comment' => [ '{# #} ', - '{# #} {% twig_component \'Alert\' %}{% end_twig_component %}', + '{# #} {% component \'Alert\' %}{% endcomponent %}', ]; yield 'file_ended_with_comments' => [ @@ -194,11 +194,11 @@ public function getLexTests(): iterable yield 'mixing_component_and_file_ended_with_comments' => [ ' {# #}', - '{% twig_component \'Alert\' %}{% end_twig_component %} {# #}', + '{% component \'Alert\' %}{% endcomponent %} {# #}', ]; yield 'slot_inside_block_content' => [ '{% block footer %}{{ user }}{% endblock %}

Hello

You have a new message
', - '{% twig_component \'Alert\' %}{% block footer %}{{ user }}{% endblock %}{% block content %}

Hello

{% slot message %}You have a new message{% endslot %}{% endblock %}{% end_twig_component %}', + '{% component \'Alert\' %}{% block footer %}{{ user }}{% endblock %}{% block content %}

Hello

{% slot message %}You have a new message{% endslot %}{% endblock %}{% endcomponent %}', ]; } } From 315ec1b275c271bd73bcbe2751c35e82c426d2c2 Mon Sep 17 00:00:00 2001 From: Matheo Daninos Date: Sun, 11 Jun 2023 15:46:49 +0200 Subject: [PATCH 13/13] add slot varialble --- src/TwigComponent/src/Twig/ComponentNode.php | 2 +- .../tests/Fixtures/templates/components/Alarm.html.twig | 2 +- .../tests/Fixtures/templates/components/Button.html.twig | 2 +- .../tests/Fixtures/templates/components/DangerButton.html.twig | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/TwigComponent/src/Twig/ComponentNode.php b/src/TwigComponent/src/Twig/ComponentNode.php index 4a9bc03f3cb..b89879219a2 100644 --- a/src/TwigComponent/src/Twig/ComponentNode.php +++ b/src/TwigComponent/src/Twig/ComponentNode.php @@ -52,7 +52,6 @@ public function compile(Compiler $compiler): void ->write('ob_start();'.\PHP_EOL) ->subcompile($this->getNode('slot')) ->write('$slot = ob_get_clean();'.\PHP_EOL) - ->write("\$slotsStack['content'] = new ".ComponentSlot::class." (\$slot);\n") ; $this->addGetTemplate($compiler); @@ -77,6 +76,7 @@ protected function addTemplateArguments(Compiler $compiler) $compiler ->write('$context,[') + ->write("'slot' => new ".ComponentSlot::class." (\$slot),\n") ->write("'slots' => \$slotsStack,") ; diff --git a/src/TwigComponent/tests/Fixtures/templates/components/Alarm.html.twig b/src/TwigComponent/tests/Fixtures/templates/components/Alarm.html.twig index b2f347c5734..19b517eb31a 100644 --- a/src/TwigComponent/tests/Fixtures/templates/components/Alarm.html.twig +++ b/src/TwigComponent/tests/Fixtures/templates/components/Alarm.html.twig @@ -3,7 +3,7 @@

You have an alert!

{% endblock %}
- {{ slots.content }} + {{ slot }}
{{ slots.footer|default('')|raw }} \ No newline at end of file diff --git a/src/TwigComponent/tests/Fixtures/templates/components/Button.html.twig b/src/TwigComponent/tests/Fixtures/templates/components/Button.html.twig index 20bd70c77a9..57ed56268b9 100644 --- a/src/TwigComponent/tests/Fixtures/templates/components/Button.html.twig +++ b/src/TwigComponent/tests/Fixtures/templates/components/Button.html.twig @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/TwigComponent/tests/Fixtures/templates/components/DangerButton.html.twig b/src/TwigComponent/tests/Fixtures/templates/components/DangerButton.html.twig index 3539c91a1cd..77dd9c1ee84 100644 --- a/src/TwigComponent/tests/Fixtures/templates/components/DangerButton.html.twig +++ b/src/TwigComponent/tests/Fixtures/templates/components/DangerButton.html.twig @@ -1 +1 @@ -
{{ slots.content }}

Danger Zone

\ No newline at end of file +
{{ slot }}

Danger Zone

\ No newline at end of file
{{ this.caption }}data table