@loadbare/app 0.8.2 → 0.10.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 (80) hide show
  1. package/README.md +3 -3
  2. package/dist/build/assemble.d.ts.map +1 -1
  3. package/dist/build/assemble.js +74 -66
  4. package/dist/build/assemble.js.map +1 -1
  5. package/dist/build/expand.d.ts.map +1 -1
  6. package/dist/build/expand.js +20 -19
  7. package/dist/build/expand.js.map +1 -1
  8. package/dist/build/locations.d.ts +2 -3
  9. package/dist/build/locations.d.ts.map +1 -1
  10. package/dist/build/locations.js +2 -3
  11. package/dist/build/locations.js.map +1 -1
  12. package/dist/build/pages.d.ts +3 -4
  13. package/dist/build/pages.d.ts.map +1 -1
  14. package/dist/build/pages.js +3 -4
  15. package/dist/build/pages.js.map +1 -1
  16. package/dist/core/lb-constants.d.ts +25 -23
  17. package/dist/core/lb-constants.d.ts.map +1 -1
  18. package/dist/core/lb-constants.js +95 -158
  19. package/dist/core/lb-constants.js.map +1 -1
  20. package/dist/core/lb-types.d.ts +73 -75
  21. package/dist/core/lb-types.d.ts.map +1 -1
  22. package/dist/core/lb-types.js +58 -5
  23. package/dist/core/lb-types.js.map +1 -1
  24. package/dist/hub/lb-apply.d.ts +47 -37
  25. package/dist/hub/lb-apply.d.ts.map +1 -1
  26. package/dist/hub/lb-apply.js +174 -193
  27. package/dist/hub/lb-apply.js.map +1 -1
  28. package/dist/hub/lb-hub.browser.d.ts +1 -1
  29. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  30. package/dist/hub/lb-hub.browser.js +419 -415
  31. package/dist/hub/lb-hub.browser.js.map +1 -1
  32. package/dist/server/lb-express.d.ts +20 -13
  33. package/dist/server/lb-express.d.ts.map +1 -1
  34. package/dist/server/lb-express.js +50 -52
  35. package/dist/server/lb-express.js.map +1 -1
  36. package/dist/server/lb-server.d.ts +81 -116
  37. package/dist/server/lb-server.d.ts.map +1 -1
  38. package/dist/server/lb-server.js +151 -48
  39. package/dist/server/lb-server.js.map +1 -1
  40. package/docs/TECHREF-1.0.md +893 -558
  41. package/docs/comparison.md +222 -185
  42. package/docs/prior-art.md +15 -14
  43. package/docs/reference/builder.md +9 -3
  44. package/docs/reference/chrome.md +107 -56
  45. package/docs/reference/custom-elements.md +199 -173
  46. package/docs/reference/data-binding.md +375 -370
  47. package/docs/reference/overview.md +12 -10
  48. package/docs/reference/page-files.md +161 -86
  49. package/docs/reference/server.md +13 -7
  50. package/docs/reference/widgets.md +104 -110
  51. package/docs/roadmap.md +36 -31
  52. package/docs/terms-of-art.md +57 -0
  53. package/docs/testing.md +97 -68
  54. package/docs/theory.md +92 -58
  55. package/docs/tutorials/010-pages-and-navigation.md +20 -12
  56. package/docs/tutorials/020-css.md +6 -3
  57. package/docs/tutorials/030-html-decomposition.md +9 -7
  58. package/docs/tutorials/040-displaying-data.md +30 -13
  59. package/docs/tutorials/{050-actions.md → 050-requests.md} +25 -15
  60. package/docs/tutorials/060-custom-element-code.md +17 -16
  61. package/docs/tutorials/065-conditional-rendering.md +34 -23
  62. package/docs/tutorials/070-displaying-a-list.md +29 -21
  63. package/docs/tutorials/072-inserting-into-a-list.md +24 -16
  64. package/docs/tutorials/074-deleting-from-a-list.md +9 -7
  65. package/docs/tutorials/076-updating-a-list-item.md +11 -10
  66. package/docs/tutorials/080-widget-requests.md +71 -43
  67. package/docs/tutorials/090-using-widget-libraries.md +22 -22
  68. package/package.json +1 -1
  69. package/skills/loadbare-app/SKILL.md +178 -111
  70. package/skills/loadbare-app/references/TECHREF-1.0.md +893 -558
  71. package/skills/loadbare-app/references/builder.md +9 -3
  72. package/skills/loadbare-app/references/chrome.md +107 -56
  73. package/skills/loadbare-app/references/custom-elements.md +199 -173
  74. package/skills/loadbare-app/references/data-binding.md +375 -370
  75. package/skills/loadbare-app/references/overview.md +12 -10
  76. package/skills/loadbare-app/references/page-files.md +161 -86
  77. package/skills/loadbare-app/references/server.md +13 -7
  78. package/skills/loadbare-app/references/widgets.md +104 -110
  79. package/docs/analysis-accidental-complexity.md +0 -149
  80. 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_SHOW, KIND_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,144 @@ 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 already showing, by the key each carries. A row of a nested
227
+ * query is that query's.
236
228
  */
237
- function showing(scope) {
229
+ function showing(el) {
238
230
  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);
242
- }
231
+ for (const row of el.querySelectorAll(`[${ATTR_KEY_VALUE}]`)) {
232
+ if (inScope(el, row))
233
+ rows.set(row.getAttribute(ATTR_KEY_VALUE), row);
243
234
  }
244
235
  return rows;
245
236
  }
