Skip to content

Commit 68e4dd3

Browse files
squriousweaverryan
authored andcommitted
[LiveComponent] Allow binding LiveProp to URL query parameter
1 parent 23100d0 commit 68e4dd3

38 files changed

+1296
-90
lines changed

src/Autocomplete/assets/dist/controller.js

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,12 @@ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
1515
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
1616
PERFORMANCE OF THIS SOFTWARE.
1717
***************************************************************************** */
18-
/* global Reflect, Promise, SuppressedError, Symbol */
19-
2018

2119
function __classPrivateFieldGet(receiver, state, kind, f) {
2220
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
2321
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
2422
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
25-
}
26-
27-
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
28-
var e = new Error(message);
29-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
30-
};
23+
}
3124

3225
var _default_1_instances, _default_1_getCommonConfig, _default_1_createAutocomplete, _default_1_createAutocompleteWithHtmlContents, _default_1_createAutocompleteWithRemoteData, _default_1_stripTags, _default_1_mergeObjects, _default_1_createTomSelect;
3326
class default_1 extends Controller {

src/LiveComponent/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- Fix instantiating LiveComponentMetadata multiple times.
1616
- Change JavaScript package to `type: module`.
1717
- Throwing an error when setting an invalid model name.
18+
- Add support for URL binding in `LiveProp`
1819

1920
## 2.12.0
2021

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Component from '../index';
2+
import { PluginInterface } from './PluginInterface';
3+
interface QueryMapping {
4+
name: string;
5+
}
6+
export default class implements PluginInterface {
7+
private readonly mapping;
8+
constructor(mapping: {
9+
[p: string]: QueryMapping;
10+
});
11+
attachToComponent(component: Component): void;
12+
}
13+
export {};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Component from '../index';
2+
import { PluginInterface } from './PluginInterface';
3+
export default class implements PluginInterface {
4+
private element;
5+
private mapping;
6+
attachToComponent(component: Component): void;
7+
private registerBindings;
8+
private updateUrl;
9+
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
3232
type: StringConstructor;
3333
default: string;
3434
};
35+
queryMapping: {
36+
type: ObjectConstructor;
37+
default: {};
38+
};
3539
};
3640
readonly nameValue: string;
3741
readonly urlValue: string;
@@ -44,6 +48,11 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
4448
readonly hasDebounceValue: boolean;
4549
readonly debounceValue: number;
4650
readonly fingerprintValue: string;
51+
readonly queryMappingValue: {
52+
[p: string]: {
53+
name: string;
54+
};
55+
};
4756
private proxiedComponent;
4857
component: Component;
4958
pendingActionTriggerModelElement: HTMLElement | null;

src/LiveComponent/assets/dist/live_controller.js

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -592,9 +592,6 @@ var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof win
592592
let insertionPoint = oldParent.firstChild;
593593
let newChild;
594594

595-
newParent.children;
596-
oldParent.children;
597-
598595
// run through all the new content
599596
while (nextNewChild) {
600597

@@ -2729,6 +2726,127 @@ class ComponentRegistry {
27292726
}
27302727
}
27312728

