From d31cd318a2093c2c094e1f036481cc109ad6b66c Mon Sep 17 00:00:00 2001 From: Pindagus Date: Sat, 29 Jun 2024 23:41:37 +0200 Subject: [PATCH] Add dynamic component (#25) --- README.md | 9 ++ src/Node/ComponentNode.php | 95 ++++++++++++++++++- tests/ComponentsTestTrait.php | 45 +++++++++ .../test_namespaced_dynamic_component.twig | 1 + ...st_namespaced_xtags_dynamic_component.twig | 6 ++ .../test_simple_dynamic_component.twig | 4 + .../test_xtags_dynamic_component.twig | 6 ++ 7 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 tests/templates/test_namespaced_dynamic_component.twig create mode 100644 tests/templates/test_namespaced_xtags_dynamic_component.twig create mode 100644 tests/templates/test_simple_dynamic_component.twig create mode 100644 tests/templates/test_xtags_dynamic_component.twig diff --git a/README.md b/README.md index 7da863f..194ada2 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,15 @@ Components can be included with the following: ``` +### Dynamic Components +Sometimes you may need to render a component but not know which component should be rendered until runtime. In this situation, you may use the built-in dynamic-component component to render the component based on a runtime value or variable: +```twig +{% set componentName = 'button' %} + + Click here! + +``` + ## Contributing Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. diff --git a/src/Node/ComponentNode.php b/src/Node/ComponentNode.php index 06167cf..77e0f95 100644 --- a/src/Node/ComponentNode.php +++ b/src/Node/ComponentNode.php @@ -2,6 +2,7 @@ namespace Performing\TwigComponents\Node; +use Exception; use Performing\TwigComponents\Configuration; use Performing\TwigComponents\View\ComponentAttributeBag; use Performing\TwigComponents\View\ComponentSlot; @@ -14,6 +15,8 @@ final class ComponentNode extends IncludeNode { + const DYNAMIC_COMPONENT_NAME = 'dynamic-component'; + private Configuration $configuration; public function __construct(string $path, Node $slot, ?AbstractExpression $variables, int $lineno, Configuration $configuration) @@ -55,14 +58,16 @@ public function compile(Compiler $compiler): void protected function addGetTemplate(Compiler $compiler) { + $repr = $this->isDynamicComponent() ? 'raw' : 'repr'; + $compiler ->raw('$this->loadTemplate(' . PHP_EOL) ->indent(1) ->write('') - ->repr($this->getTemplateName()) + ->$repr($this->getTemplateName()) ->raw(', ' . PHP_EOL) ->write('') - ->repr($this->getTemplateName()) + ->$repr($this->getTemplateName()) ->raw(', ' . PHP_EOL) ->write('') ->repr($this->getTemplateLine()) @@ -70,13 +75,80 @@ protected function addGetTemplate(Compiler $compiler) ->raw(PHP_EOL . ');' . PHP_EOL . PHP_EOL); } + public function isDynamicComponent() + { + return strpos($this->getAttribute('path'), self::DYNAMIC_COMPONENT_NAME) !== false; + } + + public function getDynamicComponent() + { + $component = null; + + foreach (array_chunk($this->getNode('variables')->nodes, 2) as $pair) { + /** @var \Twig\Node\Expression\AbstractExpression $key */ + $key = $pair[0]; + /** @var \Twig\Node\Expression\AbstractExpression $value */ + $value = $pair[1]; + + if ($key->getAttribute('value') !== 'component') { + continue; + } + + if ($value->hasAttribute('value')) { + // Returns the component string value + $component = '\'' . $value->getAttribute('value') . '\''; + break; + } + + if ($value->hasAttribute('name')) { + // Uses the context to get the component value + $component = '($context[\'' . $value->getAttribute('name') . '\'] ?? null)'; + break; + } + } + + if (!$component) { + throw new Exception('Dynamic component must have a component attribute'); + } + + return self::class . "::parseDynamicComponent('{$this->getAttribute('path')}', $component)"; + } + + public static function parseDynamicComponent($path, $component) + { + $isNamespaced = strpos($component, ':') !== false; + + // Convert namespace to @ notation + if ($isNamespaced) { + $component = preg_replace('/([a-z\.-]*):([^\']*)/i', '@$1/$2', $component); + } + + // Converts dot notation to directory separator + $component = str_replace('.', DIRECTORY_SEPARATOR, $component); + + if ($isNamespaced) { + // Strip anything from the path before the dynamic component name, so it begins with the namespace + $dynamicComponentEndPosition = strpos($path, self::DYNAMIC_COMPONENT_NAME) + strlen(self::DYNAMIC_COMPONENT_NAME); + $pathEnd = substr($path, $dynamicComponentEndPosition); + return $component . $pathEnd; + } + + return str_replace(self::DYNAMIC_COMPONENT_NAME, $component, $path); + } + public function getTemplateName(): ?string { + if ($this->isDynamicComponent()) { + return $this->getDynamicComponent(); + } + return $this->getAttribute('path'); } protected function addTemplateArguments(Compiler $compiler) { + $this->filterVariables(); + $compiler ->indent(1) ->write("\n") @@ -110,4 +182,23 @@ protected function addTemplateArguments(Compiler $compiler) $compiler->write(")\n"); } + + public function filterVariables() + { + if (!$this->isDynamicComponent()) { + return; + } + + $variables = $this->getNode('variables'); + + foreach (array_chunk($variables->nodes, 2, true) as $pair) { + /** @var \Twig\Node\Expression\AbstractExpression $key */ + $key = array_values($pair)[0]; + + if ($key->getAttribute('value') === 'component') { + $variables->removeNode(array_keys($pair)[0]); + $variables->removeNode(array_keys($pair)[1]); + } + } + } } diff --git a/tests/ComponentsTestTrait.php b/tests/ComponentsTestTrait.php index a948890..549bfd5 100644 --- a/tests/ComponentsTestTrait.php +++ b/tests/ComponentsTestTrait.php @@ -144,4 +144,49 @@ public function test_attributes_dont_conflict_with_components_in_components() $this->assertEquals('
', $html); } + + /** @test */ + public function render_simple_dynamic_component() + { + $html = $this->twig->render('test_simple_dynamic_component.twig'); + + $this->assertEquals(<< test + + HTML, $html); + } + + /** @test */ + public function render_dynamic_component_with_xtags() + { + $html = $this->twig->render('test_xtags_dynamic_component.twig'); + + $this->assertEquals(<< test1 + + + HTML, $html); + } + + /** @test */ + public function render_namespaced_dynamic_component() + { + $html = $this->twig->render('test_namespaced_dynamic_component.twig'); + + $this->assertEquals(<< test + HTML, $html); + } + + /** @test */ + public function render_namespaced_xtags_dynamic_component() + { + $html = $this->twig->render('test_namespaced_xtags_dynamic_component.twig'); + + $this->assertEquals(<< test1 + + + HTML, $html); + } } diff --git a/tests/templates/test_namespaced_dynamic_component.twig b/tests/templates/test_namespaced_dynamic_component.twig new file mode 100644 index 0000000..2ad835b --- /dev/null +++ b/tests/templates/test_namespaced_dynamic_component.twig @@ -0,0 +1 @@ +{% x:dynamic-component with {component: 'ns:button', class:'bg-blue-600'} %} test {% endx %} \ No newline at end of file diff --git a/tests/templates/test_namespaced_xtags_dynamic_component.twig b/tests/templates/test_namespaced_xtags_dynamic_component.twig new file mode 100644 index 0000000..a315644 --- /dev/null +++ b/tests/templates/test_namespaced_xtags_dynamic_component.twig @@ -0,0 +1,6 @@ + test1 + +{% set componentName = 'ns:button' %} + test2 + + test3 \ No newline at end of file diff --git a/tests/templates/test_simple_dynamic_component.twig b/tests/templates/test_simple_dynamic_component.twig new file mode 100644 index 0000000..33d0418 --- /dev/null +++ b/tests/templates/test_simple_dynamic_component.twig @@ -0,0 +1,4 @@ +{% x:dynamic-component with {component: 'button', class:'bg-blue-600'} %} test {% endx %} + +{% set componentName = 'button' %} +{% x:dynamic-component with {component: componentName, class:'bg-blue-600'} %} test {% endx %} \ No newline at end of file diff --git a/tests/templates/test_xtags_dynamic_component.twig b/tests/templates/test_xtags_dynamic_component.twig new file mode 100644 index 0000000..1430ddc --- /dev/null +++ b/tests/templates/test_xtags_dynamic_component.twig @@ -0,0 +1,6 @@ + test1 + +{% set componentName = 'simple-button.primary' %} + test2 + + test3 \ No newline at end of file