Skip to content

Commit f67e80c

Browse files
committed
Rework URL utils
1 parent 7d4c7b8 commit f67e80c

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
@@ -2703,118 +2703,131 @@ class ComponentRegistry {
27032703
}
27042704
}
27052705

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

2794+
class Tracker {
2795+
constructor(mapping, initialValue, initiallyPresentInUrl) {
2796+
this.mapping = mapping;
2797+
this.initialValue = JSON.stringify(initialValue);
2798+
this.initiallyPresentInUrl = initiallyPresentInUrl;
2799+
}
2800+
hasReturnedToInitialValue(currentValue) {
2801+
return JSON.stringify(currentValue) === this.initialValue;
2802+
}
2803+
}
27572804
class QueryStringPlugin {
27582805
constructor(mapping) {
2759-
this.mapping = new Map;
2760-
this.initialPropsValues = new Map;
2761-
this.changedProps = {};
2762-
Object.entries(mapping).forEach(([key, config]) => {
2763-
this.mapping.set(key, config);
2764-
});
2806+
this.mapping = mapping;
2807+
this.trackers = new Map;
27652808
}
27662809
attachToComponent(component) {
27672810
component.on('connect', (component) => {
2768-
for (const model of this.mapping.keys()) {
2769-
for (const prop of this.getNormalizedPropNames(component.valueStore.get(model), model)) {
2770-
this.initialPropsValues.set(prop, component.valueStore.get(prop));
2771-
}
2772-
}
2811+
const urlUtils = new UrlUtils(window.location.href);
2812+
Object.entries(this.mapping).forEach(([prop, mapping]) => {
2813+
const tracker = new Tracker(mapping, component.valueStore.get(prop), urlUtils.has(prop));
2814+
this.trackers.set(prop, tracker);
2815+
});
27732816
});
27742817
component.on('render:finished', (component) => {
2775-
this.initialPropsValues.forEach((initialValue, prop) => {
2776-
var _a;
2818+
const urlUtils = new UrlUtils(window.location.href);
2819+
this.trackers.forEach((tracker, prop) => {
27772820
const value = component.valueStore.get(prop);
2778-
(_a = this.changedProps)[prop] || (_a[prop] = JSON.stringify(value) !== JSON.stringify(initialValue));
2779-
if (this.changedProps) {
2780-
this.updateUrlParam(prop, value);
2821+
if (!tracker.initiallyPresentInUrl && tracker.hasReturnedToInitialValue(value)) {
2822+
urlUtils.remove(tracker.mapping.name);
2823+
}
2824+
else {
2825+
urlUtils.set(tracker.mapping.name, value);
27812826
}
27822827
});
2828+
HistoryStrategy.replace(urlUtils);
27832829
});
27842830
}
2785-
updateUrlParam(model, value) {
2786-
const paramName = this.getParamFromModel(model);
2787-
if (paramName === undefined) {
2788-
return;
2789-
}
2790-
this.isValueEmpty(value)
2791-
? removeQueryParam(paramName)
2792-
: setQueryParam(paramName, value);
2793-
}
2794-
getParamFromModel(model) {
2795-
const modelParts = model.split('.');
2796-
const rootPropMapping = this.mapping.get(modelParts[0]);
2797-
if (rootPropMapping === undefined) {
2798-
return undefined;
2799-
}
2800-
return rootPropMapping.name + modelParts.slice(1).map((v) => `[${v}]`).join('');
2801-
}
2802-
*getNormalizedPropNames(value, propertyPath) {
2803-
if (this.isObjectValue(value)) {
2804-
for (const key in value) {
2805-
yield* this.getNormalizedPropNames(value[key], `${propertyPath}.${key}`);
2806-
}
2807-
}
2808-
else {
2809-
yield propertyPath;
2810-
}
2811-
}
2812-
isValueEmpty(value) {
2813-
return (value === '' || value === null || value === undefined);
2814-
}
2815-
isObjectValue(value) {
2816-
return !(Array.isArray(value) || value === null || typeof value !== 'object');
2817-
}
28182831
}
28192832

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