Skip to content

Commit 27b8f57

Browse files
committed
[TwigComponent] attribute support
1 parent 582428b commit 27b8f57

File tree

6 files changed

+137
-7
lines changed

6 files changed

+137
-7
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\TwigComponent;
13+
14+
/**
15+
* @author Kevin Bond <kevinbond@gmail.com>
16+
*
17+
* @experimental
18+
*/
19+
final class ComponentAttributes
20+
{
21+
/**
22+
* @param array<string, string> $attributes
23+
*/
24+
public function __construct(public array $attributes)
25+
{
26+
}
27+
28+
public function __toString(): string
29+
{
30+
return \array_reduce(
31+
\array_keys($this->attributes),
32+
fn(string $carry, string $key) => \sprintf('%s %s="%s"', $carry, $key, $this->attributes[$key]),
33+
''
34+
);
35+
}
36+
37+
public function merge(array $with): self
38+
{
39+
foreach ($this->attributes as $key => $value) {
40+
$with[$key] = isset($with[$key]) ? "{$with[$key]} {$value}" : $value;
41+
}
42+
43+
return new self($with);
44+
}
45+
46+
public function only(string ...$keys): self
47+
{
48+
$attributes = [];
49+
50+
foreach ($this->attributes as $key => $value) {
51+
if (in_array($key, $keys, true)) {
52+
$attributes[$key] = $value;
53+
}
54+
}
55+
56+
return new self($attributes);
57+
}
58+
59+
public function without(string ...$keys): self
60+
{
61+
$clone = clone $this;
62+
63+
foreach ($keys as $key) {
64+
unset($clone->attributes[$key]);
65+
}
66+
67+
return $clone;
68+
}
69+
70+
/**
71+
* @param array<string, string> $attributes
72+
*/
73+
public function defaults(array $attributes): self
74+
{
75+
$clone = $this;
76+
77+
foreach ($attributes as $attribute => $value) {
78+
$clone->attributes[$attribute] = $clone->attributes[$attribute] ?? $value;
79+
}
80+
81+
return $clone;
82+
}
83+
84+
public function default(string $attribute, string $value): self
85+
{
86+
return $this->defaults([$attribute => $value]);
87+
}
88+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\TwigComponent;
13+
14+
/**
15+
* @author Kevin Bond <kevinbond@gmail.com>
16+
*
17+
* @experimental
18+
*/
19+
final class ComponentContext
20+
{
21+
public function __construct(public object $component, public ComponentAttributes $attributes)
22+
{
23+
}
24+
}

src/TwigComponent/src/ComponentFactory.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public function configFor($component, string $name = null): array
7878
/**
7979
* Creates the component and "mounts" it with the passed data.
8080
*/
81-
public function create(string $name, array $data = []): object
81+
public function create(string $name, array $data = []): ComponentContext
8282
{
8383
$component = $this->getComponent($name);
8484
$data = $this->preMount($component, $data);
@@ -88,13 +88,14 @@ public function create(string $name, array $data = []): object
8888
// set data that wasn't set in mount on the component directly
8989
foreach ($data as $property => $value) {
9090
if (!$this->propertyAccessor->isWritable($component, $property)) {
91-
throw new \LogicException(sprintf('Unable to write "%s" to component "%s". Make sure this is a writable property or create a mount() with a $%s argument.', $property, \get_class($component), $property));
91+
continue;
9292
}
9393

9494
$this->propertyAccessor->setValue($component, $property, $value);
95+
unset($data[$property]);
9596
}
9697

97-
return $component;
98+
return new ComponentContext($component, new ComponentAttributes($data));
9899
}
99100

100101
/**

src/TwigComponent/src/ComponentRenderer.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@ public function __construct(Environment $twig)
2727
$this->twig = $twig;
2828
}
2929

30-
public function render(object $component, string $template): string
30+
public function render(ComponentContext $context, string $template): string
3131
{
3232
// TODO: Self-Rendering components?
33-
return $this->twig->render($template, ['this' => $component]);
33+
return $this->twig->render($template, [
34+
'this' => $context->component,
35+
'attributes' => $context->attributes,
36+
]);
3437
}
3538
}

src/TwigComponent/src/Twig/ComponentExtension.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ final class ComponentExtension extends AbstractExtension
2424
public function getFunctions(): array
2525
{
2626
return [
27-
new TwigFunction('component', [ComponentRuntime::class, 'render'], ['is_safe' => ['all']]),
27+
new TwigFunction(
28+
'component',
29+
[ComponentRuntime::class, 'render'],
30+
['is_safe' => ['all'], 'needs_environment' => true]
31+
),
2832
];
2933
}
3034
}

src/TwigComponent/src/Twig/ComponentRuntime.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@
1111

1212
namespace Symfony\UX\TwigComponent\Twig;
1313

14+
use Symfony\UX\TwigComponent\ComponentAttributes;
1415
use Symfony\UX\TwigComponent\ComponentFactory;
1516
use Symfony\UX\TwigComponent\ComponentRenderer;
17+
use Twig\Environment;
18+
use Twig\Extension\EscaperExtension;
1619

1720
/**
1821
* @author Kevin Bond <kevinbond@gmail.com>
@@ -23,15 +26,22 @@ final class ComponentRuntime
2326
{
2427
private ComponentFactory $componentFactory;
2528
private ComponentRenderer $componentRenderer;
29+
private bool $safeClassesRegistered = false;
2630

2731
public function __construct(ComponentFactory $componentFactory, ComponentRenderer $componentRenderer)
2832
{
2933
$this->componentFactory = $componentFactory;
3034
$this->componentRenderer = $componentRenderer;
3135
}
3236

33-
public function render(string $name, array $props = []): string
37+
public function render(Environment $twig, string $name, array $props = []): string
3438
{
39+
if (!$this->safeClassesRegistered) {
40+
$twig->getExtension(EscaperExtension::class)->addSafeClass(ComponentAttributes::class, ['html']);
41+
42+
$this->safeClassesRegistered = true;
43+
}
44+
3545
return $this->componentRenderer->render(
3646
$this->componentFactory->create($name, $props),
3747
$this->componentFactory->configFor($name)['template']

0 commit comments

Comments
 (0)