Skip to content

[WIP] LiveUrl #2673

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

Draft
wants to merge 9 commits into
base: 2.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/LiveComponent/assets/dist/Backend/BackendResponse.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export default class {
response: Response;
private body;
private liveUrl;
constructor(response: Response);
getBody(): Promise<string>;
getLiveUrl(): Promise<string | null>;
}

This file was deleted.

9 changes: 0 additions & 9 deletions src/LiveComponent/assets/dist/live_controller.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,6 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
type: StringConstructor;
default: string;
};
queryMapping: {
type: ObjectConstructor;
default: {};
};
};
readonly nameValue: string;
readonly urlValue: string;
Expand All @@ -76,11 +72,6 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
readonly debounceValue: number;
readonly fingerprintValue: string;
readonly requestMethodValue: 'get' | 'post';
readonly queryMappingValue: {
[p: string]: {
name: string;
};
};
private proxiedComponent;
private mutationObserver;
component: Component;
Expand Down
136 changes: 11 additions & 125 deletions src/LiveComponent/assets/dist/live_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class RequestBuilder {
fetchOptions.headers = {
Accept: 'application/vnd.live-component+html',
'X-Requested-With': 'XMLHttpRequest',
'X-Live-Url': window.location.pathname + window.location.search,
};
const totalFiles = Object.entries(files).reduce((total, current) => total + current.length, 0);
const hasFingerprints = Object.keys(children).length > 0;
Expand Down Expand Up @@ -111,6 +112,12 @@ class BackendResponse {
}
return this.body;
}
async getLiveUrl() {
if (undefined === this.liveUrl) {
this.liveUrl = await this.response.headers.get('X-Live-Url');
}
return this.liveUrl;
}
}

function getElementAsTagText(element) {
Expand Down Expand Up @@ -2137,6 +2144,10 @@ class Component {
return response;
}
this.processRerender(html, backendResponse);
const liveUrl = await backendResponse.getLiveUrl();
if (liveUrl) {
history.replaceState(history.state, '', new URL(liveUrl + window.location.hash, window.location.origin));
}
this.backendRequest = null;
thisPromiseResolve(backendResponse);
if (this.isRequestPending) {
Expand Down Expand Up @@ -2741,129 +2752,6 @@ class PollingPlugin {
}
}

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 = { ...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) {
data[key] = value;
return 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);
}
});
}
}

class SetValueOntoModelFieldsPlugin {
attachToComponent(component) {
this.synchronizeValueOfModelFields(component);
Expand Down Expand Up @@ -3073,7 +2961,6 @@ class LiveControllerDefault extends Controller {
new PageUnloadingPlugin(),
new PollingPlugin(),
new SetValueOntoModelFieldsPlugin(),
new QueryStringPlugin(this.queryMappingValue),
new ChildComponentPlugin(this.component),
];
plugins.forEach((plugin) => {
Expand Down Expand Up @@ -3183,7 +3070,6 @@ LiveControllerDefault.values = {
debounce: { type: Number, default: 150 },
fingerprint: { type: String, default: '' },
requestMethod: { type: String, default: 'post' },
queryMapping: { type: Object, default: {} },
};
LiveControllerDefault.backendFactory = (controller) => new Backend(controller.urlValue, controller.requestMethodValue);

Expand Down
11 changes: 0 additions & 11 deletions src/LiveComponent/assets/dist/url_utils.d.ts

This file was deleted.

9 changes: 9 additions & 0 deletions src/LiveComponent/assets/src/Backend/BackendResponse.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export default class {
response: Response;
private body: string;
private liveUrl: string | null;

constructor(response: Response) {
this.response = response;
Expand All @@ -13,4 +14,12 @@ export default class {

return this.body;
}

async getLiveUrl(): Promise<string | null> {
if (undefined === this.liveUrl) {
this.liveUrl = await this.response.headers.get('X-Live-Url');
}

return this.liveUrl;
}
}
1 change: 1 addition & 0 deletions src/LiveComponent/assets/src/Backend/RequestBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export default class {
fetchOptions.headers = {
Accept: 'application/vnd.live-component+html',
'X-Requested-With': 'XMLHttpRequest',
'X-Live-Url': window.location.pathname + window.location.search,
};

const totalFiles = Object.entries(files).reduce((total, current) => total + current.length, 0);
Expand Down
8 changes: 8 additions & 0 deletions src/LiveComponent/assets/src/Component/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,14 @@ export default class Component {
}

this.processRerender(html, backendResponse);
const liveUrl = await backendResponse.getLiveUrl();
if (liveUrl) {
history.replaceState(
history.state,
'',
new URL(liveUrl + window.location.hash, window.location.origin)
);
}

// finally resolve this promise
this.backendRequest = null;
Expand Down

This file was deleted.

4 changes: 0 additions & 4 deletions src/LiveComponent/assets/src/live_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import LoadingPlugin from './Component/plugins/LoadingPlugin';
import PageUnloadingPlugin from './Component/plugins/PageUnloadingPlugin';
import type { PluginInterface } from './Component/plugins/PluginInterface';
import PollingPlugin from './Component/plugins/PollingPlugin';
import QueryStringPlugin from './Component/plugins/QueryStringPlugin';
import SetValueOntoModelFieldsPlugin from './Component/plugins/SetValueOntoModelFieldsPlugin';
import ValidatedFieldsPlugin from './Component/plugins/ValidatedFieldsPlugin';
import { type DirectiveModifier, parseDirectives } from './Directive/directives_parser';
Expand Down Expand Up @@ -42,7 +41,6 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
debounce: { type: Number, default: 150 },
fingerprint: { type: String, default: '' },
requestMethod: { type: String, default: 'post' },
queryMapping: { type: Object, default: {} },
};

declare readonly nameValue: string;
Expand All @@ -61,7 +59,6 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
declare readonly debounceValue: number;
declare readonly fingerprintValue: string;
declare readonly requestMethodValue: 'get' | 'post';
declare readonly queryMappingValue: { [p: string]: { name: string } };

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