Skip to content

Commit

Permalink
[LiveComponent] Allow binding LiveProp to URL query parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
squrious authored and weaverryan committed Dec 19, 2023
1 parent 23100d0 commit 68e4dd3
Show file tree
Hide file tree
Showing 38 changed files with 1,296 additions and 90 deletions.
9 changes: 1 addition & 8 deletions src/Autocomplete/assets/dist/controller.js
Expand Up @@ -15,19 +15,12 @@ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol */


function __classPrivateFieldGet(receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
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");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
}

typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
}

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;
class default_1 extends Controller {
Expand Down
1 change: 1 addition & 0 deletions src/LiveComponent/CHANGELOG.md
Expand Up @@ -15,6 +15,7 @@
- Fix instantiating LiveComponentMetadata multiple times.
- Change JavaScript package to `type: module`.
- Throwing an error when setting an invalid model name.
- Add support for URL binding in `LiveProp`

## 2.12.0

Expand Down
@@ -0,0 +1,13 @@
import Component from '../index';
import { PluginInterface } from './PluginInterface';
interface QueryMapping {
name: string;
}
export default class implements PluginInterface {
private readonly mapping;
constructor(mapping: {
[p: string]: QueryMapping;
});
attachToComponent(component: Component): void;
}
export {};
@@ -0,0 +1,9 @@
import Component from '../index';
import { PluginInterface } from './PluginInterface';
export default class implements PluginInterface {
private element;
private mapping;
attachToComponent(component: Component): void;
private registerBindings;
private updateUrl;
}
9 changes: 9 additions & 0 deletions src/LiveComponent/assets/dist/live_controller.d.ts
Expand Up @@ -32,6 +32,10 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
type: StringConstructor;
default: string;
};
queryMapping: {
type: ObjectConstructor;
default: {};
};
};
readonly nameValue: string;
readonly urlValue: string;
Expand All @@ -44,6 +48,11 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
readonly hasDebounceValue: boolean;
readonly debounceValue: number;
readonly fingerprintValue: string;
readonly queryMappingValue: {
[p: string]: {
name: string;
};
};
private proxiedComponent;
component: Component;
pendingActionTriggerModelElement: HTMLElement | null;
Expand Down
126 changes: 123 additions & 3 deletions src/LiveComponent/assets/dist/live_controller.js
Expand Up @@ -592,9 +592,6 @@ var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof win
let insertionPoint = oldParent.firstChild;
let newChild;

newParent.children;
oldParent.children;

// run through all the new content
while (nextNewChild) {

Expand Down Expand Up @@ -2729,6 +2726,127 @@ class ComponentRegistry {
}
}

function isValueEmpty(value) {
if (null === value || value === '' || undefined === value || (Array.isArray(value) && value.length === 0)) {
return true;
}
if (typeof value !== 'object') {
return false;
}
for (const key of Object.keys(value)) {
if (!isValueEmpty(value[key])) {
return false;
}
}
return true;
}
function toQueryString(data) {
const buildQueryStringEntries = (data, entries = {}, baseKey = '') => {
Object.entries(data).forEach(([iKey, iValue]) => {
const key = baseKey === '' ? iKey : `${baseKey}[${iKey}]`;
if ('' === baseKey && isValueEmpty(iValue)) {
entries[key] = '';
}
else if (null !== iValue) {
if (typeof iValue === 'object') {
entries = Object.assign(Object.assign({}, entries), buildQueryStringEntries(iValue, entries, key));
}
else {
entries[key] = encodeURIComponent(iValue)
.replace(/%20/g, '+')
.replace(/%2C/g, ',');
}
}
});
return entries;
};
const entries = buildQueryStringEntries(data);
return Object.entries(entries)
.map(([key, value]) => `${key}=${value}`)
.join('&');
}
function fromQueryString(search) {
search = search.replace('?', '');
if (search === '')
return {};
const insertDotNotatedValueIntoData = (key, value, data) => {
const [first, second, ...rest] = key.split('.');
if (!second)
return (data[key] = value);
if (data[first] === undefined) {
data[first] = Number.isNaN(Number.parseInt(second)) ? {} : [];
}
insertDotNotatedValueIntoData([second, ...rest].join('.'), value, data[first]);
};
const entries = search.split('&').map((i) => i.split('='));
const data = {};
entries.forEach(([key, value]) => {
value = decodeURIComponent(value.replace(/\+/g, '%20'));
if (!key.includes('[')) {
data[key] = value;
}
else {
if ('' === value)
return;
const dotNotatedKey = key.replace(/\[/g, '.').replace(/]/g, '');
insertDotNotatedValueIntoData(dotNotatedKey, value, data);
}
});
return data;
}
class UrlUtils extends URL {
has(key) {
const data = this.getData();
return Object.keys(data).includes(key);
}
set(key, value) {
const data = this.getData();
data[key] = value;
this.setData(data);
}
get(key) {
return this.getData()[key];
}
remove(key) {
const data = this.getData();
delete data[key];
this.setData(data);
}
getData() {
if (!this.search) {
return {};
}
return fromQueryString(this.search);
}
setData(data) {
this.search = toQueryString(data);
}
}
class HistoryStrategy {
static replace(url) {
history.replaceState(history.state, '', url);
}
}

