Skip to content

[Live] Fix parent->child fingerprint problem + always match child component #766

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

Merged
merged 1 commit into from
Mar 31, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/LiveComponent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
public User $user;
```

- [BC BREAK]: Child components are no longer automatically re-rendered when
a parent component re-renders and the value of one of the props passed to
the child has changed. Pass `acceptUpdatesFromParent: true` to any `LiveProp`
on the child component to re-enable this behavior.

- Non-persisted entity objects can now be used with `LiveProp`: it will be
serialized using the serializer.

Expand Down
14 changes: 12 additions & 2 deletions src/LiveComponent/assets/dist/Backend/Backend.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import BackendRequest from './BackendRequest';
export interface ChildrenFingerprints {
[key: string]: {
fingerprint: string;
tag: string;
};
}
export interface BackendInterface {
makeRequest(props: any, actions: BackendAction[], updated: {
[key: string]: any;
}, childrenFingerprints: any): BackendRequest;
}, children: ChildrenFingerprints, updatedPropsFromParent: {
[key: string]: any;
}): BackendRequest;
}
export interface BackendAction {
name: string;
Expand All @@ -13,5 +21,7 @@ export default class implements BackendInterface {
constructor(url: string, csrfToken?: string | null);
makeRequest(props: any, actions: BackendAction[], updated: {
[key: string]: any;
}, childrenFingerprints: any): BackendRequest;
}, children: ChildrenFingerprints, updatedPropsFromParent: {
[key: string]: any;
}): BackendRequest;
}
6 changes: 4 additions & 2 deletions src/LiveComponent/assets/dist/Backend/RequestBuilder.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { BackendAction } from './Backend';
import { BackendAction, ChildrenFingerprints } from './Backend';
export default class {
private url;
private readonly csrfToken;
constructor(url: string, csrfToken?: string | null);
buildRequest(props: any, actions: BackendAction[], updated: {
[key: string]: any;
}, childrenFingerprints: any): {
}, children: ChildrenFingerprints, updatedPropsFromParent: {
[key: string]: any;
}): {
url: string;
fetchOptions: RequestInit;
};
Expand Down
4 changes: 3 additions & 1 deletion src/LiveComponent/assets/dist/Component/ValueStore.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ export default class {
private props;
private dirtyProps;
private pendingProps;
private updatedPropsFromParent;
constructor(props: any);
get(name: string): any;
has(name: string): boolean;
set(name: string, value: any): boolean;
getOriginalProps(): any;
getDirtyProps(): any;
getUpdatedPropsFromParent(): any;
flushDirtyPropsToPending(): void;
reinitializeAllProps(props: any): void;
pushPendingPropsBackToDirty(): void;
reinitializeProvidedProps(props: any): boolean;
storeNewPropsFromParent(props: any): boolean;
}
2 changes: 1 addition & 1 deletion src/LiveComponent/assets/dist/Component/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export default class Component {
emitSelf(name: string, data: any): void;
private performEmit;
private doEmit;
updateFromNewElement(toEl: HTMLElement): boolean;
updateFromNewElementFromParentRender(toEl: HTMLElement): void;
onChildComponentModelUpdate(modelName: string, value: any, childComponent: Component): void;
private tryStartingRequest;
private performRequest;
Expand Down
1 change: 0 additions & 1 deletion src/LiveComponent/assets/dist/dom_utils.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@ export declare function getModelDirectiveFromElement(element: HTMLElement, throw
export declare function elementBelongsToThisComponent(element: Element, component: Component): boolean;
export declare function cloneHTMLElement(element: HTMLElement): HTMLElement;
export declare function htmlToElement(html: string): HTMLElement;
export declare function cloneElementWithNewTagName(element: Element, newTag: string): HTMLElement;
export declare function getElementAsTagText(element: HTMLElement): string;
5 changes: 4 additions & 1 deletion src/LiveComponent/assets/dist/live_controller.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
default: number;
};
id: StringConstructor;
fingerprint: StringConstructor;
fingerprint: {
type: StringConstructor;
default: string;
};
};
readonly nameValue: string;
readonly urlValue: string;
Expand Down
71 changes: 35 additions & 36 deletions src/LiveComponent/assets/dist/live_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,15 +305,6 @@ function htmlToElement(html) {
}
return child;
}
function cloneElementWithNewTagName(element, newTag) {
const originalTag = element.tagName;
const startRX = new RegExp('^<' + originalTag, 'i');
const endRX = new RegExp(originalTag + '>$', 'i');
const startSubst = '<' + newTag;
const endSubst = newTag + '>';
const newHTML = element.outerHTML.replace(startRX, startSubst).replace(endRX, endSubst);
return htmlToElement(newHTML);
}
function getElementAsTagText(element) {
return element.innerHTML
? element.outerHTML.slice(0, element.outerHTML.indexOf(element.innerHTML))
Expand Down Expand Up @@ -365,6 +356,7 @@ class ValueStore {
this.props = {};
this.dirtyProps = {};
this.pendingProps = {};
this.updatedPropsFromParent = {};
this.props = props;
}
get(name) {
Expand Down Expand Up @@ -398,27 +390,33 @@ class ValueStore {
getDirtyProps() {
return Object.assign({}, this.dirtyProps);
}
getUpdatedPropsFromParent() {
return Object.assign({}, this.updatedPropsFromParent);
}
flushDirtyPropsToPending() {
this.pendingProps = Object.assign({}, this.dirtyProps);
this.dirtyProps = {};
}
reinitializeAllProps(props) {
this.props = props;
this.updatedPropsFromParent = {};
this.pendingProps = {};
}
pushPendingPropsBackToDirty() {
this.dirtyProps = Object.assign(Object.assign({}, this.pendingProps), this.dirtyProps);
this.pendingProps = {};
}
reinitializeProvidedProps(props) {
storeNewPropsFromParent(props) {
let changed = false;
for (const [key, value] of Object.entries(props)) {
const currentValue = this.get(key);
if (currentValue !== value) {
changed = true;
this.props[key] = value;
}
}
if (changed) {
this.updatedPropsFromParent = props;
}
return changed;
}
}
Expand Down Expand Up @@ -1201,14 +1199,6 @@ function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements,
const childComponentMap = new Map();
childComponents.forEach((childComponent) => {
childComponentMap.set(childComponent.element, childComponent);
if (!childComponent.id) {
throw new Error('Child is missing id.');
}
const childComponentToElement = findChildComponent(childComponent.id, rootToElement);
if (childComponentToElement && childComponentToElement.tagName !== childComponent.element.tagName) {
const newTag = cloneElementWithNewTagName(childComponentToElement, childComponent.element.tagName);
childComponentToElement.replaceWith(newTag);
}
});
morphdom(rootFromElement, rootToElement, {
getNodeKey: (node) => {
Expand All @@ -1227,7 +1217,8 @@ function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements,
if (fromEl instanceof HTMLElement && toEl instanceof HTMLElement) {
if (childComponentMap.has(fromEl)) {
const childComponent = childComponentMap.get(fromEl);
return childComponent.updateFromNewElement(toEl);
childComponent.updateFromNewElementFromParentRender(toEl);
return false;
}
if (modifiedFieldElements.includes(fromEl)) {
setValueOnElement(toEl, getElementValue(fromEl));
Expand Down Expand Up @@ -1867,20 +1858,19 @@ class Component {
this.action(action, data, 1);
});
}
updateFromNewElement(toEl) {
updateFromNewElementFromParentRender(toEl) {
const props = this.elementDriver.getComponentProps(toEl);
if (props === null) {
return false;
return;
}
const isChanged = this.valueStore.reinitializeProvidedProps(props);
const isChanged = this.valueStore.storeNewPropsFromParent(props);
const fingerprint = toEl.dataset.liveFingerprintValue;
if (fingerprint !== undefined) {
this.fingerprint = fingerprint;
}
if (isChanged) {
this.render();
}
return false;
}
onChildComponentModelUpdate(modelName, value, childComponent) {
if (!childComponent.id) {
Expand Down Expand Up @@ -1909,7 +1899,7 @@ class Component {
const thisPromiseResolve = this.nextRequestPromiseResolve;
this.resetPromise();
this.unsyncedInputsTracker.resetUnsyncedFields();
this.backendRequest = this.backend.makeRequest(this.valueStore.getOriginalProps(), this.pendingActions, this.valueStore.getDirtyProps(), this.getChildrenFingerprints());
this.backendRequest = this.backend.makeRequest(this.valueStore.getOriginalProps(), this.pendingActions, this.valueStore.getDirtyProps(), this.getChildrenFingerprints(), this.valueStore.getUpdatedPropsFromParent());
this.hooks.triggerHook('loading.state:started', this.element, this.backendRequest);
this.pendingActions = [];
this.valueStore.flushDirtyPropsToPending();
Expand Down Expand Up @@ -2065,7 +2055,10 @@ class Component {
if (!child.id) {
throw new Error('missing id');
}
fingerprints[child.id] = child.fingerprint;
fingerprints[child.id] = {
fingerprint: child.fingerprint,
tag: child.element.tagName.toLowerCase(),
};
});
return fingerprints;
}
Expand Down Expand Up @@ -2129,7 +2122,7 @@ class RequestBuilder {
this.url = url;
this.csrfToken = csrfToken;
}
buildRequest(props, actions, updated, childrenFingerprints) {
buildRequest(props, actions, updated, children, updatedPropsFromParent) {
const splitUrl = this.url.split('?');
let [url] = splitUrl;
const [, queryString] = splitUrl;
Expand All @@ -2138,22 +2131,28 @@ class RequestBuilder {
fetchOptions.headers = {
Accept: 'application/vnd.live-component+html',
};
const hasFingerprints = Object.keys(childrenFingerprints).length > 0;
const hasFingerprints = Object.keys(children).length > 0;
if (actions.length === 0 &&
this.willDataFitInUrl(JSON.stringify(props), JSON.stringify(updated), params, JSON.stringify(childrenFingerprints))) {
this.willDataFitInUrl(JSON.stringify(props), JSON.stringify(updated), params, JSON.stringify(children), JSON.stringify(updatedPropsFromParent))) {
params.set('props', JSON.stringify(props));
params.set('updated', JSON.stringify(updated));
if (Object.keys(updatedPropsFromParent).length > 0) {
params.set('propsFromParent', JSON.stringify(updatedPropsFromParent));
}
if (hasFingerprints) {
params.set('childrenFingerprints', JSON.stringify(childrenFingerprints));
params.set('children', JSON.stringify(children));
}
fetchOptions.method = 'GET';
}
else {
fetchOptions.method = 'POST';
fetchOptions.headers['Content-Type'] = 'application/json';
const requestData = { props, updated };
if (Object.keys(updatedPropsFromParent).length > 0) {
requestData.propsFromParent = updatedPropsFromParent;
}
if (hasFingerprints) {
requestData.childrenFingerprints = childrenFingerprints;
requestData.children = children;
}
if (actions.length > 0) {
if (this.csrfToken) {
Expand All @@ -2176,8 +2175,8 @@ class RequestBuilder {
fetchOptions,
};
}
willDataFitInUrl(propsJson, updatedJson, params, childrenFingerprintsJson) {
const urlEncodedJsonData = new URLSearchParams(propsJson + updatedJson + childrenFingerprintsJson).toString();
willDataFitInUrl(propsJson, updatedJson, params, childrenJson, propsFromParentJson) {
const urlEncodedJsonData = new URLSearchParams(propsJson + updatedJson + childrenJson + propsFromParentJson).toString();
return (urlEncodedJsonData + params.toString()).length < 1500;
}
}
Expand All @@ -2186,8 +2185,8 @@ class Backend {
constructor(url, csrfToken = null) {
this.requestBuilder = new RequestBuilder(url, csrfToken);
}
makeRequest(props, actions, updated, childrenFingerprints) {
const { url, fetchOptions } = this.requestBuilder.buildRequest(props, actions, updated, childrenFingerprints);
makeRequest(props, actions, updated, children, updatedPropsFromParent) {
const { url, fetchOptions } = this.requestBuilder.buildRequest(props, actions, updated, children, updatedPropsFromParent);
return new BackendRequest(fetch(url, fetchOptions), actions.map((backendAction) => backendAction.name), Object.keys(updated));
}
}
Expand Down Expand Up @@ -2855,7 +2854,7 @@ LiveControllerDefault.values = {
listeners: { type: Array, default: [] },
debounce: { type: Number, default: 150 },
id: String,
fingerprint: String,
fingerprint: { type: String, default: '' },
};
LiveControllerDefault.componentRegistry = new ComponentRegistry();

Expand Down
14 changes: 11 additions & 3 deletions src/LiveComponent/assets/src/Backend/Backend.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import BackendRequest from './BackendRequest';
import RequestBuilder from './RequestBuilder';

export interface ChildrenFingerprints {
// key is the id of the child component
[key: string]: {fingerprint: string, tag: string}
}

export interface BackendInterface {
makeRequest(
props: any,
actions: BackendAction[],
updated: {[key: string]: any},
childrenFingerprints: any
children: ChildrenFingerprints,
updatedPropsFromParent: {[key: string]: any},
): BackendRequest;
}

Expand All @@ -26,13 +32,15 @@ export default class implements BackendInterface {
props: any,
actions: BackendAction[],
updated: {[key: string]: any},
childrenFingerprints: any
children: ChildrenFingerprints,
updatedPropsFromParent: {[key: string]: any},
): BackendRequest {
const { url, fetchOptions } = this.requestBuilder.buildRequest(
props,
actions,
updated,
childrenFingerprints
children,
updatedPropsFromParent
);

return new BackendRequest(
Expand Down
Loading