Skip to content

Commit 82f18ba

Browse files
committed
feature #255 [Twig][Live] Native attributes (kbond)
This PR was merged into the 2.x branch. Discussion ---------- [Twig][Live] Native attributes | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Tickets | n/a | License | MIT This removes the need for the `HasAttributesTrait` that was introduced in #220. Attributes are now "a thing all component have". `ComponentFactory::create()` returns a new `MountedComponent` object that holds the "mounted" component, attributes and component metadata. `LiveComponentHydrator::dehydrate()` now accepts an instance of `MountedComponent` and `LiveComponentHydrator::hydrate()` now returns an instance of `MountedComponent`. If the component is live, any attributes are dehydrated/hydrated as "readonly" data (they can't be modified by the frontend). **TODO** - [x] Update docs referring to `HasAttributesTrait` Commits ------- 37acff2 [Twig][Live] make attributes a "native" feature
2 parents 9f50cbb + 37acff2 commit 82f18ba

File tree

20 files changed

+213
-248
lines changed

20 files changed

+213
-248
lines changed

src/LiveComponent/src/EventListener/AddLiveAttributesSubscriber.php

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
use Symfony\Contracts\Service\ServiceSubscriberInterface;
1010
use Symfony\UX\LiveComponent\LiveComponentHydrator;
1111
use Symfony\UX\TwigComponent\ComponentAttributes;
12-
use Symfony\UX\TwigComponent\ComponentMetadata;
1312
use Symfony\UX\TwigComponent\EventListener\PreRenderEvent;
13+
use Symfony\UX\TwigComponent\MountedComponent;
1414
use Twig\Environment;
1515

1616
/**
@@ -29,7 +29,7 @@ public function onPreRender(PreRenderEvent $event): void
2929
return;
3030
}
3131

32-
$attributes = $this->getLiveAttributes($event->getComponent(), $event->getMetadata());
32+
$attributes = $this->getLiveAttributes($event->getMountedComponent());
3333
$variables = $event->getVariables();
3434

3535
if (isset($variables['attributes']) && $variables['attributes'] instanceof ComponentAttributes) {
@@ -57,12 +57,11 @@ public static function getSubscribedServices(): array
5757
];
5858
}
5959

60-
private function getLiveAttributes(object $component, ComponentMetadata $metadata): ComponentAttributes
60+
private function getLiveAttributes(MountedComponent $mounted): ComponentAttributes
6161
{
62-
$url = $this->container->get(UrlGeneratorInterface::class)
63-
->generate('live_component', ['component' => $metadata->getName()])
64-
;
65-
$data = $this->container->get(LiveComponentHydrator::class)->dehydrate($component);
62+
$name = $mounted->getName();
63+
$url = $this->container->get(UrlGeneratorInterface::class)->generate('live_component', ['component' => $name]);
64+
$data = $this->container->get(LiveComponentHydrator::class)->dehydrate($mounted);
6665
$twig = $this->container->get(Environment::class);
6766

6867
$attributes = [
@@ -73,7 +72,7 @@ private function getLiveAttributes(object $component, ComponentMetadata $metadat
7372

7473
if ($this->container->has(CsrfTokenManagerInterface::class)) {
7574
$attributes['data-live-csrf-value'] = $this->container->get(CsrfTokenManagerInterface::class)
76-
->getToken($metadata->getName())->getValue()
75+
->getToken($name)->getValue()
7776
;
7877
}
7978

src/LiveComponent/src/EventListener/LiveComponentSubscriber.php

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
use Symfony\UX\TwigComponent\ComponentFactory;
3434
use Symfony\UX\TwigComponent\ComponentMetadata;
3535
use Symfony\UX\TwigComponent\ComponentRenderer;
36+
use Symfony\UX\TwigComponent\MountedComponent;
3637

3738
/**
3839
* @author Kevin Bond <kevinbond@gmail.com>
@@ -73,6 +74,8 @@ public function onKernelRequest(RequestEvent $event): void
7374
$action = $request->get('action', 'get');
7475
$componentName = (string) $request->get('component');
7576

77+
$request->attributes->set('_component_name', $componentName);
78+
7679
try {
7780
/** @var ComponentMetadata $metadata */
7881
$metadata = $this->container->get(ComponentFactory::class)->metadataFor($componentName);
@@ -84,8 +87,6 @@ public function onKernelRequest(RequestEvent $event): void
8487
throw new NotFoundHttpException(sprintf('"%s" (%s) is not a Live Component.', $metadata->getClass(), $componentName));
8588
}
8689

