@loadbare/app 0.13.0 → 0.15.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 (51) hide show
  1. package/dist/build/assemble.d.ts.map +1 -1
  2. package/dist/build/assemble.js +31 -1
  3. package/dist/build/assemble.js.map +1 -1
  4. package/dist/build/elements.d.ts +12 -2
  5. package/dist/build/elements.d.ts.map +1 -1
  6. package/dist/build/elements.js +44 -10
  7. package/dist/build/elements.js.map +1 -1
  8. package/dist/core/lb-constants.d.ts +13 -2
  9. package/dist/core/lb-constants.d.ts.map +1 -1
  10. package/dist/core/lb-constants.js +50 -3
  11. package/dist/core/lb-constants.js.map +1 -1
  12. package/dist/core/lb-types.d.ts +52 -8
  13. package/dist/core/lb-types.d.ts.map +1 -1
  14. package/dist/core/lb-types.js.map +1 -1
  15. package/dist/hub/lb-apply.d.ts +22 -3
  16. package/dist/hub/lb-apply.d.ts.map +1 -1
  17. package/dist/hub/lb-apply.js +82 -37
  18. package/dist/hub/lb-apply.js.map +1 -1
  19. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  20. package/dist/hub/lb-hub.browser.js +382 -26
  21. package/dist/hub/lb-hub.browser.js.map +1 -1
  22. package/dist/hub/lb-lifecycle.d.ts +40 -0
  23. package/dist/hub/lb-lifecycle.d.ts.map +1 -0
  24. package/dist/hub/lb-lifecycle.js +140 -0
  25. package/dist/hub/lb-lifecycle.js.map +1 -0
  26. package/dist/server/lb-express.d.ts +13 -3
  27. package/dist/server/lb-express.d.ts.map +1 -1
  28. package/dist/server/lb-express.js +30 -12
  29. package/dist/server/lb-express.js.map +1 -1
  30. package/dist/server/lb-server.d.ts +31 -1
  31. package/dist/server/lb-server.d.ts.map +1 -1
  32. package/dist/server/lb-server.js +112 -2
  33. package/dist/server/lb-server.js.map +1 -1
  34. package/docs/TECHREF-1.0.md +343 -68
  35. package/docs/comparison.md +13 -9
  36. package/docs/possible-ideas.md +152 -26
  37. package/docs/reference/custom-elements.md +203 -88
  38. package/docs/reference/data-binding.md +310 -11
  39. package/docs/reference/page-files.md +76 -3
  40. package/docs/reference/server.md +20 -0
  41. package/docs/reference/widgets.md +35 -12
  42. package/docs/roadmap.md +9 -2
  43. package/docs/testing.md +2 -2
  44. package/package.json +1 -1
  45. package/skills/loadbare-app/SKILL.md +70 -23
  46. package/skills/loadbare-app/references/TECHREF-1.0.md +343 -68
  47. package/skills/loadbare-app/references/custom-elements.md +203 -88
  48. package/skills/loadbare-app/references/data-binding.md +310 -11
  49. package/skills/loadbare-app/references/page-files.md +76 -3
  50. package/skills/loadbare-app/references/server.md +20 -0
  51. package/skills/loadbare-app/references/widgets.md +35 -12
@@ -278,10 +278,10 @@ A custom element reads `lb-` attributes and never assigns one. The developer
278
278
  writes them in markup, and the hub and the builder write their stamps.
279
279
 
280
280
  Loadbare reserves method names beginning with `lb` on a custom element, for
