@loadbare/app 0.25.0 → 0.27.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 (56) hide show
  1. package/dist/build/assemble.d.ts +1 -2
  2. package/dist/build/assemble.d.ts.map +1 -1
  3. package/dist/build/assemble.js +31 -37
  4. package/dist/build/assemble.js.map +1 -1
  5. package/dist/build/cli.d.ts +4 -10
  6. package/dist/build/cli.d.ts.map +1 -1
  7. package/dist/build/cli.js +11 -33
  8. package/dist/build/cli.js.map +1 -1
  9. package/dist/build/declared.js +1 -1
  10. package/dist/build/declared.js.map +1 -1
  11. package/dist/core/lb-constants.d.ts +15 -13
  12. package/dist/core/lb-constants.d.ts.map +1 -1
  13. package/dist/core/lb-constants.js +47 -51
  14. package/dist/core/lb-constants.js.map +1 -1
  15. package/dist/core/lb-types.d.ts +12 -11
  16. package/dist/core/lb-types.d.ts.map +1 -1
  17. package/dist/core/lb-types.js +7 -0
  18. package/dist/core/lb-types.js.map +1 -1
  19. package/dist/hub/lb-apply.d.ts +10 -34
  20. package/dist/hub/lb-apply.d.ts.map +1 -1
  21. package/dist/hub/lb-apply.js +92 -151
  22. package/dist/hub/lb-apply.js.map +1 -1
  23. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  24. package/dist/hub/lb-hub.browser.js +115 -127
  25. package/dist/hub/lb-hub.browser.js.map +1 -1
  26. package/dist/hub/lb-lifecycle.d.ts.map +1 -1
  27. package/dist/hub/lb-lifecycle.js +4 -11
  28. package/dist/hub/lb-lifecycle.js.map +1 -1
  29. package/dist/server/lb-express.d.ts.map +1 -1
  30. package/dist/server/lb-express.js +3 -3
  31. package/dist/server/lb-express.js.map +1 -1
  32. package/dist/server/lb-server.d.ts +35 -19
  33. package/dist/server/lb-server.d.ts.map +1 -1
  34. package/dist/server/lb-server.js +141 -88
  35. package/dist/server/lb-server.js.map +1 -1
  36. package/docs/binding-cases.md +150 -0
  37. package/docs/comparison.md +34 -38
  38. package/docs/opinions.md +2 -2
  39. package/docs/reference/chrome.md +8 -10
  40. package/docs/reference/css.md +9 -10
  41. package/docs/reference/custom-elements.md +11 -8
  42. package/docs/reference/data-binding.md +93 -134
  43. package/docs/reference/names.md +21 -22
  44. package/docs/reference/page-files.md +24 -26
  45. package/docs/reference/server.md +3 -4
  46. package/docs/roadmap.md +2 -2
  47. package/docs/testing.md +5 -6
  48. package/package.json +1 -1
  49. package/skills/loadbare-app/SKILL.md +55 -48
  50. package/skills/loadbare-app/references/chrome.md +8 -10
  51. package/skills/loadbare-app/references/css.md +9 -10
  52. package/skills/loadbare-app/references/custom-elements.md +11 -8
  53. package/skills/loadbare-app/references/data-binding.md +93 -134
  54. package/skills/loadbare-app/references/names.md +21 -22
  55. package/skills/loadbare-app/references/page-files.md +24 -26
  56. package/skills/loadbare-app/references/server.md +3 -4
