Skip to content

Commit 1995668

Browse files
[Live] Fix parent->child fingerprint problem + always match child component
1 parent eb298ee commit 1995668

38 files changed

+577
-267
lines changed

src/LiveComponent/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
public User $user;
1111
```
1212

13+
- [BC BREAK]: Child components are no longer automatically re-rendered when
14+
a parent component re-renders and the value of one of the props passed to
15+
the child has changed. Pass `acceptUpdatesFromParent: true` to any `LiveProp`
16+
on the child component to re-enable this behavior.
17+
1318
- Non-persisted entity objects can now be used with `LiveProp`: it will be
1419
serialized using the serializer.
1520

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import BackendRequest from './BackendRequest';
2+
export interface ChildrenFingerprints {
3+
[key: string]: {
4+
fingerprint: string;
5+
tag: string;
6+
};
7+
}
28
export interface BackendInterface {
39
makeRequest(props: any, actions: BackendAction[], updated: {
410
[key: string]: any;
5-
}, childrenFingerprints: any): BackendRequest;
11+
}, children: ChildrenFingerprints, updatedPropsFromParent: {
12+
[key: string]: any;
13+
}): BackendRequest;
614
}
715
export interface BackendAction {
816
name: string;
@@ -13,5 +21,7 @@ export default class implements BackendInterface {
1321
constructor(url: string, csrfToken?: string | null);
1422
makeRequest(props: any, actions: BackendAction[], updated: {
1523
[key: string]: any;
16-
}, childrenFingerprints: any): BackendRequest;
24+
}, children: ChildrenFingerprints, updatedPropsFromParent: {
25+
[key: string]: any;
26+
}): BackendRequest;
1727
}

src/LiveComponent/assets/dist/Backend/RequestBuilder.d.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import { BackendAction } from './Backend';
1+
import { BackendAction, ChildrenFingerprints } from './Backend';
22
export default class {
33
private url;
44
private readonly csrfToken;
55
constructor(url: string, csrfToken?: string | null);
66
buildRequest(props: any, actions: BackendAction[], updated: {
77
[key: string]: any;
8-
}, childrenFingerprints: any): {
8+
}, children: ChildrenFingerprints, updatedPropsFromParent: {
9+
[key: string]: any;
10+
}): {
911
url: string;
1012
fetchOptions: RequestInit;
1113
};

src/LiveComponent/assets/dist/Component/ValueStore.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ export default class {
22
private props;
33
private dirtyProps;
44
private pendingProps;
5+
private updatedPropsFromParent;
56
constructor(props: any);
67
get(name: string): any;
78
has(name: string): boolean;
89
set(name: string, value: any): boolean;
910
getOriginalProps(): any;
1011
getDirtyProps(): any;
12+
getUpdatedPropsFromParent(): any;
1113
flushDirtyPropsToPending(): void;
1214
reinitializeAllProps(props: any): void;
1315
pushPendingPropsBackToDirty(): void;
14-
reinitializeProvidedProps(props: any): boolean;
16+
storeNewPropsFromParent(props: any): boolean;
1517
}

src/LiveComponent/assets/dist/Component/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export default class Component {
5151
emitSelf(name: string, data: any): void;
5252
private performEmit;
5353
private doEmit;
54-
updateFromNewElement(toEl: HTMLElement): boolean;
54+
updateFromNewElementFromParentRender(toEl: HTMLElement): void;
5555
onChildComponentModelUpdate(modelName: string, value: any, childComponent: Component): void;
5656
private tryStartingRequest;
5757
private performRequest;

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,4 @@ export declare function getModelDirectiveFromElement(element: HTMLElement, throw
88
export declare function elementBelongsToThisComponent(element: Element, component: Component): boolean;
99
export declare function cloneHTMLElement(element: HTMLElement): HTMLElement;
1010
export declare function htmlToElement(html: string): HTMLElement;
11-
export declare function cloneElementWithNewTagName(element: Element, newTag: string): HTMLElement;
1211
export declare function getElementAsTagText(element: HTMLElement): string;

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
2828
default: number;
2929
};
3030
id: StringConstructor;
31-
fingerprint: StringConstructor;
31+
fingerprint: {
32+
type: StringConstructor;
33+
default: string;
34+
};
3235
};
3336
readonly nameValue: string;
3437
readonly urlValue: string;

src/LiveComponent/assets/dist/live_controller.js

Lines changed: 35 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -305,15 +305,6 @@ function htmlToElement(html) {
305305
}
306306
return child;
307307
}
308-
function cloneElementWithNewTagName(element, newTag) {
309-
const originalTag = element.tagName;
310-
const startRX = new RegExp('^<' + originalTag, 'i');
311-
const endRX = new RegExp(originalTag + '>$', 'i');
312-
const startSubst = '<' + newTag;
313-
const endSubst = newTag + '>';
314-
const newHTML = element.outerHTML.replace(startRX, startSubst).replace(endRX, endSubst);
315-
return htmlToElement(newHTML);
316-
}
317308
function getElementAsTagText(element) {
318309
return element.innerHTML
319310
? element.outerHTML.slice(0, element.outerHTML.indexOf(element.innerHTML))
@@ -365,6 +356,7 @@ class ValueStore {
365356
this.props = {};
366357
this.dirtyProps = {};
367358
this.pendingProps = {};
359+
this.updatedPropsFromParent = {};
368360
this.props = props;
369361
}
370362
get(name) {
@@ -398,27 +390,33 @@ class ValueStore {
398390
getDirtyProps() {
399391
return Object.assign({}, this.dirtyProps);
400392
}
393+
getUpdatedPropsFromParent() {
394+
return Object.assign({}, this.updatedPropsFromParent);
395+
}
401396
flushDirtyPropsToPending() {
402397
this.pendingProps = Object.assign({}, this.dirtyProps);
403398
this.dirtyProps = {};
404399
}
405400
reinitializeAllProps(props) {
406401
this.props = props;
402+
this.updatedPropsFromParent = {};
407403
this.pendingProps = {};
408404
}
409405
pushPendingPropsBackToDirty() {
410406
this.dirtyProps = Object.assign(Object.assign({}, this.pendingProps), this.dirtyProps);
411407
this.pendingProps = {};
412408
}
413-
reinitializeProvidedProps(props) {
409+
storeNewPropsFromParent(props) {
414410
let changed = false;
415411
for (const [key, value] of Object.entries(props)) {
416412
const currentValue = this.get(key);
417413
if (currentValue !== value) {
418414
changed = true;
419-
this.props[key] = value;
420415
}
421416
}
417+
if (changed) {
418+
this.updatedPropsFromParent = props;
419+
}
422420
return changed;
423421
}
424422
}
@@ -1201,14 +1199,6 @@ function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements,
12011199
const childComponentMap = new Map();
12021200
childComponents.forEach((childComponent) => {
12031201
childComponentMap.set(childComponent.element, childComponent);
1204-
if (!childComponent.id) {
1205-
throw new Error('Child is missing id.');
1206-
}
1207-
const childComponentToElement = findChildComponent(childComponent.id, rootToElement);
1208-
if (childComponentToElement && childComponentToElement.tagName !== childComponent.element.tagName) {
1209-
const newTag = cloneElementWithNewTagName(childComponentToElement, childComponent.element.tagName);
1210-
childComponentToElement.replaceWith(newTag);
1211-
}
12121202
});
12131203
morphdom(rootFromElement, rootToElement, {
12141204
getNodeKey: (node) => {
@@ -1227,7 +1217,8 @@ function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements,
12271217
if (fromEl instanceof HTMLElement && toEl instanceof HTMLElement) {
12281218
if (childComponentMap.has(fromEl)) {
12291219
const childComponent = childComponentMap.get(fromEl);
1230-
return childComponent.updateFromNewElement(toEl);
1220+
childComponent.updateFromNewElementFromParentRender(toEl);
1221+
return false;
12311222
}
12321223
if (modifiedFieldElements.includes(fromEl)) {
12331224
setValueOnElement(toEl, getElementValue(fromEl));
@@ -1867,20 +1858,19 @@ class Component {
18671858
this.action(action, data, 1);
18681859
});
18691860
}
1870-
updateFromNewElement(toEl) {
1861+
updateFromNewElementFromParentRender(toEl) {
18711862
const props = this.elementDriver.getComponentProps(toEl);
18721863
if (props === null) {
1873-
return false;
1864+
return;
18741865
}
1875-
const isChanged = this.valueStore.reinitializeProvidedProps(props);
1866+
const isChanged = this.valueStore.storeNewPropsFromParent(props);
18761867
const fingerprint = toEl.dataset.liveFingerprintValue;
18771868
if (fingerprint !== undefined) {
18781869
this.fingerprint = fingerprint;
18791870
}
18801871
if (isChanged) {
18811872
this.render();
18821873
}
1883-
return false;
18841874
}
18851875
onChildComponentModelUpdate(modelName, value, childComponent) {
18861876
if (!childComponent.id) {
@@ -1909,7 +1899,7 @@ class Component {
19091899
const thisPromiseResolve = this.nextRequestPromiseResolve;
19101900
this.resetPromise();
19111901
this.unsyncedInputsTracker.resetUnsyncedFields();
1912-
this.backendRequest = this.backend.makeRequest(this.valueStore.getOriginalProps(), this.pendingActions, this.valueStore.getDirtyProps(), this.getChildrenFingerprints());
1902+
this.backendRequest = this.backend.makeRequest(this.valueStore.getOriginalProps(), this.pendingActions, this.valueStore.getDirtyProps(), this.getChildrenFingerprints(), this.valueStore.getUpdatedPropsFromParent());
19131903
this.hooks.triggerHook('loading.state:started', this.element, this.backendRequest);
19141904
this.pendingActions = [];
19151905
this.valueStore.flushDirtyPropsToPending();
@@ -2065,7 +2055,10 @@ class Component {
20652055
if (!child.id) {
20662056
throw new Error('missing id');
20672057
}
2068-
fingerprints[child.id] = child.fingerprint;
2058+
fingerprints[child.id] = {
2059+
fingerprint: child.fingerprint,
2060+
tag: child.element.tagName.toLowerCase(),
2061+
};
20692062
});
20702063
return fingerprints;
20712064
}
@@ -2129,7 +2122,7 @@ class RequestBuilder {
21292122
this.url = url;
21302123
this.csrfToken = csrfToken;
21312124
}
2132-
buildRequest(props, actions, updated, childrenFingerprints) {
2125+
buildRequest(props, actions, updated, children, updatedPropsFromParent) {
21332126
const splitUrl = this.url.split('?');
21342127
let [url] = splitUrl;
21352128
const [, queryString] = splitUrl;
@@ -2138,22 +2131,28 @@ class RequestBuilder {
21382131
fetchOptions.headers = {
21392132
Accept: 'application/vnd.live-component+html',
21402133
};
2141-
const hasFingerprints = Object.keys(childrenFingerprints).length > 0;
2134+
const hasFingerprints = Object.keys(children).length > 0;
21422135
if (actions.length === 0 &&
2143-
this.willDataFitInUrl(JSON.stringify(props), JSON.stringify(updated), params, JSON.stringify(childrenFingerprints))) {
2136+
this.willDataFitInUrl(JSON.stringify(props), JSON.stringify(updated), params, JSON.stringify(children), JSON.stringify(updatedPropsFromParent))) {
21442137
params.set('props', JSON.stringify(props));
21452138
params.set('updated', JSON.stringify(updated));
2139+
if (Object.keys(updatedPropsFromParent).length > 0) {
2140+
params.set('propsFromParent', JSON.stringify(updatedPropsFromParent));
2141+
}
21462142
if (hasFingerprints) {
2147-
params.set('childrenFingerprints', JSON.stringify(childrenFingerprints));
2143+
params.set('children', JSON.stringify(children));
21482144
}
21492145
fetchOptions.method = 'GET';
21502146
}
21512147
else {
21522148
fetchOptions.method = 'POST';
21532149
fetchOptions.headers['Content-Type'] = 'application/json';
21542150
const requestData = { props, updated };
2151+
if (Object.keys(updatedPropsFromParent).length > 0) {
2152+
requestData.propsFromParent = updatedPropsFromParent;
2153+
}
21552154
if (hasFingerprints) {
2156-
requestData.childrenFingerprints = childrenFingerprints;
2155+
requestData.children = children;
21572156
}
21582157
if (actions.length > 0) {
21592158
if (this.csrfToken) {
@@ -2176,8 +2175,8 @@ class RequestBuilder {
21762175
fetchOptions,
21772176
};
21782177
}
2179-
willDataFitInUrl(propsJson, updatedJson, params, childrenFingerprintsJson) {
2180-
const urlEncodedJsonData = new URLSearchParams(propsJson + updatedJson + childrenFingerprintsJson).toString();
2178+
willDataFitInUrl(propsJson, updatedJson, params, childrenJson, propsFromParentJson) {
2179+
const urlEncodedJsonData = new URLSearchParams(propsJson + updatedJson + childrenJson + propsFromParentJson).toString();
21812180
return (urlEncodedJsonData + params.toString()).length < 1500;
21822181
}
21832182
}
@@ -2186,8 +2185,8 @@ class Backend {
21862185
constructor(url, csrfToken = null) {
21872186
this.requestBuilder = new RequestBuilder(url, csrfToken);
21882187
}
2189-
makeRequest(props, actions, updated, childrenFingerprints) {
2190-
const { url, fetchOptions } = this.requestBuilder.buildRequest(props, actions, updated, childrenFingerprints);
2188+
makeRequest(props, actions, updated, children, updatedPropsFromParent) {
2189+
const { url, fetchOptions } = this.requestBuilder.buildRequest(props, actions, updated, children, updatedPropsFromParent);
21912190
return new BackendRequest(fetch(url, fetchOptions), actions.map((backendAction) => backendAction.name), Object.keys(updated));
21922191
}
21932192
}
@@ -2855,7 +2854,7 @@ LiveControllerDefault.values = {
28552854
listeners: { type: Array, default: [] },
28562855
debounce: { type: Number, default: 150 },
28572856
id: String,
2858-
fingerprint: String,
2857+
fingerprint: { type: String, default: '' },
28592858
};
28602859
LiveControllerDefault.componentRegistry = new ComponentRegistry();
28612860

src/LiveComponent/assets/src/Backend/Backend.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import BackendRequest from './BackendRequest';
22
import RequestBuilder from './RequestBuilder';
33

4+
export interface ChildrenFingerprints {
5+
// key is the id of the child component
6+
[key: string]: {fingerprint: string, tag: string}
7+
}
8+
49
export interface BackendInterface {
510
makeRequest(
611
props: any,
712
actions: BackendAction[],
813
updated: {[key: string]: any},
9-
childrenFingerprints: any
14+
children: ChildrenFingerprints,
15+
updatedPropsFromParent: {[key: string]: any},
1016
): BackendRequest;
1117
}
1218

@@ -26,13 +32,15 @@ export default class implements BackendInterface {
2632
props: any,
2733
actions: BackendAction[],
2834
updated: {[key: string]: any},
29-
childrenFingerprints: any
35+
children: ChildrenFingerprints,
36+
updatedPropsFromParent: {[key: string]: any},
3037
): BackendRequest {
3138
const { url, fetchOptions } = this.requestBuilder.buildRequest(
3239
props,
3340
actions,
3441
updated,
35-
childrenFingerprints
42+
children,
43+
updatedPropsFromParent
3644
);
3745

3846
return new BackendRequest(

0 commit comments

Comments
 (0)