Skip to content

Commit 04000ac

Browse files
authored
feat(view): add boolean attributes (#700)
1 parent 8c8bc96 commit 04000ac

File tree

5 files changed

+138
-21
lines changed

5 files changed

+138
-21
lines changed

src/Tempest/View/src/Attributes/AttributeFactory.php

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,21 @@
55
namespace Tempest\View\Attributes;
66

77
use Tempest\View\Attribute;
8+
use Tempest\View\Element;
89

910
final readonly class AttributeFactory
1011
{
11-
public function make(string $name): Attribute
12+
public function make(Element $element, string $attributeName): Attribute
1213
{
1314
return match(true) {
14-
$name === ':if' => new IfAttribute(),
15-
$name === ':elseif' => new ElseIfAttribute(),
16-
$name === ':else' => new ElseAttribute(),
17-
$name === ':foreach' => new ForeachAttribute(),
18-
$name === ':forelse' => new ForelseAttribute(),
19-
str_starts_with($name, ':') => new ExpressionAttribute($name),
20-
default => new DataAttribute($name),
15+
$attributeName === ':if' => new IfAttribute(),
16+
$attributeName === ':elseif' => new ElseIfAttribute(),
17+
$attributeName === ':else' => new ElseAttribute(),
18+
$attributeName === ':foreach' => new ForeachAttribute(),
19+
$attributeName === ':forelse' => new ForelseAttribute(),
20+
BooleanAttribute::matches($element, $attributeName) => new BooleanAttribute($attributeName),
21+
str_starts_with($attributeName, ':') => new ExpressionAttribute($attributeName),
22+
default => new DataAttribute($attributeName),
2123
};
2224
}
2325
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\View\Attributes;
6+
7+
use Exception;
8+
use Tempest\View\Attribute;
9+
use Tempest\View\Element;
10+
use Tempest\View\Elements\GenericElement;
11+
12+
final readonly class BooleanAttribute implements Attribute
13+
{
14+
public function __construct(
15+
private string $attributeName,
16+
) {
17+
}
18+
19+
public function apply(Element $element): ?Element
20+
{
21+
if (! $element instanceof GenericElement) {
22+
throw new Exception('This cannot happen');
23+
}
24+
25+
$element
26+
->addRawAttribute(sprintf(
27+
'<?php if(%s) { ?>%s<?php } ?>',
28+
$element->getAttribute($this->attributeName),
29+
ltrim($this->attributeName, ':'),
30+
))
31+
->unsetAttribute($this->attributeName);
32+
33+
return $element;
34+
}
35+
36+
public static function matches(Element $element, string $attributeName): bool
37+
{
38+
if (! $element instanceof GenericElement) {
39+
return false;
40+
}
41+
42+
$attributeName = ltrim($attributeName, ':');
43+
44+
$allowedElements = match ($attributeName) {
45+
'autofocus' => true,
46+
'allowfullscreen' => ['iframe'],
47+
'alpha', 'checked' => ['input'],
48+
'async', 'nomodule', 'defer' => ['script'],
49+
'autoplay', 'controls', 'loop', 'muted' => ['audio', 'video'],
50+
'default' => ['track'],
51+
'disabled' => ['link', 'fieldset', 'button', 'input', 'optgroup', 'option', 'select', 'textarea;'],
52+
'formnovalidate' => ['button', 'input'],
53+
'inert', 'itemscope' => ['HTML elements'],
54+
'ismap' => ['img'],
55+
'multiple' => ['input', 'select'],
56+
'open' => ['dialog', 'details'],
57+
'playsinline' => ['video'],
58+
'readonly' => ['input', 'textarea'],
59+
'required' => ['input', 'select', 'textarea'],
60+
'reversed' => ['ol'],
61+
'selected' => ['option'],
62+
'shadowrootclonable', 'shadowrootserializable', 'shadowrootdelegatesfocus' => ['template'],
63+
default => null,
64+
};
65+
66+
return match (true) {
67+
$allowedElements === null => false,
68+
$allowedElements === true => true,
69+
default => in_array($element->getTag(), $allowedElements),
70+
};
71+
}
72+
}

src/Tempest/View/src/Elements/GenericElement.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,27 @@ final class GenericElement implements Element
1111
{
1212
use IsElement;
1313

14+
private array $rawAttributes = [];
15+
1416
public function __construct(
1517
private readonly string $tag,
1618
array $attributes,
1719
) {
1820
$this->attributes = $attributes;
1921
}
2022

23+
public function getTag(): string
24+
{
25+
return $this->tag;
26+
}
27+
28+
public function addRawAttribute(string $attribute): self
29+
{
30+
$this->rawAttributes[] = $attribute;
31+
32+
return $this;
33+
}
34+
2135
public function compile(): string
2236
{
2337
$content = [];
@@ -46,7 +60,7 @@ public function compile(): string
4660
}
4761
}
4862

49-
$attributes = implode(' ', $attributes);
63+
$attributes = implode(' ', [...$attributes, ...$this->rawAttributes]);
5064

5165
if ($attributes !== '') {
5266
$attributes = ' ' . $attributes;

src/Tempest/View/src/Renderers/TempestViewCompiler.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ public function __construct(
3636

3737
public function compile(string $path): string
3838
{
39+
$this->elementFactory->setViewCompiler($this);
40+
3941
// 1. Retrieve template
4042
$template = $this->retrieveTemplate($path);
4143

@@ -99,9 +101,7 @@ private function mapToElements(DOMNodeList $domNodeList): array
99101
$elements = [];
100102

101103
foreach ($domNodeList as $node) {
102-
$element = $this->elementFactory
103-
->setViewCompiler($this)
104-
->make($node);
104+
$element = $this->elementFactory->make($node);
105105

106106
if ($element === null) {
107107
continue;
@@ -131,7 +131,7 @@ private function applyAttributes(array $elements): array
131131
->setChildren($children);
132132

133133
foreach ($element->getAttributes() as $name => $value) {
134-
$attribute = $this->attributeFactory->make($name);
134+
$attribute = $this->attributeFactory->make($element, $name);
135135

136136
$element = $attribute->apply($element);
137137

tests/Integration/View/TempestViewRendererDataPassingTest.php

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,12 @@ public function test_expression_attribute_with_object_on_view_component(): void
151151
{
152152
// <x-button :href="$object" /> 💯 always pass as variable, never set directly as attribute
153153

154-
$this->registerViewComponent('x-link', <<<'HTML'
154+
$this->registerViewComponent(
155+
'x-link',
156+
<<<'HTML'
155157
<a :href="$object->url"><x-slot/></a>
156-
HTML);
158+
HTML,
159+
);
157160

158161
$this->assertSame(
159162
'<a href="https://">a</a>',
@@ -172,9 +175,12 @@ public function test_expression_attribute_on_view_component(): void
172175
{
173176
// <x-button :href="$href" /> 💯 always pass as variable, never set directly as attribute
174177

175-
$this->registerViewComponent('x-link', <<<'HTML'
178+
$this->registerViewComponent(
179+
'x-link',
180+
<<<'HTML'
176181
<a :href="$href"><x-slot/></a>
177-
HTML);
182+
HTML,
183+
);
178184

179185
$this->assertSame(
180186
'<a href="https://">a</a>',
@@ -191,9 +197,12 @@ public function test_normal_attribute_on_view_component(): void
191197
{
192198
// <x-button href="http://…" /> 💯 always pass as variable, never set directly as attribute
193199

194-
$this->registerViewComponent('x-link', <<<'HTML'
200+
$this->registerViewComponent(
201+
'x-link',
202+
<<<'HTML'
195203
<a :href="$href"><x-slot/></a>
196-
HTML);
204+
HTML,
205+
);
197206

198207
$this->assertSame(
199208
'<a href="https://">a</a>',
@@ -209,9 +218,12 @@ public function test_expression_attribute_with_same_name(): void
209218
{
210219
// <x-button :href="$object" /> 💯 always pass as variable, never set directly as attribute
211220

212-
$this->registerViewComponent('x-link', <<<'HTML'
221+
$this->registerViewComponent(
222+
'x-link',
223+
<<<'HTML'
213224
<a :href="$href->url"><x-slot/></a>
214-
HTML);
225+
HTML,
226+
);
215227

216228
/* There's a name collision here:
217229
<?php $href = $href->url ?? null; ?>
@@ -233,4 +245,21 @@ public function test_expression_attribute_with_same_name(): void
233245
),
234246
);
235247
}
248+
249+
public function test_boolean_attributes(): void
250+
{
251+
$this->assertSame(
252+
'<option value="value" selected>name</option>',
253+
$this->render(<<<'HTML'
254+
<option value="<?= $value ?>" :selected="$selected"><?= $name ?></option>
255+
HTML, value: 'value', selected: true, name: 'name'),
256+
);
257+
258+
$this->assertSame(
259+
'<option value="value" >name</option>',
260+
$this->render(<<<'HTML'
261+
<option value="<?= $value ?>" :selected="$selected"><?= $name ?></option>
262+
HTML, value: 'value', selected: false, name: 'name'),
263+
);
264+
}
236265
}

0 commit comments

Comments
 (0)