@loadbare/app 0.9.0 → 0.11.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 (82) hide show
  1. package/README.md +3 -3
  2. package/dist/build/assemble.d.ts.map +1 -1
  3. package/dist/build/assemble.js +107 -90
  4. package/dist/build/assemble.js.map +1 -1
  5. package/dist/build/expand.d.ts +6 -1
  6. package/dist/build/expand.d.ts.map +1 -1
  7. package/dist/build/expand.js +112 -26
  8. package/dist/build/expand.js.map +1 -1
  9. package/dist/build/locations.d.ts +2 -3
  10. package/dist/build/locations.d.ts.map +1 -1
  11. package/dist/build/locations.js +2 -3
  12. package/dist/build/locations.js.map +1 -1
  13. package/dist/build/pages.d.ts +3 -4
  14. package/dist/build/pages.d.ts.map +1 -1
  15. package/dist/build/pages.js +3 -4
  16. package/dist/build/pages.js.map +1 -1
  17. package/dist/core/lb-constants.d.ts +27 -24
  18. package/dist/core/lb-constants.d.ts.map +1 -1
  19. package/dist/core/lb-constants.js +103 -168
  20. package/dist/core/lb-constants.js.map +1 -1
  21. package/dist/core/lb-types.d.ts +64 -77
  22. package/dist/core/lb-types.d.ts.map +1 -1
  23. package/dist/core/lb-types.js +40 -7
  24. package/dist/core/lb-types.js.map +1 -1
  25. package/dist/hub/lb-apply.d.ts +47 -37
  26. package/dist/hub/lb-apply.d.ts.map +1 -1
  27. package/dist/hub/lb-apply.js +195 -199
  28. package/dist/hub/lb-apply.js.map +1 -1
  29. package/dist/hub/lb-hub.browser.d.ts +1 -1
  30. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  31. package/dist/hub/lb-hub.browser.js +410 -449
  32. package/dist/hub/lb-hub.browser.js.map +1 -1
  33. package/dist/server/lb-express.d.ts +5 -5
  34. package/dist/server/lb-express.d.ts.map +1 -1
  35. package/dist/server/lb-express.js +35 -66
  36. package/dist/server/lb-express.js.map +1 -1
  37. package/dist/server/lb-server.d.ts +77 -135
  38. package/dist/server/lb-server.d.ts.map +1 -1
  39. package/dist/server/lb-server.js +132 -79
  40. package/dist/server/lb-server.js.map +1 -1
  41. package/docs/TECHREF-1.0.md +908 -585
  42. package/docs/comparison.md +243 -185
  43. package/docs/prior-art.md +15 -14
  44. package/docs/reference/builder.md +9 -3
  45. package/docs/reference/chrome.md +107 -56
  46. package/docs/reference/custom-elements.md +291 -173
  47. package/docs/reference/data-binding.md +381 -374
  48. package/docs/reference/overview.md +12 -10
  49. package/docs/reference/page-files.md +164 -99
  50. package/docs/reference/server.md +2 -2
  51. package/docs/reference/widgets.md +104 -110
  52. package/docs/roadmap.md +32 -39
  53. package/docs/terms-of-art.md +57 -0
  54. package/docs/testing.md +97 -68
  55. package/docs/theory.md +92 -58
  56. package/docs/tutorials/010-pages-and-navigation.md +20 -12
  57. package/docs/tutorials/020-css.md +6 -3
  58. package/docs/tutorials/030-html-decomposition.md +9 -7
  59. package/docs/tutorials/040-displaying-data.md +30 -13
  60. package/docs/tutorials/{050-actions.md → 050-requests.md} +25 -15
  61. package/docs/tutorials/060-custom-element-code.md +17 -16
  62. package/docs/tutorials/065-conditional-rendering.md +34 -23
  63. package/docs/tutorials/070-displaying-a-list.md +29 -21
  64. package/docs/tutorials/072-inserting-into-a-list.md +24 -16
  65. package/docs/tutorials/074-deleting-from-a-list.md +9 -7
  66. package/docs/tutorials/076-updating-a-list-item.md +11 -10
  67. package/docs/tutorials/080-widget-requests.md +71 -43
  68. package/docs/tutorials/090-using-widget-libraries.md +22 -22
  69. package/docs/what-does-loadbare-extend.md +124 -0
  70. package/package.json +1 -1
  71. package/skills/loadbare-app/SKILL.md +201 -123
  72. package/skills/loadbare-app/references/TECHREF-1.0.md +908 -585
  73. package/skills/loadbare-app/references/builder.md +9 -3
  74. package/skills/loadbare-app/references/chrome.md +107 -56
  75. package/skills/loadbare-app/references/custom-elements.md +291 -173
  76. package/skills/loadbare-app/references/data-binding.md +381 -374
  77. package/skills/loadbare-app/references/overview.md +12 -10
  78. package/skills/loadbare-app/references/page-files.md +164 -99
  79. package/skills/loadbare-app/references/server.md +2 -2
  80. package/skills/loadbare-app/references/widgets.md +104 -110
  81. package/docs/analysis-accidental-complexity.md +0 -149
  82. package/docs/analysis-closed-set.md +0 -210
