Skip to content

Commit 5a10c37

Browse files
committed
Rework URL utils
1 parent c996109 commit 5a10c37

File tree

10 files changed

+365
-274
lines changed

10 files changed

+365
-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: 102 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -2699,118 +2699,131 @@ class ComponentRegistry {
26992699
}
27002700
}
27012701

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

2790+
class Tracker {
2791+
constructor(mapping, initialValue, initiallyPresentInUrl) {
2792+
this.mapping = mapping;
2793+
this.initialValue = JSON.stringify(initialValue);
2794+
this.initiallyPresentInUrl = initiallyPresentInUrl;
2795+
}
2796+
hasReturnedToInitialValue(currentValue) {
2797+
return JSON.stringify(currentValue) === this.initialValue;
2798+
}
2799+
}
27532800
class QueryStringPlugin {
27542801
constructor(mapping) {
2755-
this.mapping = new Map;
2756-
this.initialPropsValues = new Map;
2757-
this.changedProps = {};
2758-
Object.entries(mapping).forEach(([key, config]) => {
2759-
this.mapping.set(key, config);
2760-
});
2802+
this.mapping = mapping;
2803+
this.trackers = new Map;
27612804
}
27622805
attachToComponent(component) {
27632806
component.on('connect', (component) => {
2764-
for (const model of this.mapping.keys()) {
2765-
for (const prop of this.getNormalizedPropNames(component.valueStore.get(model), model)) {
2766-
this.initialPropsValues.set(prop, component.valueStore.get(prop));
2767-
}
2768-
}
2807+
const urlUtils = new UrlUtils(window.location.href);
2808+
Object.entries(this.mapping).forEach(([prop, mapping]) => {
2809+
const tracker = new Tracker(mapping, component.valueStore.get(prop), urlUtils.has(prop));
2810+
this.trackers.set(prop, tracker);
2811+
});
27692812
});
27702813
component.on('render:finished', (component) => {
2771-
this.initialPropsValues.forEach((initialValue, prop) => {
2772-
var _a;
2814+
const urlUtils = new UrlUtils(window.location.href);
2815+
this.trackers.forEach((tracker, prop) => {
27732816
const value = component.valueStore.get(prop);
2774-
(_a = this.changedProps)[prop] || (_a[prop] = JSON.stringify(value) !== JSON.stringify(initialValue));
2775-
if (this.changedProps) {
2776-
this.updateUrlParam(prop, value);
2817+
if (!tracker.initiallyPresentInUrl && tracker.hasReturnedToInitialValue(value)) {
2818+
urlUtils.remove(tracker.mapping.name);
2819+
}
2820+
else {
2821+
urlUtils.set(tracker.mapping.name, value);
27772822
}
27782823
});
2824+
HistoryStrategy.replace(urlUtils);
27792825
});
27802826
}
2781-
updateUrlParam(model, value) {
2782-
const paramName = this.getParamFromModel(model);
2783-
if (paramName === undefined) {
2784-
return;
2785-
}
2786-
this.isValueEmpty(value)
2787-
? removeQueryParam(paramName)
2788-
: setQueryParam(paramName, value);
2789-
}
2790-
getParamFromModel(model) {
2791-
const modelParts = model.split('.');
2792-
const rootPropMapping = this.mapping.get(modelParts[0]);
2793-
if (rootPropMapping === undefined) {
2794-
return undefined;
2795-
}
2796-
return rootPropMapping.name + modelParts.slice(1).map((v) => `[${v}]`).join('');
2797-
}
2798-
*getNormalizedPropNames(value, propertyPath) {
2799-
if (this.isObjectValue(value)) {
2800-
for (const key in value) {
2801-
yield* this.getNormalizedPropNames(value[key], `${propertyPath}.${key}`);
2802-
}
2803-
}
2804-
else {
2805-
yield propertyPath;
2806-
}
2807-
}
2808-
isValueEmpty(value) {
2809-
return (value === '' || value === null || value === undefined);
2810-
}
2811-
isObjectValue(value) {
2812-
return !(Array.isArray(value) || value === null || typeof value !== 'object');
2813-
}
28142827
}
28152828

28162829
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)