Skip to content

Commit 977df51

Browse files
committed
Allow to customize mapping from template attributes
1 parent 86040e0 commit 977df51

14 files changed

+216
-11
lines changed

src/LiveComponent/src/Attribute/LiveProp.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ public function onUpdated(): null|string|array
205205
return $this->onUpdated;
206206
}
207207

208-
public function url(): bool|QueryMapping
208+
public function url(): QueryMapping|false
209209
{
210210
return $this->url;
211211
}

src/LiveComponent/src/EventListener/AddLiveAttributesSubscriber.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Contracts\Service\ServiceSubscriberInterface;
1717
use Symfony\UX\LiveComponent\Twig\TemplateMap;
1818
use Symfony\UX\LiveComponent\Util\LiveControllerAttributesCreator;
19+
use Symfony\UX\LiveComponent\Util\QueryMappingAttributeUtils;
1920
use Symfony\UX\TwigComponent\ComponentAttributes;
2021
use Symfony\UX\TwigComponent\ComponentMetadata;
2122
use Symfony\UX\TwigComponent\ComponentStack;
@@ -79,6 +80,8 @@ public function onPreRender(PreRenderEvent $event): void
7980
// this is used inside LiveControllerAttributesCreator
8081
$attributes = $attributes->without(LiveControllerAttributesCreator::KEY_PROP_NAME);
8182

83+
$attributes = QueryMappingAttributeUtils::removeAttributesForRendering($attributes);
84+
8285
$variables[$attributesKey] = $attributes;
8386

8487
$event->setVariables($variables);

src/LiveComponent/src/EventListener/QueryStringInitializeSubscriber.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
1515
use Symfony\Component\HttpFoundation\RequestStack;
1616
use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadataFactory;
17+
use Symfony\UX\LiveComponent\Util\QueryMappingAttributeUtils;
1718
use Symfony\UX\LiveComponent\Util\QueryStringPropsExtractor;
1819
use Symfony\UX\TwigComponent\Event\PreMountEvent;
1920

@@ -59,7 +60,10 @@ public function onPreMount(PreMountEvent $event): void
5960
return;
6061
}
6162

62-
$queryStringData = $this->queryStringPropsExtractor->extract($request, $metadata, $event->getComponent());
63+
$data = $event->getData();
64+
$mappingDefaults = QueryMappingAttributeUtils::getMappingFromAttributes($data);
65+
66+
$queryStringData = $this->queryStringPropsExtractor->extract($request, $metadata, $event->getComponent(), $mappingDefaults);
6367

6468
$event->setData(array_merge($event->getData(), $queryStringData));
6569
}
Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,46 @@
11
<?php
22

3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
312
namespace Symfony\UX\LiveComponent\Metadata;
413

5-
class QueryMapping
14+
final class QueryMapping
615
{
716
public function __construct(
817
/**
918
* The name of the prop that appears in the URL. If null, the LiveProp's field name is used.
1019
*/
11-
public readonly ?string $alias = null,
20+
private ?string $alias = null,
1221
) {
1322
}
23+
24+
public function getAlias(): ?string
25+
{
26+
return $this->alias;
27+
}
28+
29+
public function withAlias(?string $alias): self
30+
{
31+
$clone = clone $this;
32+
33+
$clone->alias = $alias;
34+
35+
return $clone;
36+
}
37+
38+
public function merge(self $mapping): self
39+
{
40+
$clone = clone $this;
41+
42+
$clone->alias = $mapping->alias;
43+
44+
return $clone;
45+
}
1446
}

