Skip to content

Commit 506c2d1

Browse files
committed
Fix loadingDirectives match elements from nested components
1 parent de99908 commit 506c2d1

File tree

5 files changed

+1691
-1440
lines changed

5 files changed

+1691
-1440
lines changed

src/LiveComponent/assets/dist/Component/plugins/LoadingPlugin.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ interface ElementLoadingDirectives {
88
}
99
export default class implements PluginInterface {
1010
attachToComponent(component: Component): void;
11-
startLoading(targetElement: HTMLElement | SVGElement, backendRequest: BackendRequest): void;
12-
finishLoading(targetElement: HTMLElement | SVGElement): void;
11+
startLoading(component: Component, targetElement: HTMLElement | SVGElement, backendRequest: BackendRequest): void;
12+
finishLoading(component: Component, targetElement: HTMLElement | SVGElement): void;
1313
private handleLoadingToggle;
1414
private handleLoadingDirective;
15-
getLoadingDirectives(element: HTMLElement | SVGElement): ElementLoadingDirectives[];
15+
getLoadingDirectives(component: Component, element: HTMLElement | SVGElement): ElementLoadingDirectives[];
1616
private showElement;
1717
private hideElement;
1818
private addClass;

src/LiveComponent/assets/dist/live_controller.js

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1948,7 +1948,7 @@ class Component {
19481948
}
19491949
}
19501950
this.backendRequest = this.backend.makeRequest(this.valueStore.getOriginalProps(), this.pendingActions, this.valueStore.getDirtyProps(), this.getChildrenFingerprints(), this.valueStore.getUpdatedPropsFromParent(), filesToSend);
1951-
this.hooks.triggerHook('loading.state:started', this.element, this.backendRequest);
1951+
this.hooks.triggerHook('loading.state:started', this.element, this.backendRequest, this);
19521952
this.pendingActions = [];
19531953
this.valueStore.flushDirtyPropsToPending();
19541954
this.isRequestPending = false;
@@ -1995,7 +1995,7 @@ class Component {
19951995
}
19961996
return;
19971997
}
1998-
this.hooks.triggerHook('loading.state:finished', this.element);
1998+
this.hooks.triggerHook('loading.state:finished', this.element, this);
19991999
const modifiedModelValues = {};
20002000
Object.keys(this.valueStore.getDirtyProps()).forEach((modelName) => {
20012001
modifiedModelValues[modelName] = this.valueStore.get(modelName);
@@ -2296,28 +2296,28 @@ class StandardElementDriver {
22962296

22972297
class LoadingPlugin {
22982298
attachToComponent(component) {
2299-
component.on('loading.state:started', (element, request) => {
2300-
this.startLoading(element, request);
2299+
component.on('loading.state:started', (element, request, component) => {
2300+
this.startLoading(component, element, request);
23012301
});
2302-
component.on('loading.state:finished', (element) => {
2303-
this.finishLoading(element);
2302+
component.on('loading.state:finished', (element, component) => {
2303+
this.finishLoading(component, element);
23042304
});
2305-
this.finishLoading(component.element);
2305+
this.finishLoading(component, component.element);
23062306
}
2307-
startLoading(targetElement, backendRequest) {
2308-
this.handleLoadingToggle(true, targetElement, backendRequest);
2307+
startLoading(component, targetElement, backendRequest) {
2308+
this.handleLoadingToggle(component, true, targetElement, backendRequest);
23092309
}
2310-
finishLoading(targetElement) {
2311-
this.handleLoadingToggle(false, targetElement, null);
2310+
finishLoading(component, targetElement) {
2311+
this.handleLoadingToggle(component, false, targetElement, null);
23122312
}
2313-
handleLoadingToggle(isLoading, targetElement, backendRequest) {
2313+
handleLoadingToggle(component, isLoading, targetElement, backendRequest) {
23142314
if (isLoading) {
23152315
this.addAttributes(targetElement, ['busy']);
23162316
}
23172317
else {
23182318
this.removeAttributes(targetElement, ['busy']);
23192319
}
2320-
this.getLoadingDirectives(targetElement).forEach(({ element, directives }) => {
2320+
this.getLoadingDirectives(component, targetElement).forEach(({ element, directives }) => {
23212321
if (isLoading) {
23222322
this.addAttributes(element, ['data-live-is-loading']);
23232323
}
@@ -2401,9 +2401,10 @@ class LoadingPlugin {
24012401
}
24022402
loadingDirective();
24032403
}
2404-
getLoadingDirectives(element) {
2404+
getLoadingDirectives(component, element) {
24052405
const loadingDirectives = [];
2406-
let matchingElements = element.querySelectorAll('[data-loading]');
2406+
let matchingElements = [...element.querySelectorAll('[data-loading]')];
2407+
matchingElements = matchingElements.filter((elt) => elementBelongsToThisComponent(elt, component));
24072408
if (element.hasAttribute('data-loading')) {
24082409
matchingElements = [element, ...matchingElements];
24092410
}

src/LiveComponent/assets/src/Component/plugins/LoadingPlugin.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
DirectiveModifier,
44
parseDirectives
55
} from '../../Directive/directives_parser';
6+
import { elementBelongsToThisComponent } from '../../dom_utils';
67
import { combineSpacedArray } from '../../string_utils';
78
import BackendRequest from '../../Backend/BackendRequest';
89
import Component from '../../Component';
@@ -16,32 +17,32 @@ interface ElementLoadingDirectives {
1617
export default class implements PluginInterface {
1718
attachToComponent(component: Component): void {
1819
component.on('loading.state:started', (element: HTMLElement, request: BackendRequest) => {
19-
this.startLoading(element, request);
20+
this.startLoading(component, element, request);
2021
});
2122
component.on('loading.state:finished', (element: HTMLElement) => {
22-
this.finishLoading(element);
23+
this.finishLoading(component, element);
2324
});
2425
// hide "loading" elements to begin with
2526
// This is done with CSS, but only for the most basic cases
26-
this.finishLoading(component.element);
27+
this.finishLoading(component, component.element);
2728
}
2829

29-
startLoading(targetElement: HTMLElement|SVGElement, backendRequest: BackendRequest): void {
30-
this.handleLoadingToggle(true, targetElement, backendRequest);
30+
startLoading(component: Component, targetElement: HTMLElement|SVGElement, backendRequest: BackendRequest): void {
31+
this.handleLoadingToggle(component, true, targetElement, backendRequest);
3132
}
3233

33-
finishLoading(targetElement: HTMLElement|SVGElement): void {
34-
this.handleLoadingToggle(false, targetElement, null);
34+
finishLoading(component: Component, targetElement: HTMLElement|SVGElement): void {
35+
this.handleLoadingToggle(component,false, targetElement, null);
3536
}
3637

37-
private handleLoadingToggle(isLoading: boolean, targetElement: HTMLElement|SVGElement, backendRequest: BackendRequest|null) {
38+
private handleLoadingToggle(component: Component, isLoading: boolean, targetElement: HTMLElement|SVGElement, backendRequest: BackendRequest|null) {
3839
if (isLoading) {
3940
this.addAttributes(targetElement, ['busy']);
4041
} else {
4142
this.removeAttributes(targetElement, ['busy']);
4243
}
4344

44-
this.getLoadingDirectives(targetElement).forEach(({ element, directives }) => {
45+
this.getLoadingDirectives(component, targetElement).forEach(({ element, directives }) => {
4546
// so we can track, at any point, if an element is in a "loading" state
4647
if (isLoading) {
4748
this.addAttributes(element, ['data-live-is-loading']);
@@ -148,9 +149,12 @@ export default class implements PluginInterface {
148149
loadingDirective();
149150
}
150151

151-
getLoadingDirectives(element: HTMLElement|SVGElement) {
152+
getLoadingDirectives(component: Component, element: HTMLElement|SVGElement) {
152153
const loadingDirectives: ElementLoadingDirectives[] = [];
153-
let matchingElements = element.querySelectorAll('[data-loading]');
154+
let matchingElements = [...element.querySelectorAll('[data-loading]')];
155+
156+
// ignore elements which are inside a nested "live" component
157+
matchingElements = matchingElements.filter((elt) => elementBelongsToThisComponent(elt, component));
154158

155159
// querySelectorAll doesn't include the element itself
156160
if (element.hasAttribute('data-loading')) {

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,4 +223,51 @@ describe('LiveController data-loading Tests', () => {
223223
await (new Promise(resolve => setTimeout(resolve, 30)));
224224
expect(getByTestId(test.element, 'loading-element')).not.toBeVisible();
225225
});
226+
227+
it('does not trigger loading inside component children', async () => {
228+
const childTemplate = (data: any) => `
229+
<div ${initComponent(data, {id: 'child-id'})} data-testid="child">
230+
<span data-loading="show" data-testid="child-loading-element-showing">Loading...</span>
231+
<span data-loading="hide" data-testid="child-loading-element-hiding">Loading...</span>
232+
</div>
233+
`;
234+
235+
const test = await createTest({} , (data: any) => `
236+
<div ${initComponent(data, {id: 'parent-id'})} data-testid="parent">
237+
<span data-loading="show" data-testid="parent-loading-element-showing">Loading...</span>
238+
<span data-loading="hide" data-testid="parent-loading-element-hiding">Loading...</span>
239+
${childTemplate({})}
240+
<button data-action="live#$render">Render</button>
241+
</div>
242+
`);
243+
244+
test.expectsAjaxCall()
245+
// delay so we can check loading
246+
.delayResponse(20);
247+
248+
// All showing elements should be hidden / hiding elements should be visible
249+
await waitFor(() => expect(getByTestId(test.element, 'parent-loading-element-showing')).not.toBeVisible());
250+
await waitFor(() => expect(getByTestId(test.element, 'parent-loading-element-hiding')).toBeVisible());
251+
await waitFor(() => expect(getByTestId(test.element, 'child-loading-element-showing')).not.toBeVisible());
252+
await waitFor(() => expect(getByTestId(test.element, 'child-loading-element-hiding')).toBeVisible());
253+
254+
getByText(test.element, 'Render').click();
255+
256+
// Parent: showing elements should be visible / hiding elements should be hidden
257+
expect(getByTestId(test.element, 'parent-loading-element-showing')).toBeVisible();
258+
expect(getByTestId(test.element, 'parent-loading-element-hiding')).not.toBeVisible();
259+
// Child: no change
260+
expect(getByTestId(test.element, 'child-loading-element-showing')).not.toBeVisible();
261+
expect(getByTestId(test.element, 'child-loading-element-hiding')).toBeVisible();
262+
263+
// wait for loading to finish
264+
await (new Promise(resolve => setTimeout(resolve, 30)));
265+
266+
// Parent: back to original state
267+
expect(getByTestId(test.element, 'parent-loading-element-showing')).not.toBeVisible();
268+
expect(getByTestId(test.element, 'parent-loading-element-hiding')).toBeVisible();
269+
// Child: no change
270+
expect(getByTestId(test.element, 'child-loading-element-showing')).not.toBeVisible();
271+
expect(getByTestId(test.element, 'child-loading-element-hiding')).toBeVisible();
272+
});
226273
});

0 commit comments

Comments
 (0)