@loadbare/app 0.8.1 → 0.9.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 (41) hide show
  1. package/dist/core/lb-constants.d.ts +3 -0
  2. package/dist/core/lb-constants.d.ts.map +1 -1
  3. package/dist/core/lb-constants.js +28 -4
  4. package/dist/core/lb-constants.js.map +1 -1
  5. package/dist/core/lb-types.d.ts +11 -0
  6. package/dist/core/lb-types.d.ts.map +1 -1
  7. package/dist/core/lb-types.js +20 -0
  8. package/dist/core/lb-types.js.map +1 -1
  9. package/dist/hub/lb-apply.d.ts +10 -0
  10. package/dist/hub/lb-apply.d.ts.map +1 -1
  11. package/dist/hub/lb-apply.js +15 -1
  12. package/dist/hub/lb-apply.js.map +1 -1
  13. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  14. package/dist/hub/lb-hub.browser.js +161 -38
  15. package/dist/hub/lb-hub.browser.js.map +1 -1
  16. package/dist/server/lb-express.d.ts +22 -7
  17. package/dist/server/lb-express.d.ts.map +1 -1
  18. package/dist/server/lb-express.js +79 -31
  19. package/dist/server/lb-express.js.map +1 -1
  20. package/dist/server/lb-server.d.ts +26 -0
  21. package/dist/server/lb-server.d.ts.map +1 -1
  22. package/dist/server/lb-server.js +53 -3
  23. package/dist/server/lb-server.js.map +1 -1
  24. package/docs/TECHREF-1.0.md +154 -22
  25. package/docs/comparison.md +8 -7
  26. package/docs/reference/chrome.md +5 -3
  27. package/docs/reference/data-binding.md +33 -0
  28. package/docs/reference/page-files.md +39 -0
  29. package/docs/reference/server.md +17 -0
  30. package/docs/reference/widgets.md +6 -2
  31. package/docs/roadmap.md +28 -0
  32. package/docs/theory.md +8 -1
  33. package/docs/tutorials/010-pages-and-navigation.md +2 -1
  34. package/package.json +1 -1
  35. package/skills/loadbare-app/SKILL.md +36 -8
  36. package/skills/loadbare-app/references/TECHREF-1.0.md +154 -22
  37. package/skills/loadbare-app/references/chrome.md +5 -3
  38. package/skills/loadbare-app/references/data-binding.md +33 -0
  39. package/skills/loadbare-app/references/page-files.md +39 -0
  40. package/skills/loadbare-app/references/server.md +17 -0
  41. package/skills/loadbare-app/references/widgets.md +6 -2
@@ -1,6 +1,7 @@
1
1
  /// <reference lib="dom" />
2
- import { ACTION_ROW_DELETE, ACTION_ROW_INSERT, ACTION_ROW_UPDATE, ATTR_ACTION, ATTR_CELL, ATTR_ERROR, ATTR_KEY_VALUE, ATTR_LIST, ATTR_NAV_LINK, ATTR_PAGE, ATTR_PENDING, ATTR_ROW, ATTR_UNKNOWN_PAGE, ATTR_VALUE, LB_ACTIONS, LB_RESERVED_PREFIX, HUB_TAG_NAME, NAV_CELL_LABEL, NAV_CELL_URI, NAV_ROW, LB_EVENT_NAME, LB_ENDPOINT, LB_REQUEST_TIMEOUT_MS, } from "../core/lb-constants.js";
3
- import { applyData, isValueControl, within } from "./lb-apply.js";
2
+ import { ACTION_ROW_DELETE, ACTION_ROW_INSERT, ACTION_ROW_UPDATE, ATTR_ACTION, ATTR_CELL, ATTR_ERROR, ATTR_KEY_VALUE, ATTR_LIST, ATTR_NAV_LINK, ATTR_PAGE, ATTR_PENDING, ATTR_QUERY_PARM, ATTR_QUERY_PARM_PUSH, ATTR_ROW, ATTR_UNKNOWN_PAGE, ATTR_VALUE, LB_ACTIONS, LB_RESERVED_PREFIX, HUB_TAG_NAME, NAV_CELL_LABEL, NAV_CELL_URI, NAV_ROW, QUERY_PARMS_ROW, LB_EVENT_NAME, LB_ENDPOINT, LB_REQUEST_TIMEOUT_MS, } from "../core/lb-constants.js";
3
+ import { withQueryParms, } from "../core/lb-types.js";
4
+ import { applyData, applyQueryParms, isValueControl, within, } from "./lb-apply.js";
4
5
  export { applyData, applyRow } from "./lb-apply.js";
5
6
  /** What each operation takes from scope, and what it cannot be sent without. */
6
7
  const OPERATIONS = {
@@ -39,6 +40,14 @@ function scope(request, el) {
39
40
  `but names no operation, ignoring`);
40
41
  return false;
41
42
  }
43
+ // A control that writes a query parm has said what it does with its value.
44
+ // An action beside that would send the choice twice, once as a request and
45
+ // once as a page load, so the request is the one refused.
46
+ if (el.hasAttribute(ATTR_QUERY_PARM)) {
47
+ console.error(`lb-hub: <${el.localName}> carries ${ATTR_QUERY_PARM} and sends ` +
48
+ `'${action}' as well, not sent; a query parm change reloads the page`, el);
49
+ return false;
50
+ }
42
51
  const fields = request;
43
52
  // The element's own lb-list or lb-row is what it displays. Where it sits
44
53
  // is its ancestors', the way a control belongs to the form around it.
