@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.
Files changed (35) hide show
  1. package/dist/build/elements.d.ts +12 -2
  2. package/dist/build/elements.d.ts.map +1 -1
  3. package/dist/build/elements.js +44 -10
  4. package/dist/build/elements.js.map +1 -1
  5. package/dist/core/lb-constants.d.ts +2 -1
  6. package/dist/core/lb-constants.d.ts.map +1 -1
  7. package/dist/core/lb-constants.js +16 -3
  8. package/dist/core/lb-constants.js.map +1 -1
  9. package/dist/core/lb-types.d.ts +25 -8
  10. package/dist/core/lb-types.d.ts.map +1 -1
  11. package/dist/core/lb-types.js.map +1 -1
  12. package/dist/hub/lb-apply.d.ts +9 -0
  13. package/dist/hub/lb-apply.d.ts.map +1 -1
  14. package/dist/hub/lb-apply.js +118 -35
  15. package/dist/hub/lb-apply.js.map +1 -1
  16. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  17. package/dist/hub/lb-hub.browser.js +41 -5
  18. package/dist/hub/lb-hub.browser.js.map +1 -1
  19. package/dist/hub/lb-lifecycle.d.ts +40 -0
  20. package/dist/hub/lb-lifecycle.d.ts.map +1 -0
  21. package/dist/hub/lb-lifecycle.js +140 -0
  22. package/dist/hub/lb-lifecycle.js.map +1 -0
  23. package/docs/TECHREF-1.0.md +49 -47
  24. package/docs/comparison.md +7 -5
  25. package/docs/possible-ideas.md +6 -0
  26. package/docs/reference/custom-elements.md +110 -89
  27. package/docs/reference/data-binding.md +12 -7
  28. package/docs/reference/widgets.md +2 -2
  29. package/docs/testing.md +2 -2
  30. package/package.json +1 -1
  31. package/skills/loadbare-app/SKILL.md +20 -16
  32. package/skills/loadbare-app/references/TECHREF-1.0.md +49 -47
  33. package/skills/loadbare-app/references/custom-elements.md +110 -89
  34. package/skills/loadbare-app/references/data-binding.md +12 -7
  35. package/skills/loadbare-app/references/widgets.md +2 -2
@@ -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. A custom element
24
- * shipped inside an absent branch has not upgraded, so it is filled again
25
- * once it has, and places its rows itself.
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
- /** The template holding an absent branch — see ATTR_SHOW. */
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 row or a group on its way out. */
47
- const LEAVING = `[${ATTR_ROW_LEAVING}], [${ATTR_GROUP_LEAVING}]`;
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.textContent = value;
138
+ setText(el, String(value));
139
139
  }
140
- el.setAttribute(ATTR_COLUMN_VALUE, value);
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. The attributes are already there when the row
833
- // upgrades, which is the same thing that makes a first landing and a
834
- // refresh one operation everywhere else.
835
- applyRow(row, data, true);
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.setAttribute(ATTR_QUERY_ROW_COUNT, String(liveRows(el).length));
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.setAttribute(ATTR_KEY_VALUE, String(key));
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) || waiting(el) || el.closest(LEAVING))
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
- if (!waiting(el))
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