Skip to content

Commit

Permalink
Add dynamic component (#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pindagus authored Jun 29, 2024
1 parent 9755a62 commit d31cd31
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 2 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,15 @@ Components can be included with the following:
</x-ns:button>
```

### 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' %}
<x-dynamic-component :component="componentName" class='bg-blue-600'>
<span class="text-lg">Click here!</span>
</x-dynamic-component>
```

## Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Expand Down
95 changes: 93 additions & 2 deletions src/Node/ComponentNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Performing\TwigComponents\Node;

use Exception;
use Performing\TwigComponents\Configuration;
use Performing\TwigComponents\View\ComponentAttributeBag;
use Performing\TwigComponents\View\ComponentSlot;
Expand All @@ -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)
Expand Down Expand Up @@ -55,28 +58,97 @@ 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())
->indent(-1)
->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")
Expand Down Expand Up @@ -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]);
}
}
}
}
45 changes: 45 additions & 0 deletions tests/ComponentsTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,49 @@ public function test_attributes_dont_conflict_with_components_in_components()

$this->assertEquals('<div class="mb-5 bg-red-500"><button class="text-white">Click me</button></div>', $html);
}

/** @test */
public function render_simple_dynamic_component()
{
$html = $this->twig->render('test_simple_dynamic_component.twig');

$this->assertEquals(<<<HTML
<button class="bg-blue-600 text-white"> test </button>
<button class="bg-blue-600 text-white"> test </button>
HTML, $html);
}

/** @test */
public function render_dynamic_component_with_xtags()
{
$html = $this->twig->render('test_xtags_dynamic_component.twig');

$this->assertEquals(<<<HTML
<button class="text-white bg-blue-800 rounded"> test1 </button>
<button class="text-white bg-blue-800 rounded"> test2 </button>
<button class="'text-white' bg-blue-800 rounded"> test3 </button>
HTML, $html);
}

/** @test */
public function render_namespaced_dynamic_component()
{
$html = $this->twig->render('test_namespaced_dynamic_component.twig');

$this->assertEquals(<<<HTML
<button class="bg-blue-600 ns-button text-white"> test </button>
HTML, $html);
}

/** @test */
public function render_namespaced_xtags_dynamic_component()
{
$html = $this->twig->render('test_namespaced_xtags_dynamic_component.twig');

$this->assertEquals(<<<HTML
<button class="bg-blue-600 ns-button text-white"> test1 </button>
<button class="bg-blue-600 ns-button text-white"> test2 </button>
<button class="'bg-blue-600' ns-button text-white"> test3 </button>
HTML, $html);
}
}
1 change: 1 addition & 0 deletions tests/templates/test_namespaced_dynamic_component.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% x:dynamic-component with {component: 'ns:button', class:'bg-blue-600'} %} test {% endx %}
6 changes: 6 additions & 0 deletions tests/templates/test_namespaced_xtags_dynamic_component.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<x-dynamic-component :component="'ns:button'" class="{{'bg-blue-' ~ '600' }}"> test1 </x-dynamic-component>

{% set componentName = 'ns:button' %}
<x-dynamic-component :component="componentName" :class="'bg-blue-600'"> test2 </x-dynamic-component>

<x-dynamic-component :component="componentName" class="'bg-blue-600'"> test3 </x-dynamic-component>
4 changes: 4 additions & 0 deletions tests/templates/test_simple_dynamic_component.twig
Original file line number Diff line number Diff line change
@@ -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 %}
6 changes: 6 additions & 0 deletions tests/templates/test_xtags_dynamic_component.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<x-dynamic-component :component="'simple-button.primary'" class="{{'text' ~ '-white' }}"> test1 </x-dynamic-component>

{% set componentName = 'simple-button.primary' %}
<x-dynamic-component :component="componentName" :class="'text-white'"> test2 </x-dynamic-component>

<x-dynamic-component :component="componentName" class="'text-white'"> test3 </x-dynamic-component>

0 comments on commit d31cd31

Please sign in to comment.