Skip to content

Commit bc5f27d

Browse files
mbuliardKocal
authored andcommitted
[LiveComponent] Add the possibility to map LiveProp as a path parameter
1 parent 45f31cc commit bc5f27d

35 files changed

+883
-565
lines changed

src/LiveComponent/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
- `min_length` and `max_length`: validate length from textual input elements
77
- `min_value` and `max_value`: validate value from numeral input elements
88

9+
- Add new `mapPath` options (default `false`) to `UrlMapping` of a `LiveProp`
10+
to allow the prop to be mapped to the path instead of the query in the url.
11+
912
```twig
1013
<!-- Do not trigger model update until 3 characters are typed -->
1114
<input data-model="min_length(3)|username" type="text" value="" />
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export default class {
22
response: Response;
33
private body;
4+
private liveUrl;
45
constructor(response: Response);
56
getBody(): Promise<string>;
7+
getLiveUrl(): string | null;
68
}

src/LiveComponent/assets/dist/Component/plugins/QueryStringPlugin.d.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/LiveComponent/assets/dist/live_controller.d.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,6 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
4949
type: StringConstructor;
5050
default: string;
5151
};
52-
queryMapping: {
53-
type: ObjectConstructor;
54-
default: {};
55-
};
5652
};
5753
readonly nameValue: string;
5854
readonly urlValue: string;
@@ -76,11 +72,6 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
7672
readonly debounceValue: number;
7773
readonly fingerprintValue: string;
7874
readonly requestMethodValue: 'get' | 'post';
79-
readonly queryMappingValue: {
80-
[p: string]: {
81-
name: string;
82-
};
83-
};
8475
private proxiedComponent;
8576
private mutationObserver;
8677
component: Component;

src/LiveComponent/assets/dist/live_controller.js

Lines changed: 11 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class RequestBuilder {
3333
fetchOptions.headers = {
3434
Accept: 'application/vnd.live-component+html',
3535
'X-Requested-With': 'XMLHttpRequest',
36+
'X-Live-Url': window.location.pathname + window.location.search,
3637
};
3738
const totalFiles = Object.entries(files).reduce((total, current) => total + current.length, 0);
3839
const hasFingerprints = Object.keys(children).length > 0;
@@ -111,6 +112,12 @@ class BackendResponse {
111112
}
112113
return this.body;
113114
}
115+
getLiveUrl() {
116+
if (undefined === this.liveUrl) {
117+
this.liveUrl = this.response.headers.get('X-Live-Url');
118+
}
119+
return this.liveUrl;
120+
}
114121
}
115122

116123
function getElementAsTagText(element) {
@@ -2146,6 +2153,10 @@ class Component {
21462153
return response;
21472154
}
21482155
this.processRerender(html, backendResponse);
2156+
const liveUrl = backendResponse.getLiveUrl();
2157+
if (liveUrl) {
2158+
history.replaceState(history.state, '', new URL(liveUrl + window.location.hash, window.location.origin));
2159+
}
21492160
this.backendRequest = null;
21502161
thisPromiseResolve(backendResponse);
21512162
if (this.isRequestPending) {
@@ -2770,129 +2781,6 @@ class PollingPlugin {
27702781
}
27712782
}
27722783

2773-
function isValueEmpty(value) {
2774-
if (null === value || value === '' || undefined === value || (Array.isArray(value) && value.length === 0)) {
2775-
return true;
2776-
}
2777-
if (typeof value !== 'object') {
2778-
return false;
2779-
}
2780-
for (const key of Object.keys(value)) {
2781-
if (!isValueEmpty(value[key])) {
2782-
return false;
2783-
}
2784-
}
2785-
return true;
2786-
}
2787-
function toQueryString(data) {
2788-
const buildQueryStringEntries = (data, entries = {}, baseKey = '') => {
2789-
Object.entries(data).forEach(([iKey, iValue]) => {
2790-
const key = baseKey === '' ? iKey : `${baseKey}[${iKey}]`;
2791-
if ('' === baseKey && isValueEmpty(iValue)) {
2792-
entries[key] = '';
2793-
}
2794-
else if (null !== iValue) {
2795-
if (typeof iValue === 'object') {
2796-
entries = { ...entries, ...buildQueryStringEntries(iValue, entries, key) };
2797-
}
2798-
else {
2799-
entries[key] = encodeURIComponent(iValue)
2800-
.replace(/%20/g, '+')
2801-
.replace(/%2C/g, ',');
2802-
}
2803-
}
2804-
});
2805-
return entries;
2806-
};
2807-
const entries = buildQueryStringEntries(data);
2808-
return Object.entries(entries)
2809-
.map(([key, value]) => `${key}=${value}`)
2810-
.join('&');
2811-
}
2812-
function fromQueryString(search) {
2813-
search = search.replace('?', '');
2814-
if (search === '')
2815-
return {};
2816-
const insertDotNotatedValueIntoData = (key, value, data) => {
2817-
const [first, second, ...rest] = key.split('.');
2818-
if (!second) {
2819-
data[key] = value;
2820-
return value;
2821-
}
2822-
if (data[first] === undefined) {
2823-
data[first] = Number.isNaN(Number.parseInt(second)) ? {} : [];
2824-
}
2825-
insertDotNotatedValueIntoData([second, ...rest].join('.'), value, data[first]);
2826-
};
2827-
const entries = search.split('&').map((i) => i.split('='));
2828-
const data = {};
2829-
entries.forEach(([key, value]) => {
2830-
value = decodeURIComponent(String(value || '').replace(/\+/g, '%20'));
2831-
if (!key.includes('[')) {
2832-
data[key] = value;
2833-
}
2834-
else {
2835-
if ('' === value)
2836-
return;
2837-
const dotNotatedKey = key.replace(/\[/g, '.').replace(/]/g, '');
2838-
insertDotNotatedValueIntoData(dotNotatedKey, value, data);
2839-
}
2840-
});
2841-
return data;
2842-
}
2843-
class UrlUtils extends URL {
2844-
has(key) {
2845-
const data = this.getData();
2846-
return Object.keys(data).includes(key);
2847-
}
2848-
set(key, value) {
2849-
const data = this.getData();
2850-
data[key] = value;
2851-
this.setData(data);
2852-
}
2853-
get(key) {
2854-
return this.getData()[key];
2855-
}
2856-
remove(key) {
2857-
const data = this.getData();
2858-
delete data[key];
2859-
this.setData(data);
2860-
}
2861-
getData() {
2862-
if (!this.search) {
2863-
return {};
2864-
}
2865-
return fromQueryString(this.search);
2866-
}
2867-
setData(data) {
2868-
this.search = toQueryString(data);
2869-
}
2870-
}
2871-
class HistoryStrategy {
2872-
static replace(url) {
2873-
history.replaceState(history.state, '', url);
2874-
}
2875-
}
2876-
2877-
class QueryStringPlugin {
2878-
constructor(mapping) {
2879-
this.mapping = mapping;
2880-
}
2881-
attachToComponent(component) {
2882-
component.on('render:finished', (component) => {
2883-
const urlUtils = new UrlUtils(window.location.href);
2884-
const currentUrl = urlUtils.toString();
2885-
Object.entries(this.mapping).forEach(([prop, mapping]) => {
2886-
const value = component.valueStore.get(prop);
2887-
urlUtils.set(mapping.name, value);
2888-
});
2889-
if (currentUrl !== urlUtils.toString()) {
2890-
HistoryStrategy.replace(urlUtils);
2891-
}
2892-
});
2893-
}
2894-
}
2895-
28962784
class SetValueOntoModelFieldsPlugin {
28972785
attachToComponent(component) {
28982786
this.synchronizeValueOfModelFields(component);
@@ -3102,7 +2990,6 @@ class LiveControllerDefault extends Controller {
31022990
new PageUnloadingPlugin(),
31032991
new PollingPlugin(),
31042992
new SetValueOntoModelFieldsPlugin(),
3105-
new QueryStringPlugin(this.queryMappingValue),
31062993
new ChildComponentPlugin(this.component),
31072994
];
31082995
plugins.forEach((plugin) => {
@@ -3233,7 +3120,6 @@ LiveControllerDefault.values = {
32333120
debounce: { type: Number, default: 150 },
32343121
fingerprint: { type: String, default: '' },
32353122
requestMethod: { type: String, default: 'post' },
3236-
queryMapping: { type: Object, default: {} },
32373123
};
32383124
LiveControllerDefault.backendFactory = (controller) => new Backend(controller.urlValue, controller.requestMethodValue);
32393125

src/LiveComponent/assets/dist/url_utils.d.ts

Lines changed: 0 additions & 11 deletions
This file was deleted.

src/LiveComponent/assets/src/Backend/BackendResponse.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export default class {
22
response: Response;
33
private body: string;
4+
private liveUrl: string | null;
45

56
constructor(response: Response) {
67
this.response = response;
@@ -13,4 +14,12 @@ export default class {
1314

1415
return this.body;
1516
}
17+
18+
getLiveUrl(): string | null {
19+
if (undefined === this.liveUrl) {
20+
this.liveUrl = this.response.headers.get('X-Live-Url');
21+
}
22+
23+
return this.liveUrl;
24+
}
1625
}

src/LiveComponent/assets/src/Backend/RequestBuilder.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export default class {
2626
fetchOptions.headers = {
2727
Accept: 'application/vnd.live-component+html',
2828
'X-Requested-With': 'XMLHttpRequest',
29+
'X-Live-Url': window.location.pathname + window.location.search,
2930
};
3031

3132
const totalFiles = Object.entries(files).reduce((total, current) => total + current.length, 0);

src/LiveComponent/assets/src/Component/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,14 @@ export default class Component {
328328
}
329329

330330
this.processRerender(html, backendResponse);
331+
const liveUrl = backendResponse.getLiveUrl();
332+
if (liveUrl) {
333+
history.replaceState(
334+
history.state,
335+
'',
336+
new URL(liveUrl + window.location.hash, window.location.origin)
337+
);
338+
}
331339

332340
// finally resolve this promise
333341
this.backendRequest = null;

src/LiveComponent/assets/src/Component/plugins/QueryStringPlugin.ts

Lines changed: 0 additions & 31 deletions
This file was deleted.

0 commit comments

Comments
 (0)