@@ -1 +1 @@
1
- {"version":3,"file":"lb-hub.browser.d.ts","sourceRoot":"","sources":["../../hub/lb-hub.browser.ts"],"names":[],"mappings":"AA8FA,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"lb-hub.browser.d.ts","sourceRoot":"","sources":["../../hub/lb-hub.browser.ts"],"names":[],"mappings":"AAmGA,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
@@ -1,7 +1,7 @@
1
1
  /// <reference lib="dom" />
2
- import { ATTR_COLUMN, ATTR_COLUMN_VALUE, ATTR_KEY_VALUE, ATTR_PAGE, ATTR_PAGE_TITLE, ATTR_QUERY, ATTR_REQUEST, ATTR_REQUEST_ERROR, ATTR_REQUEST_PENDING, ATTR_REQUEST_REFUSED, ATTR_ROW_EDITING, ATTR_SELECT, ATTR_URL_LINK, ATTR_URL_PUSH, ATTR_URL_UNKNOWN, CHOICES_QUERY, COMMAND_EDIT, COMMAND_NEW, COMMAND_RELEASE, COMMAND_PLACE, HUB_COLUMN_AS_OF, HUB_COLUMN_ERROR, HUB_COLUMN_LOAD_STATE, HUB_COLUMN_PENDING, HUB_COLUMN_SERVER_STATE, HUB_ERROR_NETWORK, HUB_ERROR_TIMEOUT, HUB_LOAD_FAILED, HUB_LOAD_LOADED, HUB_LOAD_LOADING, HUB_QUERY, HUB_SERVER_FAILING, HUB_SERVER_REACHABLE, HUB_SERVER_UNREACHABLE, HUB_TAG_NAME, KIND_ROW, LB_DONE_EVENT_NAME, LB_ENDPOINT, LB_EVENT_NAME, LB_REFUSAL_STATUS, LB_REQUEST_TIMEOUT_MS, LB_COMMAND_PREFIX, LB_RESERVED_PREFIX, LIVE_ROW, REQUEST_ROW_DELETE, REQUEST_ROW_INSERT, REQUEST_ROW_UPDATE, URL_COLUMN_PAGE_LABEL, URL_COLUMN_PAGE_UNKNOWN, URL_COLUMN_PATH, URL_QUERY, } from "../core/lb-constants.js";
3
- import { isRowRequest, nextLocation, pageNameFor, parmsRow, } from "../core/lb-types.js";
4
- import { addUnentered, applyResponse, discardUnentered, applyRow, blankRow, focusIn, gatherInto, select, forget, isControl, isFormAssociated, keptAnswer, placeAgain, within, } from "./lb-apply.js";
2
+ import { ATTR_COLUMN, ATTR_COLUMN_VALUE, ATTR_KEY_VALUE, ATTR_PAGE, ATTR_PAGE_TITLE, ATTR_QUERY, ATTR_REQUEST, ATTR_REQUEST_ERROR, ATTR_REQUEST_PENDING, ATTR_REQUEST_REFUSED, ATTR_ROW_EDITING, ATTR_URL_LINK, ATTR_URL_PUSH, ATTR_URL_UNKNOWN, COMMAND_EDIT, COMMAND_NEW, COMMAND_RELEASE, COMMAND_PLACE, FILTERS_COLUMN_QUERY, FILTERS_QUERY, HUB_COLUMN_AS_OF, HUB_COLUMN_ERROR, HUB_COLUMN_LOAD_STATE, HUB_COLUMN_PENDING, HUB_COLUMN_SERVER_STATE, HUB_ERROR_NETWORK, HUB_ERROR_TIMEOUT, HUB_LOAD_FAILED, HUB_LOAD_LOADED, HUB_LOAD_LOADING, HUB_QUERY, HUB_SERVER_FAILING, HUB_SERVER_REACHABLE, HUB_SERVER_UNREACHABLE, HUB_TAG_NAME, KIND_ROW, KIND_ROWS, LB_DONE_EVENT_NAME, LB_ENDPOINT, LB_EVENT_NAME, LB_REFUSAL_STATUS, LB_REQUEST_TIMEOUT_MS, LB_COMMAND_PREFIX, LB_RESERVED_PREFIX, LIVE_ROW, REQUEST_READ, REQUEST_ROW_DELETE, REQUEST_ROW_INSERT, REQUEST_ROW_UPDATE, SORT_COLUMN_ORDER, SORT_COLUMN_QUERY, SORT_QUERY, URL_COLUMN_PAGE_LABEL, URL_COLUMN_PATH, URL_QUERY, } from "../core/lb-constants.js";
3
+ import { isRowRequest, nextLocation, pageNameFor, parmsRow, oneRow, } from "../core/lb-types.js";
4
+ import { addUnentered, applyResponse, discardUnentered, applyRow, blankRow, focusIn, gatherInto, forget, isControl, isFormAssociated, keptAnswer, placeAgain, templateIn, within, } from "./lb-apply.js";
5
5
  import { attachPlaced, detachRemoved, placed, requestDone, upgradeAbsent, } from "./lb-lifecycle.js";
6
6
  export { applyResponse, applyRow } from "./lb-apply.js";
7
7
  /** What each request Loadbare provides cannot be issued without. */
@@ -55,8 +55,7 @@ function formOwnerOf(el) {
55
55
  : null;
56
56
  }
57
57
  /**
58
- * The controls a request gathers — see docs/reference/data-binding.md,
59
- * "Gathering":
58
+ * The controls a request gathers:
60
59
  *
61
60
  * 1. the element carrying `lb-request` has `lb-column`: it alone;
62
61
  * 2. the element is a `<form>`: the controls whose form owner it is;
@@ -194,24 +193,6 @@ const insertedFrom = new WeakMap();
194
193
  const pointed = new WeakMap();
195
194
  /** The pointed form each request gathered from, until it succeeds. */
196
195
  const savedFrom = new WeakMap();
197
- /**
198
- * The `lb-select` checkboxes showing for a query, under the hub an element
199
- * is in. One parked by `lb-show` is in template content, out of reach, so
200
- * only a selection the user can see counts.
201
- */
202
- function selectorsFor(el, query) {
203
- const root = el.closest(HUB_TAG_NAME) ?? el.ownerDocument;
204
- return [...root.querySelectorAll(`[${ATTR_SELECT}]`)].filter((box) => {
205
- const live = liveRowOf(box);
206
- return live !== null && queryOf(live)?.getAttribute(ATTR_QUERY) === query;
207
- });
208
- }
209
- /** The keys of the rows a query's showing checkboxes select. */
210
- function selectedKeys(boxes) {
211
- return boxes
212
- .filter((box) => box.checked)
213
- .map((box) => liveRowOf(box).getAttribute(ATTR_KEY_VALUE));
214
- }
215
196
  /**
216
197
  * Complete a request from where its element sits. Every request passes
217
198
  * through here, whoever issued it; a field it already carries is its
@@ -281,7 +262,7 @@ function complete(request, el) {
281
262
  }
282
263
  // A query the hub serves has one row, keyed by the path, so a request for
283
264
  // one from anywhere on the page is for that row.
284
- if (request.query === URL_QUERY || request.query === CHOICES_QUERY) {
265
+ if (request.query === URL_QUERY) {
285
266
  request.key ??= location.pathname;
286
267
  }
287
268
  if (name === REQUEST_ROW_INSERT && group?.form) {
@@ -307,28 +288,9 @@ function complete(request, el) {
307
288
  return false;
308
289
  }
309
290
  }
310
- // A request that names no one row acts on the rows the user selected,
311
- // when its query shows checkboxes to select them by. An insert adds a row
312
- // and selects nothing. With none ticked, an update or a delete has
313
- // nothing to act on, and is not sent, as for a row not yet entered.
314
- if (request.key === undefined &&
315
- request.keys === undefined &&
316
- request.query !== undefined &&
317
- !unentered &&
318
- name !== REQUEST_ROW_INSERT) {
319
- const boxes = selectorsFor(el, request.query);
320
- if (boxes.length > 0) {
321
- request.keys = selectedKeys(boxes);
322
- if (request.keys.length === 0 &&
323
- (name === REQUEST_ROW_UPDATE || name === REQUEST_ROW_DELETE)) {
324
- return false;
325
- }
326
- }
327
- }
328
291
  const needs = NEEDS[name];
329
292
  if (needs) {
330
- const missing = needs.filter((field) => (request[field] === undefined &&
331
- !(field === "key" && request.keys !== undefined)) ||
293
+ const missing = needs.filter((field) => request[field] === undefined ||
332
294
  (field === "values" &&
333
295
  Object.keys(request.values).length === 0 &&
334
296
  request.rows === undefined));
@@ -420,9 +382,18 @@ function pageHostFor(page) {
420
382
  * browser is showing rides after it verbatim, so every round trip reaches
421
383
  * the server with the query parms in hand.
422
384
  */
423
- async function fetchItems(page, request) {
385
+ async function fetchItems(page, request, parms) {
424
386
  const path = page.split("/").map(encodeURIComponent).join("/");
425
- const url = `${LB_ENDPOINT}/${path}${location.search}`;
387
+ // Parms set for this request alone, as a bound row's key is for the lists
388
+ // inside it. An empty one is still sent: it names no row.
389
+ let query = location.search;
390
+ if (parms) {
391
+ const search = new URLSearchParams(location.search);
392
+ for (const [name, value] of Object.entries(parms))
393
+ search.set(name, value);
394
+ query = `?${search.toString()}`;
395
+ }
396
+ const url = `${LB_ENDPOINT}/${path}${query}`;
426
397
  const controller = new AbortController();
427
398
  const timeout = setTimeout(() => controller.abort(), LB_REQUEST_TIMEOUT_MS);
428
399
  try {
@@ -516,7 +487,7 @@ function clear(form) {
516
487
  */
517
488
  function point(form, row, key, live) {
518
489
  clear(form);
519
- if (row)
490
+ if (row !== null)
520
491
  applyRow(form, row);
521
492
  form.setAttribute(ATTR_KEY_VALUE, key);
522
493
  live?.setAttribute(ATTR_ROW_EDITING, "");
@@ -599,7 +570,7 @@ class LbHub extends HTMLElement {
599
570
  * lands, so each custom element in it is filled before it is attached.
600
571
  */
601
572
  unplaced = [];
602
- /** The hub's own state, as the query `lb-hub` serves it. */
573
+ /** The hub's own state, as the query `lb-hub-status` serves it. */
603
574
  trips = 0;
604
575
  loadState = HUB_LOAD_LOADING;
605
576
  serverState = "";
@@ -621,10 +592,18 @@ class LbHub extends HTMLElement {
621
592
  // The element that issued the request, not e.currentTarget (the hub
622
593
  // itself, since the event bubbles).
623
594
  const origin = e.target ?? undefined;
624
- if (request.query?.startsWith(LB_RESERVED_PREFIX)) {
595
+ if (request.query === URL_QUERY) {
625
596
  void this.answer(request, origin);
626
597
  return;
627
598
  }
599
+ if (request.query === SORT_QUERY) {
600
+ void this.chooseSort(request, origin);
601
+ return;
602
+ }
603
+ if (request.query === FILTERS_QUERY) {
604
+ void this.chooseFilters(request, origin);
605
+ return;
606
+ }
628
607
  void this.send(request, origin);
629
608
  });
630
609
  // A form commits on submit. The submitter's own lb-request replaces the
@@ -665,14 +644,6 @@ class LbHub extends HTMLElement {
665
644
  edited(e.target);
666
645
  }, { capture: true });
667
646
  }
668
- // A selector selects its row, and is no column.
669
- this.addEventListener("change", (e) => {
670
- const box = e.target;
671
- if (box?.hasAttribute(ATTR_SELECT)) {
672
- select(box, box.checked);
673
- }
674
- });
675
- // A control commits on change.
676
647
  this.addEventListener("change", (e) => {
677
648
  const el = e.target;
678
649
  if (!el || !el.hasAttribute(ATTR_REQUEST) || !isControl(el))
@@ -781,10 +752,12 @@ class LbHub extends HTMLElement {
781
752
  return;
782
753
  }
783
754
  point(form, row, key, live);
755
+ void this.readNested(form, key);
784
756
  return;
785
757
  }
786
758
  case COMMAND_NEW:
787
759
  point(form, null, "", null);
760
+ void this.readNested(form, "");
788
761
  return;
789
762
  case COMMAND_RELEASE:
790
763
  release(form);
@@ -794,6 +767,36 @@ class LbHub extends HTMLElement {
794
767
  `names no command Loadbare provides, ignoring`);
795
768
  }
796
769
  }
770
+ /**
771
+ * Make the lists nested in a form follow the row it is now bound to: drop
772
+ * the rows not yet entered there, and ask the server for each list's
773
+ * query again with the row's key as the parm its key column names. A new
774
+ * row's key is empty, so its lists answer with none. A form bound to
775
+ * another row, or a page moved on, before the answer returns lands
776
+ * nothing.
777
+ */
778
+ async readNested(form, key) {
779
+ const lists = [...form.querySelectorAll(`[${ATTR_QUERY}]`)].filter((el) => queryOf(el) === form && templateIn(el) !== null);
780
+ if (lists.length === 0)
781
+ return;
782
+ const column = keptAnswer(this, form.getAttribute(ATTR_QUERY))?.key;
783
+ if (column === undefined)
784
+ return;
785
+ for (const list of lists) {
786
+ for (const row of list.querySelectorAll(`[${ATTR_KEY_VALUE}=""]`)) {
787
+ if (queryOf(row) === list)
788
+ discardUnentered(row);
789
+ }
790
+ }
791
+ const queries = [...new Set(lists.map((el) => el.getAttribute(ATTR_QUERY)))];
792
+ const at = here();
793
+ const items = await this.roundTrip(this.page, { name: REQUEST_READ, queries }, undefined, { [column]: key });
794
+ if (items instanceof Error || here() !== at)
795
+ return;
796
+ if (form.getAttribute(ATTR_KEY_VALUE) !== key)
797
+ return;
798
+ applyResponse(this, items);
799
+ }
797
800
  /**
798
801
  * Place a list's rows by the order and the filter again, from the rows
799
802
  * the hub holds: no round trip, since the user asks only to see what they
@@ -845,11 +848,6 @@ class LbHub extends HTMLElement {
845
848
  else
846
849
  landed = await this.follow(url, load, origin);
847
850
  }
848
- // The selected rows have been acted on, and the selection is done.
849
- if (request.keys !== undefined && request.query !== undefined) {
850
- for (const box of selectorsFor(this, request.query))
851
- select(box, false);
852
- }
853
851
  // A pointed form has done what it was pointed for, and lets go. A form
854
852
  // an insert was gathered from starts over, unless the answer landed the
855
853
  // new row on it.
@@ -889,7 +887,7 @@ class LbHub extends HTMLElement {
889
887
  * element that issued it carries `lb-request-pending` while it is in
890
888
  * flight, and `lb-request-error` when it fails. The error on failure.
891
889
  */
892
- async roundTrip(page, request, origin) {
890
+ async roundTrip(page, request, origin, parms) {
893
891
  // A page load says how the page showing got its data, unless the URL
894
892
  // has moved on by the time it returns, and a later load says instead.
895
893
  const load = request === undefined;
@@ -900,7 +898,7 @@ class LbHub extends HTMLElement {
900
898
  this.loadState = HUB_LOAD_LOADING;
901
899
  this.landHub();
902
900
  try {
903
- const items = await fetchItems(page, request);
901
+ const items = await fetchItems(page, request, parms);
904
902
  this.serverState = HUB_SERVER_REACHABLE;
905
903
  this.error = "";
906
904
  this.asOf = new Date().toISOString();
@@ -961,19 +959,15 @@ class LbHub extends HTMLElement {
961
959
  if (!this.querySelector(`[${ATTR_QUERY}="${HUB_QUERY}"]`))
962
960
  return;
963
961
  applyResponse(this, [
964
- { query: HUB_QUERY, kind: KIND_ROW, key: URL_COLUMN_PATH, row },
962
+ { query: HUB_QUERY, kind: KIND_ROW, key: URL_COLUMN_PATH, rows: [row] },
965
963
  ]);
966
964
  }
967
965
  /**
968
- * Answer a request for a query the hub serves, with no round trip. The
969
- * hub serves `lb-url` and `lb-choices`, and updates them. It serves
970
- * `lb-hub` too, which nothing but the hub writes.
966
+ * Answer a request for `lb-url`, the one query the hub serves, with no
967
+ * round trip: write the URL and load the page it names.
971
968
  */
972
969
  async answer(request, origin) {
973
- if (request.query === CHOICES_QUERY && request.name === REQUEST_ROW_UPDATE) {
974
- return done(origin, request, [this.choose(request.values ?? {})]);
975
- }
976
- if (request.query !== URL_QUERY || request.name !== REQUEST_ROW_UPDATE) {
970
+ if (request.name !== REQUEST_ROW_UPDATE) {
977
971
  const message = `lb-hub: '${request.name}' for '${request.query}' is not a request ` +
978
972
  `the hub answers, ignoring`;
979
973
  console.error(message);
@@ -986,50 +980,64 @@ class LbHub extends HTMLElement {
986
980
  done(origin, request, await this.navigate(origin));
987
981
  }
988
982
  /**
989
- * The user made choices: they join the ones already made, and the rows
990
- * that read them are placed again from what the hub holds. The server is
991
- * told, for a page that keeps them, and its answer lands like any other.
992
- * What landed: the row of `lb-choices`.
983
+ * Answer an order the user chose: land its row of `lb-sort`, which places
984
+ * the rows it holds again by the new order, and send the order to the
985
+ * server to keep for the next page. Re-sorting is the browser's, from the
986
+ * rows it holds, so the round trip only persists the change.
993
987
  */
994
- choose(values) {
995
- const row = {
996
- ...keptAnswer(this, CHOICES_QUERY)?.row,
997
- ...values,
998
- [URL_COLUMN_PATH]: location.pathname,
999
- };
988
+ async chooseSort(request, origin) {
989
+ const query = request.key ?? "";
990
+ const order = request.values?.[SORT_COLUMN_ORDER] ?? "";
1000
991
  const item = {
1001
- query: CHOICES_QUERY,
1002
- kind: KIND_ROW,
1003
- key: URL_COLUMN_PATH,
1004
- row,
992
+ query: SORT_QUERY,
993
+ kind: KIND_ROWS,
994
+ key: SORT_COLUMN_QUERY,
995
+ rows: [{ [SORT_COLUMN_QUERY]: query, [SORT_COLUMN_ORDER]: order }],
1005
996
  };
1006
997
  applyResponse(this, [item]);
1007
- void this.tell(row);
1008
- return item;
998
+ const items = await this.roundTrip(this.page, {
999
+ name: REQUEST_ROW_UPDATE,
1000
+ query: SORT_QUERY,
1001
+ key: query,
1002
+ values: { [SORT_COLUMN_ORDER]: order },
1003
+ }, undefined);
1004
+ if (!(items instanceof Error))
1005
+ applyResponse(this, items, { requested: true });
1006
+ done(origin, request, [item]);
1009
1007
  }
1010
1008
  /**
1011
- * Send the server the user's choices, by `lb-row-update` for
1012
- * `lb-choices`, which runs the page's `onChoices`. No element waits on
1013
- * it, so none is pending, and a choice made while it is in flight is sent
1014
- * in turn.
1009
+ * Answer a filter the user chose: merge it into the query's row of
1010
+ * `lb-filters`, land it, and send the whole row to the server to keep for
1011
+ * the next page. Filtering is the browser's, so no round trip answers the
1012
+ * change; the round trip only persists it.
1015
1013
  */
1016
- async tell(row) {
1017
- const { [URL_COLUMN_PATH]: _, ...choices } = row;
1018
- const values = {};
1019
- for (const [column, value] of Object.entries(choices)) {
1020
- values[column] = value === null || value === undefined ? "" : String(value);
1014
+ async chooseFilters(request, origin) {
1015
+ const query = request.key ?? "";
1016
+ const values = request.values ?? {};
1017
+ const held = keptAnswer(this, FILTERS_QUERY)?.rows?.find((row) => row[FILTERS_COLUMN_QUERY] === query);
1018
+ const row = { ...held, ...values, [FILTERS_COLUMN_QUERY]: query };
1019
+ const item = {
1020
+ query: FILTERS_QUERY,
1021
+ kind: KIND_ROWS,
1022
+ key: FILTERS_COLUMN_QUERY,
1023
+ rows: [row],
1024
+ };
1025
+ applyResponse(this, [item]);
1026
+ const columns = {};
1027
+ for (const [column, value] of Object.entries(row)) {
1028
+ if (column !== FILTERS_COLUMN_QUERY && typeof value === "string") {
1029
+ columns[column] = value;
1030
+ }
1021
1031
  }
1022
- const at = here();
1023
1032
  const items = await this.roundTrip(this.page, {
1024
1033
  name: REQUEST_ROW_UPDATE,
1025
- query: CHOICES_QUERY,
1026
- key: location.pathname,
1027
- values,
1034
+ query: FILTERS_QUERY,
1035
+ key: query,
1036
+ values: columns,
1028
1037
  }, undefined);
1029
- if (items instanceof Error || here() !== at)
1030
- return;
1031
- if (items.length > 0)
1038
+ if (!(items instanceof Error))
1032
1039
  applyResponse(this, items, { requested: true });
1040
+ done(origin, request, [item]);
1033
1041
  }
1034
1042
  /**
1035
1043
  * Write a row for `lb-url` into the URL. A new page pushes a history
@@ -1056,7 +1064,7 @@ class LbHub extends HTMLElement {
1056
1064
  * asked for again that fails is not the request's failure.
1057
1065
  */
1058
1066
  async follow(item, load, origin) {
1059
- this.moveTo(item.row ?? {}, origin?.hasAttribute(ATTR_URL_PUSH) ?? false);
1067
+ this.moveTo(oneRow(item) ?? {}, origin?.hasAttribute(ATTR_URL_PUSH) ?? false);
1060
1068
  if (load.length === 0) {
1061
1069
  const again = await this.navigate();
1062
1070
  return again instanceof Error ? [item] : [item, ...again];
@@ -1159,10 +1167,9 @@ class LbHub extends HTMLElement {
1159
1167
  this.unplaced.push(this.main);
1160
1168
  forget(this);
1161
1169
  this.entering = true;
1162
- this.landChoices();
1163
1170
  }
1164
1171
  this.landUrl(template ? titleOf(template, page) : null);
1165
- // An app that ships `<body hidden>` (docs/reference/chrome.md) stays
1172
+ // An app that ships `<body hidden>` stays
1166
1173
  // invisible until there is something coherent to show.
1167
1174
  document.body.hidden = false;
1168
1175
  if (!template) {
@@ -1174,24 +1181,6 @@ class LbHub extends HTMLElement {
1174
1181
  }
1175
1182
  return template;
1176
1183
  }
1177
- /**
1178
- * The user's choices as a page begins: none made, so every column an
1179
- * element under `lb-query="lb-choices"` shows lands empty. Choices hold
1180
- * until the page changes, unless the server restores them.
1181
- */
1182
- landChoices() {
1183
- const row = {};
1184
- for (const scope of this.querySelectorAll(`[${ATTR_QUERY}="${CHOICES_QUERY}"]`)) {
1185
- for (const el of within(scope, `[${ATTR_COLUMN}]`)) {
1186
- if (el !== scope)
1187
- row[el.getAttribute(ATTR_COLUMN)] = "";
1188
- }
1189
- }
1190
- row[URL_COLUMN_PATH] = location.pathname;
1191
- applyResponse(this, [
1192
- { query: CHOICES_QUERY, kind: KIND_ROW, key: URL_COLUMN_PATH, row },
1193
- ]);
1194
- }
1195
1184
  /**
1196
1185
  * The hub's own query: one row, keyed by the path, whose other columns are
1197
1186
  * the query parms.
@@ -1220,8 +1209,7 @@ class LbHub extends HTMLElement {
1220
1209
  Object.assign(row, parmsRow(new URLSearchParams(location.search)));
1221
1210
  row[URL_COLUMN_PATH] = location.pathname;
1222
1211
  row[URL_COLUMN_PAGE_LABEL] = label;
1223
- row[URL_COLUMN_PAGE_UNKNOWN] = label === null;
1224
- applyResponse(this, [{ query: URL_QUERY, kind: KIND_ROW, key: URL_COLUMN_PATH, row }]);
1212
+ applyResponse(this, [{ query: URL_QUERY, kind: KIND_ROW, key: URL_COLUMN_PATH, rows: [row] }]);
1225
1213
  }
1226
1214
  }
1227
1215
  customElements.define(HUB_TAG_NAME, LbHub);