@loadbare/app 0.7.4 → 0.8.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 (48) hide show
  1. package/dist/build/skills-cli.d.ts +12 -0
  2. package/dist/build/skills-cli.d.ts.map +1 -0
  3. package/dist/build/skills-cli.js +81 -0
  4. package/dist/build/skills-cli.js.map +1 -0
  5. package/dist/build/skills.d.ts +47 -0
  6. package/dist/build/skills.d.ts.map +1 -0
  7. package/dist/build/skills.js +124 -0
  8. package/dist/build/skills.js.map +1 -0
  9. package/dist/core/lb-constants.d.ts +1 -2
  10. package/dist/core/lb-constants.d.ts.map +1 -1
  11. package/dist/core/lb-constants.js +13 -12
  12. package/dist/core/lb-constants.js.map +1 -1
  13. package/dist/core/lb-types.d.ts +4 -10
  14. package/dist/core/lb-types.d.ts.map +1 -1
  15. package/dist/core/lb-types.js.map +1 -1
  16. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  17. package/dist/hub/lb-hub.browser.js +47 -24
  18. package/dist/hub/lb-hub.browser.js.map +1 -1
  19. package/dist/server/lb-express.d.ts.map +1 -1
  20. package/dist/server/lb-express.js +2 -7
  21. package/dist/server/lb-express.js.map +1 -1
  22. package/dist/server/lb-server.d.ts +8 -15
  23. package/dist/server/lb-server.d.ts.map +1 -1
  24. package/dist/server/lb-server.js +0 -3
  25. package/dist/server/lb-server.js.map +1 -1
  26. package/docs/TECHREF-1.0.md +21 -13
  27. package/docs/analysis-closed-set.md +16 -9
  28. package/docs/comparison.md +7 -6
  29. package/docs/reference/custom-elements.md +11 -6
  30. package/docs/reference/data-binding.md +36 -12
  31. package/docs/reference/page-files.md +13 -9
  32. package/docs/reference/widgets.md +4 -4
  33. package/docs/roadmap.md +1 -1
  34. package/docs/testing.md +5 -1
  35. package/docs/theory.md +1 -1
  36. package/docs/tutorials/080-widget-requests.md +9 -28
  37. package/package.json +8 -4
  38. package/skills/loadbare-app/SKILL.md +258 -0
  39. package/skills/loadbare-app/references/TECHREF-1.0.md +1189 -0
  40. package/skills/loadbare-app/references/builder.md +134 -0
  41. package/skills/loadbare-app/references/chrome.md +158 -0
  42. package/skills/loadbare-app/references/css.md +44 -0
  43. package/skills/loadbare-app/references/custom-elements.md +397 -0
  44. package/skills/loadbare-app/references/data-binding.md +457 -0
  45. package/skills/loadbare-app/references/overview.md +38 -0
  46. package/skills/loadbare-app/references/page-files.md +194 -0
  47. package/skills/loadbare-app/references/server.md +142 -0
  48. package/skills/loadbare-app/references/widgets.md +174 -0