@@ -254,12 +263,15 @@ function pageHostFor(page) {
254
263
  /**
255
264
  * Fetch one round trip against the client-side deadline. Throws — rather
256
265
  * than swallowing — on a non-ok response, a network failure, or a timeout,
257
- * so a caller that needs to know the request failed (as opposed to one that
258
- * only ever wants the freshest data, like navigate()) can tell the two
259
- * apart from a genuinely empty result.
266
+ * so a caller can tell a failed request from a genuinely empty result.
267
+ *
268
+ * The page names the path below the endpoint and the query string the
269
+ * browser is showing rides after it verbatim, so every round trip, a page
270
+ * load or an action, reaches the server with the URL on screen in hand.
260
271
  */
261
272
  async function fetchData(page, request) {
262
- const url = `${LB_ENDPOINT}?page=${encodeURIComponent(page)}`;
273
+ const path = page.split("/").map(encodeURIComponent).join("/");
274
+ const url = `${LB_ENDPOINT}/${path}${location.search}`;
263
275
  const controller = new AbortController();
264
276
  const timeout = setTimeout(() => controller.abort(), LB_REQUEST_TIMEOUT_MS);
265
277
  try {
@@ -279,6 +291,23 @@ async function fetchData(page, request) {
279
291
  clearTimeout(timeout);
280
292
  }
281
293
  }
294
+ /**
295
+ * An element's round trip is in flight, or is not. aria-busy states the same
296
+ * fact to assistive technology, which no stylesheet on lb-pending reaches.
297
+ */
298
+ function pending(el, on) {
299
+ if (!el)
300
+ return;
301
+ if (on) {
302
+ el.removeAttribute(ATTR_ERROR);
303
+ el.setAttribute(ATTR_PENDING, "");
304
+ el.setAttribute("aria-busy", "true");
305
+ }
306
+ else {
307
+ el.removeAttribute(ATTR_PENDING);
308
+ el.removeAttribute("aria-busy");
309
+ }
310
+ }
282
311
  /**
283
312
  * The hub is a singleton outside <main>. It survives every page change,
284
313
  * so it needs no id to disambiguate it.
@@ -306,26 +335,25 @@ class LbHub extends HTMLElement {
306
335
  const request = e.detail;
307
336
  // The element that dispatched the request, not e.currentTarget (the
308
337
  // hub itself, since the event bubbles) — pending/error state belongs
309
- // at the origin so a widget can watch its own attributes. aria-busy
310
- // states the same fact to assistive technology, which no stylesheet
311
- // on lb-pending reaches.
312
- const origin = e.target;
313
- origin?.removeAttribute(ATTR_ERROR);
314
- origin?.setAttribute(ATTR_PENDING, "");
315
- origin?.setAttribute("aria-busy", "true");
316
- void fetchData(this.page, request)
317
- .then((data) => {
318
- applyData(this, data);
338
+ // at the origin so a widget can watch its own attributes.
339
+ const origin = e.target ?? undefined;
340
+ const page = this.page;
341
+ pending(origin, true);
342
+ void fetchData(page, request)
343
+ .then(async (data) => {
344
+ const { [QUERY_PARMS_ROW]: written, ...load } = data;
345
+ if (written === undefined)
346
+ applyData(this, data);
347
+ else {
348
+ await this.changeQueryParms(page, written, load);
349
+ }
319
350
  resetGathered(gathered.get(request) ?? []);
320
351
  })
321
352
  .catch((err) => {
322
353
  console.error(`lb-hub: request failed`, err);
323
354
  origin?.setAttribute(ATTR_ERROR, "");
324
355
  })
325
- .finally(() => {
326
- origin?.removeAttribute(ATTR_PENDING);
327
- origin?.removeAttribute("aria-busy");
328
- });
356
+ .finally(() => pending(origin, false));
329
357
  });
330
358
  /**
331
359
  * A native element carrying an action. The hub dispatches the bare action
@@ -374,41 +402,129 @@ class LbHub extends HTMLElement {
374
402
  if (!link)
375
403
  return;
376
404
  e.preventDefault();
377
- const path = new URL(link.href).pathname;
378
- history.pushState(null, "", path);
379
- void this.navigate(path);
405
+ const { pathname, search } = new URL(link.href);
406
+ history.pushState(null, "", pathname + search);
407
+ void this.navigate();
408
+ });
409
+ /**
410
+ * A control that writes a query parm. Changing what the page shows is
411
+ * not going anywhere, so the address updates in place and stays
412
+ * copyable, and history does not accumulate unless the control asks
413
+ * for it. Only the one parm is touched: the rest of the query string may
414
+ * have arrived by link, with no control on screen to say it again.
415
+ */
416
+ this.addEventListener("change", (e) => {
417
+ const el = e.target?.closest(`[${ATTR_QUERY_PARM}]`);
418
+ if (!el || !this.contains(el))
419
+ return;
420
+ const control = controlOf(el);
421
+ if (control === undefined) {
422
+ console.warn(`lb-hub: ${ATTR_QUERY_PARM} on <${el.localName}> has no input, ` +
423
+ `select or textarea to read, ignoring it`, el);
424
+ return;
425
+ }
426
+ const name = el.getAttribute(ATTR_QUERY_PARM);
427
+ const url = new URL(location.href);
428
+ if ((url.searchParams.get(name) ?? "") === control.value)
429
+ return;
430
+ // Empty is the default, and a default is left out of the URL, so its
431
+ // length tracks how far the user has narrowed the page.
432
+ url.search = withQueryParms(url.searchParams, {
433
+ [name]: control.value,
434
+ }).toString();
435
+ if (el.hasAttribute(ATTR_QUERY_PARM_PUSH)) {
436
+ history.pushState(null, "", url);
437
+ }
438
+ else {
439
+ history.replaceState(null, "", url);
440
+ }
441
+ void this.navigate(el);
380
442
  });
381
- addEventListener("popstate", () => void this.navigate(location.pathname));
382
- void this.navigate(location.pathname);
443
+ addEventListener("popstate", () => void this.navigate());
444
+ void this.navigate();
383
445
  }
384
446
  /**
385
- * Navigation is the one operation that replaces host DOM. The host is
386
- * already in the document, so selecting a page is getElementById() and
387
- * there is no fetching mechanism on the host channel.
447
+ * Load the page at the URL the browser is showing. Every way of arriving
448
+ * comes through here — a cold load, a nav link, Back, a query parm write —
449
+ * so each one is the same page load and a URL always shows the same thing.
450
+ *
451
+ * Entering a page is the one operation that replaces host DOM. The host is
452
+ * already in the document, so selecting a page is a lookup and there is no
453
+ * fetching mechanism on the host channel. Staying on the page, with only
454
+ * the query string changed, keeps the DOM it has: the queries re-run and
455
+ * land by key, so a list that got its rows back keeps them, and its scroll.
456
+ *
457
+ * `origin` is the control that changed the query string, if one did, and
458
+ * carries the round trip's pending and error state as a request's origin
459
+ * does.
388
460
  */
389
- async navigate(path) {
390
- const page = pageNameFor(path);
391
- this.landNavigation(path, page);
461
+ async navigate(origin) {
462
+ const page = pageNameFor(location.pathname);
463
+ this.landNavigation(page);
392
464
  const template = pageHostFor(page);
393
465
  if (!template) {
394
466
  this.reportUnknownPage();
395
467
  this.reveal();
396
468
  return;
397
469
  }
398
- this.page = page;
399
- // Insert the page shell before fetching its data, not after: the
400
- // template is already in the document, so holding it back behind a
401
- // network round trip only leaves <main> empty for no reason.
402
- this.main.replaceChildren(template.content.cloneNode(true));
470
+ if (page !== this.page) {
471
+ this.page = page;
472
+ // Insert the page shell before fetching its data, not after: the
473
+ // template is already in the document, so holding it back behind a
474
+ // network round trip only leaves <main> empty for no reason.
475
+ this.main.replaceChildren(template.content.cloneNode(true));
476
+ }
403
477
  this.reveal();
404
478
  let data = {};
479
+ pending(origin, true);
405
480
  try {
406
481
  data = await fetchData(page);
407
482
  }
408
483
  catch (err) {
409
484
  console.error(`lb-hub: failed to load data for '${page}'`, err);
485
+ origin?.setAttribute(ATTR_ERROR, "");
486
+ }
487
+ finally {
488
+ pending(origin, false);
410
489
  }
490
+ this.land(data);
491
+ }
492
+ /**
493
+ * A page load's answer, landed: the page's data, then the query string on
494
+ * the controls that write it, so a picker already has the options its
495
+ * value selects.
496
+ */
497
+ land(data) {
411
498
  applyData(this, data);
499
+ applyQueryParms(this.main, new URLSearchParams(location.search));
500
+ }
501
+ /**
502
+ * A server response changed the query parms: the request wrote parms only
503
+ * the write could know, such as the key of a row it inserted. The server
504
+ * has already loaded the page at them, so this is the rest of a query parm
505
+ * write — the URL, then the load landed — with the round trip done.
506
+ *
507
+ * The history entry is replaced, as a control's write replaces it: the
508
+ * page is showing something else, not going somewhere.
509
+ *
510
+ * A load that did not come back, which the server says by sending the
511
+ * parms alone, is asked for again, so a failure reports as a page load's.
512
+ * It does not mark the element that sent the request: that request's
513
+ * write succeeded, and an error on it would invite sending it twice.
514
+ *
515
+ * `page` is where the request was sent from. A user who left it while the
516
+ * request was in flight is on a URL that is not the server's to change.
517
+ */
518
+ async changeQueryParms(page, written, load) {
519
+ if (pageNameFor(location.pathname) !== page)
520
+ return;
521
+ const url = new URL(location.href);
522
+ url.search = withQueryParms(url.searchParams, written).toString();
523
+ history.replaceState(null, "", url);
524
+ if (Object.keys(load).length === 0)
525
+ return this.navigate();
526
+ this.landNavigation(page);
527
+ this.land(load);
412
528
  }
413
529
  /**
414
530
  * An app that ships `<body hidden>` (see docs/reference/chrome.md) is
@@ -433,11 +549,15 @@ class LbHub extends HTMLElement {
433
549
  * path no anchor names lands an empty one, explicitly, so a consumer is
434
550
  * never left showing the previous page's.
435
551
  *
552
+ * The URI is the path and query string together, as the address bar has
553
+ * them: a link that opens a page narrowed is a different place from the
554
+ * bare page, though the label, which names the page, is the same.
555
+ *
436
556
  * Landed only where a subtree names it. applyData() warns about a query
437
557
  * with no scope, and a chrome that displays no navigation has nothing to
438
558
  * be warned about.
439
559
  */
440
- landNavigation(path, page) {
560
+ landNavigation(page) {
441
561
  if (!this.querySelector(`[${ATTR_ROW}="${NAV_ROW}"]`))
442
562
  return;
443
563
  let label = "";
@@ -448,7 +568,10 @@ class LbHub extends HTMLElement {
448
568
  break;
449
569
  }
450
570
  applyData(this, {
451
- [NAV_ROW]: { [NAV_CELL_LABEL]: label, [NAV_CELL_URI]: path },
571
+ [NAV_ROW]: {
572
+ [NAV_CELL_LABEL]: label,
573
+ [NAV_CELL_URI]: location.pathname + location.search,
574
+ },
452
575
  });
453
576
  }
454
577
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"lb-hub.browser.js","sourceRoot":"","sources":["../../hub/lb-hub.browser.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport {\n ACTION_ROW_DELETE,\n ACTION_ROW_INSERT,\n ACTION_ROW_UPDATE,\n ATTR_ACTION,\n ATTR_CELL,\n ATTR_ERROR,\n ATTR_KEY_VALUE,\n ATTR_LIST,\n ATTR_NAV_LINK,\n ATTR_PAGE,\n ATTR_PENDING,\n ATTR_ROW,\n ATTR_UNKNOWN_PAGE,\n ATTR_VALUE,\n LB_ACTIONS,\n LB_RESERVED_PREFIX,\n HUB_TAG_NAME,\n NAV_CELL_LABEL,\n NAV_CELL_URI,\n NAV_ROW,\n LB_EVENT_NAME,\n LB_ENDPOINT,\n LB_REQUEST_TIMEOUT_MS,\n} from \"../core/lb-constants.js\";\nimport type { HubData, HubRequest } from \"../core/lb-types.js\";\nimport { applyData, isValueControl, within } from \"./lb-apply.js\";\n\nexport { applyData, applyRow } from \"./lb-apply.js\";\n\n/** What each operation takes from scope, and what it cannot be sent without. */\nconst OPERATIONS: Record<\n string,\n { fills: readonly string[]; requires: readonly string[] }\n> = {\n [ACTION_ROW_INSERT]: { fills: [\"list\"], requires: [\"list\", \"values\"] },\n [ACTION_ROW_DELETE]: { fills: [\"list\", \"key\"], requires: [\"list\", \"key\"] },\n [ACTION_ROW_UPDATE]: {\n fills: [\"list\", \"key\"],\n requires: [\"list\", \"key\", \"values\"],\n },\n};\n\n/**\n * Complete a request from where its element sits — see docs/TECHREF-1.0.md,\n * \"Requests\". Every request passes through here, whoever built it, so a\n * widget states what it did and never where it is.\n *\n * The scope field is `list` or `row`, whichever attribute scoped the nearest\n * ancestor, so a declared action fired inside a single-row scope says so and\n * the wire never needs a noun for the addressable thing. A field the request\n * already carries is the dispatcher's to decide and is left as it is.\n *\n * False when the request cannot be sent, having said why.\n */\nfunction scope(request: HubRequest | undefined, el: Element): boolean {\n if (!request?.action) {\n console.warn(`lb-hub: event without an action, ignoring`, request);\n return false;\n }\n const { action } = request;\n const reserved = action.startsWith(LB_RESERVED_PREFIX);\n // The reserved prefix is the whole of the discriminant, so a reserved\n // name that is not one of the three operations is a typo rather than\n // something to forward. Refused here, where the widget can see why: a\n // server would only report that it declares no such action.\n if (reserved && !(LB_ACTIONS as readonly string[]).includes(action)) {\n console.error(\n `lb-hub: '${action}' begins with '${LB_RESERVED_PREFIX}' ` +\n `but names no operation, ignoring`,\n );\n return false;\n }\n\n const fields = request as unknown as Record<string, unknown>;\n // The element's own lb-list or lb-row is what it displays. Where it sits\n // is its ancestors', the way a control belongs to the form around it.\n const parent = el.parentElement;\n const enclosing = parent?.closest(`[${ATTR_LIST}], [${ATTR_ROW}]`);\n const row = parent?.closest(`[${ATTR_KEY_VALUE}]`);\n const inScope: Record<string, string | null | undefined> = {\n list: enclosing?.getAttribute(ATTR_LIST),\n row: enclosing?.hasAttribute(ATTR_LIST)\n ? null\n : enclosing?.getAttribute(ATTR_ROW),\n // Only a row of that scope: a key from outside it names a row of some\n // other list.\n key:\n row && enclosing?.contains(row) ? row.getAttribute(ATTR_KEY_VALUE) : null,\n cell: el.getAttribute(ATTR_CELL),\n };\n const fill = (name: string) => {\n const value = inScope[name];\n if (fields[name] === undefined && value !== null && value !== undefined) {\n fields[name] = value;\n }\n };\n\n const operation = OPERATIONS[action];\n if (!operation) {\n // A declared action names one scope or the other, never both.\n if (fields.list === undefined && fields.row === undefined) {\n fill(\"list\");\n fill(\"row\");\n }\n fill(\"key\");\n fill(\"cell\");\n return true;\n }\n\n for (const name of operation.fills) fill(name);\n // An element carrying lb-cell is a record of one cell, the way a control\n // has a value and a form has values: the map holds that cell and nothing\n // beside it. Its value is the one the dispatcher sent, or else what its\n // control holds.\n //\n // Anything else gathers from the record it belongs to, the way a button\n // submits its form owner: the nearest form, table row or live row around\n // it, the element itself included. A <tr> counts because it is the one\n // place a form cannot go around a row's controls. What else sits beside\n // the element never decides it. The record is the scope's, so it is never\n // the scope itself nor outside it; with none, there is nothing to say which\n // cells are meant. Outside every scope the list is what is missing, and\n // that is what gets reported.\n if (fields.values === undefined && operation.requires.includes(\"values\")) {\n const cell = el.getAttribute(ATTR_CELL);\n if (cell !== null) {\n const control = controlOf(el);\n const value =\n typeof fields.value === \"string\" ? fields.value : control?.value;\n if (value !== undefined) {\n fields.values = { [cell]: value };\n if (action === ACTION_ROW_INSERT && control?.value === value) {\n gathered.set(request, [{ cell: el, control, value }]);\n }\n }\n } else if (fields.value !== undefined) {\n console.error(\n `lb-hub: '${action}' from <${el.localName}> carries a value but no ` +\n `${ATTR_CELL} to say which column it is, not sent`,\n el,\n );\n return false;\n } else {\n const owner = el.closest(`form, tr, [${ATTR_KEY_VALUE}]`);\n const ours =\n !enclosing || (owner !== enclosing && enclosing.contains(owner));\n if (owner && ours) {\n const { values, read } = gatherValues(owner);\n if (read.length > 0) {\n fields.values = values;\n if (action === ACTION_ROW_INSERT) gathered.set(request, read);\n }\n } else if (enclosing) {\n console.error(\n `lb-hub: '${action}' from <${el.localName}> is in no form, table ` +\n `row or live row to gather values from, not sent; put its cells ` +\n `in a <form>`,\n el,\n );\n return false;\n }\n }\n }\n // An operation carries its values and nothing else: the value they were\n // built from is not a second copy on the wire.\n delete fields.value;\n const missing = operation.requires.filter((f) => fields[f] === undefined);\n if (missing.length > 0) {\n console.error(\n `lb-hub: '${action}' from <${el.localName}> has no ` +\n `${missing.join(\", \")} in scope, not sent`,\n el,\n );\n return false;\n }\n return true;\n}\n\n/** Dispatch the bare action an element carries, for the hub to scope. */\nfunction dispatchAction(el: Element): void {\n // Pressed again while its round trip is in flight. Ignoring it makes a\n // pending native action disabled in fact, so a stylesheet only has to\n // show it. A widget is not held back this way: one that sends on change\n // must have its latest value sent, not dropped.\n if (el.hasAttribute(ATTR_PENDING)) return;\n const detail = { action: el.getAttribute(ATTR_ACTION)! } as HubRequest;\n el.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));\n}\n\n/** A form control whose state is its `value`. */\ntype ValueControl = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;\n\n/** One cell a request read, the control it read, and the value it read. */\ninterface Read {\n cell: Element;\n control: ValueControl;\n value: string;\n}\n\n/**\n * What each insert gathered, until its round trip succeeds. Keyed by the\n * request itself, so a request an ancestor stops leaves nothing behind.\n */\nconst gathered = new WeakMap<HubRequest, Read[]>();\n\n/**\n * The control a cell reads from, for gathering rather than landing.\n *\n * A cell is always either a native form control itself (a bare\n * `<input lb-cell>`) or a widget that wraps one (`<lb-input>`, `<lb-select>`)\n * — that wrapping is structural, not incidental, which is what lets this be\n * one rule instead of one per widget type. The controls are the ones a value\n * lands on, so what is read back is what landed.\n */\nfunction controlOf(el: Element): ValueControl | undefined {\n return isValueControl(el)\n ? el\n : [...el.querySelectorAll(\"input, select, textarea\")].find(isValueControl);\n}\n\n/**\n * Every lb-cell of an element's scope, gathered into one values map. Shared\n * by lb-row-insert and lb-row-update — both submit a batch of cells, and\n * differ only in whether a key comes with them. What was read comes back\n * beside the map, so an insert can reset it once it succeeds.\n *\n * Found the way a row lands, so a cell inside a nested scope is that scope's\n * and is not read: the options of a picker in the row are the picker's rows,\n * while the picker's own cell is the row's.\n */\nfunction gatherValues(el: Element): {\n values: Record<string, string>;\n read: Read[];\n} {\n const values: Record<string, string> = {};\n const read: Read[] = [];\n for (const cell of within(el, `[${ATTR_CELL}]`)) {\n const name = cell.getAttribute(ATTR_CELL)!;\n const control = controlOf(cell);\n if (control === undefined) {\n console.warn(\n `lb-hub: lb-cell '${name}' has no input, select or textarea to ` +\n `read, ignoring it`,\n cell,\n );\n continue;\n }\n values[name] = control.value;\n read.push({ cell, control, value: control.value });\n }\n return { values, read };\n}\n\n/**\n * After an insert succeeds, what it gathered goes back to its default, the\n * way `form.reset()` resets a form: the row those cells held now lives in the\n * list. A control showing something other than what was read holds an edit\n * made during the round trip, which was not sent, and is left alone.\n *\n * Where a value landed, `lb-value` comes off the cell first, so a widget that\n * renders from it sees the attribute go and returns to its default. The\n * control is reset after, so it ends at its default whatever the widget did.\n * Nothing is dispatched, as `form.reset()` fires no `change`.\n */\nfunction resetGathered(read: Read[]): void {\n for (const { cell, control, value } of read) {\n if (control.value !== value) continue;\n cell.removeAttribute(ATTR_VALUE);\n control.removeAttribute(ATTR_VALUE);\n if (control.localName === \"select\") {\n for (const option of (control as HTMLSelectElement).options) {\n option.selected = option.defaultSelected;\n }\n } else {\n const text = control as HTMLInputElement | HTMLTextAreaElement;\n text.value = text.defaultValue;\n }\n }\n}\n\n/**\n * A path names a page host, and nothing finer. The bare path resolves to\n * `index`, the same convention a static file server follows — an\n * application names its landing page that way rather than the hub singling\n * one out.\n */\nfunction pageNameFor(path: string): string {\n const name = path.replace(/^\\/+|\\/+$/g, \"\");\n return name === \"\" ? \"index\" : name;\n}\n\n/**\n * The `<template>` a page's markup ships in. The builder guarantees one per\n * page name (build/locations.ts refuses two page files sharing a stub), so\n * a second match here means the document was assembled by something else,\n * and is worth saying out loud rather than resolving by document order.\n */\nfunction pageHostFor(page: string): HTMLTemplateElement | null {\n const hosts: HTMLTemplateElement[] = [];\n for (const el of document.querySelectorAll(`template[${ATTR_PAGE}]`)) {\n if (el.getAttribute(ATTR_PAGE) === page) {\n hosts.push(el as HTMLTemplateElement);\n }\n }\n if (hosts.length === 0) {\n console.error(`lb-hub: no page host for '${page}'`);\n return null;\n }\n if (hosts.length > 1) {\n console.error(\n `lb-hub: ${hosts.length} page hosts carry ${ATTR_PAGE}=\"${page}\", ` +\n `using the first`,\n );\n }\n return hosts[0]!;\n}\n\n/**\n * Fetch one round trip against the client-side deadline. Throws — rather\n * than swallowing — on a non-ok response, a network failure, or a timeout,\n * so a caller that needs to know the request failed (as opposed to one that\n * only ever wants the freshest data, like navigate()) can tell the two\n * apart from a genuinely empty result.\n */\nasync function fetchData(page: string, request?: HubRequest): Promise<HubData> {\n const url = `${LB_ENDPOINT}?page=${encodeURIComponent(page)}`;\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), LB_REQUEST_TIMEOUT_MS);\n try {\n // An empty body is how the hub asks for the page's whole query set.\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(request ?? {}),\n signal: controller.signal,\n });\n if (!res.ok) {\n throw new Error(`lb-hub: server responded ${res.status} for ${url}`);\n }\n return (await res.json()) as HubData;\n } finally {\n clearTimeout(timeout);\n }\n}\n\n/**\n * The hub is a singleton outside <main>. It survives every page change,\n * so it needs no id to disambiguate it.\n */\nclass LbHub extends HTMLElement {\n private main!: HTMLElement;\n\n /**\n * The page currently in <main>. A page declares its own requests, so a request\n * has to say which page it came from. The page rides on the URL rather\n * than in the request, leaving the operation set closed.\n */\n private page = \"\";\n\n connectedCallback() {\n this.main = this.querySelector(\"main\")!;\n\n // Scoped in the capture phase, on the way down, so every ancestor that\n // sees the request on its way back up sees it whole. A request that\n // cannot be sent is stopped here, before anything sees it at all.\n this.addEventListener(\n LB_EVENT_NAME,\n (e: Event) => {\n const origin = e.target as Element;\n const request = (e as CustomEvent<HubRequest>).detail;\n if (!scope(request, origin)) e.stopPropagation();\n },\n { capture: true },\n );\n\n this.addEventListener(LB_EVENT_NAME, (e: Event) => {\n const request = (e as CustomEvent<HubRequest>).detail;\n // The element that dispatched the request, not e.currentTarget (the\n // hub itself, since the event bubbles) — pending/error state belongs\n // at the origin so a widget can watch its own attributes. aria-busy\n // states the same fact to assistive technology, which no stylesheet\n // on lb-pending reaches.\n const origin = e.target as Element | null;\n origin?.removeAttribute(ATTR_ERROR);\n origin?.setAttribute(ATTR_PENDING, \"\");\n origin?.setAttribute(\"aria-busy\", \"true\");\n void fetchData(this.page, request)\n .then((data) => {\n applyData(this, data);\n resetGathered(gathered.get(request) ?? []);\n })\n .catch((err) => {\n console.error(`lb-hub: request failed`, err);\n origin?.setAttribute(ATTR_ERROR, \"\");\n })\n .finally(() => {\n origin?.removeAttribute(ATTR_PENDING);\n origin?.removeAttribute(\"aria-busy\");\n });\n });\n\n /**\n * A native element carrying an action. The hub dispatches the bare action\n * from that element as the ordinary bubbling event, rather than sending\n * it directly, so it is scoped exactly as a widget's is and an ancestor\n * widget can still stop it and confirm. A native action button and a\n * hand-written widget therefore produce identical events.\n *\n * A widget carrying an action is left alone — it owns its own interaction\n * and decides what counts as performing it, which for a <select> is a\n * change rather than a click. The test is the same lexical one applyData\n * uses: a hyphen in the tag name.\n */\n this.addEventListener(\"click\", (e: MouseEvent) => {\n const el = (e.target as Element | null)?.closest(`[${ATTR_ACTION}]`);\n if (!el || el.localName.includes(\"-\")) return;\n // A form performs its action on submit, whatever was clicked in it.\n if (el instanceof HTMLFormElement) return;\n // A button beside the cells is pressed once they are done. An element\n // that is a cell, or holds them, has no such moment: a click into its\n // own input would send the row. Deciding when a cell has changed is a\n // widget's.\n const action = el.getAttribute(ATTR_ACTION);\n if (\n (action === ACTION_ROW_INSERT || action === ACTION_ROW_UPDATE) &&\n (el.hasAttribute(ATTR_CELL) || el.querySelector(`[${ATTR_CELL}]`))\n ) {\n console.error(\n `lb-hub: '${action}' on <${el.localName}> is or holds a cell, so ` +\n `a click on it is not sent; put it on a <form>, a button or a widget`,\n el,\n );\n return;\n }\n e.preventDefault();\n dispatchAction(el);\n });\n\n // The same delegation for the event a form owns.\n this.addEventListener(\"submit\", (e: SubmitEvent) => {\n const form = e.target as Element | null;\n if (!form || !form.hasAttribute(ATTR_ACTION)) return;\n e.preventDefault();\n dispatchAction(form);\n });\n\n // Only anchors that opted in. Everything else is a real link.\n this.addEventListener(\"click\", (e: MouseEvent) => {\n const link = (e.target as Element | null)?.closest(`a[${ATTR_NAV_LINK}]`);\n if (!link) return;\n e.preventDefault();\n const path = new URL((link as HTMLAnchorElement).href).pathname;\n history.pushState(null, \"\", path);\n void this.navigate(path);\n });\n\n addEventListener(\"popstate\", () => void this.navigate(location.pathname));\n\n void this.navigate(location.pathname);\n }\n\n /**\n * Navigation is the one operation that replaces host DOM. The host is\n * already in the document, so selecting a page is getElementById() and\n * there is no fetching mechanism on the host channel.\n */\n private async navigate(path: string): Promise<void> {\n const page = pageNameFor(path);\n this.landNavigation(path, page);\n const template = pageHostFor(page);\n if (!template) {\n this.reportUnknownPage();\n this.reveal();\n return;\n }\n this.page = page;\n // Insert the page shell before fetching its data, not after: the\n // template is already in the document, so holding it back behind a\n // network round trip only leaves <main> empty for no reason.\n this.main.replaceChildren(template.content.cloneNode(true));\n this.reveal();\n let data: HubData = {};\n try {\n data = await fetchData(page);\n } catch (err) {\n console.error(`lb-hub: failed to load data for '${page}'`, err);\n }\n applyData(this, data);\n }\n\n /**\n * An app that ships `<body hidden>` (see docs/reference/chrome.md) is\n * asking to stay invisible until there is something coherent to show,\n * rather than flash the chrome before `<main>` has real content. This is\n * a no-op for an app that doesn't use that convention.\n */\n private reveal(): void {\n document.body.hidden = false;\n }\n\n /**\n * The hub's own row: where the page is, in the vocabulary a server\n * answer arrives in, so a chrome displays it the way it displays anything\n * — `lb-row=\"lb-navigation\"` on a subtree, `lb-cell` on what shows a\n * value. Landed before the page host is looked up, so a miss has it too;\n * the unknown-page dialog is one consumer among whatever others the\n * chrome writes.\n *\n * The label is the nav's: the text of the first lb-nav-link anchor whose\n * href names this page. Taken from the nav rather than from a click, so a\n * click, a reload and the back button all land the same label — and a\n * path no anchor names lands an empty one, explicitly, so a consumer is\n * never left showing the previous page's.\n *\n * Landed only where a subtree names it. applyData() warns about a query\n * with no scope, and a chrome that displays no navigation has nothing to\n * be warned about.\n */\n private landNavigation(path: string, page: string): void {\n if (!this.querySelector(`[${ATTR_ROW}=\"${NAV_ROW}\"]`)) return;\n let label = \"\";\n for (const a of this.querySelectorAll<HTMLAnchorElement>(\n `a[${ATTR_NAV_LINK}]`,\n )) {\n if (pageNameFor(new URL(a.href).pathname) !== page) continue;\n label = a.textContent?.trim() ?? \"\";\n break;\n }\n applyData(this, {\n [NAV_ROW]: { [NAV_CELL_LABEL]: label, [NAV_CELL_URI]: path },\n });\n }\n\n /**\n * A path that resolves to no page host is discovered client-side, after a\n * successful 200 — every route gets the same document, so there is no\n * HTTP 404 to report (see docs/tutorials/010-pages-and-navigation.md).\n * lb-unknown-page is optional: an application that declares none gets\n * nothing beyond navigate()'s console.error. The builder guarantees the\n * element it's on is a <dialog> (build/assemble.ts), so this can call\n * showModal() without checking the tag here.\n *\n * Only opens it. What the dialog says it gets from landNavigation(),\n * already run, by naming the hub's row in lb-row like any subtree.\n *\n * Scoped to the hub, like every other lookup here, so that one rule holds\n * without exception: what the hub acts on is inside the hub.\n */\n private reportUnknownPage(): void {\n const dialog = this.querySelector(`[${ATTR_UNKNOWN_PAGE}]`);\n if (!dialog) return;\n const modal = dialog as HTMLDialogElement;\n if (!modal.open) modal.showModal();\n }\n}\n\ncustomElements.define(HUB_TAG_NAME, LbHub);\n"]}
1
+ {"version":3,"file":"lb-hub.browser.js","sourceRoot":"","sources":["../../hub/lb-hub.browser.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport {\n ACTION_ROW_DELETE,\n ACTION_ROW_INSERT,\n ACTION_ROW_UPDATE,\n ATTR_ACTION,\n ATTR_CELL,\n ATTR_ERROR,\n ATTR_KEY_VALUE,\n ATTR_LIST,\n ATTR_NAV_LINK,\n ATTR_PAGE,\n ATTR_PENDING,\n ATTR_QUERY_PARM,\n ATTR_QUERY_PARM_PUSH,\n ATTR_ROW,\n ATTR_UNKNOWN_PAGE,\n ATTR_VALUE,\n LB_ACTIONS,\n LB_RESERVED_PREFIX,\n HUB_TAG_NAME,\n NAV_CELL_LABEL,\n NAV_CELL_URI,\n NAV_ROW,\n QUERY_PARMS_ROW,\n LB_EVENT_NAME,\n LB_ENDPOINT,\n LB_REQUEST_TIMEOUT_MS,\n} from \"../core/lb-constants.js\";\nimport {\n withQueryParms,\n type HubData,\n type HubRequest,\n} from \"../core/lb-types.js\";\nimport {\n applyData,\n applyQueryParms,\n isValueControl,\n within,\n} from \"./lb-apply.js\";\n\nexport { applyData, applyRow } from \"./lb-apply.js\";\n\n/** What each operation takes from scope, and what it cannot be sent without. */\nconst OPERATIONS: Record<\n string,\n { fills: readonly string[]; requires: readonly string[] }\n> = {\n [ACTION_ROW_INSERT]: { fills: [\"list\"], requires: [\"list\", \"values\"] },\n [ACTION_ROW_DELETE]: { fills: [\"list\", \"key\"], requires: [\"list\", \"key\"] },\n [ACTION_ROW_UPDATE]: {\n fills: [\"list\", \"key\"],\n requires: [\"list\", \"key\", \"values\"],\n },\n};\n\n/**\n * Complete a request from where its element sits — see docs/TECHREF-1.0.md,\n * \"Requests\". Every request passes through here, whoever built it, so a\n * widget states what it did and never where it is.\n *\n * The scope field is `list` or `row`, whichever attribute scoped the nearest\n * ancestor, so a declared action fired inside a single-row scope says so and\n * the wire never needs a noun for the addressable thing. A field the request\n * already carries is the dispatcher's to decide and is left as it is.\n *\n * False when the request cannot be sent, having said why.\n */\nfunction scope(request: HubRequest | undefined, el: Element): boolean {\n if (!request?.action) {\n console.warn(`lb-hub: event without an action, ignoring`, request);\n return false;\n }\n const { action } = request;\n const reserved = action.startsWith(LB_RESERVED_PREFIX);\n // The reserved prefix is the whole of the discriminant, so a reserved\n // name that is not one of the three operations is a typo rather than\n // something to forward. Refused here, where the widget can see why: a\n // server would only report that it declares no such action.\n if (reserved && !(LB_ACTIONS as readonly string[]).includes(action)) {\n console.error(\n `lb-hub: '${action}' begins with '${LB_RESERVED_PREFIX}' ` +\n `but names no operation, ignoring`,\n );\n return false;\n }\n\n // A control that writes a query parm has said what it does with its value.\n // An action beside that would send the choice twice, once as a request and\n // once as a page load, so the request is the one refused.\n if (el.hasAttribute(ATTR_QUERY_PARM)) {\n console.error(\n `lb-hub: <${el.localName}> carries ${ATTR_QUERY_PARM} and sends ` +\n `'${action}' as well, not sent; a query parm change reloads the page`,\n el,\n );\n return false;\n }\n\n const fields = request as unknown as Record<string, unknown>;\n // The element's own lb-list or lb-row is what it displays. Where it sits\n // is its ancestors', the way a control belongs to the form around it.\n const parent = el.parentElement;\n const enclosing = parent?.closest(`[${ATTR_LIST}], [${ATTR_ROW}]`);\n const row = parent?.closest(`[${ATTR_KEY_VALUE}]`);\n const inScope: Record<string, string | null | undefined> = {\n list: enclosing?.getAttribute(ATTR_LIST),\n row: enclosing?.hasAttribute(ATTR_LIST)\n ? null\n : enclosing?.getAttribute(ATTR_ROW),\n // Only a row of that scope: a key from outside it names a row of some\n // other list.\n key:\n row && enclosing?.contains(row) ? row.getAttribute(ATTR_KEY_VALUE) : null,\n cell: el.getAttribute(ATTR_CELL),\n };\n const fill = (name: string) => {\n const value = inScope[name];\n if (fields[name] === undefined && value !== null && value !== undefined) {\n fields[name] = value;\n }\n };\n\n const operation = OPERATIONS[action];\n if (!operation) {\n // A declared action names one scope or the other, never both.\n if (fields.list === undefined && fields.row === undefined) {\n fill(\"list\");\n fill(\"row\");\n }\n fill(\"key\");\n fill(\"cell\");\n return true;\n }\n\n for (const name of operation.fills) fill(name);\n // An element carrying lb-cell is a record of one cell, the way a control\n // has a value and a form has values: the map holds that cell and nothing\n // beside it. Its value is the one the dispatcher sent, or else what its\n // control holds.\n //\n // Anything else gathers from the record it belongs to, the way a button\n // submits its form owner: the nearest form, table row or live row around\n // it, the element itself included. A <tr> counts because it is the one\n // place a form cannot go around a row's controls. What else sits beside\n // the element never decides it. The record is the scope's, so it is never\n // the scope itself nor outside it; with none, there is nothing to say which\n // cells are meant. Outside every scope the list is what is missing, and\n // that is what gets reported.\n if (fields.values === undefined && operation.requires.includes(\"values\")) {\n const cell = el.getAttribute(ATTR_CELL);\n if (cell !== null) {\n const control = controlOf(el);\n const value =\n typeof fields.value === \"string\" ? fields.value : control?.value;\n if (value !== undefined) {\n fields.values = { [cell]: value };\n if (action === ACTION_ROW_INSERT && control?.value === value) {\n gathered.set(request, [{ cell: el, control, value }]);\n }\n }\n } else if (fields.value !== undefined) {\n console.error(\n `lb-hub: '${action}' from <${el.localName}> carries a value but no ` +\n `${ATTR_CELL} to say which column it is, not sent`,\n el,\n );\n return false;\n } else {\n const owner = el.closest(`form, tr, [${ATTR_KEY_VALUE}]`);\n const ours =\n !enclosing || (owner !== enclosing && enclosing.contains(owner));\n if (owner && ours) {\n const { values, read } = gatherValues(owner);\n if (read.length > 0) {\n fields.values = values;\n if (action === ACTION_ROW_INSERT) gathered.set(request, read);\n }\n } else if (enclosing) {\n console.error(\n `lb-hub: '${action}' from <${el.localName}> is in no form, table ` +\n `row or live row to gather values from, not sent; put its cells ` +\n `in a <form>`,\n el,\n );\n return false;\n }\n }\n }\n // An operation carries its values and nothing else: the value they were\n // built from is not a second copy on the wire.\n delete fields.value;\n const missing = operation.requires.filter((f) => fields[f] === undefined);\n if (missing.length > 0) {\n console.error(\n `lb-hub: '${action}' from <${el.localName}> has no ` +\n `${missing.join(\", \")} in scope, not sent`,\n el,\n );\n return false;\n }\n return true;\n}\n\n/** Dispatch the bare action an element carries, for the hub to scope. */\nfunction dispatchAction(el: Element): void {\n // Pressed again while its round trip is in flight. Ignoring it makes a\n // pending native action disabled in fact, so a stylesheet only has to\n // show it. A widget is not held back this way: one that sends on change\n // must have its latest value sent, not dropped.\n if (el.hasAttribute(ATTR_PENDING)) return;\n const detail = { action: el.getAttribute(ATTR_ACTION)! } as HubRequest;\n el.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));\n}\n\n/** A form control whose state is its `value`. */\ntype ValueControl = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;\n\n/** One cell a request read, the control it read, and the value it read. */\ninterface Read {\n cell: Element;\n control: ValueControl;\n value: string;\n}\n\n/**\n * What each insert gathered, until its round trip succeeds. Keyed by the\n * request itself, so a request an ancestor stops leaves nothing behind.\n */\nconst gathered = new WeakMap<HubRequest, Read[]>();\n\n/**\n * The control a cell reads from, for gathering rather than landing.\n *\n * A cell is always either a native form control itself (a bare\n * `<input lb-cell>`) or a widget that wraps one (`<lb-input>`, `<lb-select>`)\n * — that wrapping is structural, not incidental, which is what lets this be\n * one rule instead of one per widget type. The controls are the ones a value\n * lands on, so what is read back is what landed.\n */\nfunction controlOf(el: Element): ValueControl | undefined {\n return isValueControl(el)\n ? el\n : [...el.querySelectorAll(\"input, select, textarea\")].find(isValueControl);\n}\n\n/**\n * Every lb-cell of an element's scope, gathered into one values map. Shared\n * by lb-row-insert and lb-row-update — both submit a batch of cells, and\n * differ only in whether a key comes with them. What was read comes back\n * beside the map, so an insert can reset it once it succeeds.\n *\n * Found the way a row lands, so a cell inside a nested scope is that scope's\n * and is not read: the options of a picker in the row are the picker's rows,\n * while the picker's own cell is the row's.\n */\nfunction gatherValues(el: Element): {\n values: Record<string, string>;\n read: Read[];\n} {\n const values: Record<string, string> = {};\n const read: Read[] = [];\n for (const cell of within(el, `[${ATTR_CELL}]`)) {\n const name = cell.getAttribute(ATTR_CELL)!;\n const control = controlOf(cell);\n if (control === undefined) {\n console.warn(\n `lb-hub: lb-cell '${name}' has no input, select or textarea to ` +\n `read, ignoring it`,\n cell,\n );\n continue;\n }\n values[name] = control.value;\n read.push({ cell, control, value: control.value });\n }\n return { values, read };\n}\n\n/**\n * After an insert succeeds, what it gathered goes back to its default, the\n * way `form.reset()` resets a form: the row those cells held now lives in the\n * list. A control showing something other than what was read holds an edit\n * made during the round trip, which was not sent, and is left alone.\n *\n * Where a value landed, `lb-value` comes off the cell first, so a widget that\n * renders from it sees the attribute go and returns to its default. The\n * control is reset after, so it ends at its default whatever the widget did.\n * Nothing is dispatched, as `form.reset()` fires no `change`.\n */\nfunction resetGathered(read: Read[]): void {\n for (const { cell, control, value } of read) {\n if (control.value !== value) continue;\n cell.removeAttribute(ATTR_VALUE);\n control.removeAttribute(ATTR_VALUE);\n if (control.localName === \"select\") {\n for (const option of (control as HTMLSelectElement).options) {\n option.selected = option.defaultSelected;\n }\n } else {\n const text = control as HTMLInputElement | HTMLTextAreaElement;\n text.value = text.defaultValue;\n }\n }\n}\n\n/**\n * A path names a page host, and nothing finer. The bare path resolves to\n * `index`, the same convention a static file server follows — an\n * application names its landing page that way rather than the hub singling\n * one out.\n */\nfunction pageNameFor(path: string): string {\n const name = path.replace(/^\\/+|\\/+$/g, \"\");\n return name === \"\" ? \"index\" : name;\n}\n\n/**\n * The `<template>` a page's markup ships in. The builder guarantees one per\n * page name (build/locations.ts refuses two page files sharing a stub), so\n * a second match here means the document was assembled by something else,\n * and is worth saying out loud rather than resolving by document order.\n */\nfunction pageHostFor(page: string): HTMLTemplateElement | null {\n const hosts: HTMLTemplateElement[] = [];\n for (const el of document.querySelectorAll(`template[${ATTR_PAGE}]`)) {\n if (el.getAttribute(ATTR_PAGE) === page) {\n hosts.push(el as HTMLTemplateElement);\n }\n }\n if (hosts.length === 0) {\n console.error(`lb-hub: no page host for '${page}'`);\n return null;\n }\n if (hosts.length > 1) {\n console.error(\n `lb-hub: ${hosts.length} page hosts carry ${ATTR_PAGE}=\"${page}\", ` +\n `using the first`,\n );\n }\n return hosts[0]!;\n}\n\n/**\n * Fetch one round trip against the client-side deadline. Throws — rather\n * than swallowing — on a non-ok response, a network failure, or a timeout,\n * so a caller can tell a failed request from a genuinely empty result.\n *\n * The page names the path below the endpoint and the query string the\n * browser is showing rides after it verbatim, so every round trip, a page\n * load or an action, reaches the server with the URL on screen in hand.\n */\nasync function fetchData(page: string, request?: HubRequest): Promise<HubData> {\n const path = page.split(\"/\").map(encodeURIComponent).join(\"/\");\n const url = `${LB_ENDPOINT}/${path}${location.search}`;\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), LB_REQUEST_TIMEOUT_MS);\n try {\n // An empty body is how the hub asks for the page's whole query set.\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(request ?? {}),\n signal: controller.signal,\n });\n if (!res.ok) {\n throw new Error(`lb-hub: server responded ${res.status} for ${url}`);\n }\n return (await res.json()) as HubData;\n } finally {\n clearTimeout(timeout);\n }\n}\n\n/**\n * An element's round trip is in flight, or is not. aria-busy states the same\n * fact to assistive technology, which no stylesheet on lb-pending reaches.\n */\nfunction pending(el: Element | undefined, on: boolean): void {\n if (!el) return;\n if (on) {\n el.removeAttribute(ATTR_ERROR);\n el.setAttribute(ATTR_PENDING, \"\");\n el.setAttribute(\"aria-busy\", \"true\");\n } else {\n el.removeAttribute(ATTR_PENDING);\n el.removeAttribute(\"aria-busy\");\n }\n}\n\n/**\n * The hub is a singleton outside <main>. It survives every page change,\n * so it needs no id to disambiguate it.\n */\nclass LbHub extends HTMLElement {\n private main!: HTMLElement;\n\n /**\n * The page currently in <main>. A page declares its own requests, so a request\n * has to say which page it came from. The page rides on the URL rather\n * than in the request, leaving the operation set closed.\n */\n private page = \"\";\n\n connectedCallback() {\n this.main = this.querySelector(\"main\")!;\n\n // Scoped in the capture phase, on the way down, so every ancestor that\n // sees the request on its way back up sees it whole. A request that\n // cannot be sent is stopped here, before anything sees it at all.\n this.addEventListener(\n LB_EVENT_NAME,\n (e: Event) => {\n const origin = e.target as Element;\n const request = (e as CustomEvent<HubRequest>).detail;\n if (!scope(request, origin)) e.stopPropagation();\n },\n { capture: true },\n );\n\n this.addEventListener(LB_EVENT_NAME, (e: Event) => {\n const request = (e as CustomEvent<HubRequest>).detail;\n // The element that dispatched the request, not e.currentTarget (the\n // hub itself, since the event bubbles) — pending/error state belongs\n // at the origin so a widget can watch its own attributes.\n const origin = (e.target as Element | null) ?? undefined;\n const page = this.page;\n pending(origin, true);\n void fetchData(page, request)\n .then(async (data) => {\n const { [QUERY_PARMS_ROW]: written, ...load } = data;\n if (written === undefined) applyData(this, data);\n else {\n await this.changeQueryParms(\n page,\n written as Record<string, string>,\n load,\n );\n }\n resetGathered(gathered.get(request) ?? []);\n })\n .catch((err) => {\n console.error(`lb-hub: request failed`, err);\n origin?.setAttribute(ATTR_ERROR, \"\");\n })\n .finally(() => pending(origin, false));\n });\n\n /**\n * A native element carrying an action. The hub dispatches the bare action\n * from that element as the ordinary bubbling event, rather than sending\n * it directly, so it is scoped exactly as a widget's is and an ancestor\n * widget can still stop it and confirm. A native action button and a\n * hand-written widget therefore produce identical events.\n *\n * A widget carrying an action is left alone — it owns its own interaction\n * and decides what counts as performing it, which for a <select> is a\n * change rather than a click. The test is the same lexical one applyData\n * uses: a hyphen in the tag name.\n */\n this.addEventListener(\"click\", (e: MouseEvent) => {\n const el = (e.target as Element | null)?.closest(`[${ATTR_ACTION}]`);\n if (!el || el.localName.includes(\"-\")) return;\n // A form performs its action on submit, whatever was clicked in it.\n if (el instanceof HTMLFormElement) return;\n // A button beside the cells is pressed once they are done. An element\n // that is a cell, or holds them, has no such moment: a click into its\n // own input would send the row. Deciding when a cell has changed is a\n // widget's.\n const action = el.getAttribute(ATTR_ACTION);\n if (\n (action === ACTION_ROW_INSERT || action === ACTION_ROW_UPDATE) &&\n (el.hasAttribute(ATTR_CELL) || el.querySelector(`[${ATTR_CELL}]`))\n ) {\n console.error(\n `lb-hub: '${action}' on <${el.localName}> is or holds a cell, so ` +\n `a click on it is not sent; put it on a <form>, a button or a widget`,\n el,\n );\n return;\n }\n e.preventDefault();\n dispatchAction(el);\n });\n\n // The same delegation for the event a form owns.\n this.addEventListener(\"submit\", (e: SubmitEvent) => {\n const form = e.target as Element | null;\n if (!form || !form.hasAttribute(ATTR_ACTION)) return;\n e.preventDefault();\n dispatchAction(form);\n });\n\n // Only anchors that opted in. Everything else is a real link.\n this.addEventListener(\"click\", (e: MouseEvent) => {\n const link = (e.target as Element | null)?.closest(`a[${ATTR_NAV_LINK}]`);\n if (!link) return;\n e.preventDefault();\n const { pathname, search } = new URL((link as HTMLAnchorElement).href);\n history.pushState(null, \"\", pathname + search);\n void this.navigate();\n });\n\n /**\n * A control that writes a query parm. Changing what the page shows is\n * not going anywhere, so the address updates in place and stays\n * copyable, and history does not accumulate unless the control asks\n * for it. Only the one parm is touched: the rest of the query string may\n * have arrived by link, with no control on screen to say it again.\n */\n this.addEventListener(\"change\", (e: Event) => {\n const el = (e.target as Element | null)?.closest(`[${ATTR_QUERY_PARM}]`);\n if (!el || !this.contains(el)) return;\n const control = controlOf(el);\n if (control === undefined) {\n console.warn(\n `lb-hub: ${ATTR_QUERY_PARM} on <${el.localName}> has no input, ` +\n `select or textarea to read, ignoring it`,\n el,\n );\n return;\n }\n const name = el.getAttribute(ATTR_QUERY_PARM)!;\n const url = new URL(location.href);\n if ((url.searchParams.get(name) ?? \"\") === control.value) return;\n // Empty is the default, and a default is left out of the URL, so its\n // length tracks how far the user has narrowed the page.\n url.search = withQueryParms(url.searchParams, {\n [name]: control.value,\n }).toString();\n if (el.hasAttribute(ATTR_QUERY_PARM_PUSH)) {\n history.pushState(null, \"\", url);\n } else {\n history.replaceState(null, \"\", url);\n }\n void this.navigate(el);\n });\n\n addEventListener(\"popstate\", () => void this.navigate());\n\n void this.navigate();\n }\n\n /**\n * Load the page at the URL the browser is showing. Every way of arriving\n * comes through here — a cold load, a nav link, Back, a query parm write —\n * so each one is the same page load and a URL always shows the same thing.\n *\n * Entering a page is the one operation that replaces host DOM. The host is\n * already in the document, so selecting a page is a lookup and there is no\n * fetching mechanism on the host channel. Staying on the page, with only\n * the query string changed, keeps the DOM it has: the queries re-run and\n * land by key, so a list that got its rows back keeps them, and its scroll.\n *\n * `origin` is the control that changed the query string, if one did, and\n * carries the round trip's pending and error state as a request's origin\n * does.\n */\n private async navigate(origin?: Element): Promise<void> {\n const page = pageNameFor(location.pathname);\n this.landNavigation(page);\n const template = pageHostFor(page);\n if (!template) {\n this.reportUnknownPage();\n this.reveal();\n return;\n }\n if (page !== this.page) {\n this.page = page;\n // Insert the page shell before fetching its data, not after: the\n // template is already in the document, so holding it back behind a\n // network round trip only leaves <main> empty for no reason.\n this.main.replaceChildren(template.content.cloneNode(true));\n }\n this.reveal();\n let data: HubData = {};\n pending(origin, true);\n try {\n data = await fetchData(page);\n } catch (err) {\n console.error(`lb-hub: failed to load data for '${page}'`, err);\n origin?.setAttribute(ATTR_ERROR, \"\");\n } finally {\n pending(origin, false);\n }\n this.land(data);\n }\n\n /**\n * A page load's answer, landed: the page's data, then the query string on\n * the controls that write it, so a picker already has the options its\n * value selects.\n */\n private land(data: HubData): void {\n applyData(this, data);\n applyQueryParms(this.main, new URLSearchParams(location.search));\n }\n\n /**\n * A server response changed the query parms: the request wrote parms only\n * the write could know, such as the key of a row it inserted. The server\n * has already loaded the page at them, so this is the rest of a query parm\n * write — the URL, then the load landed — with the round trip done.\n *\n * The history entry is replaced, as a control's write replaces it: the\n * page is showing something else, not going somewhere.\n *\n * A load that did not come back, which the server says by sending the\n * parms alone, is asked for again, so a failure reports as a page load's.\n * It does not mark the element that sent the request: that request's\n * write succeeded, and an error on it would invite sending it twice.\n *\n * `page` is where the request was sent from. A user who left it while the\n * request was in flight is on a URL that is not the server's to change.\n */\n private async changeQueryParms(\n page: string,\n written: Record<string, string>,\n load: HubData,\n ): Promise<void> {\n if (pageNameFor(location.pathname) !== page) return;\n const url = new URL(location.href);\n url.search = withQueryParms(url.searchParams, written).toString();\n history.replaceState(null, \"\", url);\n if (Object.keys(load).length === 0) return this.navigate();\n this.landNavigation(page);\n this.land(load);\n }\n\n /**\n * An app that ships `<body hidden>` (see docs/reference/chrome.md) is\n * asking to stay invisible until there is something coherent to show,\n * rather than flash the chrome before `<main>` has real content. This is\n * a no-op for an app that doesn't use that convention.\n */\n private reveal(): void {\n document.body.hidden = false;\n }\n\n /**\n * The hub's own row: where the page is, in the vocabulary a server\n * answer arrives in, so a chrome displays it the way it displays anything\n * — `lb-row=\"lb-navigation\"` on a subtree, `lb-cell` on what shows a\n * value. Landed before the page host is looked up, so a miss has it too;\n * the unknown-page dialog is one consumer among whatever others the\n * chrome writes.\n *\n * The label is the nav's: the text of the first lb-nav-link anchor whose\n * href names this page. Taken from the nav rather than from a click, so a\n * click, a reload and the back button all land the same label — and a\n * path no anchor names lands an empty one, explicitly, so a consumer is\n * never left showing the previous page's.\n *\n * The URI is the path and query string together, as the address bar has\n * them: a link that opens a page narrowed is a different place from the\n * bare page, though the label, which names the page, is the same.\n *\n * Landed only where a subtree names it. applyData() warns about a query\n * with no scope, and a chrome that displays no navigation has nothing to\n * be warned about.\n */\n private landNavigation(page: string): void {\n if (!this.querySelector(`[${ATTR_ROW}=\"${NAV_ROW}\"]`)) return;\n let label = \"\";\n for (const a of this.querySelectorAll<HTMLAnchorElement>(\n `a[${ATTR_NAV_LINK}]`,\n )) {\n if (pageNameFor(new URL(a.href).pathname) !== page) continue;\n label = a.textContent?.trim() ?? \"\";\n break;\n }\n applyData(this, {\n [NAV_ROW]: {\n [NAV_CELL_LABEL]: label,\n [NAV_CELL_URI]: location.pathname + location.search,\n },\n });\n }\n\n /**\n * A path that resolves to no page host is discovered client-side, after a\n * successful 200 — every route gets the same document, so there is no\n * HTTP 404 to report (see docs/tutorials/010-pages-and-navigation.md).\n * lb-unknown-page is optional: an application that declares none gets\n * nothing beyond navigate()'s console.error. The builder guarantees the\n * element it's on is a <dialog> (build/assemble.ts), so this can call\n * showModal() without checking the tag here.\n *\n * Only opens it. What the dialog says it gets from landNavigation(),\n * already run, by naming the hub's row in lb-row like any subtree.\n *\n * Scoped to the hub, like every other lookup here, so that one rule holds\n * without exception: what the hub acts on is inside the hub.\n */\n private reportUnknownPage(): void {\n const dialog = this.querySelector(`[${ATTR_UNKNOWN_PAGE}]`);\n if (!dialog) return;\n const modal = dialog as HTMLDialogElement;\n if (!modal.open) modal.showModal();\n }\n}\n\ncustomElements.define(HUB_TAG_NAME, LbHub);\n"]}
@@ -8,14 +8,18 @@
8
8
  *
9
9
  * One endpoint, and no second:
10
10
  *
11
- * POST /lb?page=<name> an empty body asks for the page's whole query
11
+ * POST /lb/<page>?<query string>
12
+ * an empty body asks for the page's whole query
12
13
  * set; a body carrying an action runs that action
13
- * and answers with its refresh set
14
+ * and answers with its refresh set, or with the
15
+ * page loaded at the query parms it wrote
14
16
  *
15
- * The page rides on the query string rather than in the body, which is what
16
- * lets the operation set stay closed. Every hub call is a POST, so an
17
- * application's own GET routes never collide with this one and no page name
18
- * is reserved.
17
+ * The page rides on the path rather than in the body, which is what lets the
18
+ * operation set stay closed. The query string is the one the browser is
19
+ * showing, verbatim, and `contextFor` is handed its parms. What a parm means
20
+ * is the application's: nothing here reads one. Every hub call is a POST, so
21
+ * an application's own GET routes never collide with this one and no page
22
+ * name is reserved.
19
23
  */
20
24
  import { type Request, type Router } from "express";
21
25
  import type { Hub, HubContext } from "./lb-server.js";
@@ -25,7 +29,18 @@ import type { Hub, HubContext } from "./lb-server.js";
25
29
  * assumed. Whatever established identity has already run by the time this
26
30
  * router is reached; where that happens is the application's ordering
27
31
  * decision, not Hub's.
32
+ *
33
+ * The query parms arrive as `parms`, and are read there rather than off
34
+ * `req.query`. The two agree except after a request that wrote parms: the
35
+ * page is then loaded at the new query string in the same round trip, and
36
+ * this is called a second time for that load, with the same `req` and the
37
+ * new `parms`. So it must be safe to call twice for one request, and a
38
+ * write must be visible to the second context by the time its `run`
39
+ * returns.
40
+ *
41
+ * Parms are user input like any other, typed into an address bar or mailed
42
+ * in a link, and are validated where they are read.
28
43
  */
29
- export type ContextFor = (req: Request) => HubContext;
44
+ export type ContextFor = (req: Request, parms: URLSearchParams) => HubContext;
30
45
  export declare function hubRoutes(hub: Hub, contextFor: ContextFor): Router;
31
46
  //# sourceMappingURL=lb-express.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"lb-express.d.ts","sourceRoot":"","sources":["../../server/lb-express.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAgB,EAAE,KAAK,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAQ7D,OAAO,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEtD;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,UAAU,CAAC;AAEtD,wBAAgB,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,UAAU,GAAG,MAAM,CA2ElE"}
1
+ {"version":3,"file":"lb-express.d.ts","sourceRoot":"","sources":["../../server/lb-express.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAgB,EAEd,KAAK,OAAO,EAEZ,KAAK,MAAM,EACZ,MAAM,SAAS,CAAC;AAcjB,OAAO,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEtD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,eAAe,KAAK,UAAU,CAAC;AAE9E,wBAAgB,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,UAAU,GAAG,MAAM,CA2HlE"}