Skip to content

Commit db3eeb1

Browse files
committed
Rework URL utils
1 parent 1fdaac1 commit db3eeb1

File tree

10 files changed

+366
-274
lines changed

10 files changed

+366
-274
lines changed
Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
import Component from '../index';
22
import { PluginInterface } from './PluginInterface';
3+
interface QueryMapping {
4+
name: string;
5+
}
36
export default class implements PluginInterface {
4-
private mapping;
5-
private initialPropsValues;
6-
private changedProps;
7+
private readonly mapping;
8+
private trackers;
79
constructor(mapping: {
8-
[p: string]: any;
10+
[p: string]: QueryMapping;
911
});
1012
attachToComponent(component: Component): void;
11-
private updateUrlParam;
12-
private getParamFromModel;
13-
private getNormalizedPropNames;
14-
private isValueEmpty;
15-
private isObjectValue;
1613
}
14+
export {};

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
4848
readonly hasDebounceValue: boolean;
4949
readonly debounceValue: number;
5050
readonly fingerprintValue: string;
51-
readonly queryMappingValue: Map<string, any>;
51+
readonly queryMappingValue: {
52+
[p: string]: {
53+
name: string;
54+
};
55+
};
5256
private proxiedComponent;
5357
component: Component;
5458
pendingActionTriggerModelElement: HTMLElement | null;

src/LiveComponent/assets/dist/live_controller.js

Lines changed: 103 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -2696,118 +2696,132 @@ class ComponentRegistry {
26962696
}
26972697
}
26982698