@@ -1,26 +1,27 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
- * Landing data on a live host — see docs/reference/data-binding.md.
3
+ * Landing — see docs/reference/data-binding.md.
4
4
  *
5
- * One containment ladder, and one function per rung. `applyData` lands a
6
- * whole response, `applyList` lands one list result, `applyRow` fills one
7
- * scope from one row. Filling a live row and filling a single-row scope are
8
- * the same operation, and the only difference is how much of the page the
9
- * root covers.
5
+ * The hub lands a response item's rows on every element whose `lb-query`
6
+ * names its query. The item says the query's kind and key, so the markup
7
+ * says neither:
10
8
  *
11
- * The row machinery used to live in its own file because every list widget
12
- * imported and called it. The hub reconciles a list scope itself now, so a
13
- * widget supplies placement and scaffolding through two optional hooks and
14
- * never sees a whole result. Cloning the template, matching a row to the
15
- * element already showing it, and filling that element are the same in every
16
- * list, so no widget can get them wrong.
9
+ * kind row template the hub
10
+ * row no lands the row on the element itself
11
+ * row yes lands one live row
12
+ * rows yes lands one live row per row
13
+ * rows no lands nothing
14
+ *
15
+ * Filling a live row and filling an element a `row` lands on are the same
16
+ * operation, and the only difference is how much of the page the root
17
+ * covers.
17
18
  *
18
19
  * A condition is the one other thing landing moves. An element whose
19
20
  * `lb-show` column is off stands in the document inside a template, and
20
21
  * landing reaches in there as it reaches everywhere else, so a branch that
21
22
  * returns is already current. Nothing but landing reaches in.
22
23
  */
23
- import { ATTR_CELL, ATTR_KEY, ATTR_KEY_VALUE, ATTR_LIST, ATTR_QUERY_PARM, ATTR_ROW, ATTR_ROW_COUNT, ATTR_SHOW, ATTR_VALUE, } from "../core/lb-constants.js";
24
+ import { ATTR_COLUMN, ATTR_COLUMN_VALUE, ATTR_KEY_VALUE, ATTR_QUERY, ATTR_QUERY_ROW_COUNT, ATTR_ROW_LIVE, ATTR_SHOW, KIND_ROW, LIVE_ROW, } from "../core/lb-constants.js";
24
25
  import { isPatch, } from "../core/lb-types.js";
25
26
  /** The template holding an absent branch — see ATTR_SHOW. */
26
27
  const PARKED = `template[${ATTR_SHOW}]`;
@@ -29,64 +30,73 @@ const FRAGMENT_NODE = 11;
29
30
  /** The input types whose state is not their `value`, or cannot be set. */