class QueryStringPlugin {
constructor(mapping) {
this.mapping = mapping;
}
attachToComponent(component) {
component.on('render:finished', (component) => {
const urlUtils = new UrlUtils(window.location.href);
const currentUrl = urlUtils.toString();
Object.entries(this.mapping).forEach(([prop, mapping]) => {
const value = component.valueStore.get(prop);
urlUtils.set(mapping.name, value);
});
if (currentUrl !== urlUtils.toString()) {
HistoryStrategy.replace(urlUtils);
}
});
}
}

const getComponent = (element) => LiveControllerDefault.componentRegistry.getComponent(element);
class LiveControllerDefault extends Controller {
constructor() {
Expand Down Expand Up @@ -2756,6 +2874,7 @@ class LiveControllerDefault extends Controller {
new PageUnloadingPlugin(),
new PollingPlugin(),
new SetValueOntoModelFieldsPlugin(),
new QueryStringPlugin(this.queryMappingValue),
];
plugins.forEach((plugin) => {
this.component.addPlugin(plugin);
Expand Down Expand Up @@ -2976,6 +3095,7 @@ LiveControllerDefault.values = {
debounce: { type: Number, default: 150 },
id: String,
fingerprint: { type: String, default: '' },
queryMapping: { type: Object, default: {} },
};
LiveControllerDefault.componentRegistry = new ComponentRegistry();

Expand Down
11 changes: 11 additions & 0 deletions src/LiveComponent/assets/dist/url_utils.d.ts
@@ -0,0 +1,11 @@
export declare class UrlUtils extends URL {
has(key: string): boolean;
set(key: string, value: any): void;
get(key: string): any | undefined;
remove(key: string): void;
private getData;
private setData;
}
export declare class HistoryStrategy {
static replace(url: URL): void;
}
@@ -0,0 +1,31 @@
import Component from '../index';
import { PluginInterface } from './PluginInterface';
import { UrlUtils, HistoryStrategy } from '../../url_utils';

interface QueryMapping {
/**
* URL parameter name
*/
name: string,
}

export default class implements PluginInterface {
constructor(private readonly mapping: {[p: string]: QueryMapping}) {}

attachToComponent(component: Component): void {
component.on('render:finished', (component: Component) => {
const urlUtils = new UrlUtils(window.location.href);
const currentUrl = urlUtils.toString();

Object.entries(this.mapping).forEach(([prop, mapping]) => {
const value = component.valueStore.get(prop);
urlUtils.set(mapping.name, value);
});

// Only update URL if it has changed
if (currentUrl !== urlUtils.toString()) {
HistoryStrategy.replace(urlUtils);
}
});
}
}
4 changes: 4 additions & 0 deletions src/LiveComponent/assets/src/live_controller.ts
Expand Up @@ -18,6 +18,7 @@ import SetValueOntoModelFieldsPlugin from './Component/plugins/SetValueOntoModel
import { PluginInterface } from './Component/plugins/PluginInterface';
import getModelBinding from './Directive/get_model_binding';
import ComponentRegistry from './ComponentRegistry';
import QueryStringPlugin from './Component/plugins/QueryStringPlugin';

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

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

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

0 comments on commit 68e4dd3

Please sign in to comment.