87-
$request->attributes->set('_component_metadata', $metadata);
88-
8990
if ('get' === $action) {
9091
$defaultAction = trim($metadata->get('default_action', '__invoke'), '()');
9192

@@ -144,9 +145,13 @@ public function onKernelController(ControllerEvent $event): void
144145
throw new NotFoundHttpException(sprintf('The action "%s" either doesn\'t exist or is not allowed in "%s". Make sure it exist and has the LiveAction attribute above it.', $action, \get_class($component)));
145146
}
146147

147-
$this->container->get(LiveComponentHydrator::class)->hydrate($component, $data);
148+
$mounted = $this->container->get(LiveComponentHydrator::class)->hydrate(
149+
$component,
150+
$data,
151+
$request->attributes->get('_component_name')
152+
);
148153

149-
$request->attributes->set('_component', $component);
154+
$request->attributes->set('_mounted_component', $mounted);
150155

151156
if (!\is_string($queryString = $request->query->get('args'))) {
152157
return;
@@ -170,7 +175,7 @@ public function onKernelView(ViewEvent $event): void
170175
return;
171176
}
172177

173-
$response = $this->createResponse($request->attributes->get('_component'), $request);
178+
$response = $this->createResponse($request->attributes->get('_mounted_component'), $request);
174179

175180
$event->setResponse($response);
176181
}
@@ -187,14 +192,14 @@ public function onKernelException(ExceptionEvent $event): void
187192
return;
188193
}
189194

190-
$component = $request->attributes->get('_component');
195+
$mounted = $request->attributes->get('_mounted_component');
191196

192197
// in case the exception was too early somehow
193-
if (!$component) {
198+
if (!$mounted) {
194199
return;
195200
}
196201

197-
$response = $this->createResponse($component, $request);
202+
$response = $this->createResponse($mounted, $request);
198203
$event->setResponse($response);
199204
}
200205

@@ -232,15 +237,16 @@ public static function getSubscribedEvents(): array
232237
];
233238
}
234239

235-
private function createResponse(object $component, Request $request): Response
240+
private function createResponse(MountedComponent $mounted, Request $request): Response
236241
{
242+
$component = $mounted->getComponent();
243+
237244
foreach (AsLiveComponent::beforeReRenderMethods($component) as $method) {
238245
$component->{$method->name}();
239246
}
240247

241248
$html = $this->container->get(ComponentRenderer::class)->render(
242-
$component,
243-
$request->attributes->get('_component_metadata')
249+
$mounted,
244250
);
245251

246252
return new Response($html);

src/LiveComponent/src/LiveComponentHydrator.php

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
1818
use Symfony\UX\LiveComponent\Attribute\LivePropContext;
1919
use Symfony\UX\LiveComponent\Exception\UnsupportedHydrationException;
20+
use Symfony\UX\TwigComponent\ComponentAttributes;
21+
use Symfony\UX\TwigComponent\MountedComponent;
2022

2123
/**
2224
* @author Kevin Bond <kevinbond@gmail.com>
@@ -29,6 +31,7 @@ final class LiveComponentHydrator
2931
{
3032
private const CHECKSUM_KEY = '_checksum';
3133
private const EXPOSED_PROP_KEY = '_id';
34+
private const ATTRIBUTES_KEY = '_attributes';
3235

3336
/** @var PropertyHydratorInterface[] */
3437
private iterable $propertyHydrators;
@@ -45,8 +48,10 @@ public function __construct(iterable $propertyHydrators, PropertyAccessorInterfa
4548
$this->secret = $secret;
4649
}
4750

48-
public function dehydrate(object $component): array
51+
public function dehydrate(MountedComponent $mounted): array
4952
{
53+
$component = $mounted->getComponent();
54+
5055
foreach (AsLiveComponent::preDehydrateMethods($component) as $method) {
5156
$component->{$method->name}();
5257
}
@@ -100,15 +105,24 @@ public function dehydrate(object $component): array
100105
}
101106
}
102107

