Skip to content

Commit 4951989

Browse files
committed
initial work for action loading
1 parent 0631817 commit 4951989

File tree

3 files changed

+82
-44
lines changed

3 files changed

+82
-44
lines changed

src/LiveComponent/assets/src/live_controller.ts

Lines changed: 67 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ export default class extends Controller implements LiveController {
457457
this._onLoadingStart();
458458
const paramsString = params.toString();
459459
const thisPromise = fetch(`${url}${paramsString.length > 0 ? `?${paramsString}` : ''}`, fetchOptions);
460-
this.backendRequest = new BackendRequest(thisPromise);
460+
this.backendRequest = new BackendRequest(thisPromise, actions.map(action => action.name));
461461
thisPromise.then((response) => {
462462
response.text().then((html) => {
463463
this.#processRerender(html, response);
@@ -525,12 +525,13 @@ export default class extends Controller implements LiveController {
525525
this._handleLoadingToggle(true);
526526
}
527527

528-
_onLoadingFinish() {
529-
this._handleLoadingToggle(false);
528+
_onLoadingFinish(targetElement: HTMLElement|SVGElement|null = null) {
529+
this._handleLoadingToggle(false, targetElement);
530530
}
531531

532-
_handleLoadingToggle(isLoading: boolean) {
533-
this._getLoadingDirectives().forEach(({ element, directives }) => {
532+
_handleLoadingToggle(isLoading: boolean, targetElement: HTMLElement|SVGElement|null = null) {
533+
534+
this._getLoadingDirectives(targetElement).forEach(({ element, directives }) => {
534535
// so we can track, at any point, if an element is in a "loading" state
535536
if (isLoading) {
536537
this._addAttributes(element, ['data-live-is-loading']);
@@ -550,6 +551,38 @@ export default class extends Controller implements LiveController {
550551
_handleLoadingDirective(element: HTMLElement|SVGElement, isLoading: boolean, directive: Directive) {
551552
const finalAction = parseLoadingAction(directive.action, isLoading);
552553

554+
const targetedActions: string[] = [];
555+
let delay = 0;
556+
directive.modifiers.forEach((modifier => {
557+
switch (modifier.name) {
558+
case 'delay': {
559+
// if loading has *stopped*, the delay modifier has no effect
560+
if (!isLoading) {
561+
break;
562+
}
563+
564+
delay = modifier.value ? parseInt(modifier.value) : 200;
565+
566+
break;
567+
}
568+
case 'action': {
569+
if (!modifier.value) {
570+
throw new Error(`The "action" in data-loading must have an action name - e.g. action(foo). It's missing for ${directive.getString()}`);
571+
}
572+
targetedActions.push(modifier.value);
573+
break;
574+
}
575+
576+
default:
577+
throw new Error(`Unknown modifier ${modifier.name} used in the loading directive ${directive.getString()}`)
578+
}
579+
}));
580+
581+
// if loading is being activated + action modifier, only apply if the action is on the request
582+
if (isLoading && targetedActions.length > 0 && this.backendRequest && !this.backendRequest.containsOneOfActions(targetedActions)) {
583+
return;
584+
}
585+
553586
let loadingDirective: (() => void);
554587

555588
switch (finalAction) {
@@ -583,41 +616,24 @@ export default class extends Controller implements LiveController {
583616
throw new Error(`Unknown data-loading action "${finalAction}"`);
584617
}
585618

586-
let isHandled = false;
587-
directive.modifiers.forEach((modifier => {
588-
switch (modifier.name) {
589-
case 'delay': {
590-
// if loading has *stopped*, the delay modifier has no effect
591-
if (!isLoading) {
592-
break;
593-
}
594-
595-
const delayLength = modifier.value ? parseInt(modifier.value) : 200;
596-
window.setTimeout(() => {
597-
if (element.hasAttribute('data-live-is-loading')) {
598-
loadingDirective();
599-
}
600-
}, delayLength);
601-
602-
isHandled = true;
603-
604-
break;
619+
if (delay) {
620+
window.setTimeout(() => {
621+
if (this.isRequestActive()) {
622+
loadingDirective();
605623
}
606-
default:
607-
throw new Error(`Unknown modifier ${modifier.name} used in the loading directive ${directive.getString()}`)
608-
}
609-
}));
624+
}, delay);
610625

611-
// execute the loading directive
612-
if (!isHandled) {
613-
loadingDirective();
626+
return;
614627
}
628+
629+
loadingDirective();
615630
}
616631

617-
_getLoadingDirectives() {
632+
_getLoadingDirectives(targetElement: HTMLElement|SVGElement|null = null) {
618633
const loadingDirectives: ElementLoadingDirectives[] = [];
634+
const element = targetElement || this.element;
619635

620-
this.element.querySelectorAll('[data-loading]').forEach((element => {
636+
element.querySelectorAll('[data-loading]').forEach((element => {
621637
if (!(element instanceof HTMLElement) && !(element instanceof SVGElement)) {
622638
throw new Error('Invalid Element Type');
623639
}
@@ -676,6 +692,8 @@ export default class extends Controller implements LiveController {
676692

677693
_executeMorphdom(newHtml: string, modifiedElements: Array<HTMLElement>) {
678694
const newElement = htmlToElement(newHtml);
695+
// make sure everything is in non-loading state, the same as the HTML currently on the page
696+
this._onLoadingFinish(newElement);
679697
morphdom(this.element, newElement, {
680698
getNodeKey: (node: Node) => {
681699
if (!(node instanceof HTMLElement)) {
@@ -721,11 +739,7 @@ export default class extends Controller implements LiveController {
721739
}
722740

723741
// look for data-live-ignore, and don't update
724-
if (fromEl.hasAttribute('data-live-ignore')) {
725-
return false;
726-
}
727-
728-
return true;
742+
return !fromEl.hasAttribute('data-live-ignore');
729743
},
730744

731745
onBeforeNodeDiscarded(node) {
@@ -734,10 +748,7 @@ export default class extends Controller implements LiveController {
734748
return true;
735749
}
736750

737-
if (node.hasAttribute('data-live-ignore')) {
738-
return false;
739-
}
740-
return true;
751+
return !node.hasAttribute('data-live-ignore');
741752
}
742753
});
743754
// restore the data-original-data attribute
@@ -1025,13 +1036,26 @@ export default class extends Controller implements LiveController {
10251036
this.requestDebounceTimeout = null;
10261037
}
10271038
}
1039+
1040+
private isRequestActive(): boolean {
1041+
return !!this.backendRequest;
1042+
}
10281043
}
10291044

10301045
class BackendRequest {
10311046
promise: Promise<any>;
1047+
actions: string[];
10321048

1033-
constructor(promise: Promise<any>) {
1049+
constructor(promise: Promise<any>, actions: string[]) {
10341050
this.promise = promise;
1051+
this.actions = actions;
1052+
}
1053+
1054+
/**
1055+
* Does this BackendRequest contain at least on action in targetedActions?
1056+
*/
1057+
containsOneOfActions(targetedActions: string[]) {
1058+
return (this.actions.filter(action => targetedActions.includes(action))).length > 0;
10351059
}
10361060
}
10371061

src/LiveComponent/src/Resources/doc/index.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,19 @@ changes until loading has taken longer than a certain amount of time:
460460
<!-- Show after 500ms of loading -->
461461
<div data-loading="delay(500)|show">Loading</div>
462462
463+
Targeting Loading for a Specific Action
464+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
465+
466+
To only toggle the loading behavior when a specific action is triggered,
467+
use the ``action()`` modifier with the name of the action - e.g. ``saveForm()``:
468+
469+
.. code-block:: twig
470+
471+
<!-- show only when the "saveForm" action is triggering -->
472+
<span data-loading="action(saveForm)|show">Loading</span>
473+
<!-- multiple modifiers -->
474+
<div data-loading="action(saveForm)|delay|addClass(opacity-50)">...</div>
475+
463476
.. _actions:
464477

465478
Actions

ux.symfony.com/assets/bootstrap.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { startStimulusApp } from '@symfony/stimulus-bridge';
22
import Clipboard from 'stimulus-clipboard'
3+
import LiveController from '../live_assets/dist/live_controller';
34

45
// Registers Stimulus controllers from controllers.json and in the controllers/ directory
56
export const app = startStimulusApp(require.context(
@@ -12,5 +13,5 @@ app.debug = process.env.NODE_ENV === 'development';
1213

1314
app.register('clipboard', Clipboard);
1415
// register any custom, 3rd party controllers here
15-
// app.register('some_controller_name', SomeImportedController);
16+
app.register('live', LiveController);
1617

0 commit comments

Comments
 (0)