Skip to content

Commit 1ba1629

Browse files
authored
feat(view): support dynamic $slots and x-template (#911)
1 parent 3f8a330 commit 1ba1629

15 files changed

+276
-64
lines changed

src/Tempest/View/src/Components/AnonymousViewComponent.php

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@
1010
final readonly class AnonymousViewComponent implements ViewComponent
1111
{
1212
public function __construct(
13-
private string $contents,
14-
private string $file,
15-
) {
16-
}
13+
public string $contents,
14+
public string $file,
15+
) {}
1716

1817
public static function getName(): string
1918
{
@@ -24,9 +23,4 @@ public function compile(ViewComponentElement $element): string
2423
{
2524
return $this->contents;
2625
}
27-
28-
public function getPath(): string
29-
{
30-
return $this->file;
31-
}
32-
}
26+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,14 @@ private function makeElement(Node $node, ?Element $parent): ?Element
101101
$viewComponentClass,
102102
$attributes,
103103
);
104+
} elseif ($tagName === 'x-template') {
105+
$element = new TemplateElement(
106+
attributes: $attributes,
107+
);
104108
} elseif ($tagName === 'x-slot') {
105109
$element = new SlotElement(
106110
name: $node->getAttribute('name') ?: 'slot',
111+
attributes: $attributes,
107112
);
108113
} else {
109114
$element = new GenericElement(

src/Tempest/View/src/Elements/SlotElement.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ final class SlotElement implements Element
1111
use IsElement;
1212

1313
public function __construct(
14-
private readonly string $name,
14+
public readonly string $name,
15+
array $attributes = [],
1516
) {
17+
$this->attributes = $attributes;
1618
}
1719

1820
public function matches(string $name): bool
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace Tempest\View\Elements;
4+
5+
use Tempest\View\Element;
6+
7+
final class TemplateElement implements Element
8+
{
9+
use IsElement;
10+
11+
public function __construct(
12+
array $attributes = [],
13+
)
14+
{
15+
$this->attributes = $attributes;
16+
}
17+
18+
public function compile(): string
19+
{
20+
$content = [];
21+
22+
foreach ($this->getChildren() as $child) {
23+
$content[] = $child->compile();
24+
}
25+
26+
return implode('', $content);
27+
}
28+
}

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

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
use Tempest\View\Element;
88
use Tempest\View\Renderers\TempestViewCompiler;
9+
use Tempest\View\Slot;
910
use Tempest\View\ViewComponent;
11+
use function Tempest\Support\arr;
1012
use function Tempest\Support\str;
1113

1214
final class ViewComponentElement implements Element
@@ -17,7 +19,8 @@ public function __construct(
1719
private readonly TempestViewCompiler $compiler,
1820
private readonly ViewComponent $viewComponent,
1921
array $attributes,
20-
) {
22+
)
23+
{
2124
$this->attributes = $attributes;
2225
}
2326

@@ -26,6 +29,22 @@ public function getViewComponent(): ViewComponent
2629
return $this->viewComponent;
2730
}
2831

32+
/** @return Element[] */
33+
public function getSlots(): array
34+
{
35+
$slots = [];
36+
37+
foreach ($this->getChildren() as $child) {
38+
if (! $child instanceof SlotElement) {
39+
continue;
40+
}
41+
42+
$slots[] = $child;
43+
}
44+
45+
return $slots;
46+
}
47+
2948
public function getSlot(string $name = 'slot'): ?Element
3049
{
3150
foreach ($this->getChildren() as $child) {
@@ -57,7 +76,25 @@ public function getSlot(string $name = 'slot'): ?Element
5776

5877
public function compile(): string
5978
{
79+
/** @var Slot[] $slots */
80+
$slots = arr($this->getSlots())
81+
->mapWithKeys(fn (SlotElement $element) => yield $element->name => Slot::fromElement($element))
82+
->toArray();
83+
6084
$compiled = str($this->viewComponent->compile($this))
85+
// Add dynamic slots to the current scope
86+
->prepend(
87+
'<?php $_previousSlots = $slots ?? null; ?>', // Store previous slots in temporary variable to keep scope
88+
sprintf('<?php $slots = %s; ?>', var_export($slots, true)), // Set the new value of $slots for this view component
89+
)
90+
91+
// Cleanup slots after the view component and restore slots from previous scope
92+
->append(
93+
'<?php unset($slots); ?>', // Unset current $slots
94+
'<?php $slots = $_previousSlots ?? null; ?>', // Restore previous $slots
95+
'<?php unset($_previousSlots); ?>', // Cleanup temporary $_previousSlots
96+
)
97+
6198
// Compile slots
6299
->replaceRegex(
63100
regex: '/<x-slot\s*(name="(?<name>\w+)")?((\s*\/>)|><\/x-slot>)/',

src/Tempest/View/src/Exceptions/DuplicateViewComponent.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ public function __construct(
1717
) {
1818
$message = sprintf(
1919
"Could not register view component `{$name}` from `%s`, because a component with the same name already exists in `%s`",
20-
$pending instanceof AnonymousViewComponent ? $pending->getPath() : $pending->getName(),
21-
$existing instanceof AnonymousViewComponent ? $existing->getPath() : $existing,
20+
$pending instanceof AnonymousViewComponent ? $pending->file : $pending->getName(),
21+
$existing instanceof AnonymousViewComponent ? $existing->file : $existing,
2222
);
2323

2424
parent::__construct($message);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Tempest\View\Exceptions;
4+
5+
use Exception;
6+
7+
final class ViewVariableIsReserved extends Exception
8+
{
9+
public function __construct(string $name)
10+
{
11+
parent::__construct('Cannot use reserved variable name `' . $name . '`');
12+
}
13+
}

src/Tempest/View/src/IsView.php

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,6 @@ public function __construct(
1919
$this->data = $data;
2020
}
2121

22-
public function getPath(): string
23-
{
24-
return $this->path;
25-
}
26-
27-
public function getData(): array
28-
{
29-
return $this->data;
30-
}
31-
3222
public function get(string $key): mixed
3323
{
3424
return $this->{$key} ?? $this->data[$key] ?? null;

src/Tempest/View/src/Renderers/BladeViewRenderer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@ public function __construct(private Blade $blade)
1616

1717
public function render(View|string|null $view): string
1818
{
19-
return $this->blade->render($view->getPath(), $view->getData());
19+
return $this->blade->render($view->path, $view->data);
2020
}
2121
}

src/Tempest/View/src/Renderers/TempestViewRenderer.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Stringable;
88
use Tempest\Support\HtmlString;
99
use Tempest\View\Exceptions\ViewCompilationError;
10+
use Tempest\View\Exceptions\ViewVariableIsReserved;
1011
use Tempest\View\GenericView;
1112
use Tempest\View\View;
1213
use Tempest\View\ViewCache;
@@ -38,9 +39,11 @@ public function render(string|View $view): string
3839
{
3940
$view = is_string($view) ? new GenericView($view) : $view;
4041

42+
$this->validateView($view);
43+
4144
$path = $this->viewCache->getCachedViewPath(
42-
path: $view->getPath(),
43-
compiledView: fn () => $this->cleanupCompiled($this->compiler->compile($view->getPath())),
45+
path: $view->path,
46+
compiledView: fn () => $this->cleanupCompiled($this->compiler->compile($view->path)),
4447
);
4548

4649
return $this->renderCompiled($view, $path);
@@ -82,7 +85,7 @@ private function renderCompiled(View $_view, string $_path): string
8285
ob_start();
8386

8487
// Extract data from view into local variables so that they can be accessed directly
85-
$_data = $_view->getData();
88+
$_data = $_view->data;
8689

8790
extract($_data, flags: EXTR_SKIP);
8891

@@ -105,4 +108,13 @@ public function escape(null|string|HtmlString|Stringable $value): string
105108

106109
return htmlentities((string)$value);
107110
}
111+
112+
private function validateView(View $view): void
113+
{
114+
$data = $view->data;
115+
116+
if (array_key_exists('slots', $data)) {
117+
throw new ViewVariableIsReserved('slots');
118+
}
119+
}
108120
}

0 commit comments

Comments
 (0)