src/LiveComponent/src/Util/LiveControllerAttributesCreator.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,22 @@ public function attributesForRendering(MountedComponent $mounted, ComponentMetad
101101
$liveMetadata = $this->metadataFactory->getMetadata($mounted->getName());
102102

103103
if ($liveMetadata->hasQueryStringBindings()) {
104+
$defaultBindings = QueryMappingAttributeUtils::getMappingFromAttributes($mountedAttributes->all());
104105
$mappings = [];
105106
foreach ($liveMetadata->getAllLivePropsMetadata() as $livePropMetadata) {
106107
if ($queryMapping = $livePropMetadata->queryStringMapping()) {
108+
if (isset($defaultBindings[$livePropMetadata->getName()])) {
109+
$queryMapping = $queryMapping->merge($defaultBindings[$livePropMetadata->getName()]);
110+
}
111+
107112
$frontendName = $livePropMetadata->calculateFieldName($mounted, $livePropMetadata->getName());
108-
$mappings[$frontendName] = ['name' => $queryMapping->alias ?? $frontendName];
113+
$mappings[$frontendName] = ['name' => $queryMapping->getAlias() ?? $frontendName];
109114
}
110115
}
111116
$attributesCollection->setQueryUrlMapping($mappings);
117+
118+
// So the mapping is rendered on live update
119+
$mountedAttributes->defaults(['data-live-query-mapping-value' => $mappings]);
112120
}
113121

114122
if ($isChildComponent) {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\LiveComponent\Util;
13+
14+
use Symfony\UX\LiveComponent\Metadata\QueryMapping;
15+
use Symfony\UX\TwigComponent\ComponentAttributes;
16+
17+
class QueryMappingAttributeUtils
18+
{
19+
public const ATTRIBUTE_PREFIX = 'data-live-url-mapping-';
20+
21+
/**
22+
* @return QueryMapping[]
23+
*/
24+
public static function getMappingFromAttributes(array $data): array
25+
{
26+
$mapping = [];
27+
$attributes = array_filter($data, fn ($k) => str_starts_with($k, self::ATTRIBUTE_PREFIX), \ARRAY_FILTER_USE_KEY);
28+
foreach ($attributes as $attribute => $value) {
29+
$propMapping = substr($attribute, \strlen(self::ATTRIBUTE_PREFIX));
30+
$parts = explode('-', $propMapping, 2);
31+
if (1 === \count($parts)) {
32+
$propName = $parts[0];
33+
$mapping[$propName] ??= new QueryMapping();
34+
$mapping[$propName] = $mapping[$propName]->merge(new QueryMapping(...$value));
35+
} else {
36+
[$propName, $option] = $parts;
37+
$mapping[$propName] ??= new QueryMapping();
38+
$mapping[$propName] = $mapping[$propName]->{'with'.ucfirst($option)}(self::castOptionValue($option, $value));
39+
}
40+
}
41+
42+
return $mapping;
43+
}
44+
45+
public static function removeAttributesForRendering(ComponentAttributes $attributes): ComponentAttributes
46+
{
47+
$toRemove = [];
48+
foreach (array_keys($attributes->all()) as $name) {
49+
if (str_starts_with($name, self::ATTRIBUTE_PREFIX)) {
50+
$toRemove[] = $name;
51+
}
52+
}
53+
54+
return $attributes->without(...$toRemove);
55+
}
56+
57+
private static function castOptionValue(string $option, mixed $value): mixed
58+
{
59+
return match ($option) {
60+
'alias' => (string) $value,
61+
default => null,
62+
};
63+
}
64+
}

src/LiveComponent/src/Util/QueryStringPropsExtractor.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\UX\LiveComponent\LiveComponentHydrator;
1717
use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadata;
1818
use Symfony\UX\LiveComponent\Metadata\LivePropMetadata;
19+
use Symfony\UX\LiveComponent\Metadata\QueryMapping;
1920

2021
/**
2122
* @author Nicolas Rigaud <[email protected]>
@@ -32,8 +33,10 @@ public function __construct(private readonly LiveComponentHydrator $hydrator)
3233

3334
/**
3435
* Extracts relevant query parameters from the current URL and hydrates them.
36+
*
37+
* @param QueryMapping[] $mappingDefaults
3538
*/
36-
public function extract(Request $request, LiveComponentMetadata $metadata, object $component): array
39+
public function extract(Request $request, LiveComponentMetadata $metadata, object $component, array $mappingDefaults = []): array
3740
{
3841
$query = $request->query->all();
3942

@@ -43,9 +46,12 @@ public function extract(Request $request, LiveComponentMetadata $metadata, objec
4346
$data = [];
4447

4548
foreach ($metadata->getAllLivePropsMetadata() as $livePropMetadata) {
46-
if ($livePropMetadata->queryStringMapping()) {
49+
if ($queryMapping = $livePropMetadata->queryStringMapping()) {
50+
if (isset($mappingDefaults[$livePropMetadata->getName()])) {
51+
$queryMapping = $queryMapping->merge($mappingDefaults[$livePropMetadata->getName()]);
52+
}
4753
$frontendName = $livePropMetadata->calculateFieldName($component, $livePropMetadata->getName());
48-
if (null !== ($value = $query[$livePropMetadata->queryStringMapping()->alias ?? $frontendName] ?? null)) {
54+
if (null !== ($value = $query[$queryMapping->getAlias() ?? $frontendName] ?? null)) {
4955
if ('' === $value && null !== $livePropMetadata->getType() && (!$livePropMetadata->isBuiltIn() || 'array' === $livePropMetadata->getType())) {
5056
// Cast empty string to empty array for objects and arrays
5157
$value = [];

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,8 @@ class ComponentWithUrlBoundProps
4343
#[LiveProp(url: new QueryMapping('q'))]
4444
public ?string $prop7 = null;
4545

46+
#[LiveProp(url: new QueryMapping('originalAlias'))]
47+
public ?string $prop8 = null;
48+
4649
use DefaultActionTrait;
4750
}

src/LiveComponent/tests/Fixtures/templates/components/component_with_url_bound_props.html.twig

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@
55
Prop4: {{ prop4 }}
66
Prop5: address: {{ prop5.address ?? '' }} city: {{ prop5.city ?? '' }}
77
Prop6: {{ prop6 }}
8-
</div>
8+
Prop7: {{ prop7 }}
9+
Prop8: {{ prop8 }}
10+
</div>
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
{{ component('component_with_url_bound_props') }}
1+
{{ component('component_with_url_bound_props', {
2+
'data-live-url-mapping-prop8': {
3+
'alias': 'customAlias'
4+
}
5+
}) }}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ public function testQueryStringMappingAttribute()
151151
'prop5' => ['name' => 'prop5'],
152152
'field6' => ['name' => 'field6'],
153153
'prop7' => ['name' => 'q'],
154+
'prop8' => ['name' => 'customAlias'],
154155
];
155156

156157
$this->assertEquals($expected, $queryMapping);

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,16 @@ class QueryStringInitializerSubscriberTest extends KernelTestCase
2121
public function testQueryStringPropsInitialization()
2222
{
2323
$this->browser()
24-
->get('/render-template/render_component_with_url_bound_props?prop1=foo&prop2=42&prop3[]=foo&prop3[]=bar&prop4=unbound&prop5[address]=foo&prop5[city]=bar&field6=foo')
24+
->get('/render-template/render_component_with_url_bound_props?prop1=foo&prop2=42&prop3[]=foo&prop3[]=bar&prop4=unbound&prop5[address]=foo&prop5[city]=bar&field6=foo&q=foo&customAlias=foo')
2525
->assertSuccessful()
2626
->assertContains('Prop1: foo')
2727
->assertContains('Prop2: 42')
2828
->assertContains('Prop3: foo,bar')
2929
->assertContains('Prop4:')
3030
->assertContains('Prop5: address: foo city: bar')
3131
->assertContains('Prop6: foo')
32+
->assertContains('Prop7: foo')
33+
->assertContains('Prop8: foo')
3234
;
3335
}
3436
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\LiveComponent\Tests\Unit\Metadata;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\UX\LiveComponent\Metadata\QueryMapping;
16+
17+
class QueryMappingTest extends TestCase
18+
{
19+
public function testMerge()
20+
{
21+
$mapping = new QueryMapping();
22+
23+
$merged = $mapping->merge(new QueryMapping('foo'));
24+
25+
$this->assertEquals('foo', $merged->getAlias());
26+
}
27+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\LiveComponent\Tests\Unit\Util;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\UX\LiveComponent\Metadata\QueryMapping;
16+
use Symfony\UX\LiveComponent\Util\QueryMappingAttributeUtils;
17+
use Symfony\UX\TwigComponent\ComponentAttributes;
18+
19+
class QueryMappingAttributeUtilsTest extends TestCase
20+
{
21+
public function testGetMappingFromAttributes()
22+
{
23+
$mappings = QueryMappingAttributeUtils::getMappingFromAttributes([
24+
QueryMappingAttributeUtils::ATTRIBUTE_PREFIX.'prop1-alias' => 'foo',
25+
QueryMappingAttributeUtils::ATTRIBUTE_PREFIX.'prop2' => [
26+
'alias' => 'bar',
27+
],
28+
]);
29+
30+
$this->assertArrayHasKey('prop1', $mappings);
31+
$this->assertEquals(new QueryMapping('foo'), $mappings['prop1']);
32+
33+
$this->assertArrayHasKey('prop2', $mappings);
34+
$this->assertEquals(new QueryMapping('bar'), $mappings['prop2']);
35+
}
36+
37+
public function testRemoveAttributesForRendering()
38+
{
39+
$componentAttributes = new ComponentAttributes([
40+
'to-keep' => '',
41+
QueryMappingAttributeUtils::ATTRIBUTE_PREFIX.'prop' => [
42+
'alias' => 'foo',
43+
],
44+
QueryMappingAttributeUtils::ATTRIBUTE_PREFIX.'prop-alias' => 'bar',
45+
]);
46+
47+
$this->assertEquals(['to-keep' => ''], QueryMappingAttributeUtils::removeAttributesForRendering($componentAttributes)->all());
48+
}
49+
}

0 commit comments

Comments
 (0)