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