108+
if ($attributes = $mounted->getAttributes()->all()) {
109+
$data[self::ATTRIBUTES_KEY] = $attributes;
110+
$readonlyProperties[] = self::ATTRIBUTES_KEY;
111+
}
112+
103113
$data[self::CHECKSUM_KEY] = $this->computeChecksum($data, $readonlyProperties);
104114

105115
return $data;
106116
}
107117

108-
public function hydrate(object $component, array $data): void
118+
public function hydrate(object $component, array $data, string $componentName): MountedComponent
109119
{
110120
$readonlyProperties = [];
111121

122+
if (isset($data[self::ATTRIBUTES_KEY])) {
123+
$readonlyProperties[] = self::ATTRIBUTES_KEY;
124+
}
125+
112126
/** @var LivePropContext[] $propertyContexts */
113127
$propertyContexts = iterator_to_array(AsLiveComponent::liveProps($component));
114128

@@ -129,7 +143,9 @@ public function hydrate(object $component, array $data): void
129143

130144
$this->verifyChecksum($data, $readonlyProperties);
131145

132-
unset($data[self::CHECKSUM_KEY]);
146+
$attributes = new ComponentAttributes($data[self::ATTRIBUTES_KEY] ?? []);
147+
148+
unset($data[self::CHECKSUM_KEY], $data[self::ATTRIBUTES_KEY]);
133149

134150
foreach ($propertyContexts as $context) {
135151
$property = $context->reflectionProperty();
@@ -187,6 +203,8 @@ public function hydrate(object $component, array $data): void
187203
foreach (AsLiveComponent::postHydrateMethods($component) as $method) {
188204
$component->{$method->name}();
189205
}
206+
207+
return new MountedComponent($componentName, $component, $attributes);
190208
}
191209

192210
private function computeChecksum(array $data, array $readonlyProperties): string

src/LiveComponent/src/Resources/doc/index.rst

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -249,30 +249,15 @@ Component Attributes
249249

250250
.. versionadded:: 2.1
251251

252-
The ``HasAttributes`` trait was added in TwigComponents 2.1.
252+
Component attributes were added in TwigComponents 2.1.
253253

254254
`Component attributes`_ allows you to render your components with extra
255255
props that are are converted to html attributes and made available in
256256
your component's template as an ``attributes`` variable. When used on
257-
live components, these props are persisted between renders. You can enable
258-
this feature by having your live component use the ``HasAttributesTrait``:
257+
live components, these props are persisted between renders.
259258

260-
.. code-block:: diff
261-
262-
// ...
263-
use Symfony\UX\LiveComponent\Attribute\LiveProp;
264-
+ use Symfony\UX\TwigComponent\HasAttributesTrait;
265-
266-
#[AsLiveComponent('random_number')]
267-
class RandomNumberComponent
268-
{
269-
+ use HasAttributesTrait;
270-
271-
#[LiveProp]
272-
public int $min = 0;
273-
274-
Now, when rendering your component, you can pass html attributes
275-
as props and these will be added to ``attributes``:
259+
When rendering your component, you can pass html attributes as props and
260+
these will be added to ``attributes``:
276261

277262
.. code-block:: twig
278263

src/LiveComponent/src/Twig/LiveComponentRuntime.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ public function __construct(
3131

3232
public function getComponentUrl(string $name, array $props = []): string
3333
{
34-
$component = $this->factory->create($name, $props);
35-
$params = ['component' => $name] + $this->hydrator->dehydrate($component);
34+
$mounted = $this->factory->create($name, $props);
35+
$params = ['component' => $name] + $this->hydrator->dehydrate($mounted);
3636

3737
return $this->urlGenerator->generate('live_component', $params);
3838
}

src/LiveComponent/tests/Fixtures/Component/ComponentWithAttributes.php

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

55
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
66
use Symfony\UX\LiveComponent\DefaultActionTrait;
7-
use Symfony\UX\TwigComponent\HasAttributesTrait;
87

98
/**
109
* @author Kevin Bond <kevinbond@gmail.com>
@@ -13,5 +12,4 @@
1312
final class ComponentWithAttributes
1413
{
1514
use DefaultActionTrait;
16-
use HasAttributesTrait;
1715
}

src/LiveComponent/tests/Functional/EventListener/LiveComponentSubscriberTest.php

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@
1414
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
1515
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
1616
use Symfony\UX\LiveComponent\LiveComponentHydrator;
17-
use Symfony\UX\LiveComponent\Tests\Fixtures\Component\Component1;
18-
use Symfony\UX\LiveComponent\Tests\Fixtures\Component\Component2;
19-
use Symfony\UX\LiveComponent\Tests\Fixtures\Component\Component6;
2017
use Symfony\UX\LiveComponent\Tests\Fixtures\Entity\Entity1;
2118
use Symfony\UX\TwigComponent\ComponentFactory;
2219
use Zenstruck\Browser\Response\HtmlResponse;
@@ -42,7 +39,6 @@ public function testCanRenderComponentAsHtml(): void
4239
/** @var ComponentFactory $factory */
4340
$factory = self::getContainer()->get('ux.twig_component.component_factory');
4441

45-
/** @var Component1 $component */
4642
$component = $factory->create('component1', [
4743
'prop1' => $entity = create(Entity1::class)->object(),
4844
'prop2' => $date = new \DateTime('2021-03-05 9:23'),
@@ -72,10 +68,7 @@ public function testCanExecuteComponentAction(): void
7268
/** @var ComponentFactory $factory */
7369
$factory = self::getContainer()->get('ux.twig_component.component_factory');
7470

75-
/** @var Component2 $component */
76-
$component = $factory->create('component2');
77-
78-
$dehydrated = $hydrator->dehydrate($component);
71+
$dehydrated = $hydrator->dehydrate($factory->create('component2'));
7972
$token = null;
8073

8174
$this->browser()
@@ -160,10 +153,7 @@ public function testBeforeReRenderHookOnlyExecutedDuringAjax(): void
160153
/** @var ComponentFactory $factory */
161154
$factory = self::getContainer()->get('ux.twig_component.component_factory');
162155

163-
/** @var Component2 $component */
164-
$component = $factory->create('component2');
165-
166-
$dehydrated = $hydrator->dehydrate($component);
156+
$dehydrated = $hydrator->dehydrate($factory->create('component2'));
167157

168158
$this->browser()
169159
->visit('/render-template/template1')
@@ -183,10 +173,7 @@ public function testCanRedirectFromComponentAction(): void
183173
/** @var ComponentFactory $factory */
184174
$factory = self::getContainer()->get('ux.twig_component.component_factory');
185175

186-
/** @var Component2 $component */
187-
$component = $factory->create('component2');
188-
189-
$dehydrated = $hydrator->dehydrate($component);
176+
$dehydrated = $hydrator->dehydrate($factory->create('component2'));
190177
$token = null;
191178

192179
$this->browser()
@@ -226,10 +213,7 @@ public function testInjectsLiveArgs(): void
226213
/** @var ComponentFactory $factory */
227214
$factory = self::getContainer()->get('ux.twig_component.component_factory');
228215

229-
/** @var Component6 $component */
230-
$component = $factory->create('component6');
231-
232-
$dehydrated = $hydrator->dehydrate($component);
216+
$dehydrated = $hydrator->dehydrate($factory->create('component6'));
233217
$token = null;
234218

235219
$argsQueryParams = http_build_query(['args' => http_build_query(['arg1' => 'hello', 'arg2' => 666, 'custom' => '33.3'])]);

src/LiveComponent/tests/Functional/Form/ComponentWithFormTest.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,15 +123,18 @@ public function testFormRemembersValidationFromInitialForm(): void
123123

124124
$form = $formFactory->create(BlogPostFormType::class);
125125
$form->submit(['title' => '', 'content' => '']);
126-
/** @var FormWithCollectionTypeComponent $component */
127-
$component = $factory->create('form_with_collection_type', [
126+
127+
$mounted = $factory->create('form_with_collection_type', [
128128
'form' => $form->createView(),
129129
]);
130130

131+
/** @var FormWithCollectionTypeComponent $component */
132+
$component = $mounted->getComponent();
133+
131134
// component should recognize that it is already submitted
132135
$this->assertTrue($component->isValidated);
133136

134-
$dehydrated = $hydrator->dehydrate($component);
137+
$dehydrated = $hydrator->dehydrate($mounted);
135138
$dehydrated['blog_post_form']['content'] = 'changed description';
136139
$dehydrated['validatedFields'][] = 'blog_post_form.content';
137140

0 commit comments

Comments
 (0)