246
237
  /**
247
- * Land a list result in a list scope.
238
+ * Land rows through a row template.
248
239
  *
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.
240
+ * All rows decide membership and order: every row is placed in the order
241
+ * given, and a row whose key did not arrive is gone. A patch disturbs only
242
+ * what it names — a row it did not mention keeps its contents and its
243
+ * position.
253
244
  *
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.
245
+ * A custom element that carries `lbPlaceRow` decides where a row goes,
246
+ * because only it knows whether it sorts or groups. Without it a row lands
247
+ * immediately before the template, so rows accumulate in the order they
248
+ * arrive and the template stays put as the insertion marker.
258
249
  */
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;
250
+ export function applyRows(el, template, keyColumn, result) {
251
+ const shown = showing(el);
252
+ const host = el;
266
253
  const whole = !isPatch(result);
267
- const place = (el, row) => {
254
+ const place = (row, data) => {
268
255
  if (host.lbPlaceRow)
269
- host.lbPlaceRow(el, row, template);
256
+ host.lbPlaceRow(row, data, template);
270
257
  else
271
- template.parentElement.insertBefore(el, template);
258
+ template.parentElement.insertBefore(row, template);
272
259
  };
273
- const upsert = (row) => {
274
- if (row[keyColumn] === undefined) {
275
- console.error(`lb-hub: a row for <${scope.localName}> has no '${keyColumn}' column`);
260
+ const upsert = (data) => {
261
+ if (data[keyColumn] === undefined) {
262
+ console.error(`lb-hub: a row for <${el.localName}> has no '${keyColumn}' column`);
276
263
  return null;
277
264
  }
278
265
  // The key is stored on the row as an attribute and read back from
279
266
  // 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);
267
+ const key = String(data[keyColumn]);
268
+ let row = shown.get(key);
269
+ const fresh = row === undefined;
270
+ if (!row) {
271
+ // Imported rather than cloned: template content belongs to a document
272
+ // with no custom element definitions, so a clone would not upgrade
273
+ // until inserted, and a form-associated control would be filled as if
274
+ // it were not one.
275
+ row = el.ownerDocument.importNode(template.content.firstElementChild, true);
276
+ row.setAttribute(ATTR_KEY_VALUE, key);
277
+ shown.set(key, row);
287
278
  }
288
279
  // 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);
280
+ // upgrades, which is the same thing that makes a first landing and a
281
+ // refresh one operation everywhere else.
282
+ applyRow(row, data, true);
292
283
  if (fresh || whole)
293
- place(el, row);
284
+ place(row, data);
294
285
  return key;
295
286
  };
296
287
  if (Array.isArray(result)) {
297
288
  const arrived = new Set();
298
- for (const row of result) {
299
- const key = upsert(row);
289
+ for (const data of result) {
290
+ const key = upsert(data);
300
291
  if (key !== null)
301
292
  arrived.add(key);
302
293
  }
303
- for (const [key, el] of shown)
294
+ for (const [key, row] of shown)
304
295
  if (!arrived.has(key))
305
- el.remove();
296
+ row.remove();
306
297
  }
307
298
  else {
308
- for (const row of result.rows ?? [])
309
- upsert(row);
299
+ for (const data of result.rows ?? [])
300
+ upsert(data);
310
301
  for (const key of result.drop ?? [])
311
302
  shown.get(String(key))?.remove();
312
303
  }
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));
304
+ // Counted from the DOM rather than from either branch above, so all rows
305
+ // and a patch report the same fact the same way. Only here is the count
306
+ // known: the server answers with rows and says nothing about how many
307
+ // survived reconciliation.
308
+ el.setAttribute(ATTR_QUERY_ROW_COUNT, String(showing(el).size));
323
309
  // Derived scaffolding — a section heading, an <optgroup> — goes when its
324
- // last row does, and only the widget knows it exists.
310
+ // last row does, and only the custom element knows it exists.
325
311
  host.lbRowsLanded?.();
326
312
  }
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;
313
+ /** Land one response item on one element that names its query. */
314
+ function landItem(el, item) {
315
+ const template = templateIn(el);
316
+ if (item.kind === KIND_ROW) {
317
+ const row = item.row ?? {};
318
+ if (template) {
319
+ applyRows(el, template, item.key, [row]);
320
+ return;
343
321
  }
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;
322
+ applyRow(el, row);
323
+ const key = row[item.key];
324
+ if (key !== undefined && key !== null) {
325
+ el.setAttribute(ATTR_KEY_VALUE, String(key));
356
326
  }
357
- for (const scope of rows)
358
- applyRow(scope, result);
327
+ return;
359
328
  }
329
+ // A `rows` query with no row template lands nothing: an insert form names
330
+ // the query it adds to, and shows none of its rows.
331
+ if (!template)
332
+ return;
333
+ const result = item.patch ?? item.rows ?? [];
334
+ applyRows(el, template, item.key, result);
360
335
  }
361
336
  /**
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.
337
+ * Land response items. Every answer arrives through here.
366
338
  *
367
- * Landed after the page's data, so a picker already has the options its
368
- * value selects.
339
+ * `quiet` names queries that may have nowhere to land without a warning: the
340
+ * hub's own `lb-url` lands wherever a chrome or page names it, and nowhere
341
+ * otherwise.
369
342
  */
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)) ?? "");
343
+ export function applyResponse(root, items, quiet = []) {
344
+ for (const item of items) {
345
+ const targets = everywhere(root, `[${ATTR_QUERY}="${item.query}"]`);
346
+ if (targets.length === 0) {
347
+ if (!quiet.includes(item.query)) {
348
+ console.warn(`lb-hub: nothing names '${item.query}', skipping`);
349
+ }
350
+ continue;
351
+ }
352
+ for (const el of targets)
353
+ landItem(el, item);
373
354
  }
374
355
  }
375
356
  //# sourceMappingURL=lb-apply.js.map