@loadbare/app 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build/elements.d.ts +12 -2
- package/dist/build/elements.d.ts.map +1 -1
- package/dist/build/elements.js +44 -10
- package/dist/build/elements.js.map +1 -1
- package/dist/core/lb-constants.d.ts +2 -1
- package/dist/core/lb-constants.d.ts.map +1 -1
- package/dist/core/lb-constants.js +16 -3
- package/dist/core/lb-constants.js.map +1 -1
- package/dist/core/lb-types.d.ts +25 -8
- package/dist/core/lb-types.d.ts.map +1 -1
- package/dist/core/lb-types.js.map +1 -1
- package/dist/hub/lb-apply.d.ts +9 -0
- package/dist/hub/lb-apply.d.ts.map +1 -1
- package/dist/hub/lb-apply.js +118 -35
- package/dist/hub/lb-apply.js.map +1 -1
- package/dist/hub/lb-hub.browser.d.ts.map +1 -1
- package/dist/hub/lb-hub.browser.js +41 -5
- package/dist/hub/lb-hub.browser.js.map +1 -1
- package/dist/hub/lb-lifecycle.d.ts +40 -0
- package/dist/hub/lb-lifecycle.d.ts.map +1 -0
- package/dist/hub/lb-lifecycle.js +140 -0
- package/dist/hub/lb-lifecycle.js.map +1 -0
- package/docs/TECHREF-1.0.md +49 -47
- package/docs/comparison.md +7 -5
- package/docs/possible-ideas.md +6 -0
- package/docs/reference/custom-elements.md +110 -89
- package/docs/reference/data-binding.md +12 -7
- package/docs/reference/widgets.md +2 -2
- package/docs/testing.md +2 -2
- package/package.json +1 -1
- package/skills/loadbare-app/SKILL.md +20 -16
- package/skills/loadbare-app/references/TECHREF-1.0.md +49 -47
- package/skills/loadbare-app/references/custom-elements.md +110 -89
- package/skills/loadbare-app/references/data-binding.md +12 -7
- package/skills/loadbare-app/references/widgets.md +2 -2
package/dist/hub/lb-apply.js
CHANGED
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
* arrives after that query's answer — a picker in a new live row, a ghost
|
|
21
21
|
* row a custom element built — is filled from the kept answer at the end of
|
|
22
22
|
* the next landing, so a query need not land again, and need not land after
|
|
23
|
-
* the one whose rows hold it, just to fill what is new.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* the one whose rows hold it, just to fill what is new.
|
|
24
|
+
*
|
|
25
|
+
* Every custom element landing places is told so once the landing is done,
|
|
26
|
+
* and every one it removes is told so once it is gone — see lb-lifecycle.
|
|
26
27
|
*
|
|
27
28
|
* A `rows` query is placed by its order: the one the URL carries for it, or
|
|
28
29
|
* the one the server declared, or, with neither, the order its rows arrived
|
|
@@ -37,14 +38,13 @@
|
|
|
37
38
|
*/
|
|
38
39
|
import { AGGREGATES, ATTR_COLUMN, ATTR_COLUMN_VALUE, ATTR_COUNT, ATTR_GROUP, ATTR_GROUP_COLUMN, ATTR_GROUP_LEAVING, ATTR_GROUP_LIVE, ATTR_GROUP_VALUE, ATTR_KEY_VALUE, ATTR_MAX, ATTR_MIN, ATTR_QUERY, ATTR_QUERY_ROW_COUNT, ATTR_ROW_CHANGED, ATTR_ROW_CREATED, ATTR_ROW_LEAVING, ATTR_ROW_LIVE, ATTR_ROW_MOVED, ATTR_ROW_REQUESTED, ATTR_SHOW, ATTR_SUM, KIND_ROW, LIVE_GROUP, LIVE_ROW, REQUESTED_CHANGED, REQUESTED_CREATED, REQUESTED_MOVED, SHOW_NOT, ATTR_SELECT, ATTR_ROW_SELECTED, } from "../core/lb-constants.js";
|
|
39
40
|
import { compareRows, compareValues, isPatch, parseOrder, } from "../core/lb-types.js";
|
|
40
|
-
|
|
41
|
-
const PARKED = `template[${ATTR_SHOW}]`;
|
|
41
|
+
import { attachPlaced, detachRemoved, LEAVING, PARKED, placed, upgradeAbsent, } from "./lb-lifecycle.js";
|
|
42
42
|
/** A DocumentFragment's nodeType. Tier 3 has no `Node` global to read it from. */
|
|
43
43
|
const FRAGMENT_NODE = 11;
|
|
44
44
|
/** An Element's nodeType, for the same reason. */
|
|
45
45
|
const ELEMENT_NODE = 1;
|
|
46
|
-
/** A
|
|
47
|
-
const
|
|
46
|
+
/** A Text node's nodeType, for the same reason. */
|
|
47
|
+
const TEXT_NODE = 3;
|
|
48
48
|
/**
|
|
49
49
|
* What a group's heading and aggregates stop at: the rows and groups inside
|
|
50
50
|
* it, live or leaving, which are filled from rows of their own.
|
|
@@ -135,9 +135,29 @@ function land(el, value) {
|
|
|
135
135
|
return;
|
|
136
136
|
}
|
|
137
137
|
else if (!el.localName.includes("-")) {
|
|
138
|
-
el
|
|
138
|
+
setText(el, String(value));
|
|
139
139
|
}
|
|
140
|
-
el
|
|
140
|
+
setStamp(el, ATTR_COLUMN_VALUE, String(value));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Set an element's text, unless it already holds exactly that text. Writing
|
|
144
|
+
* the same text again replaces the text node, which is two mutations and a
|
|
145
|
+
* style check for nothing.
|
|
146
|
+
*/
|
|
147
|
+
function setText(el, text) {
|
|
148
|
+
const only = el.firstChild;
|
|
149
|
+
const same = only
|
|
150
|
+
? only === el.lastChild &&
|
|
151
|
+
only.nodeType === TEXT_NODE &&
|
|
152
|
+
only.data === text
|
|
153
|
+
: text === "";
|
|
154
|
+
if (!same)
|
|
155
|
+
el.textContent = text;
|
|
156
|
+
}
|
|
157
|
+
/** Set an attribute, unless it already has that value. */
|
|
158
|
+
function setStamp(el, name, value) {
|
|
159
|
+
if (el.getAttribute(name) !== value)
|
|
160
|
+
el.setAttribute(name, value);
|
|
141
161
|
}
|
|
142
162
|
/**
|
|
143
163
|
* Whether an element under `root` belongs to root's query, rather than to a
|
|
@@ -252,6 +272,57 @@ function present(el, on) {
|
|
|
252
272
|
parked.setAttribute(ATTR_SHOW, el.getAttribute(ATTR_SHOW));
|
|
253
273
|
el.replaceWith(parked);
|
|
254
274
|
parked.content.append(el);
|
|
275
|
+
parkedIn.set(el, parked);
|
|
276
|
+
}
|
|
277
|
+
/** The template each absent element was last put in. */
|
|
278
|
+
const parkedIn = new WeakMap();
|
|
279
|
+
/**
|
|
280
|
+
* Put a live row's element in the document or take it out, the element
|
|
281
|
+
* itself being known. Its template, while it is in one, decides first, as it
|
|
282
|
+
* does when landing finds both.
|
|
283
|
+
*/
|
|
284
|
+
function presentPlaced(el, on) {
|
|
285
|
+
const parked = parkedIn.get(el);
|
|
286
|
+
if (parked && el.parentNode === parked.content)
|
|
287
|
+
present(parked, on);
|
|
288
|
+
present(el, on);
|
|
289
|
+
}
|
|
290
|
+
const placesOf = new WeakMap();
|
|
291
|
+
/**
|
|
292
|
+
* A live row's places, found the first time it is filled. Every live row is
|
|
293
|
+
* a copy of the row template, and landing moves what it shows and hides
|
|
294
|
+
* rather than rebuilding it, so what the first search finds stays true of
|
|
295
|
+
* the row, and filling it again searches nothing.
|
|
296
|
+
*/
|
|
297
|
+
function places(row) {
|
|
298
|
+
let found = placesOf.get(row);
|
|
299
|
+
if (found)
|
|
300
|
+
return found;
|
|
301
|
+
found = { columns: new Map(), shows: new Map() };
|
|
302
|
+
const add = (map, name, el) => {
|
|
303
|
+
const list = map.get(name);
|
|
304
|
+
if (list)
|
|
305
|
+
list.push(el);
|
|
306
|
+
else
|
|
307
|
+
map.set(name, [el]);
|
|
308
|
+
};
|
|
309
|
+
if (row.hasAttribute(ATTR_COLUMN)) {
|
|
310
|
+
add(found.columns, row.getAttribute(ATTR_COLUMN), row);
|
|
311
|
+
}
|
|
312
|
+
for (const el of below(row, `[${ATTR_COLUMN}]`)) {
|
|
313
|
+
add(found.columns, el.getAttribute(ATTR_COLUMN), el);
|
|
314
|
+
}
|
|
315
|
+
for (const el of below(row, `[${ATTR_SHOW}]`)) {
|
|
316
|
+
if (el.localName === "template") {
|
|
317
|
+
const branch = el.content.firstElementChild;
|
|
318
|
+
if (branch)
|
|
319
|
+
parkedIn.set(branch, el);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
add(found.shows, el.getAttribute(ATTR_SHOW), el);
|
|
323
|
+
}
|
|
324
|
+
placesOf.set(row, found);
|
|
325
|
+
return found;
|
|
255
326
|
}
|
|
256
327
|
/**
|
|
257
328
|
* Fill from one row: every `lb-column` and `lb-show` in root's query.
|
|
@@ -261,8 +332,24 @@ function present(el, on) {
|
|
|
261
332
|
* be the column it displays. An element a `row` lands on is not its own
|
|
262
333
|
* ancestor, so its own `lb-column` reads from the row around it and is left
|
|
263
334
|
* alone here.
|
|
335
|
+
*
|
|
336
|
+
* A live row is searched once, the first time it is filled; see places().
|
|
264
337
|
*/
|
|
265
338
|
export function applyRow(root, row, liveRow = false, stop) {
|
|
339
|
+
if (liveRow && !stop) {
|
|
340
|
+
const { columns, shows } = places(root);
|
|
341
|
+
for (const [column, value] of Object.entries(row)) {
|
|
342
|
+
for (const el of columns.get(column) ?? [])
|
|
343
|
+
land(el, value);
|
|
344
|
+
const on = value !== null && value !== false;
|
|
345
|
+
for (const el of shows.get(column) ?? [])
|
|
346
|
+
presentPlaced(el, on);
|
|
347
|
+
for (const el of shows.get(`${SHOW_NOT}${column}`) ?? []) {
|
|
348
|
+
presentPlaced(el, !on);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
266
353
|
for (const [column, value] of Object.entries(row)) {
|
|
267
354
|
const selector = `[${ATTR_COLUMN}="${column}"]`;
|
|
268
355
|
const columns = below(root, selector, stop);
|
|
@@ -439,6 +526,7 @@ function depart(nodes) {
|
|
|
439
526
|
const go = () => {
|
|
440
527
|
for (const node of nodes)
|
|
441
528
|
node.remove();
|
|
529
|
+
detachRemoved(nodes);
|
|
442
530
|
};
|
|
443
531
|
if (running.length === 0)
|
|
444
532
|
go();
|
|
@@ -476,12 +564,14 @@ function leaveGroup(group) {
|
|
|
476
564
|
function makeGroup(doc, template, id, term, value) {
|
|
477
565
|
// Imported rather than cloned, for the reason a live row is.
|
|
478
566
|
const fragment = doc.importNode(template.content, true);
|
|
567
|
+
upgradeAbsent(fragment, doc);
|
|
479
568
|
const slot = slotIn(fragment);
|
|
480
569
|
const nodes = [...fragment.childNodes];
|
|
481
570
|
for (const node of nodes) {
|
|
482
571
|
if (node.nodeType !== ELEMENT_NODE || node === slot)
|
|
483
572
|
continue;
|
|
484
573
|
const el = node;
|
|
574
|
+
placed(el);
|
|
485
575
|
el.setAttribute(ATTR_GROUP_LIVE, "");
|
|
486
576
|
if (term)
|
|
487
577
|
el.setAttribute(ATTR_GROUP_COLUMN, term.column);
|
|
@@ -817,6 +907,8 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
|
|
|
817
907
|
// until inserted, and a form-associated control would be filled as if
|
|
818
908
|
// it were not one.
|
|
819
909
|
row = el.ownerDocument.importNode(list.rowTemplate.content.firstElementChild, true);
|
|
910
|
+
upgradeAbsent(row, el.ownerDocument);
|
|
911
|
+
placed(row);
|
|
820
912
|
row.setAttribute(ATTR_ROW_LIVE, "");
|
|
821
913
|
row.setAttribute(ATTR_KEY_VALUE, key);
|
|
822
914
|
shown.set(key, row);
|
|
@@ -829,10 +921,15 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
|
|
|
829
921
|
changed: (prior?.changed ?? false) || differs(before, data),
|
|
830
922
|
});
|
|
831
923
|
list.data.set(row, { ...before, ...data });
|
|
832
|
-
// Fill before insertion
|
|
833
|
-
//
|
|
834
|
-
//
|
|
835
|
-
|
|
924
|
+
// Fill before insertion, on a row already upgraded, which is the same
|
|
925
|
+
// thing that makes a first landing and a refresh one operation
|
|
926
|
+
// everywhere else.
|
|
927
|
+
//
|
|
928
|
+
// A row that already shows these values is left as it is, so a control
|
|
929
|
+
// the user has typed into and not committed keeps what was typed.
|
|
930
|
+
if (!before || options.refill || differs(before, data)) {
|
|
931
|
+
applyRow(row, data, true);
|
|
932
|
+
}
|
|
836
933
|
};
|
|
837
934
|
if (Array.isArray(result)) {
|
|
838
935
|
for (const data of result)
|
|
@@ -909,7 +1006,7 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
|
|
|
909
1006
|
// and a patch report the same fact the same way. Only here is the count
|
|
910
1007
|
// known: the server answers with rows and says nothing about how many
|
|
911
1008
|
// survived reconciliation.
|
|
912
|
-
el
|
|
1009
|
+
setStamp(el, ATTR_QUERY_ROW_COUNT, String(liveRows(el).length));
|
|
913
1010
|
host.lbRowsLanded?.();
|
|
914
1011
|
}
|
|
915
1012
|
/** Land one response item on one element that names its query. */
|
|
@@ -924,7 +1021,7 @@ function landItem(el, item, options = {}) {
|
|
|
924
1021
|
applyRow(el, row);
|
|
925
1022
|
const key = row[item.key];
|
|
926
1023
|
if (key !== undefined && key !== null) {
|
|
927
|
-
el
|
|
1024
|
+
setStamp(el, ATTR_KEY_VALUE, String(key));
|
|
928
1025
|
}
|
|
929
1026
|
return;
|
|
930
1027
|
}
|
|
@@ -993,22 +1090,6 @@ function keep(answers, item) {
|
|
|
993
1090
|
rows: patched(before.rows ?? [], item.key, item.patch),
|
|
994
1091
|
});
|
|
995
1092
|
}
|
|
996
|
-
/**
|
|
997
|
-
* A custom element whose class is defined and has not yet run on it: one the
|
|
998
|
-
* builder shipped inside an absent branch's template, where nothing upgrades.
|
|
999
|
-
* It has none of the hooks landing calls, so rows that land on it are not
|
|
1000
|
-
* placed by it. A landing on it does not count, and once `lb-show` brings it
|
|
1001
|
-
* into the document and it upgrades, it is filled again from the kept answer.
|
|
1002
|
-
*
|
|
1003
|
-
* An element with no class, such as one defined by an element file alone,
|
|
1004
|
-
* never upgrades, and is never waiting.
|
|
1005
|
-
*/
|
|
1006
|
-
function waiting(el) {
|
|
1007
|
-
const registry = globalThis
|
|
1008
|
-
.customElements;
|
|
1009
|
-
const upgraded = registry?.get(el.localName);
|
|
1010
|
-
return upgraded !== undefined && !(el instanceof upgraded);
|
|
1011
|
-
}
|
|
1012
1093
|
/**
|
|
1013
1094
|
* Fill every element that names a kept query and has had nothing land on it.
|
|
1014
1095
|
* Filling one can build more — a live row holding a picker of its own — so
|
|
@@ -1020,10 +1101,10 @@ function fillArrivals(root, state) {
|
|
|
1020
1101
|
filled = false;
|
|
1021
1102
|
for (const [query, item] of answers) {
|
|
1022
1103
|
for (const el of everywhere(root, `[${ATTR_QUERY}="${query}"]`)) {
|
|
1023
|
-
if (landed.has(el) ||
|
|
1104
|
+
if (landed.has(el) || el.closest(LEAVING))
|
|
1024
1105
|
continue;
|
|
1025
1106
|
landed.add(el);
|
|
1026
|
-
landItem(el, item, { order: orderOf(state, item) });
|
|
1107
|
+
landItem(el, item, { order: orderOf(state, item), refill: true });
|
|
1027
1108
|
filled = true;
|
|
1028
1109
|
}
|
|
1029
1110
|
}
|
|
@@ -1032,6 +1113,8 @@ function fillArrivals(root, state) {
|
|
|
1032
1113
|
/**
|
|
1033
1114
|
* Land response items. Every answer arrives through here, and is kept for
|
|
1034
1115
|
* `root`, so that what arrives after it is filled from it — see forget().
|
|
1116
|
+
* Once everything has landed, each custom element placed since the last
|
|
1117
|
+
* landing is attached.
|
|
1035
1118
|
*/
|
|
1036
1119
|
export function applyResponse(root, items, options = {}) {
|
|
1037
1120
|
const state = keptFor(root);
|
|
@@ -1047,8 +1130,7 @@ export function applyResponse(root, items, options = {}) {
|
|
|
1047
1130
|
continue;
|
|
1048
1131
|
}
|
|
1049
1132
|
for (const el of targets) {
|
|
1050
|
-
|
|
1051
|
-
state.landed.add(el);
|
|
1133
|
+
state.landed.add(el);
|
|
1052
1134
|
landItem(el, item, {
|
|
1053
1135
|
order: orderOf(state, item),
|
|
1054
1136
|
requested: options.requested,
|
|
@@ -1056,6 +1138,7 @@ export function applyResponse(root, items, options = {}) {
|
|
|
1056
1138
|
}
|
|
1057
1139
|
}
|
|
1058
1140
|
fillArrivals(root, state);
|
|
1141
|
+
attachPlaced();
|
|
1059
1142
|
}
|
|
1060
1143
|
/**
|
|
1061
1144
|
* Land a query's kept answer again, as all of its rows: what the hub does
|