@loadbare/app 0.4.0 → 0.5.1

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 (165) hide show
  1. package/README.md +53 -82
  2. package/dist/build/assemble.d.ts +7 -5
  3. package/dist/build/assemble.d.ts.map +1 -1
  4. package/dist/build/assemble.js +29 -9
  5. package/dist/build/cli.d.ts +20 -12
  6. package/dist/build/cli.d.ts.map +1 -1
  7. package/dist/build/cli.js +34 -16
  8. package/dist/build/elements.d.ts +15 -29
  9. package/dist/build/elements.d.ts.map +1 -1
  10. package/dist/build/elements.js +25 -111
  11. package/dist/build/expand.d.ts +1 -1
  12. package/dist/build/expand.js +1 -1
  13. package/dist/build/format.d.ts +6 -3
  14. package/dist/build/format.d.ts.map +1 -1
  15. package/dist/build/format.js +6 -3
  16. package/dist/build/locations.d.ts +14 -37
  17. package/dist/build/locations.d.ts.map +1 -1
  18. package/dist/build/locations.js +31 -69
  19. package/dist/build/origins.d.ts +109 -0
  20. package/dist/build/origins.d.ts.map +1 -0
  21. package/dist/build/origins.js +270 -0
  22. package/dist/core/lb-constants.d.ts +1 -0
  23. package/dist/core/lb-constants.d.ts.map +1 -1
  24. package/dist/core/lb-constants.js +15 -8
  25. package/dist/core/lb-types.d.ts +2 -2
  26. package/dist/core/lb-types.d.ts.map +1 -1
  27. package/dist/hub/lb-apply.js +1 -1
  28. package/dist/hub/lb-hub.d.ts.map +1 -1
  29. package/dist/hub/lb-hub.js +44 -17
  30. package/dist/hub/lb-rows.d.ts.map +1 -1
  31. package/dist/hub/lb-rows.js +3 -3
  32. package/dist/server/lb-express.d.ts.map +1 -1
  33. package/dist/server/lb-server.d.ts +5 -4
  34. package/dist/server/lb-server.d.ts.map +1 -1
  35. package/dist/tests/assemble.test.js +11 -4
  36. package/dist/tests/elements.test.js +47 -51
  37. package/dist/tests/expand.test.d.ts +1 -1
  38. package/dist/tests/expand.test.js +2 -2
  39. package/dist/tests/fixtures/elements/collision/imports.d.ts +3 -0
  40. package/dist/tests/fixtures/elements/collision/imports.d.ts.map +1 -0
  41. package/dist/tests/fixtures/elements/collision/imports.js +1 -0
  42. package/dist/tests/fixtures/elements/manifest/imports.d.ts +3 -0
  43. package/dist/tests/fixtures/elements/manifest/imports.d.ts.map +1 -0
  44. package/dist/tests/fixtures/elements/manifest/imports.js +1 -0
  45. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts +3 -0
  46. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts.map +1 -0
  47. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.js +1 -0
  48. package/dist/tests/fixtures/elements/{collision/elements.d.ts → manifest-not-array/imports.d.ts} +1 -1
  49. package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts.map +1 -0
  50. package/dist/tests/fixtures/elements/manifest-not-array/imports.js +1 -0
  51. package/dist/tests/fixtures/elements/pkg/acme-widget.d.ts +2 -0
  52. package/dist/tests/fixtures/elements/pkg/acme-widget.d.ts.map +1 -0
  53. package/dist/tests/fixtures/elements/pkg/acme-widget.js +1 -0
  54. package/dist/tests/lb-express.test.js +1 -1
  55. package/dist/tests/origins.test.d.ts +10 -0
  56. package/dist/tests/origins.test.d.ts.map +1 -0
  57. package/dist/tests/origins.test.js +326 -0
  58. package/dist/tests/pages.test.js +3 -3
  59. package/dist/tests/styles.test.js +7 -4
  60. package/docs/reference/builder.md +128 -0
  61. package/docs/reference/chrome.md +75 -0
  62. package/docs/reference/css.md +44 -0
  63. package/docs/reference/custom-elements.md +327 -0
  64. package/docs/reference/data-binding.md +240 -0
  65. package/docs/reference/overview.md +38 -0
  66. package/docs/reference/page-files.md +175 -0
  67. package/docs/reference/server.md +123 -0
  68. package/docs/reference/widgets.md +163 -0
  69. package/docs/roadmap.md +130 -0
  70. package/docs/testing.md +228 -0
  71. package/docs/theory.md +344 -223
  72. package/docs/tutorials/000-getting-started.md +86 -0
  73. package/docs/tutorials/010-pages-and-navigation.md +129 -0
  74. package/docs/tutorials/020-css.md +103 -0
  75. package/docs/tutorials/030-html-decomposition.md +79 -0
  76. package/docs/tutorials/040-displaying-data.md +169 -0
  77. package/docs/tutorials/050-actions.md +77 -0
  78. package/docs/tutorials/060-custom-element-code.md +73 -0
  79. package/docs/tutorials/065-conditional-rendering.md +161 -0
  80. package/docs/tutorials/070-displaying-a-list.md +137 -0
  81. package/docs/tutorials/072-inserting-into-a-list.md +88 -0
  82. package/docs/tutorials/074-deleting-from-a-list.md +77 -0
  83. package/docs/tutorials/076-updating-a-list-item.md +86 -0
  84. package/docs/tutorials/080-widget-requests.md +124 -0
  85. package/docs/tutorials/090-using-widget-libraries.md +75 -0
  86. package/package.json +10 -18
  87. package/dist/client.js +0 -522
  88. package/dist/demo-static/src/widgets/app-box.d.ts +0 -15
  89. package/dist/demo-static/src/widgets/app-box.d.ts.map +0 -1
  90. package/dist/demo-static/src/widgets/app-box.js +0 -19
  91. package/dist/tests/fixtures/elements/collision/elements.d.ts.map +0 -1
  92. package/dist/tests/fixtures/elements/collision/elements.js +0 -3
  93. package/dist/tests/fixtures/elements/manifest/elements.d.ts +0 -5
  94. package/dist/tests/fixtures/elements/manifest/elements.d.ts.map +0 -1
  95. package/dist/tests/fixtures/elements/manifest/elements.js +0 -3
  96. package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts +0 -5
  97. package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts.map +0 -1
  98. package/dist/tests/fixtures/elements/manifest-bad-tag/elements.js +0 -3
  99. package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts +0 -5
  100. package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts.map +0 -1
  101. package/dist/tests/fixtures/elements/manifest-bad-value/elements.js +0 -3
  102. package/dist/tests/golden.test.d.ts +0 -19
  103. package/dist/tests/golden.test.d.ts.map +0 -1
  104. package/dist/tests/golden.test.js +0 -60
  105. package/dist/tests/helpers/window.d.ts +0 -43
  106. package/dist/tests/helpers/window.d.ts.map +0 -1
  107. package/dist/tests/helpers/window.js +0 -78
  108. package/dist/tests/lb-input.test.d.ts +0 -9
  109. package/dist/tests/lb-input.test.d.ts.map +0 -1
  110. package/dist/tests/lb-input.test.js +0 -78
  111. package/dist/tests/lb-list.test.d.ts +0 -12
  112. package/dist/tests/lb-list.test.d.ts.map +0 -1
  113. package/dist/tests/lb-list.test.js +0 -44
  114. package/dist/tests/lb-options.test.d.ts +0 -10
  115. package/dist/tests/lb-options.test.d.ts.map +0 -1
  116. package/dist/tests/lb-options.test.js +0 -121
  117. package/dist/tests/lb-picker.test.d.ts +0 -14
  118. package/dist/tests/lb-picker.test.d.ts.map +0 -1
  119. package/dist/tests/lb-picker.test.js +0 -59
  120. package/dist/tests/lb-select.test.d.ts +0 -9
  121. package/dist/tests/lb-select.test.d.ts.map +0 -1
  122. package/dist/tests/lb-select.test.js +0 -71
  123. package/dist/tests/lb-table.test.d.ts +0 -15
  124. package/dist/tests/lb-table.test.d.ts.map +0 -1
  125. package/dist/tests/lb-table.test.js +0 -205
  126. package/dist/widgets/index.d.ts +0 -7
  127. package/dist/widgets/index.d.ts.map +0 -1
  128. package/dist/widgets/index.js +0 -6
  129. package/dist/widgets/lb-input.d.ts +0 -2
  130. package/dist/widgets/lb-input.d.ts.map +0 -1
  131. package/dist/widgets/lb-input.js +0 -48
  132. package/dist/widgets/lb-list.d.ts +0 -2
  133. package/dist/widgets/lb-list.d.ts.map +0 -1
  134. package/dist/widgets/lb-list.js +0 -17
  135. package/dist/widgets/lb-options.d.ts +0 -26
  136. package/dist/widgets/lb-options.d.ts.map +0 -1
  137. package/dist/widgets/lb-options.js +0 -72
  138. package/dist/widgets/lb-picker.d.ts +0 -2
  139. package/dist/widgets/lb-picker.d.ts.map +0 -1
  140. package/dist/widgets/lb-picker.js +0 -25
  141. package/dist/widgets/lb-select.d.ts +0 -2
  142. package/dist/widgets/lb-select.d.ts.map +0 -1
  143. package/dist/widgets/lb-select.js +0 -43
  144. package/dist/widgets/lb-table.d.ts +0 -2
  145. package/dist/widgets/lb-table.d.ts.map +0 -1
  146. package/dist/widgets/lb-table.js +0 -113
  147. package/docs/application-chrome.md +0 -36
  148. package/docs/building-html-pages.md +0 -130
  149. package/docs/getting-started.md +0 -120
  150. package/docs/guide.md +0 -1164
  151. package/docs/hosting.md +0 -218
  152. package/docs/latent-risks.md +0 -20
  153. package/widgets/index.ts +0 -6
  154. package/widgets/lb-input.html +0 -1
  155. package/widgets/lb-input.ts +0 -64
  156. package/widgets/lb-list.html +0 -1
  157. package/widgets/lb-list.ts +0 -21
  158. package/widgets/lb-options.html +0 -4
  159. package/widgets/lb-options.ts +0 -88
  160. package/widgets/lb-picker.html +0 -7
  161. package/widgets/lb-picker.ts +0 -27
  162. package/widgets/lb-select.html +0 -4
  163. package/widgets/lb-select.ts +0 -55
  164. package/widgets/lb-table.html +0 -8
  165. package/widgets/lb-table.ts +0 -126
