Skip to content

Commit 8fb29ee

Browse files
JohananOppongAmoatengadamghill
authored andcommitted
Action modifier to disable element while in-flight
1 parent f6c7486 commit 8fb29ee

File tree

5 files changed

+78
-0
lines changed

5 files changed

+78
-0
lines changed

src/django_unicorn/static/unicorn/js/component.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export class Component {
4545
this.poll = {};
4646

4747
this.actionQueue = [];
48+
this.actionCleanups = [];
4849
this.currentActionQueue = null;
4950
this.lastTriggeringElements = [];
5051

src/django_unicorn/static/unicorn/js/element.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export class Element {
118118
action.isPrevent = false;
119119
action.isStop = false;
120120
action.isDiscard = false;
121+
action.isDisable = false;
121122
action.debounceTime = 0;
122123

123124
if (attribute.modifiers) {
@@ -128,6 +129,8 @@ export class Element {
128129
action.isStop = true;
129130
} else if (modifier === "discard") {
130131
action.isDiscard = true;
132+
} else if (modifier === "disable") {
133+
action.isDisable = true;
131134
} else if (modifier === "debounce") {
132135
action.debounceTime = attribute.modifiers.debounce
133136
? parseInt(attribute.modifiers.debounce, 10) || 0

src/django_unicorn/static/unicorn/js/eventListeners.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,13 @@ export function addActionEventListener(component, eventType) {
212212
});
213213

214214
if (!action.key || action.key === toKebabCase(event.key)) {
215+
if (action.isDisable) {
216+
element.el.disabled = true;
217+
component.actionCleanups.push(() => {
218+
element.el.disabled = false;
219+
});
220+
}
221+
215222
handleLoading(component, targetElement);
216223
component.callMethod(
217224
actionName,

src/django_unicorn/static/unicorn/js/messageSender.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ export function send(component, callback) {
6262
loadingElement.handleDirty(true);
6363
});
6464

65+
component.actionCleanups.forEach((cleanup) => cleanup());
66+
component.actionCleanups = [];
67+
6568
// HTTP status code of 304 is `Not Modified`. This null gets caught in the next promise
6669
// and stops any more processing.
6770
if (response.status === 304) {
@@ -73,6 +76,9 @@ export function send(component, callback) {
7376
);
7477
})
7578
.then((responseJson) => {
79+
component.actionCleanups.forEach((cleanup) => cleanup());
80+
component.actionCleanups = [];
81+
7682
if (!responseJson) {
7783
return;
7884
}
@@ -296,6 +302,9 @@ export function send(component, callback) {
296302
component.currentActionQueue = null;
297303
component.lastTriggeringElements = [];
298304

305+
component.actionCleanups.forEach((cleanup) => cleanup());
306+
component.actionCleanups = [];
307+
299308
if (isFunction(callback)) {
300309
callback(null, null, err);
301310
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import test from "ava";
2+
import { getComponent } from "../utils.js";
3+
4+
test("action disable modifier", async (t) => {
5+
const html = `
6+
<div unicorn:id="5jypjiyb" unicorn:name="text-inputs" unicorn:checksum="GXzew3Km">
7+
<button unicorn:click.disable="test()"></button>
8+
</div>
9+
`;
10+
const component = getComponent(html);
11+
const button = component.root.querySelector("button");
12+
13+
// 1. Initial state: Enabled
14+
t.false(button.hasAttribute("disabled"));
15+
16+
const MouseEvent = component.document.defaultView.MouseEvent;
17+
const event = new MouseEvent("click", {
18+
bubbles: true,
19+
cancelable: true,
20+
view: component.document.defaultView,
21+
});
22+
23+
// 2. Click: Should become disabled immediately
24+
button.dispatchEvent(event);
25+
t.true(button.disabled);
26+
27+
// 3. Wait for debounce (0ms) + fetch (async) to complete
28+
// We can use a short timeout to let the event loop process the fetch
29+
await new Promise(resolve => setTimeout(resolve, 10));
30+
31+
// 4. Final state: Should be enabled again
32+
t.false(button.disabled);
33+
});
34+
35+
test("action disable modifier with error", async (t) => {
36+
const html = `
37+
<div unicorn:id="error-test" unicorn:name="text-inputs" unicorn:checksum="GXzew3Km">
38+
<button unicorn:click.disable="test()"></button>
39+
</div>
40+
`;
41+
const component = getComponent(html, "error-test");
42+
const button = component.root.querySelector("button");
43+
44+
global.fetch.post("/test/error-input", 500);
45+
component.syncUrl = "/test/error-input";
46+
47+
const MouseEvent = component.document.defaultView.MouseEvent;
48+
button.dispatchEvent(new MouseEvent("click", {
49+
bubbles: true,
50+
view: component.document.defaultView
51+
}));
52+
53+
t.true(button.disabled);
54+
55+
await new Promise(resolve => setTimeout(resolve, 20));
56+
57+
t.false(button.disabled);
58+
});

0 commit comments

Comments
 (0)