Skip to content

Commit 0f6f261

Browse files
authored
feat(view): make default slot available as dynamic slot (#1419)
1 parent 55cb06f commit 0f6f261

File tree

5 files changed

+72
-84
lines changed

5 files changed

+72
-84
lines changed

packages/view/src/Elements/ElementFactory.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,15 @@
77
use Tempest\Container\Container;
88
use Tempest\Core\AppConfig;
99
use Tempest\View\Attributes\PhpAttribute;
10-
use Tempest\View\Components\AnonymousViewComponent;
1110
use Tempest\View\Components\DynamicViewComponent;
1211
use Tempest\View\Element;
1312
use Tempest\View\Parser\TempestViewCompiler;
1413
use Tempest\View\Parser\Token;
1514
use Tempest\View\Parser\TokenType;
15+
use Tempest\View\Slot;
1616
use Tempest\View\ViewComponent;
1717
use Tempest\View\ViewConfig;
1818

19-
use function Tempest\Support\str;
20-
2119
final class ElementFactory
2220
{
2321
private TempestViewCompiler $compiler;
@@ -104,7 +102,7 @@ private function makeElement(Token $token, ?Element $parent): ?Element
104102
);
105103
} elseif ($token->tag === 'x-slot') {
106104
$element = new SlotElement(
107-
name: $token->getAttribute('name') ?? 'slot',
105+
name: $token->getAttribute('name') ?? Slot::DEFAULT,
108106
attributes: $attributes,
109107
);
110108
} else {

packages/view/src/Elements/ViewComponentElement.php

Lines changed: 40 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Tempest\View\Elements;
66

77
use Tempest\Core\Environment;
8+
use Tempest\Support\Arr\ImmutableArray;
89
use Tempest\Support\Str\ImmutableString;
910
use Tempest\Support\Str\MutableString;
1011
use Tempest\View\Element;
@@ -41,58 +42,30 @@ public function getViewComponent(): ViewComponent
4142
return $this->viewComponent;
4243
}
4344

44-
/** @return Element[] */
45-
public function getSlots(): array
45+
/** @return ImmutableArray<array-key, Slot> */
46+
public function getSlots(): ImmutableArray
4647
{
47-
$slots = [];
48+
$slots = arr();
4849

49-
foreach ($this->getChildren() as $child) {
50-
if (! ($child instanceof SlotElement)) {
51-
continue;
52-
}
53-
54-
$slots[] = $child;
55-
}
50+
$default = [];
5651

57-
return $slots;
58-
}
59-
60-
public function getSlot(string $name = 'slot'): ?Element
61-
{
6252
foreach ($this->getChildren() as $child) {
63-
if (! ($child instanceof SlotElement)) {
64-
continue;
65-
}
53+
if ($child instanceof SlotElement) {
54+
$slot = Slot::fromElement($child);
6655

67-
if ($child->matches($name)) {
68-
return $child;
56+
$slots[$slot->name] = $slot;
57+
} else {
58+
$default[] = $child;
6959
}
7060
}
7161

72-
if ($name === 'slot') {
73-
$elements = [];
74-
75-
foreach ($this->getChildren() as $child) {
76-
if ($child instanceof SlotElement) {
77-
continue;
78-
}
79-
80-
$elements[] = $child;
81-
}
82-
83-
return new CollectionElement($elements);
84-
}
62+
$slots[Slot::DEFAULT] = Slot::fromElement(new CollectionElement($default));
8563

86-
return null;
64+
return $slots;
8765
}
8866

8967
public function compile(): string
9068
{
91-
/** @var Slot[] $slots */
92-
$slots = arr($this->getSlots())
93-
->mapWithKeys(fn (SlotElement $element) => yield $element->name => Slot::fromElement($element))
94-
->toArray();
95-
9669
$compiled = str($this->viewComponent->compile($this));
9770

9871
$compiled = $compiled
@@ -135,6 +108,9 @@ public function compile(): string
135108
},
136109
);
137110

111+
// Add scoped variables
112+
$slots = $this->getSlots()->toArray();
113+
138114
$compiled = $compiled
139115
->prepend(
140116
// Add attributes to the current scope
@@ -155,38 +131,39 @@ public function compile(): string
155131
'<?php unset($attributes); ?>',
156132
'<?php $attributes = $_previousAttributes ?? null; ?>',
157133
'<?php unset($_previousAttributes); ?>',
158-
)
159-
// Compile slots
160-
->replaceRegex(
161-
regex: '/<x-slot\s*(name="(?<name>[\w-]+)")?((\s*\/>)|>(?<default>(.|\n)*?)<\/x-slot>)/',
162-
replace: function ($matches) {
163-
$name = $matches['name'] ?: 'slot';
134+
);
164135

165-
$slot = $this->getSlot($name);
136+
// Compile slots
137+
$compiled = $compiled->replaceRegex(
138+
regex: '/<x-slot\s*(name="(?<name>[\w-]+)")?((\s*\/>)|>(?<default>(.|\n)*?)<\/x-slot>)/',
139+
replace: function ($matches) use ($slots) {
140+
$name = $matches['name'] ?: Slot::DEFAULT;
166141

167-
$default = $matches['default'] ?? null;
142+
$slot = $slots[$name] ?? null;
168143

169-
if ($slot === null) {
170-
if ($default) {
171-
// There's no slot, but there's a default value in the view component
172-
return $default;
173-
}
144+
$default = $matches['default'] ?? null;
174145

175-
// A slot doesn't have any content, so we'll comment it out.
176-
// This is to prevent DOM parsing errors (slots in <head> tags is one example, see #937)
177-
return $this->environment->isProduction() ? '' : ('<!--' . $matches[0] . '-->');
146+
if ($slot === null) {
147+
if ($default) {
148+
// There's no slot, but there's a default value in the view component
149+
return $default;
178150
}
179151

180-
$compiled = $slot->compile();
152+
// A slot doesn't have any content, so we'll comment it out.
153+
// This is to prevent DOM parsing errors (slots in <head> tags is one example, see #937)
154+
return $this->environment->isProduction() ? '' : ('<!--' . $matches[0] . '-->');
155+
}
181156

182-
// There's no default slot content, but there's a default value in the view component
183-
if (trim($compiled) === '') {
184-
return $default;
185-
}
157+
$compiled = $slot->content;
186158

187-
return $compiled;
188-
},
189-
);
159+
// There's no default slot content, but there's a default value in the view component
160+
if (trim($compiled) === '') {
161+
return $default;
162+
}
163+
164+
return $compiled;
165+
},
166+
);
190167

