Skip to content

Commit

Permalink
bug #1392 [LiveComponent] Fix loadingDirectives match elements from n…
Browse files Browse the repository at this point in the history
…ested components (smnandre)

This PR was squashed before being merged into the 2.x branch.

Discussion
----------

[LiveComponent] Fix loadingDirectives match elements from nested components

| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | no
| Issues        | Fix #1382
| License       | MIT

The elements with loading directives were used in all LiveController they were in.

This PR fixes that and restrict loadingDirective to the scope of the LiveContoller (and filter those belonging to nested / child components)

Commits
-------

7867e44 [LiveComponent] Fix loadingDirectives match elements from nested components
  • Loading branch information
weaverryan committed Jan 30, 2024
2 parents d933be3 + 7867e44 commit ed57804
Show file tree
Hide file tree
Showing 5 changed files with 1,685 additions and 1,434 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ interface ElementLoadingDirectives {
}
export default class implements PluginInterface {
attachToComponent(component: Component): void;
startLoading(targetElement: HTMLElement | SVGElement, backendRequest: BackendRequest): void;
finishLoading(targetElement: HTMLElement | SVGElement): void;
startLoading(component: Component, targetElement: HTMLElement | SVGElement, backendRequest: BackendRequest): void;
finishLoading(component: Component, targetElement: HTMLElement | SVGElement): void;
private handleLoadingToggle;
private handleLoadingDirective;
getLoadingDirectives(element: HTMLElement | SVGElement): ElementLoadingDirectives[];
getLoadingDirectives(component: Component, element: HTMLElement | SVGElement): ElementLoadingDirectives[];
private showElement;
private hideElement;
private addClass;
Expand Down
23 changes: 12 additions & 11 deletions src/LiveComponent/assets/dist/live_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -2370,27 +2370,27 @@ class StandardElementDriver {
class LoadingPlugin {
attachToComponent(component) {
component.on('loading.state:started', (element, request) => {
this.startLoading(element, request);
this.startLoading(component, element, request);
});
component.on('loading.state:finished', (element) => {
this.finishLoading(element);
this.finishLoading(component, element);
});
this.finishLoading(component.element);
this.finishLoading(component, component.element);
}
startLoading(targetElement, backendRequest) {
this.handleLoadingToggle(true, targetElement, backendRequest);
startLoading(component, targetElement, backendRequest) {
this.handleLoadingToggle(component, true, targetElement, backendRequest);
}
finishLoading(targetElement) {
this.handleLoadingToggle(false, targetElement, null);
finishLoading(component, targetElement) {
this.handleLoadingToggle(component, false, targetElement, null);
}
handleLoadingToggle(isLoading, targetElement, backendRequest) {
handleLoadingToggle(component, isLoading, targetElement, backendRequest) {
if (isLoading) {
this.addAttributes(targetElement, ['busy']);
}
else {
this.removeAttributes(targetElement, ['busy']);
}
this.getLoadingDirectives(targetElement).forEach(({ element, directives }) => {
this.getLoadingDirectives(component, targetElement).forEach(({ element, directives }) => {
if (isLoading) {
this.addAttributes(element, ['data-live-is-loading']);
}
Expand Down Expand Up @@ -2474,9 +2474,10 @@ class LoadingPlugin {
}
loadingDirective();
}
getLoadingDirectives(element) {
getLoadingDirectives(component, element) {
const loadingDirectives = [];
let matchingElements = element.querySelectorAll('[data-loading]');
let matchingElements = [...element.querySelectorAll('[data-loading]')];
matchingElements = matchingElements.filter((elt) => elementBelongsToThisComponent(elt, component));
if (element.hasAttribute('data-loading')) {
matchingElements = [element, ...matchingElements];
}
Expand Down
26 changes: 15 additions & 11 deletions src/LiveComponent/assets/src/Component/plugins/LoadingPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
DirectiveModifier,
parseDirectives
} from '../../Directive/directives_parser';
import { elementBelongsToThisComponent } from '../../dom_utils';
import { combineSpacedArray } from '../../string_utils';
import BackendRequest from '../../Backend/BackendRequest';
import Component from '../../Component';
Expand All @@ -16,32 +17,32 @@ interface ElementLoadingDirectives {
export default class implements PluginInterface {
attachToComponent(component: Component): void {
component.on('loading.state:started', (element: HTMLElement, request: BackendRequest) => {
this.startLoading(element, request);
this.startLoading(component, element, request);
});
component.on('loading.state:finished', (element: HTMLElement) => {
this.finishLoading(element);
this.finishLoading(component, element);
});
// hide "loading" elements to begin with
// This is done with CSS, but only for the most basic cases
this.finishLoading(component.element);
this.finishLoading(component, component.element);
}

startLoading(targetElement: HTMLElement|SVGElement, backendRequest: BackendRequest): void {
this.handleLoadingToggle(true, targetElement, backendRequest);
startLoading(component: Component, targetElement: HTMLElement|SVGElement, backendRequest: BackendRequest): void {
this.handleLoadingToggle(component, true, targetElement, backendRequest);
}

finishLoading(targetElement: HTMLElement|SVGElement): void {
this.handleLoadingToggle(false, targetElement, null);
finishLoading(component: Component, targetElement: HTMLElement|SVGElement): void {
this.handleLoadingToggle(component,false, targetElement, null);
}

private handleLoadingToggle(isLoading: boolean, targetElement: HTMLElement|SVGElement, backendRequest: BackendRequest|null) {
private handleLoadingToggle(component: Component, isLoading: boolean, targetElement: HTMLElement|SVGElement, backendRequest: BackendRequest|null) {
if (isLoading) {
this.addAttributes(targetElement, ['busy']);
} else {
this.removeAttributes(targetElement, ['busy']);
}

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

getLoadingDirectives(element: HTMLElement|SVGElement) {
getLoadingDirectives(component: Component, element: HTMLElement|SVGElement) {
const loadingDirectives: ElementLoadingDirectives[] = [];
let matchingElements = element.querySelectorAll('[data-loading]');
let matchingElements = [...element.querySelectorAll('[data-loading]')];

// ignore elements which are inside a nested "live" component
matchingElements = matchingElements.filter((elt) => elementBelongsToThisComponent(elt, component));

// querySelectorAll doesn't include the element itself
if (element.hasAttribute('data-loading')) {
Expand Down
47 changes: 47 additions & 0 deletions src/LiveComponent/assets/test/controller/loading.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,51 @@ describe('LiveController data-loading Tests', () => {
await (new Promise(resolve => setTimeout(resolve, 30)));
expect(getByTestId(test.element, 'loading-element')).not.toBeVisible();
});

it('does not trigger loading inside component children', async () => {
const childTemplate = (data: any) => `
<div ${initComponent(data, {id: 'child-id'})} data-testid="child">
<span data-loading="show" data-testid="child-loading-element-showing">Loading...</span>
<span data-loading="hide" data-testid="child-loading-element-hiding">Loading...</span>
</div>
`;

const test = await createTest({} , (data: any) => `
<div ${initComponent(data, {id: 'parent-id'})} data-testid="parent">
<span data-loading="show" data-testid="parent-loading-element-showing">Loading...</span>
<span data-loading="hide" data-testid="parent-loading-element-hiding">Loading...</span>
${childTemplate({})}
<button data-action="live#$render">Render</button>
</div>
`);

test.expectsAjaxCall()
// delay so we can check loading
.delayResponse(20);

// All showing elements should be hidden / hiding elements should be visible
await waitFor(() => expect(getByTestId(test.element, 'parent-loading-element-showing')).not.toBeVisible());
await waitFor(() => expect(getByTestId(test.element, 'parent-loading-element-hiding')).toBeVisible());
await waitFor(() => expect(getByTestId(test.element, 'child-loading-element-showing')).not.toBeVisible());
await waitFor(() => expect(getByTestId(test.element, 'child-loading-element-hiding')).toBeVisible());

getByText(test.element, 'Render').click();

// Parent: showing elements should be visible / hiding elements should be hidden
expect(getByTestId(test.element, 'parent-loading-element-showing')).toBeVisible();
expect(getByTestId(test.element, 'parent-loading-element-hiding')).not.toBeVisible();
// Child: no change
expect(getByTestId(test.element, 'child-loading-element-showing')).not.toBeVisible();
expect(getByTestId(test.element, 'child-loading-element-hiding')).toBeVisible();

// wait for loading to finish
await (new Promise(resolve => setTimeout(resolve, 30)));

// Parent: back to original state
expect(getByTestId(test.element, 'parent-loading-element-showing')).not.toBeVisible();
expect(getByTestId(test.element, 'parent-loading-element-hiding')).toBeVisible();
// Child: no change
expect(getByTestId(test.element, 'child-loading-element-showing')).not.toBeVisible();
expect(getByTestId(test.element, 'child-loading-element-hiding')).toBeVisible();
});
});
Loading

0 comments on commit ed57804

Please sign in to comment.