Skip to content

Commit 47be741

Browse files
committed
feature #470 [Live] Action and Model-based data-loading behavior (weaverryan)
This PR was merged into the 2.x branch. Discussion ---------- [Live] Action and Model-based data-loading behavior | Q | A | ------------- | --- | Bug fix? | yes/no | New feature? | yes | Tickets | Fix #462 and Feature E | License | MIT NOTE: BUILT ON TOP OF #466. Been wanting this for awhile :): only trigger loading behavior on an element for a specific action. ``` <span data-loading="action(saveForm)|show">Loading</span> ``` Or only when a specific model has been updated: ``` <span data-loading="model(email)|show">Loading</span> ``` TODO: * [x] Let's also add "model" loading to this PR as well Commits ------- 4e846d8 data-load only for specific actions or model updates
2 parents d5d8252 + 4e846d8 commit 47be741

File tree

9 files changed

+496
-143
lines changed

9 files changed

+496
-143
lines changed

src/LiveComponent/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@
2626
<input data-model="firstName">
2727
```
2828
29+
- Added the ability to add `data-loading` behavior, which is only activated
30+
when a specific **action** is triggered - e.g. `<span data-loading="action(save)|show">Loading</span>`.
31+
32+
- Added the ability to add `data-loading` behavior, which is only activated
33+
when a specific **model** has been updated - e.g. `<span data-loading="model(firstName)|show">Loading</span>`.
34+
2935
## 2.4.0
3036
3137
- [BC BREAK] Previously, the `id` attribute was used with `morphdom` as the

src/LiveComponent/assets/dist/live_controller.js

Lines changed: 104 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,7 +1030,9 @@ class ValueStore {
10301030
}
10311031
set(name, value) {
10321032
const normalizedName = normalizeModelName(name);
1033-
this.updatedModels.push(normalizedName);
1033+
if (!this.updatedModels.includes(normalizedName)) {
1034+
this.updatedModels.push(normalizedName);
1035+
}
10341036
this.controller.dataValue = setDeepData(this.controller.dataValue, normalizedName, value);
10351037
}
10361038
hasAtTopLevel(name) {
@@ -1043,6 +1045,9 @@ class ValueStore {
10431045
all() {
10441046
return this.controller.dataValue;
10451047
}
1048+
areAnyModelsUpdated(targetedModels) {
1049+
return (this.updatedModels.filter(modelName => targetedModels.includes(modelName))).length > 0;
1050+
}
10461051
}
10471052

10481053
function getValueFromElement(element, valueStore) {
@@ -1298,32 +1303,35 @@ class default_1 extends Controller {
12981303
args: directive.named
12991304
});
13001305
let handled = false;
1306+
const validModifiers = new Map();
1307+
validModifiers.set('prevent', () => {
1308+
event.preventDefault();
1309+
});
1310+
validModifiers.set('stop', () => {
1311+
event.stopPropagation();
1312+
});
1313+
validModifiers.set('self', () => {
1314+
if (event.target !== event.currentTarget) {
1315+
return;
1316+
}
1317+
});
1318+
validModifiers.set('debounce', (modifier) => {
1319+
const length = modifier.value ? parseInt(modifier.value) : this.getDefaultDebounce();
1320+
__classPrivateFieldGet(this, _instances, "m", _clearRequestDebounceTimeout).call(this);
1321+
this.requestDebounceTimeout = window.setTimeout(() => {
1322+
this.requestDebounceTimeout = null;
1323+
__classPrivateFieldGet(this, _instances, "m", _startPendingRequest).call(this);
1324+
}, length);
1325+
handled = true;
1326+
});
13011327
directive.modifiers.forEach((modifier) => {
1302-
switch (modifier.name) {
1303-
case 'prevent':
1304-
event.preventDefault();
1305-
break;
1306-
case 'stop':
1307-
event.stopPropagation();
1308-
break;
1309-
case 'self':
1310-
if (event.target !== event.currentTarget) {
1311-
return;
1312-
}
1313-
break;
1314-
case 'debounce': {
1315-
const length = modifier.value ? parseInt(modifier.value) : this.getDefaultDebounce();
1316-
__classPrivateFieldGet(this, _instances, "m", _clearRequestDebounceTimeout).call(this);
1317-
this.requestDebounceTimeout = window.setTimeout(() => {
1318-
this.requestDebounceTimeout = null;
1319-
__classPrivateFieldGet(this, _instances, "m", _startPendingRequest).call(this);
1320-
}, length);
1321-
handled = true;
1322-
break;
1323-
}
1324-
default:
1325-
console.warn(`Unknown modifier ${modifier.name} in action ${rawAction}`);
1328+
var _a;
1329+
if (validModifiers.has(modifier.name)) {
1330+
const callable = (_a = validModifiers.get(modifier.name)) !== null && _a !== void 0 ? _a : (() => { });
1331+
callable(modifier);
1332+
return;
13261333
}
1334+
console.warn(`Unknown modifier ${modifier.name} in action "${rawAction}". Available modifiers are: ${Array.from(validModifiers.keys()).join(', ')}.`);
13271335
});
13281336
if (!handled) {
13291337
if (getModelDirectiveFromElement(event.currentTarget, false)) {
@@ -1436,11 +1444,17 @@ class default_1 extends Controller {
14361444
_onLoadingStart() {
14371445
this._handleLoadingToggle(true);
14381446
}
1439-
_onLoadingFinish() {
1440-
this._handleLoadingToggle(false);
1447+
_onLoadingFinish(targetElement = null) {
1448+
this._handleLoadingToggle(false, targetElement);
14411449
}
1442-
_handleLoadingToggle(isLoading) {
1443-
this._getLoadingDirectives().forEach(({ element, directives }) => {
1450+
_handleLoadingToggle(isLoading, targetElement = null) {
1451+
if (isLoading) {
1452+
this._addAttributes(this.element, ['busy']);
1453+
}
1454+
else {
1455+
this._removeAttributes(this.element, ['busy']);
1456+
}
1457+
this._getLoadingDirectives(targetElement).forEach(({ element, directives }) => {
14441458
if (isLoading) {
14451459
this._addAttributes(element, ['data-live-is-loading']);
14461460
}
@@ -1454,6 +1468,43 @@ class default_1 extends Controller {
14541468
}
14551469
_handleLoadingDirective(element, isLoading, directive) {
14561470
const finalAction = parseLoadingAction(directive.action, isLoading);
1471+
const targetedActions = [];
1472+
const targetedModels = [];
1473+
let delay = 0;
1474+
const validModifiers = new Map();
1475+
validModifiers.set('delay', (modifier) => {
1476+
if (!isLoading) {
1477+
return;
1478+
}
1479+
delay = modifier.value ? parseInt(modifier.value) : 200;
1480+
});
1481+
validModifiers.set('action', (modifier) => {
1482+
if (!modifier.value) {
1483+
throw new Error(`The "action" in data-loading must have an action name - e.g. action(foo). It's missing for "${directive.getString()}"`);
1484+
}
1485+
targetedActions.push(modifier.value);
1486+
});
1487+
validModifiers.set('model', (modifier) => {
1488+
if (!modifier.value) {
1489+
throw new Error(`The "model" in data-loading must have an action name - e.g. model(foo). It's missing for "${directive.getString()}"`);
1490+
}
1491+
targetedModels.push(modifier.value);
1492+
});
1493+
directive.modifiers.forEach((modifier) => {
1494+
var _a;
1495+
if (validModifiers.has(modifier.name)) {
1496+
const callable = (_a = validModifiers.get(modifier.name)) !== null && _a !== void 0 ? _a : (() => { });
1497+
callable(modifier);
1498+
return;
1499+
}
1500+
throw new Error(`Unknown modifier "${modifier.name}" used in data-loading="${directive.getString()}". Available modifiers are: ${Array.from(validModifiers.keys()).join(', ')}.`);
1501+
});
1502+
if (isLoading && targetedActions.length > 0 && this.backendRequest && !this.backendRequest.containsOneOfActions(targetedActions)) {
1503+
return;
1504+
}
1505+
if (isLoading && targetedModels.length > 0 && !this.valueStore.areAnyModelsUpdated(targetedModels)) {
1506+
return;
1507+
}
14571508
let loadingDirective;
14581509
switch (finalAction) {
14591510
case 'show':
@@ -1479,33 +1530,20 @@ class default_1 extends Controller {
14791530
default:
14801531
throw new Error(`Unknown data-loading action "${finalAction}"`);
14811532
}
1482-
let isHandled = false;
1483-
directive.modifiers.forEach((modifier => {
1484-
switch (modifier.name) {
1485-
case 'delay': {
1486-
if (!isLoading) {
1487-
break;
1488-
}
1489-
const delayLength = modifier.value ? parseInt(modifier.value) : 200;
1490-
window.setTimeout(() => {
1491-
if (element.hasAttribute('data-live-is-loading')) {
1492-
loadingDirective();
1493-
}
1494-
}, delayLength);
1495-
isHandled = true;
1496-
break;
1533+
if (delay) {
1534+
window.setTimeout(() => {
1535+
if (this.isRequestActive()) {
1536+
loadingDirective();
14971537
}
1498-
default:
1499-
throw new Error(`Unknown modifier ${modifier.name} used in the loading directive ${directive.getString()}`);
1500-
}
1501-
}));
1502-
if (!isHandled) {
1503-
loadingDirective();
1538+
}, delay);
1539+
return;
15041540
}
1541+
loadingDirective();
15051542
}
1506-
_getLoadingDirectives() {
1543+
_getLoadingDirectives(targetElement = null) {
15071544
const loadingDirectives = [];
1508-
this.element.querySelectorAll('[data-loading]').forEach((element => {
1545+
const element = targetElement || this.element;
1546+
element.querySelectorAll('[data-loading]').forEach((element => {
15091547
if (!(element instanceof HTMLElement) && !(element instanceof SVGElement)) {
15101548
throw new Error('Invalid Element Type');
15111549
}
@@ -1548,6 +1586,7 @@ class default_1 extends Controller {
15481586
}
15491587
_executeMorphdom(newHtml, modifiedElements) {
15501588
const newElement = htmlToElement(newHtml);
1589+
this._onLoadingFinish(newElement);
15511590
morphdom(this.element, newElement, {
15521591
getNodeKey: (node) => {
15531592
if (!(node instanceof HTMLElement)) {
@@ -1578,19 +1617,13 @@ class default_1 extends Controller {
15781617
&& !this._shouldChildLiveElementUpdate(fromEl, toEl)) {
15791618
return false;
15801619
}
1581-
if (fromEl.hasAttribute('data-live-ignore')) {
1582-
return false;
1583-
}
1584-
return true;
1620+
return !fromEl.hasAttribute('data-live-ignore');
15851621
},
15861622
onBeforeNodeDiscarded(node) {
15871623
if (!(node instanceof HTMLElement)) {
15881624
return true;
15891625
}
1590-
if (node.hasAttribute('data-live-ignore')) {
1591-
return false;
1592-
}
1593-
return true;
1626+
return !node.hasAttribute('data-live-ignore');
15941627
}
15951628
});
15961629
this._exposeOriginalData();
@@ -1846,6 +1879,9 @@ class default_1 extends Controller {
18461879
}
18471880
});
18481881
}
1882+
isRequestActive() {
1883+
return !!this.backendRequest;
1884+
}
18491885
}
18501886
_instances = new WeakSet(), _startPendingRequest = function _startPendingRequest() {
18511887
if (!this.backendRequest && (this.pendingActions.length > 0 || this.isRerenderRequested)) {
@@ -1865,7 +1901,6 @@ _instances = new WeakSet(), _startPendingRequest = function _startPendingRequest
18651901
'Accept': 'application/vnd.live-component+html',
18661902
};
18671903
const updatedModels = this.valueStore.updatedModels;
1868-
this.valueStore.updatedModels = [];
18691904
if (actions.length === 0 && this._willDataFitInUrl(this.valueStore.asJson(), params)) {
18701905
params.set('data', this.valueStore.asJson());
18711906
updatedModels.forEach((model) => {
@@ -1893,10 +1928,11 @@ _instances = new WeakSet(), _startPendingRequest = function _startPendingRequest
18931928
}
18941929
fetchOptions.body = JSON.stringify(requestData);
18951930
}
1896-
this._onLoadingStart();
18971931
const paramsString = params.toString();
18981932
const thisPromise = fetch(`${url}${paramsString.length > 0 ? `?${paramsString}` : ''}`, fetchOptions);
1899-
this.backendRequest = new BackendRequest(thisPromise);
1933+
this.backendRequest = new BackendRequest(thisPromise, actions.map(action => action.name));
1934+
this._onLoadingStart();
1935+
this.valueStore.updatedModels = [];
19001936
thisPromise.then(async (response) => {
19011937
const html = await response.text();
19021938
if (response.headers.get('Content-Type') !== 'application/vnd.live-component+html') {
@@ -1946,8 +1982,12 @@ default_1.values = {
19461982
debounce: Number,
19471983
};
19481984
class BackendRequest {
1949-
constructor(promise) {
1985+
constructor(promise, actions) {
19501986
this.promise = promise;
1987+
this.actions = actions;
1988+
}
1989+
containsOneOfActions(targetedActions) {
1990+
return (this.actions.filter(action => targetedActions.includes(action))).length > 0;
19511991
}
19521992
}
19531993
const parseLoadingAction = function (action, isLoading) {

src/LiveComponent/assets/src/ValueStore.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ export default class {
3434
*/
3535
set(name: string, value: any): void {
3636
const normalizedName = normalizeModelName(name);
37-
this.updatedModels.push(normalizedName);
37+
if (!this.updatedModels.includes(normalizedName)) {
38+
this.updatedModels.push(normalizedName);
39+
}
3840

3941
this.controller.dataValue = setDeepData(this.controller.dataValue, normalizedName, value);
4042
}
@@ -55,4 +57,11 @@ export default class {
5557
all(): any {
5658
return this.controller.dataValue;
5759
}
60+
61+
/**
62+
* Are any of the passed models currently "updated"?
63+
*/
64+
areAnyModelsUpdated(targetedModels: string[]): boolean {
65+
return (this.updatedModels.filter(modelName => targetedModels.includes(modelName))).length > 0;
66+
}
5867
}

src/LiveComponent/assets/src/directives_parser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* A modifier for a directive
33
*/
4-
interface DirectiveModifier {
4+
export interface DirectiveModifier {
55
/**
66
* The name of the modifier (e.g. delay)
77
*/

0 commit comments

Comments
 (0)