@@ -9,6 +9,7 @@ import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
9
9
  import { tmpdir } from "node:os";
10
10
  import path from "node:path";
11
11
  import { resolveLocations } from "../build/locations";
12
+ import { cssFrom, scanAll } from "../build/origins";
12
13
  import { stylesSource } from "../build/styles";
13
14
  const CHROME = `<!doctype html><body><lb-hub><main></main></lb-hub></body>`;
14
15
  async function tree(files) {
@@ -23,12 +24,13 @@ async function tree(files) {
23
24
  describe("discovering CSS", () => {
24
25
  it("finds a .css file anywhere under src", async () => {
25
26
  const src = await tree({
26
- "chrome.chrome.html": CHROME,
27
+ "chrome.html": CHROME,
27
28
  "widgets/app-box.css": ".app-box {}",
28
29
  "styles/00-reset.css": "* { margin: 0; }",
29
30
  });
30
31
  try {
31
- const { cssFiles } = await resolveLocations({ src, out: `${src}/dist` });
32
+ const locations = await resolveLocations({ src, out: `${src}/dist` });
33
+ const cssFiles = cssFrom(await scanAll(locations, src, locations.importsFile));
32
34
  assert.equal(cssFiles.length, 2);
33
35
  }
34
36
  finally {
@@ -37,12 +39,13 @@ describe("discovering CSS", () => {
37
39
  });
38
40
  it("orders by filename, not by directory, so a global sorts first by naming itself 00-", async () => {
39
41
  const src = await tree({
40
- "chrome.chrome.html": CHROME,
42
+ "chrome.html": CHROME,
41
43
  "widgets/app-box.css": ".app-box {}",
42
44
  "00-reset.css": "* { margin: 0; }",
43
45
  });
44
46
  try {
45
- const { cssFiles } = await resolveLocations({ src, out: `${src}/dist` });
47
+ const locations = await resolveLocations({ src, out: `${src}/dist` });
48
+ const cssFiles = cssFrom(await scanAll(locations, src, locations.importsFile));
46
49
  assert.deepEqual(cssFiles.map((f) => path.basename(f)), ["00-reset.css", "app-box.css"]);
47
50
  }
48
51
  finally {
@@ -0,0 +1,128 @@
1
+ # The Builder
2
+
3
+ The application runs `loadbare-app-build`. The builder reads the application's
4
+ source tree and writes the files the server serves.
5
+
6
+ ## Running the builder
7
+
8
+ ```
9
+ loadbare-app-build [--src src] [--out dist] [--watch] [--minify]
10
+ ```
11
+
12
+ | Option | Effect |
13
+ |------------|------------------------------------------------------|
14
+ | `--src` | The tree to scan, default `src` |
15
+ | `--out` | Where output lands, default `dist` |
16
+ | `--watch` | Build once, then again on every change under `--src` |
17
+ | `--minify` | Minify `client.js` and `app.css` |
18
+
19
+ Run it from the application's own `package.json`:
20
+
21
+ ```json
22
+ {
23
+ "scripts": {
24
+ "dev": "loadbare-app-build --watch & tsx server.ts"
25
+ }
26
+ }
27
+ ```
28
+
29
+ `--watch` watches every `.html`, `.ts`, and `.css` file under `--src`, and
30
+ reports a failed build to the console without stopping. It runs no dev server
31
+ and reloads no browser.
32
+
33
+ `--minify` leaves `app.html` alone.
34
+
35
+ ## What the builder reads
36
+
37
+ The builder classifies by name, not location.
38
+
39
+ | Name | Role |
40
+ |----------------------------|---------------------------------------------------|
41
+ | `chrome.html` | The chrome — see [`chrome.html`](./chrome.md) |
42
+ | `*.page.html` | A page |
43
+ | `*.hooks.ts` | A page's hooks, matched by base name |
44
+ | `*.queries.ts` | A page's queries, matched by base name |
45
+ | `imports.ts` | The packages this app takes widgets from |
46
+ | `*.css` | A stylesheet — see [CSS](./css.md) |
47
+ | Any other `.html` | A widget definition, named for the tag it defines |
48
+ | Any other tag-shaped `.ts` | A widget script, named for the tag it registers |
49
+
50
+ Give the application exactly one `chrome.html` and at most one `imports.ts`.
51
+ Give every `.hooks.ts` and `.queries.ts` a `.page.html` of the same base name.
52
+
53
+ ## Widgets from packages
54
+
55
+ List a package in `imports.ts` to take widgets from it:
56
+
57
+ ```ts
58
+ // src/imports.ts
59
+ export default ["@acme/widgets"];
60
+ ```
61
+
62
+ The builder scans one directory of that package the same way it scans `--src`,
63
+ finding a widget by its filename. Nothing names a tag: `acme-widget.html` and
64
+ `acme-widget.js` there define and register `<acme-widget>`.
65
+
66
+ Which directory is the package's own to declare, in its `package.json`:
67
+
68
+ ```json
69
+ {
70
+ "loadbare": { "widgets": "./dist" }
71
+ }
72
+ ```
73
+
74
+ A package that declares nothing has its whole installed directory scanned.
75
+ That is what a package of hand-written widgets wants and it needs no field at
76
+ all; a package that compiles wants the field, because its source and its
77
+ compiled output both carry `acme-widget`, and two files claiming one tag in one
78
+ origin is an error. Declaring the directory that ships settles which one the
79
+ builder means, and leaves the rest of the package — the README, the docs, the
80
+ tests — out of the question entirely.
81
+
82
+ The path must be inside the package and must exist in it once installed;
83
+ either failure names the package and stops the build.
84
+
85
+ An application author writes nothing for this and installs the package as they
86
+ would any other. The declaration is the package author's, made once.
87
+
88
+ ## Where the builder looks
89
+
90
+ Definitions, scripts and stylesheets all come from the same ordered origins:
91
+
92
+ | Order | Origin |
93
+ |-------|----------------------------------------------------|
94
+ | 1 | `@loadbare/app` itself, which supplies `lb-hub` |
95
+ | 2 | Each package in `imports.ts`, in the order listed |
96
+ | 3 | The application's own `--src` tree |
97
+
98
+ The first origin holds one tag. Every application has a hub whether or not it
99
+ says so, so the builder supplies that one and nothing else; a widget comes
100
+ from a package the application lists, including
101
+ [`@loadbare/widgets`](./widgets.md).
102
+
103
+ Two files claiming one tag within a single origin is an error, naming both.
104
+ Across origins the later one wins, so the application overrides a package,
105
+ and a package overrides `lb-hub`. Stylesheets follow the same order — see
106
+ [CSS](./css.md).
107
+
108
+ A tag with neither a script nor a definition in any origin is an error.
109
+
110
+ ## What the builder writes
111
+
112
+ | File | Contents |
113
+ |-------------|-----------------------------------------------------|
114
+ | `app.html` | The chrome, with every page inside a `<template>` |
115
+ | `client.js` | Every widget class the document uses, in one bundle |
116
+ | `app.css` | Every stylesheet, concatenated |
117
+ | `pages.ts` | The `hub` the server passes to `hubRoutes` |
118
+
119
+ The builder expands the chrome and every page against the available widget
120
+ definitions — see [Custom Elements](./custom-elements.md#html) for the
121
+ substitution rules — wraps each expanded page in
122
+ `<template id="page-<name>">`, and splices them into the chrome's `<body>`. It formats the result with
123
+ Prettier when the application has it installed.
124
+
125
+ The builder writes `app.css` only when it finds a stylesheet, and `pages.ts`
126
+ only when some page has a `.hooks.ts` or a `.queries.ts` file. Import `hub`
127
+ from `pages.ts` — see [The Express Server](./server.md) for the rest of the
128
+ wiring.
@@ -0,0 +1,75 @@
1
+ # chrome.html
2
+
3
+ The chrome is the application's one HTML document. Banner, navigation,
4
+ footer, dialogs — everything that is not a page lives in the chrome, written
5
+ once.
6
+
7
+ The builder finds the chrome by name: exactly one file called `chrome.html`,
8
+ anywhere in the `src/` tree. Any directory will do.
9
+
10
+ ## A complete chrome
11
+
12
+ Here is a minimal but fully complaint chrome for a typical app:
13
+
14
+ ```html
15
+ <!-- src/chrome.html -->
16
+ <!doctype html>
17
+ <html lang="en">
18
+ <head>
19
+ <meta charset="utf-8" />
20
+ <title>Membership Roster</title>
21
+ <script src="/client.js" defer></script>
22
+ <link rel="stylesheet" href="/app.css" />
23
+ </head>
24
+ <body>
25
+ <lb-hub>
26
+ <header><h1>Membership Roster</h1></header>
27
+ <nav>
28
+ <a href="/" lb-nav-link>Home</a>
29
+ <a href="/members" lb-nav-link>Members</a>
30
+ <a href="https://example.org/">Our website</a>
31
+ </nav>
32
+ <main></main>
33
+ <dialog lb-unknown-page>
34
+ The page <span lb-cell="page"></span> is not in this app.
35
+ </dialog>
36
+ </lb-hub>
37
+ </body>
38
+ </html>
39
+ ```
40
+
41
+ The chrome is plain HTML; nothing in it is generated or templated.
42
+
43
+ ## The parts of a chrome
44
+
45
+ | Required | Description |
46
+ |-------------------------------------|-------------------------------------------|
47
+ | Exactly one `chrome.html` in `src/` | The one chrome file, found by name |
48
+ | A complete HTML document | Doctype, `<html>`, `<head>`, `<body>` |
49
+ | `<lb-hub>` inside `<body>` | The application's live element |
50
+ | An empty `<main>` inside `<lb-hub>` | Holds the current page |
51
+ | `<script src="/client.js" defer>` | Defines `<lb-hub>` and every other widget |
52
+
53
+ Everything else is optional:
54
+
55
+ | Optional | Description |
56
+ |-------------------------------------------|---------------------------------------------|
57
+ | `<link rel="stylesheet" href="/app.css">` | The bundled stylesheet |
58
+ | `<a lb-nav-link>` | Navigation between pages |
59
+ | `<dialog lb-unknown-page>` | A message when a URL matches no page |
60
+ | Custom elements | The chrome, decomposed into widget files |
61
+ | Any other HTML | Header, footer, skip links, meta tags, etc. |
62
+
63
+ ## Rules for writing chrome
64
+
65
+ Anything the user interacts with must be inside `<lb-hub>`. The hub normally
66
+ sits directly inside `<body>`, with banner, nav, footer and `<main>` inside
67
+ it, so Loadbare can act on all of them.
68
+
69
+ A navigation anchor's `href` is a path, and the path names a page:
70
+ `/members` shows `members.page.html`. A bare `/` resolves to `index`, so the
71
+ landing page is the one named `index.page.html`. An anchor without
72
+ `lb-nav-link` is left alone and behaves like any other link.
73
+
74
+ The `lb-unknown-page` attribute, if used, must appear on a `<dialog>`.
75
+ Inside it, `lb-cell="page"` shows the name of the page that was asked for.
@@ -0,0 +1,44 @@
1
+ # CSS
2
+
3
+ The builder concatenates every `.css` file it finds into one stylesheet,
4
+ `<out>/app.css`. Nothing is scoped, renamed, or removed.
5
+
6
+ Write ordinary CSS against ordinary markup. Loadbare uses light DOM, so
7
+ every selector reaches every element, in the chrome and in a widget alike.
8
+
9
+ Put stylesheets anywhere under `src/`. The builder finds them by extension
10
+ and pairs them with nothing — a `.css` file is not a widget's, a page's, or
11
+ the chrome's.
12
+
13
+ ## Order
14
+
15
+ The bundle is assembled in partitions, one per origin:
16
+
17
+ | Partition | Holds |
18
+ |----------------------------------|-------------------------------------------|
19
+ | Each package in `imports.ts` | Every `.css` file inside that package |
20
+ | The application's own `src` tree | Every `.css` file under `--src` |
21
+
22
+ Package partitions come first, in the order `imports.ts` first mentions each
23
+ package. The application's own partition is always last, so its rules have
24
+ the final say over any imported package's.
25
+
26
+ A package's partition excludes that package's own nested `node_modules`.
27
+
28
+ Within a partition, files sort by filename. The directory does not affect
29
+ the order, and the full path only breaks a tie. Name a global stylesheet
30
+ `00-reset.css`, or similar, to sort it to the front of its partition.
31
+
32
+ ## What ships
33
+
34
+ The builder writes `app.css` only when it finds at least one stylesheet.
35
+ Serve it at `/app.css` and link it from the chrome:
36
+
37
+ ```html
38
+ <link rel="stylesheet" href="/app.css" />
39
+ ```
40
+
41
+ See [`chrome.html`](./chrome.md) for the link, [The Express
42
+ server](./server.md) for the route, and
43
+ [`loadbare-app-build`](./builder.md#running-the-builder) for `--minify`.
44
+ </content>
@@ -0,0 +1,327 @@
1
+ # Custom Elements
2
+
3
+ A widget is a custom element, written as a set of files sharing one tag
4
+ name. It serves two purposes, and an application uses it for either or for
5
+ both at once.
6
+
7
+ | File | Holds |
8
+ |------------------|----------------------------------|
9
+ | `<tag-name>.html` | The markup the tag expands into |
10
+ | `<tag-name>.ts` | The class the tag registers |
11
+
12
+ Write one of the two, or both, but write at least one. A tag with neither is
13
+ a build error.
14
+
15
+ Custom elements are the only mechanism Loadbare offers for either purpose.
16
+ An application decomposes its HTML by defining a tag, and delivers behavior
17
+ to the browser by registering one; the builder recognizes no other way to do
18
+ either.
19
+
20
+ Put the files anywhere under `src/`. The builder finds them by name, the
21
+ same way it finds pages — see [The Builder](./builder.md).
22
+
23
+ Loadbare uses light DOM throughout. A widget's markup is ordinary markup in
24
+ the document, visible in view-source, reachable by `closest()` and by the
25
+ application's stylesheets.
26
+
27
+ ## HTML
28
+
29
+ An `.html` file named for a tag defines that tag. When the builder finds the
30
+ tag in the chrome, in a page, or in another definition, it inserts the
31
+ definition's content as children of the tag. We call this process
32
+ 'HTML expansion'.
33
+
34
+ A package listed in `imports.ts` loads first and an application's own
35
+ definitions load after, so a tag defined in both resolves to the
36
+ application's.
37
+
38
+ ### Parameters
39
+
40
+ A definition takes parameters. Three namespaces share the attributes of a
41
+ widget tag, and the prefix says which namespace an attribute is in:
42
+
43
+ | Prefix | Belongs to | Read |
44
+ |-------------|------------|-----------------------------|
45
+ | `exp-` | Expansion | By the builder, at build time |
46
+ | `lb-` | The hub | By Loadbare, in the browser |
47
+ | No prefix | HTML | By the browser, as HTML says |
48
+
49
+ Pass a parameter by writing `exp-<name>` on the tag. Read it in the
50
+ definition by writing `{{<name>}}`. The prefix marks the attribute as
51
+ expansion's input and is not part of the parameter's name, so `exp-label`
52
+ supplies `{{label}}`:
53
+
54
+ ```html
55
+ <!-- definition: src/note-field.html -->
56
+ <label>{{label}} <input readonly="{{readonly}}" /></label>
57
+ ```
58
+
59
+ ```html
60
+ <!-- authored -->
61
+ <note-field lb-cell="name" exp-label="Name"></note-field>
62
+ ```
63
+
64
+ Write `class`, `title`, or any other unprefixed attribute on a widget
65
+ freely. They are HTML's, and never collide with a definition's parameters.
66
+
67
+ Write a placeholder as an entire attribute value or an entire text node.
68
+ Nothing inside one is evaluated, and expansion has no data to branch on —
69
+ see [Conditional rendering](./data-binding.md#conditional-rendering) for
70
+ what to write instead of a branch.
71
+
72
+ Name a parameter in lowercase. HTML lowercases attribute names before
73
+ expansion sees them, so `exp-inputClass` arrives as `exp-inputclass` and
74
+ could never fill `{{inputClass}}`. Write `{{input-class}}` instead; a
75
+ definition declaring such a name is rejected when it loads.
76
+
77
+ Leave a parameter unsupplied to drop what reads it. An attribute whose
78
+ value is a placeholder is removed rather than shipped empty, which is how
79
+ `readonly="{{readonly}}"` works as an ordinary boolean attribute. A
80
+ placeholder in a text node resolves to nothing, and the whitespace around
81
+ it survives.
82
+
83
+ Every attribute stays on the tag after expansion, `exp-` ones included.
84
+
85
+ Parameter values reach a definition through the DOM rather than through
86
+ string substitution, so a value is never reparsed as markup and needs no
87
+ escaping.
88
+
89
+ ### The slot
90
+
91
+ `lb-slot` marks the one element in a definition whose children receive
92
+ whatever the author wrote inside the tag:
93
+
94
+ ```html
95
+ <!-- definition: src/note-card.html -->
96
+ <article class="card"><div lb-slot></div></article>
97
+ ```
98
+
99
+ ```html
100
+ <!-- authored -->
101
+ <note-card>
102
+ <h3>Meeting notes</h3>
103
+ <p>Bring the roster.</p>
104
+ </note-card>
105
+ ```
106
+
107
+ ```html
108
+ <!-- ships -->
109
+ <note-card>
110
+ <article class="card">
111
+ <div>
112
+ <h3>Meeting notes</h3>
113
+ <p>Bring the roster.</p>
114
+ </div>
115
+ </article>
116
+ </note-card>
117
+ ```
118
+
119
+ The tag stays and the definition becomes its children. `lb-slot` itself is
120
+ gone from what ships, having done its job at build time.
121
+
122
+ Give a definition at most one `lb-slot`. A definition with a slot and
123
+ nothing written inside it leaves the slot empty.
124
+
125
+ `lb-slot` is an attribute rather than an element because HTML's content
126
+ model discards foreign elements inside `<select>` and `<table>`. A slot
127
+ marker has to survive on an element the surrounding tag already permits.
128
+
129
+ ### Destinations
130
+
131
+ `lb-template` names a destination in a definition. On an authored
132
+ `<template lb-template="name">`, it names the destination that template
133
+ fills:
134
+
135
+ ```html
136
+ <!-- definition: src/ledger-table.html -->
137
+ <table>
138
+ <caption>{{caption}}</caption>
139
+ <thead lb-template="head"></thead>
140
+ <tbody lb-slot></tbody>
141
+ </table>
142
+ ```
143
+
144
+ ```html
145
+ <!-- authored -->
146
+ <ledger-table exp-caption="Ledger">
147
+ <template lb-template="head">
148
+ <tr><th>Date</th><th>Amount</th></tr>
149
+ </template>
150
+ <tbody>
151
+ <!-- row template -->
152
+ </tbody>
153
+ </ledger-table>
154
+ ```
155
+
156
+ Declare as many destinations as the definition needs, one per name. An
157
+ author fills none, some, or all of them, and a destination nobody fills
158
+ stays and is empty.
159
+
160
+ Wrap authored content for a destination in a `<template>`, which exists only
161
+ to survive the parser. A `<thead>` written directly inside `<ledger-table>`
162
+ is not inside a `<table>`, so the tokenizer drops the tag before expansion
163
+ sees it. A `<template>` survives anywhere and parses its contents as though
164
+ already in place. What lands in the destination is the template's contents —
165
+ an ordinary `<thead>` in view-source — not the template element.
166
+
167
+ ### Recursion
168
+
169
+ A definition may use other widgets. Expansion repeats until nothing new
170
+ appears, so a widget built out of other widgets needs nothing declared for
171
+ it.
172
+
173
+ Keep the definition graph acyclic. A cycle is a build error, reported as the
174
+ path that closes it.
175
+
176
+ Expansion runs over `<template>` contents wherever they occur, so a widget's
177
+ row template ships already expanded, `{{placeholder}}` included.
178
+
179
+ ### What fails at build time
180
+
181
+ Each of these is reported with the tag and file name, and nothing is
182
+ shipped:
183
+
184
+ - a tag in the `LB-*` namespace with no definition
185
+ - a cycle in the definition graph
186
+ - an `exp-` attribute the definition never declared
187
+ - a `{{placeholder}}` spelled so that no `exp-` attribute could supply it
188
+ - content written inside a tag whose definition has no `lb-slot`
189
+ - more than one `lb-slot` in one definition
190
+ - a `<template lb-template="name">` naming a destination the definition
191
+ does not have
192
+ - two templates for the same destination
193
+
194
+ An unfilled `{{placeholder}}` is not among these. That is presence
195
+ propagation working as intended, indistinguishable from an author who left
196
+ an optional parameter out.
197
+
198
+ ## Code
199
+
200
+ A `.ts` file named for a tag registers that tag's class. A widget is an
201
+ ordinary custom element — Loadbare imposes no base class — and it takes part
202
+ in data binding through three contracts: it receives a value, it sends a
203
+ request, and it accepts a set of rows.
204
+
205
+ Import every attribute name from `@loadbare/app/constants`. Never write one
206
+ as a string literal.
207
+
208
+ | Constant | Value |
209
+ |------------------|------------|
210
+ | `ATTR_VALUE` | `lb-value` |
211
+ | `ATTR_CELL` | `lb-cell` |
212
+ | `ATTR_QUERY` | `lb-query` |
213
+ | `ATTR_KEY` | `lb-key` |
214
+ | `ATTR_GROUP` | `lb-group` |
215
+ | `ATTR_SORT` | `lb-sort` |
216
+ | `ATTR_ACTION` | `lb-action`|
217
+ | `ATTR_ROW_COUNT` | `data-rows`|
218
+ | `LB_EVENT_NAME` | `lb-request` |
219
+
220
+ ### Receiving a value
221
+
222
+ A bound cell lands on a widget as the `lb-value` attribute rather than as
223
+ text — see [Data Binding](./data-binding.md#where-a-bound-value-lands).
224
+ Observe it, and render the value however the widget renders things:
225
+
226
+ ```ts
227
+ // src/visit-count.ts
228
+ import { ATTR_VALUE } from "@loadbare/app/constants";
229
+
230
+ class VisitCount extends HTMLElement {
231
+ static observedAttributes = [ATTR_VALUE];
232
+
233
+ attributeChangedCallback(_name: string, _old: string, value: string) {
234
+ this.textContent = `visited ${value} times`;
235
+ }
236
+ }
237
+
238
+ customElements.define("visit-count", VisitCount);
239
+ ```
240
+
241
+ List `ATTR_VALUE` in `observedAttributes`, or `attributeChangedCallback`
242
+ never fires. The browser calls it for an attribute already present when an
243
+ element upgrades, not only for one that changes afterward, so a widget's
244
+ first render and every later refresh go through the one callback. There is
245
+ no separate hydration path to write.
246
+
247
+ ### Sending a request
248
+
249
+ A widget that owns its own interaction — a `<select>`'s choice rather than a
250
+ click — builds its own request and dispatches it as a bubbling
251
+ `CustomEvent` named `LB_EVENT_NAME`, carrying one of the `HubRequest` shapes
252
+ as its `detail`:
253
+
254
+ ```ts
255
+ import { ATTR_CELL, ATTR_KEY, ATTR_QUERY, LB_EVENT_NAME } from "@loadbare/app/constants";
256
+ import type { HubRequest } from "@loadbare/app/types";
257
+
258
+ const detail: HubRequest = {
259
+ op: "cell-change",
260
+ query: this.closest(`[${ATTR_QUERY}]`)!.getAttribute(ATTR_QUERY)!,
261
+ key: this.closest(`[${ATTR_KEY}]`)!.getAttribute(ATTR_KEY)!,
262
+ cell: this.getAttribute(ATTR_CELL)!,
263
+ value: input.value,
264
+ };
265
+ this.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
266
+ ```
267
+
268
+ Find `query` and `key` by walking up with `closest()`, the same way the hub
269
+ finds the binding for a native element. See
270
+ [Data Binding](./data-binding.md#requests) for the request vocabulary and
271
+ what each operation carries.
272
+
273
+ Let the event bubble, so an ancestor widget can intercept and stop it before
274
+ the hub sees it. A hand-written widget and a native element carrying
275
+ `lb-action` produce the same event.
276
+
277
+ ### Accepting rows
278
+
279
+ A query answering with many rows is delivered to whichever element carries
280
+ an `acceptRows` method. It is a protocol, not a tag name — the hub tests for
281
+ the method and never for the element:
282
+
283
+ ```ts
284
+ import { applyRows } from "@loadbare/app/rows";
285
+ import type { Projection, HubRowHost } from "@loadbare/app/types";
286
+
287
+ class UpdateList extends HTMLElement implements HubRowHost {
288
+ acceptRows(result: Projection) {
289
+ applyRows(this, result);
290
+ }
291
+ }
292
+ ```
293
+
294
+ Call `applyRows(scope, result, place?)` rather than reimplementing rows.
295
+ Every built-in list widget uses it, and it settles four things:
296
+
297
+ | Concern | What `applyRows` does |
298
+ |-------------|---------------------------------------------------------|
299
+ | Cloning | Clones the `<template lb-key="...">` in the widget |
300
+ | Matching | Updates the row already showing that key, or clones one |
301
+ | Reconciling | Removes the rows the response says are gone |
302
+ | Counting | Stamps `data-rows` with the number of rows showing |
303
+
304
+ `rows` is the whole set, so it decides membership and order, and a key
305
+ absent from it is removed. `patch` touches only the rows it names and leaves
306
+ every other row's contents and position alone.
307
+
308
+ Supply a `place` function to decide where a row goes — `(row, tuple,
309
+ template) => void`, called with a fresh or reordered row. The default
310
+ inserts immediately before the template, so rows accumulate in arrival
311
+ order. A widget that groups or sorts supplies its own `place` instead of
312
+ reimplementing matching and cloning around it; `lb-options.ts` and
313
+ `lb-table.ts` in [`@loadbare/widgets`](./widgets.md) are two different
314
+ `place` functions over the same `applyRows`.
315
+
316
+ Style an empty list against `data-rows` rather than carrying an empty-state
317
+ conditional in the widget — see
318
+ [Conditional rendering](./data-binding.md#conditional-rendering).
319
+
320
+ ### Filling a scope by hand
321
+
322
+ `@loadbare/app/rows` also exports `applyTuple(root, cells)`, the same
323
+ tuple-landing operation a page host uses. Call it in a widget that builds
324
+ its own rows or scopes rather than relying on `acceptRows`. It fills `root`
325
+ itself when `root` carries a matching `lb-cell`, and every matching
326
+ descendant.
327
+ </content>