2699-
class AdvancedURLSearchParams extends URLSearchParams {
2700-
set(name, value) {
2701-
if (typeof value !== 'object') {
2702-
super.set(name, value);
2703-
}
2704-
else {
2705-
this.delete(name);
2706-
if (Array.isArray(value)) {
2707-
value.forEach((v) => {
2708-
this.append(`${name}[]`, v);
2709-
});
2699+
function isObject(subject) {
2700+
return typeof subject === 'object' && subject !== null;
2701+
}
2702+
function toQueryString(data) {
2703+
let buildQueryStringEntries = (data, entries = {}, baseKey = '') => {
2704+
Object.entries(data).forEach(([iKey, iValue]) => {
2705+
let key = baseKey === '' ? iKey : `${baseKey}[${iKey}]`;
2706+
if (!isObject(iValue)) {
2707+
if (iValue !== null) {
2708+
entries[key] = encodeURIComponent(iValue)
2709+
.replace(/%20/g, '+')
2710+
.replace(/%2C/g, ',');
2711+
}
27102712
}
27112713
else {
2712-
Object.entries(value).forEach(([index, v]) => {
2713-
if (v !== null && v !== '' && v !== undefined) {
2714-
this.append(`${name}[${index}]`, v);
2715-
}
2716-
});
2714+
entries = Object.assign(Object.assign({}, entries), buildQueryStringEntries(iValue, entries, key));
27172715
}
2716+
});
2717+
return entries;
2718+
};
2719+
let entries = buildQueryStringEntries(data);
2720+
return Object.entries(entries)
2721+
.map(([key, value]) => `${key}=${value}`)
2722+
.join('&');
2723+
}
2724+
function fromQueryString(search) {
2725+
search = search.replace('?', '');
2726+
if (search === '')
2727+
return {};
2728+
let insertDotNotatedValueIntoData = (key, value, data) => {
2729+
let [first, second, ...rest] = key.split('.');
2730+
if (!second)
2731+
return (data[key] = value);
2732+
if (data[first] === undefined) {
2733+
data[first] = Number.isNaN(second) ? {} : [];
2734+
}
2735+
insertDotNotatedValueIntoData([second, ...rest].join('.'), value, data[first]);
2736+
};
2737+
let entries = search.split('&').map((i) => i.split('='));
2738+
let data = {};
2739+
entries.forEach(([key, value]) => {
2740+
if (!value)
2741+
return;
2742+
value = decodeURIComponent(value.replace(/\+/g, '%20'));
2743+
if (!key.includes('[')) {
2744+
data[key] = value;
2745+
}
2746+
else {
2747+
let dotNotatedKey = key.replace(/\[/g, '.').replace(/]/g, '');
2748+
insertDotNotatedValueIntoData(dotNotatedKey, value, data);
27182749
}
2750+
});
2751+
return data;
2752+
}
2753+
class UrlUtils extends URL {
2754+
has(key) {
2755+
const data = this.getData();
2756+
return Object.keys(data).includes(key);
27192757
}
2720-
delete(name) {
2721-
super.delete(name);
2722-
const pattern = new RegExp(`^${name}(\\[.*])?$`);
2723-
for (const key of Array.from(this.keys())) {
2724-
if (key.match(pattern)) {
2725-
super.delete(key);
2726-
}
2758+
set(key, value) {
2759+
const data = this.getData();
2760+
data[key] = value;
2761+
this.setData(data);
2762+
}
2763+
get(key) {
2764+
return this.getData()[key];
2765+
}
2766+
remove(key) {
2767+
const data = this.getData();
2768+
delete data[key];
2769+
this.setData(data);
2770+
}
2771+
getData() {
2772+
if (!this.search) {
2773+
return {};
27272774
}
2775+
return fromQueryString(this.search);
2776+
}
2777+
setData(data) {
2778+
this.search = toQueryString(data);
27282779
}
27292780
}
2730-
function setQueryParam(param, value) {
2731-
const queryParams = new AdvancedURLSearchParams(window.location.search);
2732-
queryParams.set(param, value);
2733-
const url = urlFromQueryParams(queryParams);
2734-
history.replaceState(history.state, '', url);
2735-
}
2736-
function removeQueryParam(param) {
2737-
const queryParams = new AdvancedURLSearchParams(window.location.search);
2738-
queryParams.delete(param);
2739-
const url = urlFromQueryParams(queryParams);
2740-
history.replaceState(history.state, '', url);
2741-
}
2742-
function urlFromQueryParams(queryParams) {
2743-
let queryString = '';
2744-
if (Array.from(queryParams.entries()).length > 0) {
2745-
queryString += '?' + queryParams.toString();
2781+
class HistoryStrategy {
2782+
static replace(url) {
2783+
history.replaceState(history.state, '', url);
27462784
}
2747-
return window.location.origin + window.location.pathname + queryString + window.location.hash;
27482785
}
27492786

2787+
class Tracker {
2788+
constructor(mapping, initialValue, initiallyPresentInUrl) {
2789+
this.mapping = mapping;
2790+
this.initialValue = JSON.stringify(initialValue);
2791+
this.initiallyPresentInUrl = initiallyPresentInUrl;
2792+
}
2793+
hasReturnedToInitialValue(currentValue) {
2794+
return JSON.stringify(currentValue) === this.initialValue;
2795+
}
2796+
;
2797+
}
27502798
class QueryStringPlugin {
27512799
constructor(mapping) {
2752-
this.mapping = new Map;
2753-
this.initialPropsValues = new Map;
2754-
this.changedProps = {};
2755-
Object.entries(mapping).forEach(([key, config]) => {
2756-
this.mapping.set(key, config);
2757-
});
2800+
this.mapping = mapping;
2801+
this.trackers = new Map;
27582802
}
27592803
attachToComponent(component) {
27602804
component.on('connect', (component) => {
2761-
for (const model of this.mapping.keys()) {
2762-
for (const prop of this.getNormalizedPropNames(component.valueStore.get(model), model)) {
2763-
this.initialPropsValues.set(prop, component.valueStore.get(prop));
2764-
}
2765-
}
2805+
const urlUtils = new UrlUtils(window.location.href);
2806+
Object.entries(this.mapping).forEach(([prop, mapping]) => {
2807+
const tracker = new Tracker(mapping, component.valueStore.get(prop), urlUtils.has(prop));
2808+
this.trackers.set(prop, tracker);
2809+
});
27662810
});
27672811
component.on('render:finished', (component) => {
2768-
this.initialPropsValues.forEach((initialValue, prop) => {
2769-
var _a;
2812+
let urlUtils = new UrlUtils(window.location.href);
2813+
this.trackers.forEach((tracker, prop) => {
27702814
const value = component.valueStore.get(prop);
2771-
(_a = this.changedProps)[prop] || (_a[prop] = JSON.stringify(value) !== JSON.stringify(initialValue));
2772-
if (this.changedProps) {
2773-
this.updateUrlParam(prop, value);
2815+
if (!tracker.initiallyPresentInUrl && tracker.hasReturnedToInitialValue(value)) {
2816+
urlUtils.remove(tracker.mapping.name);
2817+
}
2818+
else {
2819+
urlUtils.set(tracker.mapping.name, value);
27742820
}
27752821
});
2822+
HistoryStrategy.replace(urlUtils);
27762823
});
27772824
}
2778-
updateUrlParam(model, value) {
2779-
const paramName = this.getParamFromModel(model);
2780-
if (paramName === undefined) {
2781-
return;
2782-
}
2783-
this.isValueEmpty(value)
2784-
? removeQueryParam(paramName)
2785-
: setQueryParam(paramName, value);
2786-
}
2787-
getParamFromModel(model) {
2788-
const modelParts = model.split('.');
2789-
const rootPropMapping = this.mapping.get(modelParts[0]);
2790-
if (rootPropMapping === undefined) {
2791-
return undefined;
2792-
}
2793-
return rootPropMapping.name + modelParts.slice(1).map((v) => `[${v}]`).join('');
2794-
}
2795-
*getNormalizedPropNames(value, propertyPath) {
2796-
if (this.isObjectValue(value)) {
2797-
for (const key in value) {
2798-
yield* this.getNormalizedPropNames(value[key], `${propertyPath}.${key}`);
2799-
}
2800-
}
2801-
else {
2802-
yield propertyPath;
2803-
}
2804-
}
2805-
isValueEmpty(value) {
2806-
return (value === '' || value === null || value === undefined);
2807-
}
2808-
isObjectValue(value) {
2809-
return !(Array.isArray(value) || value === null || typeof value !== 'object');
2810-
}
28112825
}
28122826

28132827
const getComponent = (element) => LiveControllerDefault.componentRegistry.getComponent(element);
Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,11 @@
1-
export declare function setQueryParam(param: string, value: any): void;
2-
export declare function removeQueryParam(param: string): void;
1+
export declare class UrlUtils extends URL {
2+
has(key: string): boolean;
3+
set(key: string, value: any): void;
4+
get(key: string): any | undefined;
5+
remove(key: string): void;
6+
private getData;
7+
private setData;
8+
}
9+
export declare class HistoryStrategy {
10+
static replace(url: URL): void;
11+
}

0 commit comments

Comments
 (0)