Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[LiveComponent] Allow binding LiveProp to URL query parameter #1230

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be updated. It looks like it has not been released in v2.13.3: https://github.com/symfony/ux/blob/v2.13.3/src/LiveComponent/CHANGELOG.md


## 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