@@ -0,0 +1,397 @@
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>.browser.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
+ The script says `.browser` because that is the file that crosses to the
16
+ browser. Name it `<tag-name>.ts` and the builder leaves it alone: an
17
+ unmarked module is server-side code as far as the build is concerned, which
18
+ is what keeps a `stripe.ts` or a `config.ts` out of the bundle no matter what
19
+ tag someone writes later.
20
+
21
+ Custom elements are the only mechanism Loadbare offers for either purpose.
22
+ An application decomposes its HTML by defining a tag, and delivers behavior
23
+ to the browser by registering one; the builder recognizes no other way to do
24
+ either.
25
+
26
+ Put the files anywhere under `src/`. The builder finds them by name, the
27
+ same way it finds pages — see [The Builder](./builder.md).
28
+
29
+ Loadbare uses light DOM throughout. A widget's markup is ordinary markup in
30
+ the document, visible in view-source, reachable by `closest()` and by the
31
+ application's stylesheets.
32
+
33
+ ## HTML
34
+
35
+ An `.html` file named for a tag defines that tag. When the builder finds the
36
+ tag in the chrome, in a page, or in another definition, it inserts the
37
+ definition's content as children of the tag. We call this process
38
+ 'HTML expansion'.
39
+
40
+ A package listed in `imports.ts` loads first and an application's own
41
+ definitions load after, so a tag defined in both resolves to the
42
+ application's.
43
+
44
+ ### Parameters
45
+
46
+ A definition takes parameters. Three namespaces share the attributes of a
47
+ widget tag, and the prefix says which namespace an attribute is in:
48
+
49
+ | Prefix | Belongs to | Read |
50
+ |-------------|------------|-----------------------------|
51
+ | `exp-` | Expansion | By the builder, at build time |
52
+ | `lb-` | The hub | By Loadbare, in the browser |
53
+ | No prefix | HTML | By the browser, as HTML says |
54
+
55
+ Pass a parameter by writing `exp-<name>` on the tag. Read it in the
56
+ definition by writing `{{<name>}}`. The prefix marks the attribute as
57
+ expansion's input and is not part of the parameter's name, so `exp-label`
58
+ supplies `{{label}}`:
59
+
60
+ ```html
61
+ <!-- definition: src/note-field.html -->
62
+ <label>{{label}} <input readonly="{{readonly}}" /></label>
63
+ ```
64
+
65
+ ```html
66
+ <!-- authored -->
67
+ <note-field lb-cell="name" exp-label="Name"></note-field>
68
+ ```
69
+
70
+ Write `class`, `title`, or any other unprefixed attribute on a widget
71
+ freely. They are HTML's, and never collide with a definition's parameters.
72
+
73
+ Write a placeholder as an entire attribute value or an entire text node.
74
+ Nothing inside one is evaluated, and expansion has no data to branch on —
75
+ see [Conditional rendering](./data-binding.md#conditional-rendering) for
76
+ what to write instead of a branch.
77
+
78
+ Name a parameter in lowercase. HTML lowercases attribute names before
79
+ expansion sees them, so `exp-inputClass` arrives as `exp-inputclass` and
80
+ could never fill `{{inputClass}}`. Write `{{input-class}}` instead; a
81
+ definition declaring such a name is rejected when it loads.
82
+
83
+ Leave a parameter unsupplied to drop what reads it. An attribute whose
84
+ value is a placeholder is removed rather than shipped empty, which is how
85
+ `readonly="{{readonly}}"` works as an ordinary boolean attribute. A
86
+ placeholder in a text node resolves to nothing, and the whitespace around
87
+ it survives.
88
+
89
+ Give a placeholder a default with a pipe: `{{button-text|OK}}` reads
90
+ `exp-button-text` when the tag supplies it and `OK` when it does not. The
91
+ default is a literal, not an expression. Whitespace around the name and the
92
+ default is not part of either, so `{{ button-text | OK }}` is the same
93
+ placeholder. An empty default, `readonly="{{readonly|}}"`, is the one that
94
+ differs from no default at all: it emits the attribute, empty, rather than
95
+ dropping it, so a definition can ship a boolean attribute switched on.
96
+
97
+ Every attribute stays on the tag after expansion, `exp-` ones included.
98
+
99
+ Parameter values reach a definition through the DOM rather than through
100
+ string substitution, so a value is never reparsed as markup and needs no
101
+ escaping.
102
+
103
+ ### The slot
104
+
105
+ `lb-slot` marks the one element in a definition whose children receive
106
+ whatever the author wrote inside the tag:
107
+
108
+ ```html
109
+ <!-- definition: src/note-card.html -->
110
+ <article class="card"><div lb-slot></div></article>
111
+ ```
112
+
113
+ ```html
114
+ <!-- authored -->
115
+ <note-card>
116
+ <h3>Meeting notes</h3>
117
+ <p>Bring the roster.</p>
118
+ </note-card>
119
+ ```
120
+
121
+ ```html
122
+ <!-- ships -->
123
+ <note-card>
124
+ <article class="card">
125
+ <div>
126
+ <h3>Meeting notes</h3>
127
+ <p>Bring the roster.</p>
128
+ </div>
129
+ </article>
130
+ </note-card>
131
+ ```
132
+
133
+ The tag stays and the definition becomes its children. `lb-slot` itself is
134
+ gone from what ships, having done its job at build time.
135
+
136
+ Give a definition at most one `lb-slot`. A definition with a slot and
137
+ nothing written inside it leaves the slot empty.
138
+
139
+ `lb-slot` is an attribute rather than an element because HTML's content
140
+ model discards foreign elements inside `<select>` and `<table>`. A slot
141
+ marker has to survive on an element the surrounding tag already permits.
142
+
143
+ ### Destinations
144
+
145
+ `lb-template` names a destination in a definition. On an authored
146
+ `<template lb-template="name">`, it names the destination that template
147
+ fills:
148
+
149
+ ```html
150
+ <!-- definition: src/ledger-table.html -->
151
+ <table>
152
+ <caption>{{caption}}</caption>
153
+ <thead lb-template="head"></thead>
154
+ <tbody lb-slot></tbody>
155
+ </table>
156
+ ```
157
+
158
+ ```html
159
+ <!-- authored -->
160
+ <ledger-table exp-caption="Ledger">
161
+ <template lb-template="head">
162
+ <tr><th>Date</th><th>Amount</th></tr>
163
+ </template>
164
+ <tbody>
165
+ <!-- row template -->
166
+ </tbody>
167
+ </ledger-table>
168
+ ```
169
+
170
+ Declare as many destinations as the definition needs, one per name. An
171
+ author fills none, some, or all of them, and a destination nobody fills
172
+ stays and is empty.
173
+
174
+ Wrap authored content for a destination in a `<template>`, which exists only
175
+ to survive the parser. A `<thead>` written directly inside `<ledger-table>`
176
+ is not inside a `<table>`, so the tokenizer drops the tag before expansion
177
+ sees it. A `<template>` survives anywhere and parses its contents as though
178
+ already in place. What lands in the destination is the template's contents —
179
+ an ordinary `<thead>` in view-source — not the template element.
180
+
181
+ ### Recursion
182
+
183
+ A definition may use other widgets. Expansion repeats until nothing new
184
+ appears, so a widget built out of other widgets needs nothing declared for
185
+ it.
186
+
187
+ Keep the definition graph acyclic. A cycle is a build error, reported as the
188
+ path that closes it.
189
+
190
+ Expansion runs over `<template>` contents wherever they occur, so a widget's
191
+ row template ships already expanded, `{{placeholder}}` included.
192
+
193
+ ### What fails at build time
194
+
195
+ Each of these is reported with the tag and file name, and nothing is
196
+ shipped:
197
+
198
+ - a tag in the `LB-*` namespace with no definition
199
+ - a cycle in the definition graph
200
+ - an `exp-` attribute the definition never declared
201
+ - a `{{placeholder}}` spelled so that no `exp-` attribute could supply it
202
+ - content written inside a tag whose definition has no `lb-slot`
203
+ - more than one `lb-slot` in one definition
204
+ - a `<template lb-template="name">` naming a destination the definition
205
+ does not have
206
+ - two templates for the same destination
207
+
208
+ An unfilled `{{placeholder}}` is not among these. That is presence
209
+ propagation working as intended, indistinguishable from an author who left
210
+ an optional parameter out.
211
+
212
+ ## Code
213
+
214
+ A `<tag>.browser.ts` file registers that tag's class. A widget is an
215
+ ordinary custom element — Loadbare imposes no base class — and it takes part
216
+ in data binding through three contracts: it receives a value, it sends a
217
+ request, and it accepts a set of rows.
218
+
219
+ Import every attribute name from `@loadbare/app/constants`. Never write one
220
+ as a string literal.
221
+
222
+ | Constant | Value |
223
+ |------------------|------------|
224
+ | `ATTR_VALUE` | `lb-value` |
225
+ | `ATTR_CELL` | `lb-cell` |
226
+ | `ATTR_SHOW` | `lb-show` |
227
+ | `ATTR_LIST` | `lb-list` |
228
+ | `ATTR_ROW` | `lb-row` |
229
+ | `ATTR_KEY` | `lb-key` |
230
+ | `ATTR_KEY_VALUE` | `lb-key-value` |
231
+ | `ATTR_ACTION` | `lb-action`|
232
+ | `ACTION_ROW_INSERT`, `ACTION_ROW_DELETE`, `ACTION_ROW_UPDATE` | the reserved `lb-action` values |
233
+ | `LB_ACTIONS` | all three of them, in one array |
234
+ | `LB_RESERVED_PREFIX` | `lb-`, the prefix every reserved name begins with |
235
+ | `ATTR_ROW_COUNT` | `lb-row-count`|
236
+ | `LB_EVENT_NAME` | `lb-request` |
237
+
238
+ ### Receiving a value
239
+
240
+ A bound cell lands on a widget as the `lb-value` attribute rather than as
241
+ text — see [Data Binding](./data-binding.md#where-a-bound-value-lands).
242
+ Observe it, and render the value however the widget renders things:
243
+
244
+ ```ts
245
+ // src/visit-count.browser.ts
246
+ import { ATTR_VALUE } from "@loadbare/app/constants";
247
+
248
+ class VisitCount extends HTMLElement {
249
+ static observedAttributes = [ATTR_VALUE];
250
+
251
+ attributeChangedCallback(_name: string, _old: string, value: string) {
252
+ this.textContent = `visited ${value} times`;
253
+ }
254
+ }
255
+
256
+ customElements.define("visit-count", VisitCount);
257
+ ```
258
+
259
+ `value` is `null` when the attribute is removed, which the hub does to the
260
+ cells a successful insert resets — see
261
+ [TECHREF-1.0](./TECHREF-1.0.md#the-round-trip). Read it as nothing landed,
262
+ and show the widget's default. A blank string is a value that landed.
263
+
264
+ A widget carrying `lb-show`, or inside an element that does, is moved into a
265
+ template while its column is off and back out when it turns on — see
266
+ [Conditional rendering](./data-binding.md#conditional-rendering). Going in, it
267
+ sees `disconnectedCallback` and then `adoptedCallback`; coming out,
268
+ `adoptedCallback` and then `connectedCallback`. It is the same instance
269
+ throughout, and it still receives `lb-value` while it is away, so write
270
+ `connectedCallback` to run more than once, as a row a list reorders already
271
+ requires.
272
+
273
+ List `ATTR_VALUE` in `observedAttributes`, or `attributeChangedCallback`
274
+ never fires. The browser calls it for an attribute already present when an
275
+ element upgrades, not only for one that changes afterward, so a widget's
276
+ first render and every later refresh go through the one callback. There is
277
+ no separate hydration path to write.
278
+
279
+ ### Sending a request
280
+
281
+ A widget that owns its own interaction — a `<select>`'s choice rather than a
282
+ click — dispatches its own request as a bubbling `CustomEvent` named
283
+ `LB_EVENT_NAME`, carrying the action and, where it wraps a control, that
284
+ control's value as its `detail`:
285
+
286
+ ```ts
287
+ import { LB_EVENT_NAME } from "@loadbare/app/constants";
288
+ import type { HubRequest } from "@loadbare/app/types";
289
+
290
+ const detail: HubRequest = { action: "lb-row-update", value: input.value };
291
+ this.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
292
+ ```
293
+
294
+ The hub fills in the scope the widget sits in before any ancestor sees the
295
+ event, and does not send a request missing a field its operation requires.
296
+ See [TECHREF-1.0](./TECHREF-1.0.md#requests) for what each operation is
297
+ filled with.
298
+
299
+ A widget that carries `lb-cell` and sends `lb-row-insert` or `lb-row-update`
300
+ sends that one cell. The hub builds `values` from the cell's name and the
301
+ `value` the widget sent, or reads the control the widget wraps when it sent
302
+ none.
303
+
304
+ A widget that carries no `lb-cell` doesn't read its own controls. It
305
+ dispatches the bare action from the row, or from anything inside it, and the
306
+ hub gathers `values` from the row the event came from:
307
+ the nearest `<form>`, `<tr>` or live row around the dispatching element,
308
+ itself included, inside its scope. Every readable `lb-cell` in that row is
309
+ gathered, wherever in the row it sits, except the cells of a scope nested in
310
+ it: a picker carrying `lb-list` and `lb-cell` gives its own value, not its
311
+ options'. A request from an element in no such
312
+ row is not sent, and the console says to use a `<form>`.
313
+
314
+ ```ts
315
+ row.dispatchEvent(
316
+ new CustomEvent(LB_EVENT_NAME, {
317
+ bubbles: true,
318
+ detail: { action: "lb-row-insert" } as HubRequest,
319
+ }),
320
+ );
321
+ ```
322
+
323
+ Values the widget supplies itself are kept, and nothing is gathered over
324
+ them.
325
+
326
+ When an insert succeeds, the hub resets the controls it gathered to their
327
+ defaults, so a blank row for entering a new one is blank again without the
328
+ widget watching the request settle. A hidden input the widget added keeps
329
+ its value, since a hidden input's default is its value. Values the widget
330
+ supplied in the request were not gathered, and are not reset.
331
+
332
+ Let the event bubble, so an ancestor widget can intercept and stop it before
333
+ the hub sees it. A hand-written widget and a native element carrying
334
+ `lb-action` produce the same event.
335
+
336
+ ### Decorating a list
337
+
338
+ The hub reconciles every list scope itself. A plain element carrying
339
+ `lb-list` with a row template inside it is a whole list and needs no widget,
340
+ so a widget exists only when the rows need scaffolding or placement that
341
+ only it can decide.
342
+
343
+ Two optional methods say what it decides. Both are named in the `lb`
344
+ namespace, which Loadbare reserves for methods it calls on classes it does
345
+ not own, so a widget's own methods can never collide with a later one:
346
+
347
+ ```ts
348
+ import type { ListHost, Row } from "@loadbare/app/types";
349
+
350
+ class SortedList extends HTMLElement implements ListHost {
351
+ lbPlaceRow(el: Element, row: Row, template: HTMLTemplateElement) {
352
+ // Where this row goes. Called with the element detached, on its first
353
+ // appearance and again whenever a whole set decides the order.
354
+ }
355
+
356
+ lbRowsLanded() {
357
+ // Once, after the result has landed. For scaffolding derived from the
358
+ // rows: a section heading, an <optgroup>, anything that goes when its
359
+ // last row does.
360
+ }
361
+ }
362
+ ```
363
+
364
+ Everything else is the hub's, and a widget never reimplements it:
365
+
366
+ | Concern | What the hub does |
367
+ |-------------|---------------------------------------------------------|
368
+ | Cloning | Clones the `<template lb-key="...">` in the scope |
369
+ | Matching | Updates the row already showing that key, or clones one |
370
+ | Reconciling | Removes the rows the response says are gone |
371
+ | Counting | Stamps `lb-row-count` with the number of rows showing |
372
+
373
+ An array is the whole set, so it decides membership and order, and a key
374
+ absent from it is removed. A patch touches only the rows it names and leaves
375
+ every other row's contents and position alone.
376
+
377
+ `lbPlaceRow` is called with the row already filled and not yet in the
378
+ document, so a widget that reads a cell to decide where the row goes can. A
379
+ scope without it lands rows immediately before the template, in arrival
380
+ order. `lb-options.browser.ts` and `lb-table.browser.ts` in
381
+ [`@loadbare/widgets`](./widgets.md) are two different placements over the
382
+ same machinery.
383
+
384
+ A list scope with no row template displays nothing, which is not an error.
385
+
386
+ Style an empty list against `lb-row-count` rather than carrying an empty-state
387
+ conditional in the widget — see
388
+ [Conditional rendering](./data-binding.md#conditional-rendering).
389
+
390
+ ### Filling a scope by hand
391
+
392
+ `@loadbare/app` also exports `applyRow(root, row)`, the same row-landing
393
+ operation a page host uses. Call it in a widget that builds a scope of its
394
+ own rather than one the hub reconciles. It fills `root`
395
+ itself when `root` carries a matching `lb-cell`, and every matching
396
+ descendant.
397
+ </content>