Skip to content

Commit 06be1af

Browse files
authored
feat(view): dynamic view components (#1169)
1 parent 077cc7d commit 06be1af

File tree

4 files changed

+103
-3
lines changed

4 files changed

+103
-3
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
namespace Tempest\View\Components;
4+
5+
use Tempest\Container\Container;
6+
use Tempest\Core\AppConfig;
7+
use Tempest\View\Elements\ElementFactory;
8+
use Tempest\View\Elements\ViewComponentElement;
9+
use Tempest\View\GenericView;
10+
use Tempest\View\Parser\TempestViewCompiler;
11+
use Tempest\View\Parser\Token;
12+
use Tempest\View\ViewComponent;
13+
use Tempest\View\ViewConfig;
14+
use Tempest\View\ViewRenderer;
15+
16+
final class DynamicViewComponent implements ViewComponent
17+
{
18+
private Token $token;
19+
20+
public function __construct(
21+
private AppConfig $appConfig,
22+
private TempestViewCompiler $compiler,
23+
private ElementFactory $elementFactory,
24+
private ViewConfig $viewConfig,
25+
) {}
26+
27+
public function setToken(Token $token): void
28+
{
29+
$this->token = $token;
30+
}
31+
32+
public static function getName(): string
33+
{
34+
return 'x-dynamic-component';
35+
}
36+
37+
public function compile(ViewComponentElement $element): string
38+
{
39+
$name = $this->token->getAttribute('is') ?? $this->token->getAttribute(':is');
40+
$isExpression = $this->token->getAttribute(':is') !== null;
41+
42+
return sprintf(
43+
'<?php eval(\'?>\' . \Tempest\get(%s::class)->render(%s, %s)); ?>',
44+
self::class,
45+
$isExpression ? $name : "'{$name}'",
46+
var_export($element->getAttributes(), true), // @mago-expect best-practices/no-debug-symbols
47+
);
48+
}
49+
50+
public function render(string $name, array $attributes): string
51+
{
52+
$viewComponent = $this->viewConfig->viewComponents[$name] ?? null;
53+
54+
$element = new ViewComponentElement(
55+
environment: $this->appConfig->environment,
56+
compiler: $this->compiler,
57+
elementFactory: $this->elementFactory,
58+
viewComponent: $viewComponent,
59+
attributes: $attributes,
60+
);
61+
62+
return $element->compile();
63+
}
64+
}

src/Tempest/View/src/Elements/ElementFactory.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Tempest\Container\Container;
88
use Tempest\Core\AppConfig;
99
use Tempest\View\Attributes\PhpAttribute;
10+
use Tempest\View\Components\DynamicViewComponent;
1011
use Tempest\View\Element;
1112
use Tempest\View\Parser\TempestViewCompiler;
1213
use Tempest\View\Parser\Token;
@@ -81,6 +82,11 @@ private function makeElement(Token $token, ?Element $parent): ?Element
8182
}
8283

8384
if ($viewComponentClass = $this->viewConfig->viewComponents[$token->tag] ?? null) {
85+
if ($token->getAttribute('is') || $token->getAttribute(':is')) {
86+
$viewComponentClass = $this->container->get(DynamicViewComponent::class);
87+
$viewComponentClass->setToken($token);
88+
}
89+
8490
if (! ($viewComponentClass instanceof ViewComponent)) {
8591
$viewComponentClass = $this->container->get($viewComponentClass);
8692
}

src/Tempest/View/src/Elements/ViewComponentElement.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
namespace Tempest\View\Elements;
66

7-
use Dom\HTMLDocument;
87
use Tempest\Core\Environment;
98
use Tempest\View\Element;
109
use Tempest\View\Parser\TempestViewCompiler;
@@ -15,8 +14,6 @@
1514
use function Tempest\Support\arr;
1615
use function Tempest\Support\str;
1716

18-
use const Dom\HTML_NO_DEFAULT_NS;
19-
2017
final class ViewComponentElement implements Element
2118
{
2219
use IsElement;

tests/Integration/View/ViewComponentTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,39 @@ public function test_nested_table_components(): void
777777
HTML, $html);
778778
}
779779

780+
public function test_dynamic_view_component_with_string_name(): void
781+
{
782+
$this->registerViewComponent('x-test', '<div>{{ $prop }}</div>');
783+
784+
$html = $this->render(<<<'HTML'
785+
<x-component is="x-test" prop="test"/>
786+
HTML);
787+
788+
$this->assertSame('<div>test</div>', $html);
789+
}
790+
791+
public function test_dynamic_view_component_with_expression_name(): void
792+
{
793+
$this->registerViewComponent('x-test', '<div>{{ $prop }}</div>');
794+
795+
$html = $this->render(<<<'HTML'
796+
<x-component :is="$name" prop="test" />
797+
HTML, name: 'x-test');
798+
799+
$this->assertSame('<div>test</div>', $html);
800+
}
801+
802+
public function test_dynamic_view_component_with_variable_attribute(): void
803+
{
804+
$this->registerViewComponent('x-test', '<div>{{ $prop }}</div>');
805+
806+
$html = $this->render(<<<'HTML'
807+
<x-component :is="$name" :prop="$input" />
808+
HTML, name: 'x-test', input: 'test');
809+
810+
$this->assertSame('<div>test</div>', $html);
811+
}
812+
780813
private function assertSnippetsMatch(string $expected, string $actual): void
781814
{
782815
$expected = str_replace([PHP_EOL, ' '], '', $expected);

0 commit comments

Comments
 (0)