Skip to content

Commit 5faf43e

Browse files
committed
Add alias option to url query mapping
1 parent 2562a6c commit 5faf43e

15 files changed

+161
-33
lines changed

src/LiveComponent/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
page is rendered, either when the page loads (`loading="defer"`) or when
99
the component becomes visible in the viewport (`loading="lazy"`).
1010
- Deprecate the `defer` attribute.
11+
- Add `UrlMapping` configuration object for URL bindings in LiveComponents
1112

1213
## 2.16.0
1314

src/LiveComponent/assets/test/controller/query-binding.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,6 @@ describe('LiveController query string binding', () => {
144144
expectCurrentSearch().toEqual('?prop=');
145145
});
146146

147-
148147
it('updates the URL with props changed by the server', async () => {
149148
const test = await createTest({ prop: ''}, (data: any) => `
150149
<div ${initComponent(data, {queryMapping: {prop: {name: 'prop'}}})}>
@@ -165,4 +164,26 @@ describe('LiveController query string binding', () => {
165164

166165
expectCurrentSearch().toEqual('?prop=foo');
167166
});
167+
168+
it('uses custom name instead of prop name in the URL', async () => {
169+
const test = await createTest({ prop1: ''}, (data: any) => `
170+
<div ${initComponent(data, { queryMapping: {prop1: {name: 'alias1'} }})}></div>
171+
`)
172+
173+
// Set value
174+
test.expectsAjaxCall()
175+
.expectUpdatedData({prop1: 'foo'});
176+
177+
await test.component.set('prop1', 'foo', true);
178+
179+
expectCurrentSearch().toEqual('?alias1=foo');
180+
181+
// Remove value
182+
test.expectsAjaxCall()
183+
.expectUpdatedData({prop1: ''});
184+
185+
await test.component.set('prop1', '', true);
186+
187+
expectCurrentSearch().toEqual('?alias1=');
188+
});
168189
})

src/LiveComponent/doc/index.rst

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2489,11 +2489,6 @@ If you load this URL in your browser, the ``LiveProp`` value will be initialized
24892489

24902490
The URL is changed via ``history.replaceState()``. So no new entry is added.
24912491

2492-
.. warning::
2493-
2494-
You can use multiple components with URL bindings in the same page, as long as bound field names don't collide.
2495-
Otherwise, you will observe unexpected behaviors.
2496-
24972492
Supported Data Types
24982493
~~~~~~~~~~~~~~~~~~~~
24992494

@@ -2537,6 +2532,65 @@ For example, if you declare the following bindings::
25372532
And you only set the ``query`` value, then your URL will be updated to
25382533
``https://my.domain/search?query=my+query+string&mode=fulltext``.
25392534

2535+
Controlling the Query Parameter Name
2536+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2537+
2538+
.. versionadded:: 2.17
2539+
2540+
The ``as`` option was added in LiveComponents 2.17.
2541+
2542+
2543+
Instead of using the prop's field name as the query parameter name, you can use the ``as`` option in your ``LiveProp``
2544+
definition::
2545+
2546+
// ...
2547+
use Symfony\UX\LiveComponent\Metadata\UrlMapping;
2548+
2549+
#[AsLiveComponent]
2550+
class SearchModule
2551+
{
2552+
#[LiveProp(writable: true, url: new UrlMapping(as: 'q')]
2553+
public string $query = '';
2554+
2555+
// ...
2556+
}
2557+
2558+
Then the ``query`` value will appear in the URL like ``https://my.domain/search?q=my+query+string``.
2559+
2560+
If you need to change the parameter name on a specific page, you can leverage the :ref:`modifier <modifier>` option::
2561+
2562+
// ...
2563+
use Symfony\UX\LiveComponent\Metadata\UrlMapping;
2564+
2565+
#[AsLiveComponent]
2566+
class SearchModule
2567+
{
2568+
#[LiveProp(writable: true, url: true, modifier: 'modifyQueryProp')]
2569+
public string $query = '';
2570+
2571+
#[LiveProp]
2572+
public ?string $alias = null;
2573+
2574+
public function modifyQueryProp(LiveProp $liveProp): LiveProp
2575+
{
2576+
if ($this->alias) {
2577+
$liveProp = $liveProp->withUrl(new UrlMapping(as: $this->alias));
2578+
}
2579+
return $liveProp;
2580+
}
2581+
}
2582+
2583+
.. code-block:: html+twig
2584+
2585+
<twig:SearchModule alias="q" />
2586+
2587+
This way you can also use the component multiple times in the same page and avoid collisions in parameter names:
2588+
2589+
.. code-block:: html+twig
2590+
2591+
<twig:SearchModule alias="q1" />
2592+
<twig:SearchModule alias="q2" />
2593+
25402594
Validating the Query Parameter Values
25412595
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
25422596

@@ -2564,8 +2618,8 @@ validated. To validate it, you have to set up a `PostMount hook`_::
25642618
#[PostMount]
25652619
public function postMount(): void
25662620
{
2567-
// Validate 'mode' field without throwing an exception, so the component can be mounted anyway and a
2568-
// validation error can be shown to the user
2621+
// Validate 'mode' field without throwing an exception, so the component can
2622+
// be mounted anyway and a validation error can be shown to the user
25692623
if (!$this->validateField('mode', false)) {
25702624
// Do something when validation fails
25712625
}
@@ -3501,6 +3555,8 @@ the change of one specific key::
35013555
}
35023556
}
35033557

3558+
.. _modifier:
3559+
35043560
Set LiveProp Options Dynamically
35053561
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
35063562

src/LiveComponent/src/Attribute/LiveProp.php

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\UX\LiveComponent\Attribute;
1313

14+
use Symfony\UX\LiveComponent\Metadata\UrlMapping;
15+
1416
/**
1517
* An attribute to mark a property as a "LiveProp".
1618
*
@@ -97,10 +99,11 @@ public function __construct(
9799
private string|array|null $onUpdated = null,
98100

99101
/**
100-
* If true, this property will be synchronized with a query parameter
101-
* in the URL.
102+
* Whether to synchronize this property with a query parameter
103+
* in the URL. Pass true to configure the mapping automatically, or a
104+
* {@see UrlMapping} instance to configure the mapping.
102105
*/
103-
private bool $url = false,
106+
private bool|UrlMapping $url = false,
104107

105108
/**
106109
* A hook that will be called when this LiveProp is used.
@@ -114,6 +117,10 @@ public function __construct(
114117
private ?string $modifier = null,
115118
) {
116119
self::validateHydrationStrategy($this);
120+
121+
if (true === $url) {
122+
$this->url = new UrlMapping();
123+
}
117124
}
118125

119126
/**
@@ -277,15 +284,15 @@ public function withOnUpdated(string|array|null $onUpdated): self
277284
return $clone;
278285
}
279286

280-
public function url(): bool
287+
public function url(): UrlMapping|false
281288
{
282289
return $this->url;
283290
}
284291

285-
public function withUrl(bool $url): self
292+
public function withUrl(bool|UrlMapping $url): self
286293
{
287294
$clone = clone $this;
288-
$clone->url = $url;
295+
$clone->url = (true === $url) ? new UrlMapping() : $url;
289296

290297
return $clone;
291298
}

src/LiveComponent/src/Metadata/LiveComponentMetadata.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public function getOnlyPropsThatAcceptUpdatesFromParent(array $inputProps): arra
6969
public function hasQueryStringBindings($component): bool
7070
{
7171
foreach ($this->getAllLivePropsMetadata($component) as $livePropMetadata) {
72-
if ($livePropMetadata->queryStringMapping()) {
72+
if ($livePropMetadata->urlMapping()) {
7373
return true;
7474
}
7575
}

src/LiveComponent/src/Metadata/LivePropMetadata.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ public function allowsNull(): bool
5151
return $this->allowsNull;
5252
}
5353

54-
public function queryStringMapping(): bool
54+
public function urlMapping(): ?UrlMapping
5555
{
56-
return $this->liveProp->url();
56+
return $this->liveProp->url() ?: null;
5757
}
5858

5959
public function calculateFieldName(object $component, string $fallback): string
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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\LiveComponent\Metadata;
13+
14+
/**
15+
* Mapping configuration to bind a LiveProp to a URL query parameter.
16+
*
17+
* @author Nicolas Rigaud <squrious@protonmail.com>
18+
*/
19+
final class UrlMapping
20+
{
21+
public function __construct(
22+
/**
23+
* The name of the prop that appears in the URL. If null, the LiveProp's field name is used.
24+
*/
25+
private ?string $as = null,
26+
) {
27+
}
28+
29+
public function getAs(): ?string
30+
{
31+
return $this->as;
32+
}
33+
}

src/LiveComponent/src/Util/LiveControllerAttributesCreator.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,14 +104,14 @@ public function attributesForRendering(MountedComponent $mounted, ComponentMetad
104104
}
105105

106106
if ($liveMetadata->hasQueryStringBindings($mounted->getComponent())) {
107-
$queryMapping = [];
107+
$mappings = [];
108108
foreach ($liveMetadata->getAllLivePropsMetadata($mounted->getComponent()) as $livePropMetadata) {
109-
if ($livePropMetadata->queryStringMapping()) {
109+
if ($urlMapping = $livePropMetadata->urlMapping()) {
110110
$frontendName = $livePropMetadata->calculateFieldName($mounted->getComponent(), $livePropMetadata->getName());
111-
$queryMapping[$frontendName] = ['name' => $frontendName];
111+
$mappings[$frontendName] = ['name' => $urlMapping->getAs() ?? $frontendName];
112112
}
113113
}
114-
$attributesCollection->setQueryUrlMapping($queryMapping);
114+
$attributesCollection->setQueryUrlMapping($mappings);
115115
}
116116

117117
if ($isChildComponent) {

src/LiveComponent/src/Util/QueryStringPropsExtractor.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ public function extract(Request $request, LiveComponentMetadata $metadata, objec
4141
$data = [];
4242

4343
foreach ($metadata->getAllLivePropsMetadata($component) as $livePropMetadata) {
44-
if ($livePropMetadata->queryStringMapping()) {
44+
if ($queryMapping = $livePropMetadata->urlMapping()) {
4545
$frontendName = $livePropMetadata->calculateFieldName($component, $livePropMetadata->getName());
46-
if (null !== ($value = $query[$frontendName] ?? null)) {
46+
if (null !== ($value = $query[$queryMapping->getAs() ?? $frontendName] ?? null)) {
4747
if ('' === $value && null !== $livePropMetadata->getType() && (!$livePropMetadata->isBuiltIn() || 'array' === $livePropMetadata->getType())) {
4848
// Cast empty string to empty array for objects and arrays
4949
$value = [];

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
1515
use Symfony\UX\LiveComponent\Attribute\LiveProp;
1616
use Symfony\UX\LiveComponent\DefaultActionTrait;
17+
use Symfony\UX\LiveComponent\Metadata\UrlMapping;
1718
use Symfony\UX\LiveComponent\Tests\Fixtures\Dto\Address;
1819

1920
#[AsLiveComponent('component_with_url_bound_props')]
@@ -57,4 +58,7 @@ public function modifyProp8(LiveProp $prop): LiveProp
5758
{
5859
return $prop->withUrl($this->prop8InUrl);
5960
}
61+
62+
#[LiveProp(url: new UrlMapping('q'))]
63+
public ?string $prop9 = null;
6064
}

0 commit comments

Comments
 (0)