281
- the methods it calls; see [Holding rows](#holding-rows). The build refuses
282
- a script that declares a method, accessor or field whose name is `lb`, a
283
- capital, and anything else Loadbare does not call, so a misspelled
284
- `lbRowLanded` fails the build rather than never being called.
281
+ the methods it calls; see [When its code runs](#when-its-code-runs). The
282
+ build refuses a script that declares a method, accessor or field whose name
283
+ is `lb`, a capital, and anything else Loadbare does not call, so a
284
+ misspelled `lbRowLanded` fails the build rather than never being called.
285
285
 
286
286
  ### What the hub and an element say to each other
287
287
 
@@ -291,39 +291,80 @@ Each direction has one mechanism for each kind of message:
291
291
  |----------------|--------------------------------------------|-----------------------------|-------|
292
292
  | Hub to element | State that lasts | An attribute the hub stamps | `lb-column-value`, `lb-key-value`, `lb-query-row-count`, `lb-group-*`, `lb-row-*`, `lb-request-pending`, `lb-request-error` |
293
293
  | Hub to element | State the platform already names | A property | A control's `value` |
294
- | Hub to element | Work the hub needs done now, while landing | An optional `lb` method | `lbRowsLanded` |
294
+ | Hub to element | What the hub did | An optional `lb` method | `lbAttached`, `lbRowsLanded`, `lbRequestDone`, `lbDetached` |
295
295
  | Hub to element | A moment an element started | A bubbling event | `lb-request-done` |
296
296
  | Element to hub | A moment | A bubbling event | `lb-request`, and `change`, `click` and `submit` |
297
297
 
298
- State is an attribute: a stylesheet can select on it, and an element that
299
- upgrades late still finds it. A method is for work the hub cannot go on
300
- without, done by one element at one point in landing. A moment is an event,
301
- because the element that cares is often an ancestor of the one it concerns,
302
- and an event reaches it knowing neither.
298
+ State is an attribute, because a stylesheet can select on it. A method is
299
+ how the hub tells one element what it just did to that element. A moment is
300
+ an event, because the element that cares is often an ancestor of the one it
301
+ concerns, and an event reaches it knowing neither.
303
302
 
304
303
  ### When its code runs
305
304
 
306
- The hub creates, places and parks elements, and each of those reaches a
307
- custom element as the browser's lifecycle callbacks. It is the same instance
308
- from its constructor on, however often it moves:
305
+ The hub tells a custom element what it did through four optional methods,
306
+ named for the hub's own work. The `HubLifecycle` interface in
307
+ `@loadbare/app/types` declares them. Here is an element that closes its
308
+ popup on Escape, listening on `document` only while the hub has it on the
309
+ page:
309
310
 
310
- | What the hub does | Callbacks, in order |
311
- |----------------------------------------------------|---------------------------------------------|
312
- | Shows a page | `constructor`, `connectedCallback` |
313
- | Creates a live row, filled before it is placed | `constructor` |
314
- | Places a live row for the first time | `connectedCallback` |
315
- | Places it again, as it does on every set of all rows | `disconnectedCallback`, `connectedCallback` |
316
- | Removes a live row | `disconnectedCallback` |
317
- | Turns `lb-show` off | `disconnectedCallback`, `adoptedCallback` |
318
- | Turns `lb-show` on | `adoptedCallback`, `connectedCallback` |
319
- | Turns on an element the builder shipped absent | `constructor`, `connectedCallback` |
311
+ ```ts
312
+ // src/note-popup.browser.ts
313
+ import type { HubLifecycle } from "@loadbare/app/types";
314
+
315
+ class NotePopup extends HTMLElement implements HubLifecycle {
316
+ #onKey = (e: KeyboardEvent) => {
317
+ if (e.key === "Escape") this.querySelector("dialog")?.close();
318
+ };
319
+
320
+ lbAttached() {
321
+ document.addEventListener("keydown", this.#onKey);
322
+ }
320
323
 
321
- The builder ships every `lb-show` element inside its template, and nothing
322
- in a template is upgraded, so such an element has no instance until its
323
- column first turns on. See
324
- [Conditional rendering](./data-binding.md#conditional-rendering).
324
+ lbDetached() {
325
+ document.removeEventListener("keydown", this.#onKey);
326
+ }
327
+ }
328
+
329
+ customElements.define("note-popup", NotePopup);
330
+ ```
325
331
 
326
- So each kind of work has one place:
332
+ | Method | The hub calls it |
333
+ |---------------------------------|-------------------------------------------------------------------|
334
+ | `lbAttached()` | Once, after it places the element and every column has landed on it |
335
+ | `lbRowsLanded()` | After rows land on the element, with every row placed and stamped |
336
+ | `lbRequestDone(detail, origin)` | After a request committed by the element, or inside it, has finished |
337
+ | `lbDetached()` | Once, after it removes the element |
338
+
339
+ The hub places an element when it starts up the chrome, shows a page,
340
+ creates a live row, or builds a group. It places the chrome and a page as
341
+ the page's first answer lands, or once that round trip fails, so an element
342
+ in either hears `lbAttached` with the page's data in place. It removes an
343
+ element when a live row or a group has finished leaving, and when it shows
344
+ another page in place of the one holding it. Write setup in `lbAttached` and its undoing in
345
+ `lbDetached`; the two pair exactly, so neither guards against running
346
+ twice.
347
+
348
+ Moving a row and turning `lb-show` off and on are neither placing nor
349
+ removing. The element keeps its instance and hears nothing. An element
350
+ under an `lb-show` that is off is placed inside its template, so its
351
+ `lbAttached` runs whether or not it shows. Measure and focus when the user
352
+ acts, never in `lbAttached`, where the element may not be rendered.
353
+
354
+ The hub calls `lbAttached` on an outer custom element before the ones
355
+ inside it, and `lbDetached` on the inner ones first. A table that sets the
356
+ `form` attribute on its controls in `lbAttached` does so before each control
357
+ hears its own.
358
+
359
+ An element a script creates hears none of the four, and neither does the
360
+ copy of a moved row the hub leaves behind while it fades. The hub calls
361
+ them only on what it places.
362
+
363
+ Never write `connectedCallback`, `disconnectedCallback`, `adoptedCallback`
364
+ or `connectedMoveCallback`. The browser calls those on every move, and the
365
+ build refuses a script that declares one.
366
+
367
+ Each kind of work then has one place:
327
368
 
328
369
  1. **A listener on the element itself goes in the constructor.** It is
329
370
  attached once and survives every move, and a live row gets its own
@@ -331,15 +372,11 @@ So each kind of work has one place:
331
372
  element, so the listener needs no child to exist yet. The constructor
332
373
  reads no attribute and no child: an element made with
333
374
  `document.createElement` has neither when it runs.
334
- 2. **A child is looked up when it is needed**, in a handler, a getter,
335
- or `lbRowsLanded`, and never held from setup.
336
- 3. **`connectedCallback` runs on every move**, so it is written to run
337
- again. It rearranges the element's own children into a state it checks
338
- for first, or adds a listener to `document` or `window`, which
339
- `disconnectedCallback` removes. Work done once per instance that needs
340
- the element's attributes, such as a control taking up a value that landed
341
- before it upgraded, goes behind a flag there; see
342
- [Being a control](#being-a-control).
375
+ 2. **A child is looked up when it is needed**, in a handler, a getter, or
376
+ one of the four methods, and never held from setup.
377
+ 3. **Work done once per instance goes in `lbAttached`**, with its undoing
378
+ in `lbDetached`: rearranging the element's own children, or a listener on
379
+ `document` or `window`.
343
380
 
344
381
  ### Being a control
345
382
 
@@ -350,20 +387,10 @@ and commits its `lb-request` on `change`:
350
387
 
351
388
  ```ts
352
389
  // src/star-rating.browser.ts
353
- import { ATTR_COLUMN_VALUE } from "@loadbare/app/constants";
354
-
355
390
  class StarRating extends HTMLElement {
356
391
  static formAssociated = true;
357
392
  #internals = this.attachInternals();
358
393
  #value = "";
359
- #connected = false;
360
-
361
- connectedCallback() {
362
- if (this.#connected) return;
363
- this.#connected = true;
364
- const stamped = this.getAttribute(ATTR_COLUMN_VALUE);
365
- if (stamped !== null) this.value = stamped;
366
- }
367
394
 
368
395
  get value(): string {
369
396
  return this.#value;
@@ -399,22 +426,109 @@ customElements.define("star-rating", StarRating);
399
426
  Dispatch `change` with `bubbles: true`, as a native control's does, when the
400
427
  user commits an edit. Setting `value` from the hub fires nothing.
401
428
 
402
- A control can receive its value before it is a control. An element the
403
- builder shipped absent, or one whose definition loads after the hub has
404
- landed, is not upgraded when its column lands, so the hub cannot set its
405
- `value` and the value arrives as `lb-column-value` alone. Take the stamp up
406
- the first time the control connects, as `connectedCallback` does above, and
407
- never again: after that the hub sets `value` itself, and a later move must
408
- not undo an edit.
409
-
410
429
  A form owns a form-associated custom element the way it owns an `<input>`,
411
430
  including through the HTML `form` attribute, so a form gathers it on submit.
412
431
  Write `formResetCallback` to restore the default value: the hub resets a form
413
432
  after a successful insert.
414
433
 
434
+ A control takes the message of a refusal naming its column when it has
435
+ `setCustomValidity` and `reportValidity`, as a native control does; see
436
+ [Refusals](./data-binding.md#refusals). A control wrapping a native one
437
+ passes both to it, so the browser shows the message there and focus lands
438
+ there.
439
+
415
440
  The `<lb-input>`, `<lb-select>`, `<lb-options>` and `<lb-picker>` widgets
416
441
  are controls; see [The Basic Widget Library](./widgets.md).
417
442
 
443
+ ### Example: a file's text as a value
444
+
445
+ The hub takes no file input as a control. A custom element wraps one and is
446
+ the control in its place. This one reads the chosen file as text, which
447
+ suits a CSV or a bank statement, and sends the file's name under a second
448
+ column:
449
+
450
+ ```html
451
+ <!-- src/file-text.html -->
452
+ <label>{{label}} <input type="file" accept="{{accept}}" /></label>
453
+ <input type="hidden" lb-column="{{name-column}}" />
454
+ ```
455
+
456
+ ```ts
457
+ // src/file-text.browser.ts
458
+ class FileText extends HTMLElement {
459
+ static formAssociated = true;
460
+ #internals = this.attachInternals();
461
+ #value = "";
462
+
463
+ constructor() {
464
+ super();
465
+ this.addEventListener("change", (e) => {
466
+ const input = e.target as HTMLInputElement;
467
+ if (e.target === this || input.type !== "file") return;
468
+ e.stopPropagation();
469
+ void this.read(input);
470
+ });
471
+ }
472
+
473
+ get value(): string {
474
+ return this.#value;
475
+ }
476
+
477
+ set value(value: string) {
478
+ this.#value = value ?? "";
479
+ this.#internals.setFormValue(this.#value);
480
+ }
481
+
482
+ formResetCallback() {
483
+ this.value = "";
484
+ this.setName("");
485
+ }
486
+
487
+ async read(input: HTMLInputElement) {
488
+ const file = input.files?.[0] ?? null;
489
+ this.value = file ? await file.text() : "";
490
+ this.setName(file?.name ?? "");
491
+ this.dispatchEvent(new Event("change", { bubbles: true }));
492
+ }
493
+
494
+ setName(name: string) {
495
+ const hidden = this.querySelector<HTMLInputElement>('input[type="hidden"]');
496
+ if (hidden) hidden.value = name;
497
+ }
498
+ }
499
+
500
+ customElements.define("file-text", FileText);
501
+ ```
502
+
503
+ ```html
504
+ <!-- src/uploads.page.html -->
505
+ <form lb-query="uploads" lb-request="lb-row-insert">
506
+ <file-text
507
+ lb-column="filetext"
508
+ exp-name-column="filename"
509
+ exp-label="Statement file"
510
+ exp-accept=".csv,.ofx"
511
+ ></file-text>
512
+ <button type="submit">Upload</button>
513
+ </form>
514
+ ```
515
+
516
+ Reading a file is asynchronous, so the element stops the file input's own
517
+ `change` and fires its own once the text is in hand. A form submitted before
518
+ then gathers the empty value, and the handler refuses it as no file.
519
+
520
+ A control gathers one column, so the name rides in a hidden input that the
521
+ hub gathers as a control of its own. The form gathers both on submit. Its
522
+ reset after the insert clears the file input but not the hidden one, whose
523
+ `value` is its default, so `formResetCallback` clears the name itself.
524
+
525
+ The text travels in the request's JSON body, which `hubRoutes` limits to
526
+ 100 KB unless the server raises it; see [Options](./server.md#options).
527
+
528
+ The choices here are this application's: text rather than binary, the whole
529
+ file in the request, the name in a second column. A different application
530
+ writes its own element, under its own name, to make different ones.
531
+
418
532
  ### Receiving a value
419
533
 
420
534
  A custom element that is not a control keeps the content the builder placed
@@ -476,35 +590,42 @@ Let the event bubble, so an ancestor can stop it before the hub sends it.
476
590
  ### Hearing how it turned out
477
591
 
478
592
  Once what a request brought back has landed, or its round trip has failed,
479
- the hub dispatches `lb-request-done` from the element that committed. It
480
- bubbles, so the element that cares need not be the one that committed: a
481
- dialog hears it from the form inside it.
593
+ the hub calls `lbRequestDone(detail, origin)` on the custom element that
594
+ committed and on every custom element around it, innermost first. `origin`
595
+ is the element that committed, so the element that cares need not be the
596
+ one that committed: a dialog hears it from the form inside it.
482
597
 
483
598
  ```ts
484
- import { LB_DONE_EVENT_NAME } from "@loadbare/app/constants";
485
- import type { HubRequestDone } from "@loadbare/app/types";
486
-
487
- this.addEventListener(LB_DONE_EVENT_NAME, (e) => {
488
- const { request, items, error } = (e as CustomEvent<HubRequestDone>).detail;
489
- if (error) return;
490
- const item = items.find((i) => i.query === request.query);
491
- const created = item?.patch?.rows?.[0];
492
- if (created) this.choose(String(created[item!.key]));
493
- });
599
+ import type { HubLifecycle, HubRequestDone } from "@loadbare/app/types";
600
+
601
+ class NewRowDialog extends HTMLElement implements HubLifecycle {
602
+ lbRequestDone({ request, items, error }: HubRequestDone) {
603
+ if (error) return;
604
+ const item = items.find((i) => i.query === request.query);
605
+ const created = item?.patch?.rows?.[0];
606
+ if (created) this.choose(String(created[item!.key]));
607
+ }
608
+ }
494
609
  ```
495
610
 
611
+ The hub then dispatches `lb-request-done` from the element that committed,
612
+ carrying the same `detail` as a bubbling `CustomEvent`, for any other
613
+ listener. Import its name as `LB_DONE_EVENT_NAME`.
614
+
496
615
  `items` is every response item that landed because of the request, in
497
- order. An answer that moved the URL contributes its `lb-url` item, which
616
+ order. `refusal` is set beside `error` when the handler refused the
617
+ request, holding its `message` and `columns`. An answer that moved the URL contributes its `lb-url` item, which
498
618
  carries a key only the server knew, and the page load. `error` is set when
499
619
  the round trip failed, and then nothing landed. A new row's key is in the
500
620
  answer only when the handler returned it in a patch: a refresh sends all
501
621
  rows, and nothing marks which one is new.
502
622
 
503
- By the time the event is dispatched, `lb-request-pending` is gone and the
504
- page is as the answer left it. A request that was never sent, because the
505
- hub or an ancestor stopped it, gets no `lb-request-done`. An element the
506
- answer removed, such as the row a delete took away, dispatches the event
507
- outside the document, and no ancestor it had there hears it.
623
+ By the time either is called, `lb-request-pending` is gone and the page is
624
+ as the answer left it. A request that was never sent, because the hub or an
625
+ ancestor stopped it, gets neither. An element the answer removed, such as
626
+ the row a delete took away, is outside the document by then: no ancestor it
627
+ had there hears the event, and no custom element the hub has removed hears
628
+ `lbRequestDone`.
508
629
 
509
630
  ### Holding rows
510
631
 
@@ -515,26 +636,22 @@ plain element carrying `lb-query` with a row template inside it needs no
515
636
  code, so a custom element holds rows only to do something with them once
516
637
  they land.
517
638
 
518
- A custom element carrying `lb-query` and a row template may implement one
519
- optional method, which the hub calls:
639
+ A custom element carrying `lb-query` and a row template implements
640
+ `lbRowsLanded`, which the hub calls once the rows have landed, with every
641
+ row placed and stamped:
520
642
 
521
643
  ```ts
522
- import type { RowsHost } from "@loadbare/app/types";
644
+ import type { HubLifecycle } from "@loadbare/app/types";
523
645
  import { ATTR_ROW_REQUESTED, REQUESTED_CREATED } from "@loadbare/app/constants";
524
646
 
525
- class ScrollingList extends HTMLElement implements RowsHost {
647
+ class ScrollingList extends HTMLElement implements HubLifecycle {
526
648
  lbRowsLanded() {
527
- // Once, after the rows have landed, every row placed and stamped.
528
649
  this.querySelector(`[${ATTR_ROW_REQUESTED}="${REQUESTED_CREATED}"]`)
529
650
  ?.scrollIntoView({ block: "nearest" });
530
651
  }
531
652
  }
532
653
  ```
533
654
 
534
- | Method | The hub calls it |
535
- |----------------|-------------------------------------------------------|
536
- | `lbRowsLanded` | After the rows land, with every row placed and stamped |
537
-
538
655
  Everything else is the hub's:
539
656
 
540
657
  | Concern | The hub |
@@ -553,11 +670,9 @@ gives each option and `<optgroup>` what landing does not, a value and a
553
670
  label, and the other scrolls to the row the page's request created or
554
671
  moved.
555
672
 
556
- A custom element the builder shipped absent under `lb-show` has not
557
- upgraded while its column is off, and the hub places the rows that land on
558
- it then as it places any. When the column first turns on and the element
559
- upgrades, the hub lands the query's last answer on it again, as all rows,
560
- so `lbRowsLanded` runs.
673
+ A custom element under an `lb-show` that is off still holds its rows. The
674
+ hub places the rows that land on it then as it places any, and calls its
675
+ `lbRowsLanded`, so it returns current.
561
676
 
562
677
  A custom element that walks its own rows finds them with `LIVE_ROW` from
563
678
  `@loadbare/app/constants`, never by `lb-key-value`: an element a `row` lands