2729+
function isValueEmpty(value) {
2730+
if (null === value || value === '' || undefined === value || (Array.isArray(value) && value.length === 0)) {
2731+
return true;
2732+
}
2733+
if (typeof value !== 'object') {
2734+
return false;
2735+
}
2736+
for (const key of Object.keys(value)) {
2737+
if (!isValueEmpty(value[key])) {
2738+
return false;
2739+
}
2740+
}
2741+
return true;
2742+
}
2743+
function toQueryString(data) {
2744+
const buildQueryStringEntries = (data, entries = {}, baseKey = '') => {
2745+
Object.entries(data).forEach(([iKey, iValue]) => {
2746+
const key = baseKey === '' ? iKey : `${baseKey}[${iKey}]`;
2747+
if ('' === baseKey && isValueEmpty(iValue)) {
2748+
entries[key] = '';
2749+
}
2750+
else if (null !== iValue) {
2751+
if (typeof iValue === 'object') {
2752+
entries = Object.assign(Object.assign({}, entries), buildQueryStringEntries(iValue, entries, key));
2753+
}
2754+
else {
2755+
entries[key] = encodeURIComponent(iValue)
2756+
.replace(/%20/g, '+')
2757+
.replace(/%2C/g, ',');
2758+
}
2759+
}
2760+
});
2761+
return entries;
2762+
};
2763+
const entries = buildQueryStringEntries(data);
2764+
return Object.entries(entries)
2765+
.map(([key, value]) => `${key}=${value}`)
2766+
.join('&');
2767+
}
2768+
function fromQueryString(search) {
2769+
search = search.replace('?', '');
2770+
if (search === '')
2771+
return {};
2772+
const insertDotNotatedValueIntoData = (key, value, data) => {
2773+
const [first, second, ...rest] = key.split('.');
2774+
if (!second)
2775+
return (data[key] = value);
2776+
if (data[first] === undefined) {
2777+
data[first] = Number.isNaN(Number.parseInt(second)) ? {} : [];
2778+
}
2779+
insertDotNotatedValueIntoData([second, ...rest].join('.'), value, data[first]);
2780+
};
2781+
const entries = search.split('&').map((i) => i.split('='));
2782+
const data = {};
2783+
entries.forEach(([key, value]) => {
2784+
value = decodeURIComponent(value.replace(/\+/g, '%20'));
2785+
if (!key.includes('[')) {
2786+
data[key] = value;
2787+
}
2788+
else {
2789+
if ('' === value)
2790+
return;
2791+
const dotNotatedKey = key.replace(/\[/g, '.').replace(/]/g, '');
2792+
insertDotNotatedValueIntoData(dotNotatedKey, value, data);
2793+
}
2794+
});
2795+
return data;
2796+
}
2797+
class UrlUtils extends URL {
2798+
has(key) {
2799+
const data = this.getData();
2800+
return Object.keys(data).includes(key);
2801+
}
2802+
set(key, value) {
2803+
const data = this.getData();
2804+
data[key] = value;
2805+
this.setData(data);
2806+
}
2807+
get(key) {
2808+
return this.getData()[key];
2809+
}
2810+
remove(key) {
2811+
const data = this.getData();
2812+
delete data[key];
2813+
this.setData(data);
2814+
}
2815+
getData() {
2816+
if (!this.search) {
2817+
return {};
2818+
}
2819+
return fromQueryString(this.search);
2820+
}
2821+
setData(data) {
2822+
this.search = toQueryString(data);
2823+
}
2824+
}
2825+
class HistoryStrategy {
2826+
static replace(url) {
2827+
history.replaceState(history.state, '', url);
2828+
}
2829+
}
2830+
2831+
class QueryStringPlugin {
2832+
constructor(mapping) {
2833+
this.mapping = mapping;
2834+
}
2835+
attachToComponent(component) {
2836+
component.on('render:finished', (component) => {
2837+
const urlUtils = new UrlUtils(window.location.href);
2838+
const currentUrl = urlUtils.toString();
2839+
Object.entries(this.mapping).forEach(([prop, mapping]) => {
2840+
const value = component.valueStore.get(prop);
2841+
urlUtils.set(mapping.name, value);
2842+
});
2843+
if (currentUrl !== urlUtils.toString()) {
2844+
HistoryStrategy.replace(urlUtils);
2845+
}
2846+
});
2847+
}
2848+
}
2849+
27322850
const getComponent = (element) => LiveControllerDefault.componentRegistry.getComponent(element);
27332851
class LiveControllerDefault extends Controller {
27342852
constructor() {
@@ -2756,6 +2874,7 @@ class LiveControllerDefault extends Controller {
27562874
new PageUnloadingPlugin(),
27572875
new PollingPlugin(),
27582876
new SetValueOntoModelFieldsPlugin(),
2877+
new QueryStringPlugin(this.queryMappingValue),
27592878
];
27602879
plugins.forEach((plugin) => {
27612880
this.component.addPlugin(plugin);
@@ -2976,6 +3095,7 @@ LiveControllerDefault.values = {
29763095
debounce: { type: Number, default: 150 },
29773096
id: String,
29783097
fingerprint: { type: String, default: '' },
3098+
queryMapping: { type: Object, default: {} },
29793099
};
29803100
LiveControllerDefault.componentRegistry = new ComponentRegistry();
29813101

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import Component from '../index';
2+
import { PluginInterface } from './PluginInterface';
3+
import { UrlUtils, HistoryStrategy } from '../../url_utils';
4+
5+
interface QueryMapping {
6+
/**
7+
* URL parameter name
8+
*/
9+
name: string,
10+
}
11+
12+
export default class implements PluginInterface {
13+
constructor(private readonly mapping: {[p: string]: QueryMapping}) {}
14+
15+
attachToComponent(component: Component): void {
16+
component.on('render:finished', (component: Component) => {
17+
const urlUtils = new UrlUtils(window.location.href);
18+
const currentUrl = urlUtils.toString();
19+
20+
Object.entries(this.mapping).forEach(([prop, mapping]) => {
21+
const value = component.valueStore.get(prop);
22+
urlUtils.set(mapping.name, value);
23+
});
24+
25+
// Only update URL if it has changed
26+
if (currentUrl !== urlUtils.toString()) {
27+
HistoryStrategy.replace(urlUtils);
28+
}
29+
});
30+
}
31+
}

src/LiveComponent/assets/src/live_controller.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import SetValueOntoModelFieldsPlugin from './Component/plugins/SetValueOntoModel
1818
import { PluginInterface } from './Component/plugins/PluginInterface';
1919
import getModelBinding from './Directive/get_model_binding';
2020
import ComponentRegistry from './ComponentRegistry';
21+
import QueryStringPlugin from './Component/plugins/QueryStringPlugin';
2122

2223
export { Component };
2324
export const getComponent = (element: HTMLElement): Promise<Component> =>
@@ -44,6 +45,7 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
4445
debounce: { type: Number, default: 150 },
4546
id: String,
4647
fingerprint: { type: String, default: '' },
48+
queryMapping: { type: Object, default: {} },
4749
};
4850

4951
declare readonly nameValue: string;
@@ -54,6 +56,7 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
5456
declare readonly hasDebounceValue: boolean;
5557
declare readonly debounceValue: number;
5658
declare readonly fingerprintValue: string;
59+
declare readonly queryMappingValue: { [p: string]: { name: string } };
5760

5861
/** The component, wrapped in the convenience Proxy */
5962
private proxiedComponent: Component;
@@ -102,6 +105,7 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
102105
new PageUnloadingPlugin(),
103106
new PollingPlugin(),
104107
new SetValueOntoModelFieldsPlugin(),
108+
new QueryStringPlugin(this.queryMappingValue),
105109
];
106110
plugins.forEach((plugin) => {
107111
this.component.addPlugin(plugin);

0 commit comments

Comments
 (0)