191168
return $this->compiler->compile($compiled->toString());
192169
}

packages/view/src/Slot.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44

55
namespace Tempest\View;
66

7+
use Tempest\View\Elements\CollectionElement;
78
use Tempest\View\Elements\SlotElement;
89

910
final class Slot
1011
{
12+
public const string DEFAULT = 'default';
13+
1114
public function __construct(
1215
public string $name,
1316
public array $attributes,
@@ -19,10 +22,10 @@ public function __get(string $name): mixed
1922
return $this->attributes[$name] ?? null;
2023
}
2124

22-
public static function fromElement(SlotElement $element): self
25+
public static function fromElement(SlotElement|CollectionElement $element): self
2326
{
2427
return new self(
25-
name: $element->name,
28+
name: $element->name ?? self::DEFAULT,
2629
attributes: $element->getAttributes(),
2730
content: $element->compile(),
2831
);

tests/Integration/View/TempestViewRendererTest.php

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -203,20 +203,14 @@ public function test_forelse_attribute(): void
203203

204204
public function test_default_slot(): void
205205
{
206-
$this->assertStringEqualsStringIgnoringLineEndings(
206+
$this->assertSnippetsMatch(
207207
<<<'HTML'
208-
<div class="base">
209-
210-
Test
211-
212-
</div>
208+
<div class="base">Test</div>
213209
HTML,
214210
$this->render(
215211
<<<'HTML'
216212
<x-base-layout>
217-
<x-slot>
218-
Test
219-
</x-slot>
213+
Test
220214
</x-base-layout>
221215
HTML,
222216
),

tests/Integration/View/ViewComponentTest.php

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public function test_view_component_with_php_code_in_slot(): void
6464
public function test_view_can_access_dynamic_slots(): void
6565
{
6666
$this->registerViewComponent('x-test', <<<'HTML'
67-
<div :foreach="$slots as $slot">
67+
<div :foreach="$slots as $slot" :if="$slot->name !== 'default'">
6868
<div>{{ $slot->name }}</div>
6969
<div>{{ $slot->attributes['language'] }}</div>
7070
<div>{{ $slot->language }}</div>
@@ -79,7 +79,7 @@ public function test_view_can_access_dynamic_slots(): void
7979
</x-test>
8080
HTML_WRAP);
8181

82-
$this->assertStringEqualsStringIgnoringLineEndings(<<<'HTML_WRAP'
82+
$this->assertSnippetsMatch(<<<'HTML_WRAP'
8383
<div><div>slot-php</div><div>PHP</div><div>PHP</div><div>PHP Body</div></div>
8484
<div><div>slot-html</div><div>HTML</div><div>HTML</div><div>HTML Body</div></div>
8585
HTML_WRAP, $html);
@@ -88,7 +88,7 @@ public function test_view_can_access_dynamic_slots(): void
8888
public function test_dynamic_slots_are_cleaned_up(): void
8989
{
9090
$this->registerViewComponent('x-test', <<<'HTML'
91-
<div :foreach="$slots as $slot">
91+
<div :foreach="$slots as $slot" :if="$slot->name !== 'default'">
9292
<div>{{ $slot->name }}</div>
9393
</div>
9494
<x-slot />
@@ -110,6 +110,24 @@ public function test_dynamic_slots_are_cleaned_up(): void
110110
$this->assertStringContainsString('<div>slots are cleared</div>', $html);
111111
}
112112

113+
public function test_dynamic_slots_include_the_default_slot(): void
114+
{
115+
$this->registerViewComponent('x-test', <<<'HTML'
116+
<div>{{ $slots['default']->name }}</div>
117+
<div>{{ $slots['default']->content }}</div>
118+
HTML);
119+
120+
$html = $this->render('<x-test>Hello</x-test>');
121+
122+
$this->assertSnippetsMatch(
123+
<<<'HTML'
124+
<div>default</div>
125+
<div>Hello</div>
126+
HTML,
127+
$html,
128+
);
129+
}
130+
113131
public function test_slots_with_nested_view_components(): void
114132
{
115133
$this->registerViewComponent('x-a', <<<'HTML'
@@ -863,9 +881,7 @@ public function test_nested_slots_with_escaping(): void
863881

864882
$html = $this->render(<<<'HTML'
865883
<x-a>
866-
<x-slot>
867-
<x-b />
868-
</x-slot>
884+
<x-b />
869885
</x-a>
870886
HTML);
871887

0 commit comments

Comments
 (0)