Skip to content

Commit 6b7fcd2

Browse files
committed
Sending childrenFingerprints data to server on Ajax call
1 parent 526e570 commit 6b7fcd2

File tree

4 files changed

+85
-18
lines changed

4 files changed

+85
-18
lines changed

src/LiveComponent/assets/src/Backend.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import BackendRequest from './BackendRequest';
22

33
export interface BackendInterface {
4-
makeRequest(data: any, actions: BackendAction[], updatedModels: string[]): BackendRequest;
4+
makeRequest(data: any, actions: BackendAction[], updatedModels: string[], childrenFingerprints: any): BackendRequest;
55
}
66

77
export interface BackendAction {
@@ -11,14 +11,14 @@ export interface BackendAction {
1111

1212
export default class implements BackendInterface {
1313
private url: string;
14-
private csrfToken: string|null;
14+
private readonly csrfToken: string|null;
1515

1616
constructor(url: string, csrfToken: string|null = null) {
1717
this.url = url;
1818
this.csrfToken = csrfToken;
1919
}
2020

21-
makeRequest(data: any, actions: BackendAction[], updatedModels: string[]): BackendRequest {
21+
makeRequest(data: any, actions: BackendAction[], updatedModels: string[], childrenFingerprints: any): BackendRequest {
2222
const splitUrl = this.url.split('?');
2323
let [url] = splitUrl
2424
const [, queryString] = splitUrl;
@@ -29,8 +29,9 @@ export default class implements BackendInterface {
2929
'Accept': 'application/vnd.live-component+html',
3030
};
3131

32-
if (actions.length === 0 && this.willDataFitInUrl(JSON.stringify(data), params)) {
32+
if (actions.length === 0 && this.willDataFitInUrl(JSON.stringify(data), params, JSON.stringify(childrenFingerprints))) {
3333
params.set('data', JSON.stringify(data));
34+
params.set('childrenFingerprints', JSON.stringify(childrenFingerprints));
3435
updatedModels.forEach((model) => {
3536
params.append('updatedModels[]', model);
3637
});
@@ -39,7 +40,12 @@ export default class implements BackendInterface {
3940
fetchOptions.method = 'POST';
4041
fetchOptions.headers['Content-Type'] = 'application/json';
4142
const requestData: any = { data };
42-
requestData.updatedModels = updatedModels;
43+
if (updatedModels) {
44+
requestData.updatedModels = updatedModels;
45+
}
46+
if (childrenFingerprints) {
47+
requestData.childrenFingerprints = childrenFingerprints;
48+
}
4349

4450
if (actions.length > 0) {
4551
// one or more ACTIONs
@@ -70,8 +76,8 @@ export default class implements BackendInterface {
7076
);
7177
}
7278

73-
private willDataFitInUrl(dataJson: string, params: URLSearchParams) {
74-
const urlEncodedJsonData = new URLSearchParams(dataJson).toString();
79+
private willDataFitInUrl(dataJson: string, params: URLSearchParams, childrenFingerprintsJson: string) {
80+
const urlEncodedJsonData = new URLSearchParams(dataJson + childrenFingerprintsJson).toString();
7581

7682
// if the URL gets remotely close to 2000 chars, it may not fit
7783
return (urlEncodedJsonData + params.toString()).length < 1500;

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,8 @@ export default class Component {
193193
this.backendRequest = this.backend.makeRequest(
194194
this.valueStore.all(),
195195
this.pendingActions,
196-
this.valueStore.updatedModels
196+
this.valueStore.updatedModels,
197+
this.getChildrenFingerprints()
197198
);
198199
this.hooks.triggerHook('loading.state:started', this.element, this.backendRequest);
199200

@@ -371,6 +372,20 @@ export default class Component {
371372
});
372373
modal.focus();
373374
}
375+
376+
private getChildrenFingerprints(): any {
377+
const fingerprints: any = {};
378+
379+
this.children.forEach((child) => {
380+
if (!child.id) {
381+
throw new Error('missing id');
382+
}
383+
384+
fingerprints[child.id] = child.fingerprint;
385+
});
386+
387+
return fingerprints;
388+
}
374389
}
375390

376391
/**

src/LiveComponent/assets/test/controller/child.test.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
import { createTest, initComponent, shutdownTest } from '../tools';
1313
import {getByTestId, waitFor} from '@testing-library/dom';
14-
import Component from "../../src/Component";
14+
import Component from '../../src/Component';
1515

1616
describe('LiveController parent -> child component tests', () => {
1717
afterEach(() => {
@@ -25,7 +25,7 @@ describe('LiveController parent -> child component tests', () => {
2525

2626
const test = await createTest({}, (data: any) => `
2727
<div ${initComponent(data)}>
28-
${childTemplate({food: 'pizza'})}
28+
${childTemplate({})}
2929
</div>
3030
`);
3131

@@ -66,4 +66,24 @@ describe('LiveController parent -> child component tests', () => {
6666
expect(parentComponent.getChildren().get('the-child-id')).toEqual(childComponent);
6767
expect(childComponent.getParent()).toEqual(parentComponent);
6868
});
69+
70+
it('sends a map of child fingerprints on re-render', async () => {
71+
const test = await createTest({}, (data: any) => `
72+
<div ${initComponent(data)}>
73+
<div ${initComponent({}, {id: 'the-child-id1', fingerprint: 'child-fingerprint1'})}>Child1</div>
74+
<div ${initComponent({}, {id: 'the-child-id2', fingerprint: 'child-fingerprint2'})}>Child2</div>
75+
</div>
76+
`);
77+
78+
test.expectsAjaxCall('get')
79+
.expectSentData(test.initialData)
80+
.expectChildFingerprints({
81+
'the-child-id1': 'child-fingerprint1',
82+
'the-child-id2': 'child-fingerprint2'
83+
})
84+
.init();
85+
86+
test.component.render();
87+
await waitFor(() => expect(test.element).toHaveAttribute('busy'));
88+
});
6989
});

src/LiveComponent/assets/test/tools.ts

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export function shutdownTest() {
3838
requestInfo.push(` HEADERS: ${JSON.stringify(unmatchedFetchError.headers)}`);
3939
requestInfo.push(` DATA: ${unmatchedFetchError.method === 'GET' ? urlParams.get('data') : unmatchedFetchError.body}`);
4040

41-
console.log(`UNMATCHED request was made with the following info:`, "\n", requestInfo.join("\n"));
41+
console.log('UNMATCHED request was made with the following info:', '\n', requestInfo.join('\n'));
4242
});
4343
unmatchedFetchErrors = [];
4444

@@ -118,6 +118,7 @@ class MockedAjaxCall {
118118
private expectedSentData?: any;
119119
private expectedActions: Array<{ name: string, args: any }> = [];
120120
private expectedHeaders: any = {};
121+
private expectedChildFingerprints: any = null;
121122
private changeDataCallback?: (data: any) => void;
122123
private template?: (data: any) => string
123124
options: any = {};
@@ -142,6 +143,14 @@ class MockedAjaxCall {
142143
return this;
143144
}
144145

146+
expectChildFingerprints = (fingerprints: any): MockedAjaxCall => {
147+
this.checkInitialization('expectSentData');
148+
149+
this.expectedChildFingerprints = fingerprints;
150+
151+
return this;
152+
}
153+
145154
/**
146155
* Call if the "server" will change the data before it re-renders
147156
*/
@@ -245,15 +254,20 @@ class MockedAjaxCall {
245254
} else {
246255
requestInfo.push(` DATA: ${JSON.stringify(this.getRequestBody())}`);
247256
}
257+
258+
if (this.expectedChildFingerprints) {
259+
requestInfo.push(` CHILD FINGERPRINTS: ${JSON.stringify(this.expectedChildFingerprints)}`)
260+
}
261+
248262
if (this.expectedActions.length === 1) {
249263
requestInfo.push(` Expected URL to contain action /${this.expectedActions[0].name}`)
250264
}
251265

252-
return requestInfo.join("\n");
266+
return requestInfo.join('\n');
253267
}
254268

255269
// https://www.wheresrhys.co.uk/fetch-mock/#api-mockingmock_matcher
256-
private getMockMatcher(forError = false): any {
270+
private getMockMatcher(createMatchForShowingError = false): any {
257271
if (!this.expectedSentData) {
258272
throw new Error('expectedSentData not set yet');
259273
}
@@ -265,15 +279,23 @@ class MockedAjaxCall {
265279
}
266280

267281
if (this.method === 'GET') {
268-
const params = new URLSearchParams({
282+
const paramsData: any = {
269283
data: JSON.stringify(this.expectedSentData)
270-
});
271-
if (forError) {
284+
};
285+
if (this.expectedChildFingerprints) {
286+
paramsData.childrenFingerprints = JSON.stringify(this.expectedChildFingerprints);
287+
}
288+
const params = new URLSearchParams(paramsData);
289+
if (createMatchForShowingError) {
272290
// simplified version for error reporting
273291
matcherObject.url = `?${params.toString()}`;
274292
} else {
275293
matcherObject.functionMatcher = (url: string) => {
276-
return url.includes(`?${params.toString()}`);
294+
const actualUrl = new URL(url);
295+
const actualParams = new URLSearchParams(actualUrl.search);
296+
actualParams.delete('updatedModels');
297+
298+
return actualParams.toString() === params.toString();
277299
};
278300
}
279301
} else {
@@ -289,7 +311,7 @@ class MockedAjaxCall {
289311
if (this.expectedActions.length === 1) {
290312
matcherObject.url = `end:/${this.expectedActions[0].name}`;
291313
} else if (this.expectedActions.length > 1) {
292-
matcherObject.url = `end:/_batch`;
314+
matcherObject.url = 'end:/_batch';
293315
}
294316
}
295317

@@ -308,6 +330,10 @@ class MockedAjaxCall {
308330
data: this.expectedSentData
309331
};
310332

333+
if (this.expectedChildFingerprints) {
334+
body.childrenFingerprints = this.expectedChildFingerprints;
335+
}
336+
311337
if (this.expectedActions.length === 1) {
312338
body.args = this.expectedActions[0].args;
313339
} else if (this.expectedActions.length > 1) {

0 commit comments

Comments
 (0)