@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
package/docs/guide.md DELETED
@@ -1,1164 +0,0 @@
1
- # Programmer's Guide
2
-
3
- Loadbare App is a web application framework for data-driven applications.
4
-
5
- Loadbare App's "Axiom zero" is that both users and developers are best served
6
- when static host code displays and allows user interactions with
7
- fixed-schema data.
8
-
9
- Most frameworks assume that the entire DOM is subject to change, and they
10
- carry considerable plumbing to chase down the implications of that assumption.
11
- Loadbare App assumes only data values change. When the data populates a SELECT,
12
- TABLE, or some type of tree, there are necessary DOM changes, but they fit
13
- within the model of a fixed static application, the application author does
14
- not need to think about how the DOM changes happen.
15
-
16
- ---
17
-
18
- ## The Counter App But Server Persisted
19
-
20
- Loadbare App assumes the critical path is server state. Here we will show the
21
- universal example of a counter (two counters actually) where the value is
22
- persisted on the server.
23
-
24
- This section shows the files that make a page: the host, its
25
- queries, its hooks, and the service they call. That is the whole of what you
26
- write per page.
27
-
28
- ### The host: `demo/pages/hello.html`
29
-
30
- The host declares the structure of the page and declares which elements
31
- dynamically display data. Whenever a request is made to the server, the
32
- response contains data updates. The hub matches the returned data
33
- to the declarations and updates whatever matches.
34
-
35
- A complete Loadbare App page includes named queries that retrieve data. In this
36
- example there are two queries, each of which returns a tuple with one
37
- keyed value:
38
-
39
-
40
- ```html
41
- <h2>Hello, World!</h2>
42
-
43
- <div lb-query="pageStats">
44
- <p>
45
- This page has been visited
46
- <span lb-cell="visit_count"></span> times!
47
- </p>
48
- </div>
49
-
50
- <p>Reload, or navigate away and back, to increase the count.</p>
51
-
52
- <div lb-query="counter">
53
- <p>Button count: <span lb-cell="count"></span></p>
54
- <button lb-action="increment">+1</button>
55
- </div>
56
- ```
57
-
58
- `lb-query` marks the region that displays one query. All DOM nodes
59
- within the subtree are scoped to that query. The `lb-cell` attribute marks
60
- the leaf that displays a single keyed value.
61
-
62
- In this demo, both queries return a plain object with one value, and both
63
- displays are simple text. When `lb-cell` is on an ordinary element, Loadbare App
64
- assigns the value to the element's `textContent` property.
65
-
66
- The attribute `lb-action` names something the page declares it can do on the server.
67
- The hub turns the
68
- click into a request and sends it. In this case there are no parameters
69
- for the task, so the button carries no additional attributes.
70
-
71
- ### The request context in `demo/services.ts:19`
72
-
73
- Server code consists of hooks and queries, which we will see shortly.
74
- Each of those is an asynchronous routine that takes a context, `ctx` as
75
- it only argument. Loadbare App declares it as an empty interface:
76
-
77
-
78
- ```ts
79
- export interface HubContext {}
80
- ```
81
-
82
- The application expands it using `declare module`. In the demo this code
83
- is in `demo/services.ts`, and declares that the demo pages will recieve an
84
- object providing database access. The internals of that object are not
85
- discussed here, they can be found in `demo/services.ts`.
86
-
87
- ```ts
88
- declare module "@loadbare/app/server" {
89
- interface HubContext {
90
- db: Db;
91
- }
92
- }
93
- ```
94
-
95
- ### The queries: `pages/hello.queries.ts`
96
-
97
- A query is a named asynchronous routine that calls out to the
98
- application data store and returns results that can be displayed
99
- in the host HTML. Here are the two queries we declared in `pages/hello.html`.
100
-
101
- ```ts
102
- import type { Queries } from "@loadbare/app/server";
103
-
104
- export const queries: Queries = {
105
- pageStats: async (ctx) => ({
106
- visit_count: String(await ctx.db.visitCount()),
107
- }),
108
- counter: async (ctx) => ({ count: String(await ctx.db.buttonCount()) }),
109
- };
110
- ```
111
-
112
- The query namespace is local to the page, so query names must be unique within
113
- a page but need not be unique across pages. The file is named for its page, so
114
- nothing declares which page runs it.
115
-
116
- ### The hooks and actions: `pages/hello.hooks.ts`
117
-
118
- When the app loads a page, a request is sent to the server to run all
119
- queries and return the results. The `beforeGet` hook fires before
120
- the queries run. In this case the `recordVisit()` implementation increases
121
- the count by one.
122
-
123
- ```ts
124
- import type { Hooks } from "@loadbare/app/server";
125
-
126
- export const hooks: Hooks = {
127
- beforeGet: (ctx) => ctx.db.recordVisit(),
128
- actions: {
129
- increment: {
130
- run: (ctx) => ctx.db.raiseButtonCount(),
131
- refresh: ["counter"],
132
- },
133
- },
134
- };
135
- ```
136
-
137
- The actions are declared with the hooks. In our host HTML we declared that
138
- a button had `lb-action` named 'increment', so we must provide an implementation
139
- of action 'increment'. The action names a routine to run, and which queries
140
- should be refreshed afterward.
141
-
142
- ### Serving it
143
-
144
- Those are all the files the page needed. Naming the page in a registry,
145
- assembling the document that carries it, and answering the two endpoints are
146
- done once for an application rather than once per page, so they are in
147
- [Hosting a Loadbare App Application](./hosting.md). The demo does them in
148
- `demo/pages.ts` and `demo/server.ts`.
149
-
150
- ### Run it
151
-
152
- ```
153
- npm run dev
154
- ```
155
-
156
- Open `http://localhost:8787/hello`. The visit count rises on every load and
157
- on every navigation back to the page. The button raises its own count and
158
- leaves the visit count alone.
159
-
160
- ---
161
-
162
-
163
- ## Widgets
164
-
165
- A widget is the basic composable unit of the UI. A widget is an HTML
166
- file and a Typescript containing the class.
167
-
168
- A page author writes a widget's tag, and the attributes written on that tag
169
- control how the definition expands. Expansion happens at compile time. It
170
- is a build operation, not a runtime one: it runs once, before any request
171
- exists, and what it produces is permanent. The expanded markup ships to the
172
- browser and never changes there.
173
-
174
- ### Simple Example
175
-
176
- A widget is two files with the same basename, in the same directory. The tag
177
- name is the filename. Here is `lb-input`, which ships with the framework
178
- and wraps a labeled `<input>`.
179
-
180
- The definition, `widgets/lb-input.html`, is markup that will go inside of
181
- the custom element.
182
-
183
- ```html
184
- <label>{{label}} <input readonly="{{readonly}}" /></label>
185
- ```
186
-
187
- A name in double braces is a placeholder. It stands for a parameter the page
188
- author supplies on the tag, and it may occupy an entire attribute value or an
189
- entire text node.
190
-
191
- The element, `widgets/lb-input.ts`, is the class that gives the definition
192
- behavior:
193
-
194
- ```ts
195
- import { ATTR_VALUE } from "@loadbare/app/constants";
196
-
197
- class LbInput extends HTMLElement {
198
- static observedAttributes = [ATTR_VALUE];
199
-
200
- attributeChangedCallback(_name: string, _old: string, value: string) {
201
- this.querySelector("input")!.value = value;
202
- }
203
- }
204
-
205
- customElements.define("lb-input", LbInput);
206
- ```
207
-
208
- When the hub receives a data payload from the server, and finds a
209
- match on `lb-query` and `lb-cell`, it updates the attribute value
210
- `lb-value`. The widget's job is to forward that value to whatever
211
- control it owns. Here the `<input>` is not checked for before it is used,
212
- because the definition beside this file is what places it.
213
-
214
- A page author writes the tag, as `demo/pages/entity.html` does, supplying each
215
- parameter as an `exp-` attribute:
216
-
217
- ```html
218
- <lb-input lb-cell="name" exp-label="Name" exp-readonly></lb-input>
219
- ```
220
-
221
- In Loadbare App terms, we say the `<lb-input>` is "expanded" at build time,
222
- resulting in the following HTML going out to the page:
223
-
224
- ```html
225
- <lb-input lb-cell="name" exp-label="Name" exp-readonly=""><label>Name <input readonly=""></label></lb-input>
226
- ```
227
-
228
- The tag survives expansion exactly as written, and the definition became its
229
- children. So the shipped page shows what was asked for beside what it
230
- produced, and the class can find its `<input>` with an ordinary
231
- `querySelector`.
232
-
233
- Three namespaces share the tag, and each has one owner. `lb-` belongs to the
234
- hub, which is how it addresses this widget at run time. `exp-` belongs to
235
- expansion, and is gone by the time the browser matters. Everything unprefixed
236
- belongs to HTML and means exactly what HTML says it means, so `class`,
237
- `title`, `hidden` and their kind can be written on any widget, whatever that
238
- widget's parameters happen to be called.
239
-
240
- Marking the parameters buys an error message. A definition declares which
241
- ones it has, so `exp-labl` is a name that resolves to nothing, and the build
242
- says so rather than shipping a blank label:
243
-
244
- ```
245
- expand: <lb-input> was given exp-labl, but its definition has no {{labl}}.
246
- ```
247
-
248
- The reverse is not an error, because it is how an optional parameter works.
249
- Written without `exp-readonly`, the placeholder has nothing behind it and the
250
- attribute is dropped rather than emitted empty, which is HTML's own rule for
251
- boolean attributes:
252
-
253
- ```html
254
- <lb-input lb-cell="name" exp-label="Name"></lb-input>
255
- <!-- ships as -->
256
- <lb-input lb-cell="name" exp-label="Name"><label>Name <input></label></lb-input>
257
- ```
258
-
259
- Because expansion runs before any request exists, it has no data to branch on,
260
- and definitions have no control flow. If you find yourself wanting a
261
- conditional in a definition, something data-shaped has leaked into the static
262
- half; find the leak.
263
-
264
- ### Slots and Children
265
-
266
- A parameter carries a word. Some widgets need to be given markup instead —
267
- the options of a select, the cells of a row, the contents of a panel. A
268
- widget that accepts markup marks one element in its definition with
269
- `lb-slot`, and whatever the page author writes inside the tag becomes that
270
- element's children.
271
-
272
- `lb-select` is the case. Its definition, `widgets/lb-select.html`,
273
- supplies the label and the control and says where the choices go:
274
-
275
- ```html
276
- <label
277
- >{{label}}
278
- <select lb-slot></select
279
- ></label>
280
- ```
281
-
282
- The page author writes them, as `demo/pages/entity.html` does:
283
-
284
- ```html
285
- <lb-select lb-cell="id" lb-action="selectEntity" exp-label="Show:">
286
- <option value="e1">Ada</option>
287
- <option value="e2">Grace</option>
288
- <option value="e3">Alan</option>
289
- </lb-select>
290
- ```
291
-
292
- And the two are joined at build time. The definition became the tag's
293
- children, and the authored content landed inside the `<select>`, which no
294
- longer carries the marker:
295
-
296
- ```html
297
- <lb-select lb-cell="id" lb-action="selectEntity" exp-label="Show:"><label>Show:
298
- <select>
299
- <option value="e1">Ada</option>
300
- <option value="e2">Grace</option>
301
- <option value="e3">Alan</option>
302
- </select></label></lb-select>
303
- ```
304
-
305
- The content moves rather than copies, so each option appears once, in the
306
- place the definition chose for it.
307
-
308
- The slot is an attribute rather than an element because of where slots are
309
- needed. HTML's content models discard foreign elements inside `<select>` and
310
- `<table>`, so a `<lb-slot>` tag written in either would be dropped by the
311
- parser before expansion ever saw it. An attribute rides on an element the
312
- content model already accepts.
313
-
314
- A definition may mark at most one element. Two is an error, because nothing
315
- would say which one the content meant:
316
-
317
- ```
318
- expand: <lb-panel> declares more than one lb-slot
319
- ```
320
-
321
- Writing content inside a widget whose definition has no slot is also an error,
322
- rather than content silently vanishing:
323
-
324
- ```
325
- expand: content was written inside <lb-input>, whose definition has no lb-slot
326
- ```
327
-
328
- A definition with a slot that nobody fills is not an error. The slot element
329
- ships empty, exactly as an unsupplied parameter ships nothing.
330
-
331
- Two things follow from the order the build works in. Placeholders are
332
- substituted into the definition before the slot is filled, so `{{label}}`
333
- written in authored content is literal text, not a parameter — parameters
334
- belong to the tag, content belongs to the page. Expansion then descends into
335
- what it just produced, so a widget written inside another widget's slot is
336
- expanded in place.
337
-
338
- Children are static, like everything else in the host. The three options
339
- above are the three options the application has; they are not a list that came
340
- from a query. The value that arrives from the server is the one that gets
341
- selected, not the set that gets offered. Choices that come from data are list
342
- processing, below.
343
-
344
- ### Markup the Parser Would Drop
345
-
346
- A slot receives what the author wrote inside the tag. Some of what an author
347
- needs to write cannot get there, and the obstacle is the parser rather than the
348
- slot.
349
-
350
- A `<thead>` written inside `<lb-table>` is not inside a table. The
351
- tokenizer has nowhere to put the tag, so it drops it and keeps the text:
352
-
353
- ```html
354
- <lb-table><thead><tr><th>Name</th></tr></thead></lb-table>
355
- <!-- is parsed as -->
356
- <lb-table>Name</lb-table>
357
- ```
358
-
359
- That is the same fact as the one that keeps a custom element out of `<tbody>`,
360
- seen from the other side. Table markup and a custom element cannot be written
361
- adjacent, in either order.
362
-
363
- A `<template>` survives anywhere, and its contents are parsed as though they
364
- were already in the place they describe, so the wrapper is what gets the markup
365
- through intact. The author names a destination on it, and the definition marks
366
- the destination by the same name:
367
-
368
- ```html
369
- <!-- widgets/lb-table.html -->
370
- <table>
371
- <caption>{{caption}}</caption>
372
- <thead lb-template="head"></thead>
373
- <tbody lb-slot></tbody>
374
- <tfoot lb-template="foot"></tfoot>
375
- </table>
376
- ```
377
-
378
- ```html
379
- <!-- demo/pages/staff.html -->
380
- <lb-table lb-query="staff" exp-caption="Everyone, by team">
381
- <template lb-template="head">
382
- <tr><th>Name</th><th>Role</th><th></th></tr>
383
- </template>
384
-
385
- <template lb-key="id" lb-group="team" lb-sort="name">
386
- <tr>…</tr>
387
- </template>
388
- </lb-table>
389
- ```
390
-
391
- `lb-template` is one name in two positions, like `lb-key`: in a definition it
392
- marks a destination, on an authored `<template>` it names the destination that
393
- template is for. It is never ambiguous, because a destination is not a
394
- template.
395
-
396
- The destination is filled with the template's contents rather than with the
397
- template, so the wrapper is gone by the time anything ships and what the browser
398
- holds is an ordinary `<thead>` with ordinary rows in it:
399
-
400
- ```html
401
- <lb-table lb-query="staff" exp-caption="Everyone, by team"><table>
402
- <caption>Everyone, by team</caption>
403
- <thead><tr><th>Name</th><th>Role</th><th></th></tr></thead>
404
- <tbody><template lb-key="id" lb-group="team" lb-sort="name">…</template></tbody>
405
- <tfoot></tfoot>
406
- </table></lb-table>
407
- ```
408
-
409
- The `<tfoot>` is empty because this page named no template for it. A
410
- destination nobody fills is an empty element rather than an absent one, which
411
- is the same rule presence propagation follows for an attribute: what the
412
- definition declares, the definition emits.
413
-
414
- Named templates are taken first and the slot gets what is left, which is why
415
- the row template above needs no name — it is content, and content goes where
416
- content goes. Whitespace between named templates is not content, so a widget
417
- with destinations and no slot is not accused of having been given any.
418
-
419
- A definition may declare several destinations, and only one slot. They are
420
- counted differently because the reason for each is different. One slot is a
421
- statement that a widget has one place for what an author writes. Destinations
422
- exist to carry markup past the parser, and how many are needed is a question
423
- about the parser: a table has a head, a body and a foot, and each needs its own
424
- way through.
425
-
426
- Both directions of a mismatch are build errors, and a name is matched exactly
427
- rather than approximately:
428
-
429
- ```
430
- expand: a <template lb-template="footer"> was written inside <lb-table>, whose definition has no destination by that name.
431
- ```
432
-
433
- The name may be empty, which is a name. A widget with one destination costs the
434
- author nothing but the attribute.
435
-
436
- ### Conditional Rendering With Visibility Flags and CSS
437
-
438
- A framework that mutates the DOM needs a conditional, and gets one cheaply. If
439
- the tree is a function of the data, an `if` in the template is the natural way
440
- to say that a panel is absent. Loadbare App hydrates attributes and text and does
441
- nothing else, so there is no operation here that could make an element exist,
442
- and no place to put the `if` even if there were.
443
-
444
- What replaces it is stated in [data-binding.md](../docs-llm-slop/data-binding.md): visibility
445
- is the boolean case, cells are the general one. Everything ships. A value
446
- decides what is showing, never what is there. So the section that would be
447
- about conditionals in another framework is, here, three examples and one line
448
- of framework code.
449
-
450
- #### Tabs
451
-
452
- `demo/pages/tabs.html` ships all three panels, with two of them carrying
453
- `hidden`, which is HTML's own visibility flag and needs nothing from us. The
454
- widget is addressed like any leaf:
455
-
456
- ```html
457
- <lb-tabs lb-query="prefs" lb-cell="active_tab" lb-action="selectTab">
458
- <template lb-template="strip">
459
- <button data-tab="summary" aria-selected="true">Summary</button>
460
- <button data-tab="detail">Detail</button>
461
- <button data-tab="history">History</button>
462
- </template>
463
-
464
- <section data-panel="summary">…</section>
465
- <section data-panel="detail" hidden>…</section>
466
- <section data-panel="history" hidden>…</section>
467
- </lb-tabs>
468
- ```
469
-
470
- Which tab is showing is a cell, so it arrives as `lb-value` and the widget
471
- forwards it — to the `hidden` attribute of its panels rather than to the value
472
- of an `<input>`, which is the whole of the difference between `lb-tabs` and
473
- `lb-input`. Nothing in the vocabulary is new, and the framework was not
474
- consulted.
475
-
476
- A click is the widget's own interaction, so it switches the panel itself and
477
- sends the choice afterward, and the action refreshes nothing — the browser is
478
- already showing what it just asked for, and saying so again would spend a round
479
- trip on nothing. Persistence rides along behind a paint that already happened.
480
-
481
- That is what makes the returning user work. The choice is a stored value like
482
- the visit count, so navigating away and back re-runs the query and the third
483
- tab is what the server answers with. There is no blink, and no mechanism was
484
- needed for that either: the hub inserts a page host and lands its data in one
485
- synchronous block, and the browser does not paint mid-task, so the first tab is
486
- never on screen. Hydration is early enough by construction rather than by
487
- timing.
488
-
489
- A value naming no panel changes nothing. That is deliberate, and it is what
490
- makes the page's authored state the starting state: the host ships showing
491
- something, and a value only ever moves it.
492
-
493
- #### A wizard, decided by the server
494
-
495
- `demo/pages/wizard.html` is the same shape with the decision on the other side.
496
- Every step ships, one of them visible. Back and Next are ordinary buttons
497
- carrying `lb-action`, so the hub sends the click, `wizard.hooks.ts` computes
498
- the step and clamps it at the ends, and the step comes back as a value. The
499
- widget owns no arithmetic, and the browser never displays a step the server has
500
- not confirmed — the rule the counter follows for its number.
501
-
502
- Tabs switch first and tell the server after; a wizard waits. A tab is a view
503
- of what is already there, and a wizard step is a position the server is
504
- entitled to refuse. Both are the widget's decision about its own interaction,
505
- and neither is a framework feature.
506
-
507
- The disabled ends are derived rather than sent. Which step is first is a fact
508
- about the page's markup, so the widget reads it there rather than being told
509
- twice — the same argument that gives a section heading its `colspan`.
510
-
511
- #### The empty list
512
-
513
- One conditional cannot be sent, and it is the one this framework owes a page.
514
- The server answers a list query with rows and says nothing about how many
515
- survived reconciliation, so after a delete the count exists only in the DOM.
516
- Without help, every list widget would grow its own copy of the same three
517
- lines.
518
-
519
- So `applyRows` stamps it, once, for every list widget there will ever be:
520
-
521
- ```html
522
- <lb-list lb-query="roster" data-rows="0">
523
- ```
524
-
525
- It is `data-` rather than `lb-` for the reason the section heading in
526
- `lb-table` is: it is derived from rows the hub already knows, and nothing
527
- addresses it. The page then says what empty looks like in a stylesheet, and no
528
- widget has a conditional in it:
529
-
530
- ```css
531
- .empty {
532
- display: none;
533
- }
534
- [data-rows="0"] .empty {
535
- display: block;
536
- }
537
- ```
538
-
539
- ```html
540
- <lb-list lb-query="roster">
541
- <table>…</table>
542
- <p class="empty">No members yet.</p>
543
- </lb-list>
544
- ```
545
-
546
- Hidden is the default and shown is the rule, which is the ordering that matters:
547
- the stamp is absent until a projection has landed, and absent is not zero. A
548
- list still waiting for its first response says nothing, rather than announcing
549
- that it is empty and then correcting itself.
550
-
551
- #### Where a flag may land
552
-
553
- A value lands on a widget as `lb-value` and on a native element as its
554
- `textContent`. That is the whole of `land`, and two things follow for anyone
555
- writing a visibility rule.
556
-
557
- A flag cell must be a leaf. `<section lb-cell="state">` wrapping anything at
558
- all replaces its children with a string, because assigning `textContent` is
559
- what a native cell means. A cell names a place a value goes, and a container
560
- is not one.
561
-
562
- And CSS reaches an ancestor with `:has()`, which is how a leaf flag governs the
563
- box around it:
564
-
565
- ```css
566
- .panel:has([lb-cell="state"][lb-value="stale"]) { … }
567
- ```
568
-
569
- Only a widget carries `lb-value`, so a page with no widget in it can branch on
570
- a flag's text but not on an attribute. That is the limit, and it is not much of
571
- one: by the time a page has a visibility rule worth writing it has behavior, and
572
- behavior is a widget.
573
-
574
- The flag is an attribute and the branching is a stylesheet. Neither an inline
575
- `style` nor a placeholder that produces one is available — those are unsafe
576
- sinks, and expansion has no data to fill them with anyway.
577
-
578
- ### List processing
579
-
580
- Everything so far has been one tuple. A query may instead return many, and
581
- what displays them is a `<template>` the page author wrote and a widget that
582
- owns where each row goes.
583
-
584
- #### The table
585
-
586
- `demo/pages/roster.html` is the plain case. The page author writes the table,
587
- and writes one row inside a `<template>`:
588
-
589
- ```html
590
- <lb-list lb-query="roster">
591
- <table>
592
- <thead>
593
- <tr><th>Name</th><th>Role</th><th>Team</th><th></th></tr>
594
- </thead>
595
- <tbody>
596
- <template lb-key="id">
597
- <tr>
598
- <td lb-cell="name"></td>
599
- <td lb-cell="role"></td>
600
- <td lb-cell="team"></td>
601
- <td><button lb-action="deleteMember">Delete</button></td>
602
- </tr>
603
- </template>
604
- </tbody>
605
- </table>
606
- </lb-list>
607
- ```
608
-
609
- Inside the template, nothing is new. `lb-cell` names a column and lands a
610
- value exactly as it does anywhere else on the page, because a row is a scope
611
- that happens to be small. One function fills both, and a list widget imports
612
- it rather than reimplementing it:
613
-
614
- ```ts
615
- import { applyTuple } from "@loadbare/app";
616
- ```
617
-
618
- `lb-key` on the template names the column that identifies a row. On a live
619
- row the same attribute carries that row's value, which is never ambiguous
620
- because a template is not a row. The rows are the template's preceding
621
- siblings, so the template stays put as the insertion marker and static markup
622
- can sit on either side of the list.
623
-
624
- The widget is around the table rather than in it. A custom element written
625
- inside `<tbody>` is discarded by the parser before expansion could ever see
626
- it, but a `<template>` is allowed there, which is why the template is the unit
627
- the page author writes and the widget is the element that wraps it.
628
-
629
- The delete button needs no wiring. The hub builds a request from the
630
- attributes the element already sits under — `lb-query` from the widget,
631
- `lb-key` from the row — so the action arrives knowing which row was clicked,
632
- and the page never passed an argument.
633
-
634
- #### Two results, because a widget must know the difference
635
-
636
- A query returns the whole of its set. It cannot know why it was re-run, so it
637
- never sends a delta:
638
-
639
- ```ts
640
- import { rows, type Queries } from "@loadbare/app/server";
641
-
642
- export const queries: Queries = {
643
- roster: async (ctx) => rows(await ctx.db.members()),
644
- };
645
- ```
646
-
647
- An action does know what it changed, and can say only that. So an action may
648
- return results of its own, laid over whatever its refresh set produced:
649
-
650
- ```ts
651
- import { patch, type Hooks } from "@loadbare/app/server";
652
-
653
- export const hooks: Hooks = {
654
- actions: {
655
- addMember: {
656
- run: async (ctx) => ({ roster: patch({ rows: [await ctx.db.addMember()] }) }),
657
- refresh: [],
658
- },
659
- deleteMember: {
660
- run: async (ctx, where) => {
661
- await ctx.db.deleteMember(where.key ?? "");
662
- return { roster: patch({ drop: [where.key ?? ""] }) };
663
- },
664
- refresh: [],
665
- },
666
- resetRoster: {
667
- run: (ctx) => ctx.db.resetMembers(),
668
- refresh: ["roster"],
669
- },
670
- },
671
- };
672
- ```
673
-
674
- Those are the two results a list can receive:
675
-
676
- `rows` is the entire set, and therefore also the order. Every row it names is
677
- placed in the order given, and a row whose key it does not name is gone.
678
-
679
- `patch` names only what changed. Rows it lists arrive or are updated, keys in
680
- `drop` are gone, and a row it does not mention keeps both its contents and its
681
- position.
682
-
683
- Adding and removing are one result rather than two because the interesting
684
- cases are both at once — a row whose sort key changed has to move, a swap is
685
- one out and one in — and two messages would paint the state in between.
686
-
687
- `resetRoster` could have been a patch too, and is not, because membership and
688
- order both changed and the whole set is the honest answer. The other two
689
- declare no refresh set at all. Re-running `roster` would have shown the same
690
- thing, but it would be the server saying "here is everything" when it knows
691
- the answer is "one more row" — and only the narrower statement is something a
692
- widget that sorts or groups can act on.
693
-
694
- #### Where a row goes is the widget's
695
-
696
- `demo/pages/picker.html` puts the same rows in a `<select>`:
697
-
698
- ```html
699
- <lb-options lb-query="choices" lb-action="pickMember" exp-label="Member:">
700
- <template lb-key="id" lb-group="team">
701
- <option lb-cell="name"></option>
702
- </template>
703
- </lb-options>
704
- ```
705
-
706
- Two things are different, and both follow from `<option>`.
707
-
708
- Its content model is text, so there is no element to put inside it and the row
709
- itself carries `lb-cell`. A row root counts as a cell when it declares one.
710
-
711
- It also needs a `value`, which is a second destination, and a native element
712
- has only one. Rather than invent a way to aim a cell at an attribute, the
713
- widget uses what it already has: the identity of the row is the value of the
714
- option, so `lb-key` supplies both and the page declares it once.
715
-
716
- Then the grouping. Nothing on the wire knows what an `<optgroup>` is — the
717
- rows arrive flat, and `team` is a column like any other. `lb-group` names it
718
- and `lb-options` builds one group per distinct value, reuses a group a
719
- later row belongs to, and removes one when its last row leaves:
720
-
721
- ```html
722
- <select>
723
- <optgroup label="Engines">
724
- <option lb-key="m1" value="m1" lb-cell="name">Ada Lovelace</option>
725
- <option lb-key="m3" value="m3" lb-cell="name">Alan Turing</option>
726
- </optgroup>
727
- <optgroup label="Compilers">
728
- <option lb-key="m2" value="m2" lb-cell="name">Grace Hopper</option>
729
- </optgroup>
730
- <template lb-key="id" lb-group="team">…</template>
731
- </select>
732
- ```
733
-
734
- This is the whole reason placement belongs to the widget. Sorting, grouping,
735
- and section headings are one question — *where does this row go?* — and the
736
- answer is always local. The hub delivers flat keyed rows and stops. A widget
737
- that has nothing to say supplies nothing and rows accumulate in the order the
738
- server sent, which is what `lb-list` does.
739
-
740
- Because grouping is placement, it is derived and not addressed. An
741
- `<optgroup>` carries no `lb-key` and the hub cannot see it; it is the
742
- widget's own scaffolding around rows the hub does know.
743
-
744
- #### How it reaches the widget
745
-
746
- A projection has to land on a widget. A native element has one destination
747
- for a value and no way to acquire children, so a list is not something it can
748
- be asked to show, and the hub says so rather than doing nothing:
749
-
750
- ```
751
- lb-hub: query 'oops' returned rows, but <div> is not a list widget
752
- ```
753
-
754
- The hub calls one method and stops. It tests for the method, never for the
755
- element, so it holds no table of tag names:
756
-
757
- ```ts
758
- export interface HubRowHost {
759
- acceptRows(result: Projection): void;
760
- }
761
- ```
762
-
763
- A list widget is small, because everything above the placement decision is
764
- shared. `lb-list` in its entirety:
765
-
766
- ```ts
767
- class LbList extends HTMLElement implements HubRowHost {
768
- acceptRows(result: Projection) {
769
- applyRows(this, result);
770
- }
771
- }
772
- ```
773
-
774
- `applyRows` clones the template, matches each tuple to the row already showing
775
- it, fills that row with `applyTuple`, and drops what left. A widget that
776
- places rows itself passes a third argument and writes nothing else:
777
-
778
- ```ts
779
- applyRows(this, result, (row, tuple, template) => { … });
780
- ```
781
-
782
- Rows are filled before they are inserted, so a widget inside a row has its
783
- attributes already set when it upgrades. That is the same thing that makes
784
- hydration and refresh one operation everywhere else on the page.
785
-
786
- #### Widgets that own the scaffolding
787
-
788
- `lb-list` and `lb-options` are the framework's two answers to *where does
789
- a row go?* — nowhere in particular, and in a group. Both leave the rest of the
790
- markup to the page. `demo/pages/staff.html` and `demo/pages/chooser.html` are
791
- the same two lists again, in widgets that take the markup as well.
792
-
793
- `lb-table` supplies the table and answers the placement question twice.
794
- `lb-sort` names the column rows are ordered by, `lb-group` names the column
795
- they are sectioned by, and the page writes only what the page knows:
796
-
797
- ```html
798
- <lb-table lb-query="staff" exp-caption="Everyone, by team">
799
- <template lb-template="head">
800
- <tr><th>Name</th><th>Role</th><th></th></tr>
801
- </template>
802
-
803
- <template lb-key="id" lb-group="team" lb-sort="name">
804
- <tr>
805
- <td lb-cell="name"></td>
806
- <td lb-cell="role"></td>
807
- <td><button lb-action="dropMember">Delete</button></td>
808
- </tr>
809
- </template>
810
- </lb-table>
811
- ```
812
-
813
- Its `place` inserts a row before the first row in its section that sorts after
814
- it, which keeps the section ordered whatever else is in it — so a whole set and
815
- a single patched row take the same path and neither needs to know which it is.
816
- Add one member and the server sends one row and says nothing about order; it
817
- lands in its team's section, in name order, because that is the whole of what
818
- placement decides.
819
-
820
- The section heading is a `<tr>` the widget builds, exactly as `lb-options`
821
- builds an `<optgroup>`, and it is scaffolding for the same reason: derived from
822
- rows the hub knows, invisible to the hub itself, and gone when its last row
823
- leaves. It carries `data-group` rather than a name from the vocabulary,
824
- because nothing addresses it. Its `colspan` is the number of cells in the row
825
- the page author wrote, which is the only place that number exists.
826
-
827
- `lb-picker` goes the other way and supplies the row template. Every option
828
- is one column of one row, so what is left to say is which columns — and those
829
- are words, which is what a parameter carries:
830
-
831
- ```html
832
- <lb-picker lb-query="choices" lb-action="chooseMember"
833
- exp-label="Member:" exp-key="id" exp-cell="name" exp-group="team"></lb-picker>
834
- ```
835
-
836
- ```html
837
- <!-- widgets/lb-picker.html -->
838
- <label>{{label}}
839
- <select>
840
- <template lb-key="{{key}}" lb-group="{{group}}">
841
- <option lb-cell="{{cell}}"></option>
842
- </template>
843
- </select>
844
- </label>
845
- ```
846
-
847
- What ships is the template the picker page wrote by hand, and the behavior is
848
- `lb-options` inherited whole — a distinct class exists only because
849
- `customElements.define` wants one constructor per name. Drop `exp-group` and
850
- the placeholder has nothing behind it, so `lb-group` is dropped with it and
851
- the options arrive ungrouped: presence propagation doing the work a conditional
852
- would do elsewhere.
853
-
854
- Neither widget is more capable than the pair it wraps, and that is the point.
855
- A page that wants a second element in a row, or an option built from two
856
- columns, writes the template itself and gets the plain repeater. These are for
857
- when it does not.
858
-
859
- #### Subtotals, and where a total goes
860
-
861
- `demo/pages/ledger.html` is a report: detail lines, a subtotal under each
862
- section, and a total under the table. It is the shape a balance sheet has, and
863
- it is the one case that looks like it needs the browser to calculate something.
864
-
865
- It does not, and the reason is that the calculation was already done by
866
- something better at it. Every SQL database produces a set with subtotals in it
867
- — `GROUP BY ROLLUP`, or `GROUPING SETS` for finer control — so a report can
868
- leave the database finished, in report order, as flat keyed rows. That is the
869
- shape the data channel already carries. Nothing new is on the wire, and the
870
- page writes one row template:
871
-
872
- ```html
873
- <lb-table class="report" lb-query="ledger" exp-caption="Expenses by department">
874
- <template lb-template="head">
875
- <tr><th>Account</th><th class="amount">Amount</th></tr>
876
- </template>
877
-
878
- <template lb-key="key" lb-group="dept">
879
- <tr>
880
- <td lb-cell="line"></td>
881
- <td class="amount" lb-cell="amount"></td>
882
- </tr>
883
- </template>
884
-
885
- <template lb-template="foot">
886
- <tr lb-query="ledgerTotal">
887
- <th scope="row" lb-cell="line"></th>
888
- <td class="amount" lb-cell="amount"></td>
889
- </tr>
890
- </template>
891
- </lb-table>
892
- ```
893
-
894
- There is no `lb-sort`, and its absence is what makes this a report rather than
895
- a table. A set is the order it arrived in, so the server decides where a
896
- subtotal sits — which is the only place that decision can be correct, since
897
- only the server knows what the subtotal is under. `lb-group` stays, and the
898
- section headings are the scaffolding `lb-table` already builds. A subtotal
899
- row carries its section like any other row, so it lands in that section, at the
900
- end, because that is where it arrived.
901
-
902
- **The key says which kind of row it is.** A subtotal has no `id` of its own
903
- and needs a key anyway, so the server mints one, and the prefix it chooses is
904
- the only discriminator the page needs:
905
-
906
- ```json
907
- { "key": "d:x1", "dept": "Engineering", "line": "Salaries", "amount": "184,500.00" },
908
- { "key": "s:Engineering", "dept": "Engineering", "line": "Total, Engineering", "amount": "213,650.00" }
909
- ```
910
-
911
- `lb-key` lands on the row as an attribute, which is what makes this reachable.
912
- A cell would not be: a cell lands as *text* on a native element, and no selector
913
- matches text. So the stylesheet is the whole of the difference between a
914
- subtotal and a detail line, and no widget carries a conditional:
915
-
916
- ```css
917
- .report tr[lb-key^="d:"] td:first-child { padding-left: 1.5rem; }
918
- .report tr[lb-key^="s:"] { font-weight: bold; }
919
- .report tr[lb-key^="s:"] td { border-top: 1px solid; padding-top: 0.35rem; }
920
- ```
921
-
922
- Blank lines are `padding` and rules are `border`. A spacer row would be a row
923
- with a key, which is data standing in for whitespace, and a delete away from
924
- being wrong.
925
-
926
- **The total under the table is not a row.** It is one line that does not
927
- repeat, so it is a tuple, from a second query, landing in a scope of its own —
928
- `lb-query="ledgerTotal"` on the `<tfoot>` row. Two projections of one table
929
- on one page is what the query name is for, and `applyData` resolves the two
930
- independently: one selector finds the widget and delivers rows, another finds
931
- the footer row and fills its cells, and neither knows the second is inside the
932
- first.
933
-
934
- A full `ROLLUP` would have produced that total as a row as well, with a null
935
- department. It is a tuple here for two reasons, and the first is mechanical: a
936
- row belonging to no section has nowhere to land in a grouped body. The second
937
- is that a footer is often not a sum of what is above it at all — a prior
938
- period, a budget, a check figure — and those are the same markup and a
939
- different query.
940
-
941
- **What the server spends and the page never sees.** `GROUPING(line)` is what
942
- tells a subtotal from a detail row, and it is the query's own answer rather
943
- than a column anyone invented. It is spent server-side, on the key prefix and
944
- the label, and never reaches the browser. Amounts are integer cents until the
945
- last moment and arrive formatted, because cells are strings and formatting
946
- money is the same kind of decision as rounding it.
947
-
948
- The whole of it generalizes downward without further mechanism. A third-order
949
- subtotal is another grouping set and another key prefix; depth is a column like
950
- any other. A report of any depth is still a flat ordered list of keyed rows,
951
- because that is what a rendered report is.
952
-
953
- Two things do not survive, and both are shape rather than value. A subtotal
954
- label cannot span columns, since `colspan` is not something a value can
955
- produce — the label goes in the first cell and the rest stay empty. And
956
- indentation by arbitrary depth needs a `style` attribute, which is an unsafe
957
- sink and closed: a fixed ladder of selectors covers a report whose depth is
958
- known, and a deeper one indents in the label.
959
-
960
- #### What a page still cannot do
961
-
962
- A projection is a top-level query result and its values are strings. A tuple
963
- may not contain one, so a page that wants master and detail declares two
964
- queries, as `picker` does: `choices` returns the set, `picked` returns the one
965
- that was chosen. A projection and a tuple cannot arrive on the same element,
966
- so they arrive on two.
967
-
968
- That limit is what a report has to be flattened past rather than a wall a
969
- report stops at, and the section above is what paying it looks like: the
970
- hierarchy is resolved by whatever computed it, and what arrives is flat. What
971
- remains genuinely out of reach is structure whose *depth* is data — a comment
972
- tree, an org chart — where the number of levels is not known when the template
973
- is written.
974
-
975
- An empty set is an empty list and nothing more. Saying "no members yet" is a
976
- conditional rather than a repetition, so it is not here: `applyRows` stamps
977
- `data-rows` and a stylesheet says the rest, in the section above.
978
-
979
- ---
980
-
981
- ## Reference
982
-
983
- ### Attribute vocabulary
984
-
985
- Import every name from `core/lb-constants.ts`. Do not write one as a string
986
- literal in a widget, a page, a test or a document.
987
-
988
- | attribute | constant | goes on | names |
989
- | -------------- | --------------- | ------------------- | --------------------------- |
990
- | `lb-query` | `ATTR_QUERY` | scope element | a declared query |
991
- | `lb-key` | `ATTR_KEY` | row template, row | key column, then its value |
992
- | `lb-group` | `ATTR_GROUP` | a row template | column a widget groups by |
993
- | `lb-cell` | `ATTR_CELL` | leaf widget | column within scope |
994
- | `lb-value` | `ATTR_VALUE` | leaf widget | where the value lands |
995
- | `lb-action` | `ATTR_ACTION` | any element | a declared action |
996
- | `lb-sort` | `ATTR_SORT` | a row template | column a widget sorts by |
997
- | `lb-slot` | `ATTR_SLOT` | inside a definition | where authored content goes |
998
- | `lb-template` | `ATTR_TEMPLATE` | definition, template | a named destination |
999
- | `lb-nav-link` | `ATTR_NAV_LINK` | an anchor | intercept this navigation |
1000
- | `data-rows` | `ATTR_ROW_COUNT`| a list widget | how many rows are showing |
1001
-
1002
- `data-rows` is the one name in the table that is not Loadbare App's namespace. The row
1003
- machinery writes it and a stylesheet reads it; nothing addresses it, so it is
1004
- `data-`, like the section heading `lb-table` builds. It is in the table
1005
- because it is written by shared code and read by pages, which makes it
1006
- published — and a published name is imported like any other.
1007
-
1008
- `lb-group` and `lb-sort` are the two names in the table the hub never reads.
1009
- Placement is the widget's, but the names are shared, because a grouped
1010
- `<select>` and a table with section headings ask the same thing of the same
1011
- data, and a table that sorts asks it of the same column a page author would
1012
- name anywhere else. Loadbare App has no namespace for an attribute that is a
1013
- widget's alone.
1014
-
1015
- The `lb-` prefix is load bearing rather than decorative. `data-*` is the
1016
- handler-prop namespace, so a prop named `cell` or `key` would collide with an
1017
- addressing attribute.
1018
-
1019
- Endpoints: `LB_DATA_ENDPOINT` is `/lb/data`, `LB_REQUEST_ENDPOINT` is
1020
- `/lb`. Page hosts ship as `<template id="page-<name>">`.
1021
-
1022
- ### The definition language
1023
-
1024
- The rules a definition obeys, beyond the ones the Widgets section shows by
1025
- example.
1026
-
1027
- Registry. Every `.html` file in a definition directory defines the tag matching
1028
- its basename. Directories are searched in order and later ones win, so an
1029
- application overrides a built-in widget by putting a file of the same name in
1030
- its own directory.
1031
-
1032
- Scope. Only tags matching `lb-*` are expanded, and every one of them must
1033
- have a definition. An unknown widget tag fails the build by name.
1034
-
1035
- Lowercase names. Placeholder names are lowercase, hyphenated if they need a
1036
- break: `{{input-class}}`, never `{{inputClass}}`. HTML lowercases attribute
1037
- names, so a capital is a name no tag could ever supply. A definition that
1038
- declares one is rejected when definitions load, naming the file and the
1039
- spelling to use.
1040
-
1041
- Attribute survival. Every authored attribute stays on the tag after expansion,
1042
- `exp-` ones included. Placing a parameter in the definition copies it inward;
1043
- it does not move it.
1044
-
1045
- Recursion. A definition may use other widgets. The graph is checked for cycles
1046
- once at load and rejected by name. There is no depth cap.
1047
-
1048
- Templates. Expansion descends into `<template>` content, for substitution as
1049
- well as for expansion, so a row template ships already expanded — whether the
1050
- page author wrote it or the definition did — and the browser only ever clones a
1051
- finished tree.
1052
-
1053
- Named destinations. A definition may mark elements with `lb-template`, and an
1054
- authored `<template>` carrying the same name is unwrapped into the matching
1055
- one. Named templates are taken before the slot, both directions of a mismatch
1056
- fail the build, and there may be several, because each exists to carry markup
1057
- past a parse context the slot cannot reach.
1058
-
1059
- Substitution is applied to a parsed tree with `setAttribute` and node data, and
1060
- serialized once through the DOM's own serializer. Write no escaping code.
1061
-
1062
- Two known holes. A placeholder nobody supplies cannot be detected, because that
1063
- is indistinguishable from presence propagation dropping an attribute. And a
1064
- definition is parsed in a body context, so one whose root is `<tr>` or
1065
- `<option>` is mangled before substitution runs.
1066
-
1067
- ### Applying a response
1068
-
1069
- Every response is a set of query results, and every one lands through the same
1070
- path, whether it is a cold start or the narrowest refresh. The hub calls
1071
- `applyData(main, data)`, which for each query finds every element carrying that
1072
- `lb-query`, and within it sets `lb-value` on every element carrying a matching
1073
- `lb-cell`.
1074
-
1075
- A query in the response with no scope on the page logs a warning and is skipped.
1076
- A cell with no element is silently ignored.
1077
-
1078
- Hydration and refresh are the same call. The browser runs
1079
- `attributeChangedCallback` for attributes already present at upgrade, so a widget
1080
- never asks when its data arrived.
1081
-
1082
- ### Requests
1083
-
1084
- A widget has two options and no third. Handle the interaction itself, or send a
1085
- request. It never asks the framework to change something on its behalf.
1086
-
1087
- To send, dispatch one bubbling `CustomEvent` named `LB_EVENT_NAME` carrying a
1088
- `HubRequest` as its detail:
1089
-
1090
- ```ts
1091
- this.dispatchEvent(
1092
- new CustomEvent(LB_EVENT_NAME, {
1093
- bubbles: true,
1094
- detail: { op: "action", name, value: select.value },
1095
- }),
1096
- );
1097
- ```
1098
-
1099
- The hub sits above every widget, POSTs anything that reaches it to `/lb`, and
1100
- applies the response through the path above. Build the request from the
1101
- attributes the widget already carries — its `lb-cell`, the `lb-key` of its
1102
- row, the `lb-query` of its scope. The vocabulary runs in both directions;
1103
- declare nothing twice.
1104
-
1105
- On a native element the hub turns a click into the request. On a widget it does
1106
- not: the widget owns its own interaction and decides what counts as performing
1107
- it, which for `lb-select` is a change rather than a click.
1108
-
1109
- The operation set is closed:
1110
-
1111
- ```ts
1112
- type HubRequest =
1113
- | { op: "cell-change"; query: string; key: string; cell: string; value: string }
1114
- | { op: "tuple-insert"; query: string; values: Record<string, string> }
1115
- | { op: "tuple-update"; query: string; key: string; values: Record<string, string> }
1116
- | { op: "tuple-delete"; query: string; key: string }
1117
- | { op: "action"; name: string; query?: string; key?: string; cell?: string; value?: string };
1118
- ```
1119
-
1120
- The four CRUD operations name a query, never a table. The fifth names something
1121
- the page declared it can do, and what keeps it from being an RPC endpoint is
1122
- that the name must already appear in the page's hooks — the wire cannot reach
1123
- anything the page has not published.
1124
-
1125
- An action carries at most one value, because a control has at most one. A
1126
- button has none, so the server computes the whole of the result. A `<select>`
1127
- has one, because the choice is the interaction. There is no argument list.
1128
-
1129
- State the refresh set yourself, in `pages/<name>.hooks.ts`. There is no
1130
- automatic mapping from an operation to the queries it affects.
1131
-
1132
- ### Widget author's contract
1133
-
1134
- - Extend `HTMLElement`. No Shadow DOM.
1135
- - Events travel up, method calls travel down.
1136
- - List the attributes you read in `observedAttributes` and render in
1137
- `attributeChangedCallback`. Do not ask when the data arrived.
1138
- - Take your value from `lb-value` and forward it to whatever control you own.
1139
- The hub writes one attribute name for every widget type and holds no table of
1140
- native attribute names.
1141
- - Find your control with `querySelector`. It is a child, placed by your
1142
- definition.
1143
- - Build no children on upgrade. Everything in the live DOM must appear in
1144
- view-source, except values, control state, and clones whose count came from a
1145
- query.
1146
- - To show a list, implement `acceptRows` and call `applyRows`. Decide where a
1147
- row goes and nothing else — cloning, matching and filling are shared, and a
1148
- second implementation of them is a second set of bugs.
1149
- - Attach listeners in `connectedCallback`. A parent may read its children there
1150
- but may not call widget methods on them until insertion completes.
1151
- - Ship no guard against your own definition. The control you `querySelector` is
1152
- placed by the file beside you, so a check for its absence is unreachable code
1153
- that a test should have caught. Write the straight line.
1154
- - Guard what a page author supplied. A missing `lb-action` or an unset
1155
- `lb-cell` comes from a hand-written page, and nothing rejects it today.
1156
- `console.error` with the tag name, return, and disturb nothing else.
1157
-
1158
- The line between the last two is who wrote the thing you are checking. Your
1159
- definition is yours and is tested. A page host is the application's and is not.
1160
-
1161
- A widget is for behavior. `lb-select` is one: it receives its value like any
1162
- other cell, and on change it sends a request. A widget that only forwards a
1163
- value to a control it wraps, or only sends a click, is doing work the hub
1164
- already does.