30
31
  const UNLANDED_INPUTS = ["checkbox", "radio", "file"];
31
32
  /**
32
- * A form control whose state is its `value`: a `<select>`, a `<textarea>`,
33
- * or an `<input>` of any type but checkbox, radio and file. Landing and
34
- * gathering draw the same line, so a value read back from a form is the one
35
- * that landed there.
33
+ * A native control whose state is its `value`: a `<select>`, a
34
+ * `<textarea>`, or an `<input>` of any type but checkbox, radio and file.
36
35
  *
37
36
  * Told apart by tag name and type rather than by class, so it holds for an
38
37
  * element from any document.
39
38
  */
40
- export function isValueControl(el) {
39
+ function isNativeControl(el) {
41
40
  if (el.localName === "select" || el.localName === "textarea")
42
41
  return true;
43
42
  return (el.localName === "input" &&
44
43
  !UNLANDED_INPUTS.includes(el.type));
45
44
  }
46
45
  /**
47
- * A cell lands one of three ways. A custom element owns whatever control it
48
- * wraps, so it receives the value as an attribute and renders it itself.
49
- * Because the browser runs `attributeChangedCallback` for attributes already
50
- * present when a widget upgrades, that is the same operation whether the host
51
- * was inserted a microsecond ago or an hour ago.
52
- *
53
- * A form control shows its state as its `value`, so that is where the value
54
- * goes; its text is its options, for a select. Any other native element has
55
- * no behavior of its own, so its value is its text. Either one also carries
56
- * the value as `lb-value`, so a stylesheet can select on what landed.
57
- * Checkboxes and radio buttons are not implemented, and receive nothing — see
58
- * docs/TECHREF-1.0.md, "Blockers".
46
+ * A form-associated custom element with a `value` property. HTML marks one
47
+ * with `static formAssociated = true` on its class, and a control has a value.
48
+ */
49
+ export function isFormAssociated(el) {
50
+ const ctor = el.constructor;
51
+ return ctor.formAssociated === true && "value" in el;
52
+ }
53
+ /**
54
+ * A control: an `<input>`, `<select>` or `<textarea>`, or a form-associated
55
+ * custom element with a `value` property. Landing and gathering draw the
56
+ * same line, so a value read back is the one that landed there.
57
+ */
58
+ export function isControl(el) {
59
+ return isNativeControl(el) || isFormAssociated(el);
60
+ }
61
+ /**
62
+ * Set an element from a column. A control's `value` is set. A custom element
63
+ * that is not a control keeps its content, which the builder placed there
64
+ * from its element file, and renders the stamp instead. Any other element's
65
+ * text content is set. Every one carries the value as `lb-column-value`.
59
66
  */
60
67
  function land(el, value) {
61
68
  // Untouched, whatever the server sent: the browser decides what a
62
69
  // non-string looks like. See Row in core/lb-types.ts.
63
- if (el.localName.includes("-")) {
64
- el.setAttribute(ATTR_VALUE, value);
70
+ if (value === null || value === undefined) {
71
+ if (isControl(el))
72
+ el.value = "";
73
+ else if (!el.localName.includes("-") && el.localName !== "input") {
74
+ el.textContent = "";
75
+ }
76
+ el.removeAttribute(ATTR_COLUMN_VALUE);
77
+ return;
65
78
  }
66
- else if (isValueControl(el)) {
79
+ if (isControl(el)) {
67
80
  el.value = value;
68
- el.setAttribute(ATTR_VALUE, value);
69
81
  }
70
82
  else if (el.localName === "input") {
71
83
  console.warn(`lb-hub: a value does not land on <input type="${el.type}">, ignoring`, el);
84
+ return;
72
85
  }
73
- else {
86
+ else if (!el.localName.includes("-")) {
74
87
  el.textContent = value;
75
- el.setAttribute(ATTR_VALUE, value);
76
88
  }
89
+ el.setAttribute(ATTR_COLUMN_VALUE, value);
77
90
  }
78
91
  /**
79
- * Whether an element under `root` belongs to root's scope, rather than to a
80
- * scope nested inside it.
92
+ * Whether an element under `root` belongs to root's query, rather than to a
93
+ * query nested inside it.
81
94
  *
82
- * Both scope attributes scope their DOM children, and a nested one of either
83
- * kind begins a new scope (docs/reference/data-binding.md). So a descendant
84
- * is in root's scope unless something between it and root carries one. The
85
- * element's own attribute does not count: it names what the element
86
- * displays, and where it belongs is its ancestors', so a picker carrying
87
- * `lb-list` and `lb-cell` is a cell of the row around it. Root's own
88
- * attribute does not count either: root is the scope being filled, whatever
89
- * it carries.
95
+ * A descendant is root's unless something between it and root carries
96
+ * `lb-query`. The element's own `lb-query` does not count: it names what the
97
+ * element holds, and the row it reads from is its ancestors'. So a picker
98
+ * carrying `lb-query` and `lb-column` is set from the row around it. Root's
99
+ * own attribute does not count either: root is what is being filled.
90
100
  *
91
101
  * Root may be the content of a template holding an absent branch. The walk
92
102
  * up from an element inside it stops at the top of that content, which has
@@ -94,17 +104,17 @@ function land(el, value) {
94
104
  */
95
105
  function inScope(root, el) {
96
106
  for (let node = el.parentElement; node && node !== root;) {
97
- if (node.hasAttribute(ATTR_LIST) || node.hasAttribute(ATTR_ROW)) {
107
+ if (node.hasAttribute(ATTR_QUERY))
98
108
  return false;
99
- }
100
109
  node = node.parentElement;
101
110
  }
102
111
  return true;
103
112
  }
104
113
  /**
105
- * Root if it matches, then every descendant in root's scope that does.
106
- * Gathering finds cells through here. It does not reach into an absent
107
- * branch, so a form reads back from the places a row lands that are showing.
114
+ * Root if it matches, then every descendant in root's query that does.
115
+ * Gathering finds columns through here. It does not reach into an absent
116
+ * branch, so a live row is read back from the places a row lands that are
117
+ * showing.
108
118
  */
109
119
  export function within(root, selector) {
110
120
  const found = root.matches(selector) ? [root] : [];
@@ -115,11 +125,11 @@ export function within(root, selector) {
115
125
  return found;
116
126
  }
117
127
  /**
118
- * Every descendant in root's scope that matches, absent branches included.
128
+ * Every descendant in root's query that matches, absent branches included.
119
129
  *
120
130
  * `querySelectorAll` does not enter template content, so an element that is
121
131
  * off would otherwise miss everything landed while it was away, and return
122
- * stale or with its lists empty. A template holding one is in root's scope by
132
+ * stale or with its rows empty. A template holding one is in root's query by
123
133
  * the ordinary rule, and its content is searched as if it stood there.
124
134
  */
125
135
  function below(root, selector) {
@@ -135,15 +145,9 @@ function below(root, selector) {
135
145
  }
136
146
  return found;
137
147
  }
138
- /** What `within` is for gathering, `reach` is for landing. */
139
- function reach(root, selector) {
140
- const found = below(root, selector);
141
- return root.matches(selector) ? [root, ...found] : found;
142
- }
143
148
  /**
144
- * Every element in a whole response's root that matches, absent branches
145
- * included. A name is looked up this way before any scope is known, so
146
- * nothing is filtered out by scope.
149
+ * Every element in root that matches, absent branches included. A query is
150
+ * looked up this way before any row is known, so nothing is filtered out.
147
151
  */
148
152
  function everywhere(root, selector) {
149
153
  const found = [...root.querySelectorAll(selector)];
@@ -158,9 +162,9 @@ function everywhere(root, selector) {
158
162
  * says.
159
163
  *
160
164
  * Off is inside a template that stands where the element stood, carrying the
161
- * same `lb-show`. The element is moved rather than rebuilt, so a widget keeps
162
- * its instance and a control keeps what was typed into it. On moves it back
163
- * and the template goes.
165
+ * same `lb-show`. The element is moved rather than rebuilt, so a custom
166
+ * element keeps its instance and a control keeps what was typed into it. On
167
+ * moves it back and the template goes.
164
168
  *
165
169
  * Landing reaches into that template, so both it and the element it holds
166
170
  * arrive here. The template decides; the element it holds, whose parent is
@@ -181,23 +185,27 @@ function present(el, on) {
181
185
  parked.content.append(el);
182
186
  }
183
187
  /**
184
- * Fill one scope from one row.
188
+ * Fill from one row: every `lb-column` and `lb-show` in root's query.
185
189
  *
186
- * The root counts as a cell if it carries one. A `<tr>` holds its cells in
187
- * `<td>` children, but `<option>`'s content model is text, so an option row
188
- * has to be the cell it displays. Requiring a wrapper there would require an
189
- * element HTML does not allow.
190
+ * A live row is its own row, so its root counts when it carries
191
+ * `lb-column`: `<option>`'s content model is text, so an option row has to
192
+ * be the column it displays. An element a `row` lands on is not its own
193
+ * ancestor, so its own `lb-column` reads from the row around it and is left
194
+ * alone here.
190
195
  */
191
- export function applyRow(root, row) {
196
+ export function applyRow(root, row, liveRow = false) {
192
197
  for (const [column, value] of Object.entries(row)) {
193
- for (const el of reach(root, `[${ATTR_CELL}="${column}"]`)) {
198
+ const selector = `[${ATTR_COLUMN}="${column}"]`;
199
+ const columns = below(root, selector);
200
+ if (liveRow && root.matches(selector))
201
+ columns.unshift(root);
202
+ for (const el of columns)
194
203
  land(el, value);
195
- }
196
204
  // Only null and false are off. A string is never read, so "false" is on,
197
205
  // and the query spells a condition as a boolean or a null.
198
206
  //
199
- // Never the root's own: a scope's condition is a cell of the row around
200
- // it, the way its lb-cell is, and a row template's root carries none.
207
+ // Never the root's own: a row template's root carries none, and the
208
+ // element a row lands on reads its condition from the row around it.
201
209
  const on = value !== null && value !== false;
202
210
  for (const el of below(root, `[${ATTR_SHOW}="${column}"]`)) {
203
211
  present(el, on);
@@ -205,171 +213,159 @@ export function applyRow(root, row) {
205
213
  }
206
214
  }
207
215
  /**
208
- * The row template: the one the developer wrote inside this list scope, or
209
- * null when this scope shows nothing. A template inside a nested scope is
210
- * that scope's, and a template holding an absent branch is not a row
211
- * template: rows land before the row template, so one inside the first live
212
- * row comes first in document order.
213
- *
214
- * A scope with no template is bound to the list without displaying it.
215
- * That is not a mistake, so it is silent. A template that does not name its
216
- * key column is a mistake, and says so.
216
+ * The row template: the first `<template>` among the element's descendants,
217
+ * outside any nested `lb-query`. A template holding an absent branch is not a
218
+ * row template: rows land before the row template, so one inside the first
219
+ * live row comes first in document order.
217
220
  */
218
- function templateIn(scope) {
219
- const template = within(scope, `template:not([${ATTR_SHOW}])`)[0];
220
- if (!template)
221
- return null;
222
- if (!template.getAttribute(ATTR_KEY)) {
223
- console.error(`lb-hub: the <template> in <${scope.localName}> has no ${ATTR_KEY} ` +
224
- `naming the column that identifies a row`);
225
- return null;
226
- }
227
- return template;
221
+ export function templateIn(el) {
222
+ const template = within(el, `template:not([${ATTR_SHOW}])`).find((t) => t !== el);
223
+ return template ?? null;
228
224
  }
229
225
  /**
230
- * The rows already showing, by the key value each carries. Only a live row
231
- * carries one: the template names the key column under a different name.
232
- *
233
- * A row of a nested scope is that scope's. A row's own `lb-list` or `lb-row`
234
- * names what it displays, not which list it is a row of, which is the same
235
- * test a cell gets.
226
+ * The live rows of the element's query, in document order. A row of a nested
227
+ * query is that query's. An element a `row` query landed on carries a key and
228
+ * is not a live row, so a `row` shown in a table's head or foot is not one of
229
+ * the table's rows.
236
230
  */
237
- function showing(scope) {
231
+ function liveRows(el) {
232
+ return [...el.querySelectorAll(LIVE_ROW)].filter((row) => inScope(el, row));
233
+ }
234
+ /**
235
+ * The live rows already showing, by the key each carries. Two live rows with
236
+ * one key are a mistake nothing downstream can recover from: only one of them
237
+ * would ever be matched, and the other would stay until the page goes.
238
+ */
239
+ function showing(el) {
238
240
  const rows = new Map();
239
- for (const el of scope.querySelectorAll(`[${ATTR_KEY_VALUE}]`)) {
240
- if (inScope(scope, el)) {
241
- rows.set(el.getAttribute(ATTR_KEY_VALUE), el);
241
+ for (const row of liveRows(el)) {
242
+ const key = row.getAttribute(ATTR_KEY_VALUE);
243
+ if (rows.has(key)) {
244
+ console.error(`lb-hub: <${el.localName}> shows two rows with the key '${key}'`);
242
245
  }
246
+ rows.set(key, row);
243
247
  }
244
248
  return rows;
245
249
  }
246
250
  /**
247
- * Land a list result in a list scope.
251
+ * Land rows through a row template.
248
252
  *
249
- * An array is the whole set, so it decides membership and order: every row is
250
- * placed in the order given, and a row whose key did not arrive is gone. A
251
- * patch disturbs only what it names — a row it did not mention keeps its
252
- * contents and its position.
253
+ * All rows decide membership and order: every row is placed in the order
254
+ * given, and a row whose key did not arrive is gone. A patch disturbs only
255
+ * what it names — a row it did not mention keeps its contents and its
256
+ * position.
253
257
  *
254
- * A widget that carries `lbPlaceRow` decides where a row goes, because only
255
- * it knows whether it sorts or groups. Without it a row lands immediately
256
- * before the template, so rows accumulate in the order they arrive and the
257
- * template stays put as the insertion marker.
258
+ * A custom element that carries `lbPlaceRow` decides where a row goes,
259
+ * because only it knows whether it sorts or groups. Without it a row lands
260
+ * immediately before the template, so rows accumulate in the order they
261
+ * arrive and the template stays put as the insertion marker.
258
262
  */
259
- export function applyList(scope, result) {
260
- const template = templateIn(scope);
261
- if (!template)
262
- return;
263
- const keyColumn = template.getAttribute(ATTR_KEY);
264
- const shown = showing(scope);
265
- const host = scope;
263
+ export function applyRows(el, template, keyColumn, result) {
264
+ const shown = showing(el);
265
+ const host = el;
266
266
  const whole = !isPatch(result);
267
- const place = (el, row) => {
267
+ const landed = new Set();
268
+ const place = (row, data) => {
268
269
  if (host.lbPlaceRow)
269
- host.lbPlaceRow(el, row, template);
270
+ host.lbPlaceRow(row, data, template);
270
271
  else
271
- template.parentElement.insertBefore(el, template);
272
+ template.parentElement.insertBefore(row, template);
272
273
  };
273
- const upsert = (row) => {
274
- if (row[keyColumn] === undefined) {
275
- console.error(`lb-hub: a row for <${scope.localName}> has no '${keyColumn}' column`);
276
- return null;
274
+ const upsert = (data) => {
275
+ if (data[keyColumn] === undefined) {
276
+ console.error(`lb-hub: a row for <${el.localName}> has no '${keyColumn}' column`);
277
+ return;
277
278
  }
278
279
  // The key is stored on the row as an attribute and read back from
279
280
  // there, so it is compared as the string the attribute holds.
280
- const key = String(row[keyColumn]);
281
- let el = shown.get(key);
282
- const fresh = el === undefined;
283
- if (!el) {
284
- el = template.content.firstElementChild.cloneNode(true);
285
- el.setAttribute(ATTR_KEY_VALUE, key);
286
- shown.set(key, el);
281
+ const key = String(data[keyColumn]);
282
+ if (landed.has(key)) {
283
+ console.error(`lb-hub: two rows for <${el.localName}> have the key '${key}'; ` +
284
+ `the last one shows`);
285
+ }
286
+ landed.add(key);
287
+ let row = shown.get(key);
288
+ const fresh = row === undefined;
289
+ if (!row) {
290
+ // Imported rather than cloned: template content belongs to a document
291
+ // with no custom element definitions, so a clone would not upgrade
292
+ // until inserted, and a form-associated control would be filled as if
293
+ // it were not one.
294
+ row = el.ownerDocument.importNode(template.content.firstElementChild, true);
295
+ row.setAttribute(ATTR_ROW_LIVE, "");
296
+ row.setAttribute(ATTR_KEY_VALUE, key);
297
+ shown.set(key, row);
287
298
  }
288
299
  // Fill before insertion. The attributes are already there when the row
289
- // upgrades, which is the same thing that makes hydration and refresh one
290
- // operation everywhere else.
291
- applyRow(el, row);
300
+ // upgrades, which is the same thing that makes a first landing and a
301
+ // refresh one operation everywhere else.
302
+ applyRow(row, data, true);
292
303
  if (fresh || whole)
293
- place(el, row);
294
- return key;
304
+ place(row, data);
295
305
  };
296
306
  if (Array.isArray(result)) {
297
- const arrived = new Set();
298
- for (const row of result) {
299
- const key = upsert(row);
300
- if (key !== null)
301
- arrived.add(key);
302
- }
303
- for (const [key, el] of shown)
304
- if (!arrived.has(key))
305
- el.remove();
307
+ for (const data of result)
308
+ upsert(data);
309
+ for (const [key, row] of shown)
310
+ if (!landed.has(key))
311
+ row.remove();
306
312
  }
307
313
  else {
308
- for (const row of result.rows ?? [])
309
- upsert(row);
314
+ for (const data of result.rows ?? [])
315
+ upsert(data);
310
316
  for (const key of result.drop ?? [])
311
317
  shown.get(String(key))?.remove();
312
318
  }
313
- // How many rows are showing, counted from the DOM rather than from either
314
- // branch above, so a whole set and a patch report the same fact the same
315
- // way.
316
- //
317
- // It is stamped here because only this function knows the count: it is the
318
- // one conditional a page cannot be sent, since the server answers with rows
319
- // and says nothing about how many survived reconciliation. A page says what
320
- // an empty list looks like in a stylesheet, and no list widget carries code
321
- // for it. See docs/reference/data-binding.md.
322
- scope.setAttribute(ATTR_ROW_COUNT, String(showing(scope).size));
319
+ // Counted from the DOM rather than from either branch above, so all rows
320
+ // and a patch report the same fact the same way. Only here is the count
321
+ // known: the server answers with rows and says nothing about how many
322
+ // survived reconciliation.
323
+ el.setAttribute(ATTR_QUERY_ROW_COUNT, String(liveRows(el).length));
323
324
  // Derived scaffolding — a section heading, an <optgroup> — goes when its
324
- // last row does, and only the widget knows it exists.
325
+ // last row does, and only the custom element knows it exists.
325
326
  host.lbRowsLanded?.();
326
327
  }
327
- /** Land a whole response. Every result arrives through here. */
328
- export function applyData(root, data) {
329
- for (const [name, result] of Object.entries(data)) {
330
- const lists = everywhere(root, `[${ATTR_LIST}="${name}"]`);
331
- const rows = everywhere(root, `[${ATTR_ROW}="${name}"]`);
332
- if (lists.length === 0 && rows.length === 0) {
333
- console.warn(`lb-hub: no scope for '${name}', skipping`);
334
- continue;
335
- }
336
- // Cardinality is a property of the name, so one name is a list or a row
337
- // and never both. Two spellings of it on one page is a mistake in the
338
- // markup rather than a case to reconcile.
339
- if (lists.length > 0 && rows.length > 0) {
340
- console.error(`lb-hub: '${name}' is bound as a list in one place and a row in ` +
341
- `another; a name answers with one shape`);
342
- continue;
328
+ /** Land one response item on one element that names its query. */
329
+ function landItem(el, item) {
330
+ const template = templateIn(el);
331
+ if (item.kind === KIND_ROW) {
332
+ const row = item.row ?? {};
333
+ if (template) {
334
+ applyRows(el, template, item.key, [row]);
335
+ return;
343
336
  }
344
- if (lists.length > 0) {
345
- for (const scope of lists)
346
- applyList(scope, result);
347
- continue;
348
- }
349
- // The only disagreement visible from here: a patch and a row are both
350
- // objects, so an array arriving at a row scope is the one case the
351
- // browser can name. createHub holds the stronger check, because it knows
352
- // what the query declared.
353
- if (Array.isArray(result)) {
354
- console.error(`lb-hub: '${name}' is bound with ${ATTR_ROW} but answered with rows`);
355
- continue;
337
+ applyRow(el, row);
338
+ const key = row[item.key];
339
+ if (key !== undefined && key !== null) {
340
+ el.setAttribute(ATTR_KEY_VALUE, String(key));
356
341
  }
357
- for (const scope of rows)
358
- applyRow(scope, result);
342
+ return;
359
343
  }
344
+ // A `rows` query with no row template lands nothing: an insert form names
345
+ // the query it adds to, and shows none of its rows.
346
+ if (!template)
347
+ return;
348
+ const result = item.patch ?? item.rows ?? [];
349
+ applyRows(el, template, item.key, result);
360
350
  }
361
351
  /**
362
- * Land the query string on every control that writes one of its parms, the
363
- * way a cell lands a column. A parm the URL does not carry lands empty, so a
364
- * control shows what the address bar says after Back as well as after a
365
- * reload: the URL is what is on screen, and nothing else is.
352
+ * Land response items. Every answer arrives through here.
366
353
  *
367
- * Landed after the page's data, so a picker already has the options its
368
- * value selects.
354
+ * `quiet` names queries that may have nowhere to land without a warning: the
355
+ * hub's own `lb-url` lands wherever a chrome or page names it, and nowhere
356
+ * otherwise.
369
357
  */
370
- export function applyQueryParms(root, parms) {
371
- for (const el of everywhere(root, `[${ATTR_QUERY_PARM}]`)) {
372
- land(el, parms.get(el.getAttribute(ATTR_QUERY_PARM)) ?? "");
358
+ export function applyResponse(root, items, quiet = []) {
359
+ for (const item of items) {
360
+ const targets = everywhere(root, `[${ATTR_QUERY}="${item.query}"]`);
361
+ if (targets.length === 0) {
362
+ if (!quiet.includes(item.query)) {
363
+ console.warn(`lb-hub: nothing names '${item.query}', skipping`);
364
+ }
365
+ continue;
366
+ }
367
+ for (const el of targets)
368
+ landItem(el, item);
373
369
  }
374
370
  }
375
371
  //# sourceMappingURL=lb-apply.js.map