Skip to content

Commit 11b544a

Browse files
committed
initial work for action loading
1 parent 5d1e652 commit 11b544a

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
@@ -468,7 +468,7 @@ export default class extends Controller implements LiveController {
468468
this._onLoadingStart();
469469
const paramsString = params.toString();
470470
const thisPromise = fetch(`${url}${paramsString.length > 0 ? `?${paramsString}` : ''}`, fetchOptions);
471-
this.backendRequest = new BackendRequest(thisPromise);
471+
this.backendRequest = new BackendRequest(thisPromise, actions.map(action => action.name));
472472
thisPromise.then((response) => {
473473
response.text().then((html) => {
474474
this.#processRerender(html, response);
@@ -536,12 +536,13 @@ export default class extends Controller implements LiveController {
536536
this._handleLoadingToggle(true);
537537
}
538538

539-
_onLoadingFinish() {
540-
this._handleLoadingToggle(false);
539+
_onLoadingFinish(targetElement: HTMLElement|SVGElement|null = null) {
540+
this._handleLoadingToggle(false, targetElement);
541541
}
542542

543-
_handleLoadingToggle(isLoading: boolean) {
544-
this._getLoadingDirectives().forEach(({ element, directives }) => {
543+
_handleLoadingToggle(isLoading: boolean, targetElement: HTMLElement|SVGElement|null = null) {
544+
545+
this._getLoadingDirectives(targetElement).forEach(({ element, directives }) => {
545546
// so we can track, at any point, if an element is in a "loading" state
546547
if (isLoading) {
547548
this._addAttributes(element, ['data-live-is-loading']);
@@ -561,6 +562,38 @@ export default class extends Controller implements LiveController {
561562
_handleLoadingDirective(element: HTMLElement|SVGElement, isLoading: boolean, directive: Directive) {
562563
const finalAction = parseLoadingAction(directive.action, isLoading);
563564

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

566599
switch (finalAction) {
@@ -594,41 +627,24 @@ export default class extends Controller implements LiveController {
594627
throw new Error(`Unknown data-loading action "${finalAction}"`);
595628
}
596629

597-
let isHandled = false;
598-
directive.modifiers.forEach((modifier => {
599-
switch (modifier.name) {
600-
case 'delay': {
601-
// if loading has *stopped*, the delay modifier has no effect
602-
if (!isLoading) {
603-
break;
604-
}
605-
606-
const delayLength = modifier.value ? parseInt(modifier.value) : 200;
607-
window.setTimeout(() => {
608-
if (element.hasAttribute('data-live-is-loading')) {
609-
loadingDirective();
610-
}
611-
}, delayLength);
612-
613-
isHandled = true;
614-
615-
break;
630+
if (delay) {
631+
window.setTimeout(() => {
632+
if (this.isRequestActive()) {
633+
loadingDirective();
616634
}
617-
default:
618-
throw new Error(`Unknown modifier ${modifier.name} used in the loading directive ${directive.getString()}`)
619-
}
620-
}));
635+
}, delay);
621636

622-
// execute the loading directive
623-
if (!isHandled) {
624-
loadingDirective();
637+
return;
625638
}
639+
640+
loadingDirective();
626641
}
627642

628-
_getLoadingDirectives() {
643+
_getLoadingDirectives(targetElement: HTMLElement|SVGElement|null = null) {
629644
const loadingDirectives: ElementLoadingDirectives[] = [];
645+
const element = targetElement || this.element;
630646

631-
this.element.querySelectorAll('[data-loading]').forEach((element => {
647+
element.querySelectorAll('[data-loading]').forEach((element => {
632648
if (!(element instanceof HTMLElement) && !(element instanceof SVGElement)) {
633649
throw new Error('Invalid Element Type');
634650
}
@@ -687,6 +703,8 @@ export default class extends Controller implements LiveController {
687703

688704
_executeMorphdom(newHtml: string, modifiedElements: Array<HTMLElement>) {
689705
const newElement = htmlToElement(newHtml);
706+
// make sure everything is in non-loading state, the same as the HTML currently on the page
707+
this._onLoadingFinish(newElement);
690708
morphdom(this.element, newElement, {
691709
getNodeKey: (node: Node) => {
692710
if (!(node instanceof HTMLElement)) {
@@ -732,11 +750,7 @@ export default class extends Controller implements LiveController {
732750
}
733751

734752
// look for data-live-ignore, and don't update
735-
if (fromEl.hasAttribute('data-live-ignore')) {
736-
return false;
737-
}
738-
739-
return true;
753+
return !fromEl.hasAttribute('data-live-ignore');
740754
},
741755

742756
onBeforeNodeDiscarded(node) {
@@ -745,10 +759,7 @@ export default class extends Controller implements LiveController {
745759
return true;
746760
}
747761

748-
if (node.hasAttribute('data-live-ignore')) {
749-
return false;
750-
}
751-
return true;
762+
return !node.hasAttribute('data-live-ignore');
752763
}
753764
});
754765
// restore the data-original-data attribute
@@ -1036,13 +1047,26 @@ export default class extends Controller implements LiveController {
10361047
this.requestDebounceTimeout = null;
10371048
}
10381049
}
1050+
1051+
private isRequestActive(): boolean {
1052+
return !!this.backendRequest;
1053+
}
10391054
}
10401055

10411056
class BackendRequest {
10421057
promise: Promise<any>;
1058+
actions: string[];
10431059

1044-
constructor(promise: Promise<any>) {
1060+
constructor(promise: Promise<any>, actions: string[]) {
10451061
this.promise = promise;
1062+
this.actions = actions;
1063+
}
1064+
1065+
/**
1066+
* Does this BackendRequest contain at least on action in targetedActions?
1067+
*/
1068+
containsOneOfActions(targetedActions: string[]) {
1069+
return (this.actions.filter(action => targetedActions.includes(action))).length > 0;
10461070
}
10471071
}
10481072

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)