Skip to content

Commit 7867e44

Browse files
smnandreweaverryan
authored andcommitted
[LiveComponent] Fix loadingDirectives match elements from nested components
1 parent d933be3 commit 7867e44

File tree

5 files changed

+1685
-1434
lines changed

5 files changed

+1685
-1434
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: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2370,27 +2370,27 @@ class StandardElementDriver {
23702370
class LoadingPlugin {
23712371
attachToComponent(component) {
23722372
component.on('loading.state:started', (element, request) => {
2373-
this.startLoading(element, request);
2373+
this.startLoading(component, element, request);
23742374
});
23752375
component.on('loading.state:finished', (element) => {
2376-
this.finishLoading(element);
2376+
this.finishLoading(component, element);
23772377
});
2378-
this.finishLoading(component.element);
2378+
this.finishLoading(component, component.element);
23792379
}
2380-
startLoading(targetElement, backendRequest) {
2381-
this.handleLoadingToggle(true, targetElement, backendRequest);
2380+
startLoading(component, targetElement, backendRequest) {
2381+
this.handleLoadingToggle(component, true, targetElement, backendRequest);
23822382
}
2383-
finishLoading(targetElement) {
2384-
this.handleLoadingToggle(false, targetElement, null);
2383+
finishLoading(component, targetElement) {
2384+
this.handleLoadingToggle(component, false, targetElement, null);
23852385
}
2386-
handleLoadingToggle(isLoading, targetElement, backendRequest) {
2386+
handleLoadingToggle(component, isLoading, targetElement, backendRequest) {
23872387
if (isLoading) {
23882388
this.addAttributes(targetElement, ['busy']);
23892389
}
23902390
else {
23912391
this.removeAttributes(targetElement, ['busy']);
23922392
}
2393-
this.getLoadingDirectives(targetElement).forEach(({ element, directives }) => {
2393+
this.getLoadingDirectives(component, targetElement).forEach(({ element, directives }) => {
23942394
if (isLoading) {
23952395
this.addAttributes(element, ['data-live-is-loading']);
23962396
}
@@ -2474,9 +2474,10 @@ class LoadingPlugin {
24742474
}
24752475
loadingDirective();
24762476
}
2477-
getLoadingDirectives(element) {
2477+
getLoadingDirectives(component, element) {
24782478
const loadingDirectives = [];
2479-
let matchingElements = element.querySelectorAll('[data-loading]');
2479+
let matchingElements = [...element.querySelectorAll('[data-loading]')];
2480+
matchingElements = matchingElements.filter((elt) => elementBelongsToThisComponent(elt, component));
24802481
if (element.hasAttribute('data-loading')) {
24812482
matchingElements = [element, ...matchingElements];
24822483
}

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)