@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,457 @@
1
+ # Data Binding
2
+
3
+ A page binds its elements to server data with four attributes, and asks the
4
+ server to change that data with one more. The developer writes all five into
5
+ the HTML; the server declares which queries it answers and which operations
6
+ it permits, and refuses anything it has not declared.
7
+
8
+ One vocabulary and one containment ladder: a list holds rows, a row holds
9
+ cells. A column is the second axis, and it is what the value of `lb-cell` or
10
+ `lb-key` always holds.
11
+
12
+ ## A page that binds data
13
+
14
+ Here is a page that binds a scalar, a list, and three requests:
15
+
16
+ ```html
17
+ <!-- src/pages/members.page.html -->
18
+ <h1>Members</h1>
19
+
20
+ <p lb-row="dues">Dues collected this year: <span lb-cell="total"></span></p>
21
+
22
+ <section lb-list="roster">
23
+ <form lb-action="lb-row-insert">
24
+ <input lb-cell="name" placeholder="Name" />
25
+ <button type="submit">Add member</button>
26
+ </form>
27
+
28
+ <ul>
29
+ <template lb-key="id">
30
+ <li>
31
+ <lb-input lb-cell="name"></lb-input>
32
+ <button lb-action="lb-row-delete">Remove</button>
33
+ </li>
34
+ </template>
35
+ </ul>
36
+ </section>
37
+ ```
38
+
39
+ The queries named here — `dues` and `roster` — and the operations the page
40
+ asks for are declared on the server; see [page files](./page-files.md).
41
+
42
+ ## Binding
43
+
44
+ | Attribute | Written by | Names |
45
+ |----------------|-------------|-------------------------------------------------------|
46
+ | `lb-list` | a developer | The set of rows a subtree displays |
47
+ | `lb-row` | a developer | The one row a subtree displays |
48
+ | `lb-key` | a developer | The column that identifies a row |
49
+ | `lb-cell` | a developer | The column an element displays |
50
+ | `lb-show` | a developer | The column that decides whether an element is present |
51
+ | `lb-key-value` | the hub | A live row's own key |
52
+ | `lb-value` | the hub | The value that landed on a cell |
53
+
54
+ A binding is scoped by ancestry. Either scope attribute scopes its DOM
55
+ children, and a nested one of either kind begins a new scope, so an element
56
+ binds to the name on its nearest ancestor carrying one, and to the row on its
57
+ nearest ancestor carrying `lb-key-value`. Nothing else establishes scope: an
58
+ element outside every scope is bound to nothing and displays nothing, and a
59
+ result never crosses into a nested scope.
60
+
61
+ Which of the two a subtree writes is not a choice about display. Cardinality
62
+ is a property of the name, so one name answers with one shape, always. A page
63
+ that shows the roster both as a set and as a single row declares two queries,
64
+ `rosterList` and `rosterRow`, and binds each with the attribute that matches
65
+ what it answers with.
66
+
67
+ Bind an element to a cell by putting `lb-cell` on the element that shows the
68
+ value. Its value is a column name and never a cell name: a cell has no name
69
+ of its own, because it is identified by its row and its column, and the row
70
+ arrives from scope. The scope's own root counts as a cell if it carries one,
71
+ which is how an `<option>` — whose content model is text — displays the value
72
+ it is.
73
+
74
+ An element that carries a scope and `lb-cell` both is a cell of the scope
75
+ around it. Its own `lb-list` or `lb-row` names what it displays, and its
76
+ ancestors name where it belongs, so a `<select lb-list="accounts"
77
+ lb-cell="account">` in a row displays the accounts and holds that row's
78
+ `account`. The value lands on it and is gathered from it; the cells inside
79
+ it are the accounts', and neither.
80
+
81
+ Name the same query on more than one subtree to display it in more than one
82
+ place. Every subtree gets the result.
83
+
84
+ One name is the hub's rather than the server's: `lb-navigation`, one row
85
+ landed on every navigation with the columns `page-label` and `page-uri`. It binds the
86
+ same way. Its name is reserved, as every value beginning with `lb-` is in
87
+ every `lb-` attribute: the server refuses a page that declares a query so
88
+ named — see [Where the page is](./chrome.md#where-the-page-is).
89
+
90
+ A key has a name and a value, and they are two attributes. `lb-key` on a row
91
+ template names the column that identifies a row: it is a property of the
92
+ list, since a list without a fixed key column is meaningless, written where
93
+ the rows land. `lb-key-value` on a row that is showing carries that row's
94
+ value of it. A developer writes the first and never the second.
95
+
96
+ ### Where a bound value lands
97
+
98
+ A value lands on a bound element one of three ways.
99
+
100
+ | Element | Receives the value as |
101
+ |----------------------------------------|-----------------------------------|
102
+ | A custom element | Its `lb-value` attribute |
103
+ | `<select>`, `<textarea>`, or `<input>` | Its `value`, and `lb-value` |
104
+ | Any other native element | Its `textContent`, and `lb-value` |
105
+
106
+ A widget owns whatever control it wraps, so it is handed the value and
107
+ renders it itself — see [Custom Elements](./custom-elements.md#code) for
108
+ observing `lb-value`. A form control shows its state as its `value`, so a
109
+ `<select>` keeps its options. Any other native element has no behavior of its
110
+ own, so its value is its text. Checkboxes, radio buttons and file inputs
111
+ receive nothing, not even `lb-value`, and the hub reports it to the console.
112
+
113
+ Nothing an application writes ever sets `lb-value`. It is written by
114
+ Loadbare and read by a widget or a stylesheet.
115
+
116
+ ### Lists
117
+
118
+ Write `lb-list` on any element, and a `<template>` inside it carrying
119
+ `lb-key`. The hub clones that template once per row, fills each clone, and
120
+ reconciles what is showing against what arrived. No widget is involved, and
121
+ none is needed.
122
+
123
+ An array is the whole set, so it decides membership and order: a row whose
124
+ key did not arrive is gone. A patch touches only the rows it names and leaves
125
+ every other row's contents and position alone.
126
+
127
+ A widget enters only where the rows need scaffolding or placement that only
128
+ it can decide — a `<select>` that builds an `<optgroup>` per distinct value, a
129
+ table that sections and sorts. Such a widget carries `lb-list` itself and
130
+ implements one or both of two optional methods; see
131
+ [Decorating a list](./custom-elements.md#decorating-a-list) and
132
+ [The Basic Widget Library](./widgets.md).
133
+
134
+ A scope with `lb-list` and no row template displays nothing, and that is not
135
+ an error. It is bound to the list without showing it, which is what an insert
136
+ form naming the list it adds a row to already is.
137
+
138
+ ## Requests
139
+
140
+ One attribute turns an interaction into a request. `lb-action` names what the
141
+ server is asked for: an action the page declared, or one of Loadbare's
142
+ reserved names, which are the CRUD operations.
143
+
144
+ One attribute name, one wire field, one set of values. The request field
145
+ `action` carries this attribute's value verbatim, so nothing is translated
146
+ between the markup and the server, and the CRUD key is the same value with
147
+ the prefix stripped and the rest camel-cased.
148
+
149
+ | Written | Asks for | Carries |
150
+ |-------------------------------------------|--------------|-------------------------------|
151
+ | `lb-action="name"` | That action | The scope, and what is in it |
152
+ | `lb-action="lb-row-delete"` | `rowDelete` | `list`, `key` |
153
+ | `lb-action="lb-row-insert"` on a `<form>` | `rowInsert` | `list`, `values` |
154
+ | `lb-action="lb-row-update"` on a `<form>` | `rowUpdate` | `list`, `key`, `values` |
155
+ | `lb-action="lb-row-update"` on a widget cell | `rowUpdate` | `list`, `key`, `values` of one cell |
156
+
157
+ All three operations are list operations. Each needs a key, and a key exists
158
+ only on a live row the hub stamped inside a list, so a single-row scope is
159
+ read-only and a declared action is the only thing it can send. An application
160
+ that wants a writable single row declares a list that answers with one row.
161
+
162
+ A name beginning with `lb-` is reserved, in `lb-action` and in either scope
163
+ attribute alike. The server refuses a page that declares an action or a query
164
+ so named, which is what lets a reserved name be added later without colliding
165
+ with one an application already uses. That reservation is also the whole of
166
+ the wire discriminant: a value beginning with `lb-` is an operation, and
167
+ anything else is a name the page declared.
168
+
169
+ The hub sends a request from a native element on the element's own event: a
170
+ form on submit, anything else on click. An insert or update clicked from a
171
+ button reads the form or row the button is in (see [Forms](#forms)). To
172
+ commit one cell as it changes, put `lb-row-update` on a widget cell instead
173
+ (see [Committing one cell](#committing-one-cell)).
174
+
175
+ Declare every action on the server, and permit every operation on its list. A
176
+ name the page has not declared, and an operation a list does not permit, are
177
+ refused; see
178
+ [requests, actions, CRUD](./page-files.md#requests-actions-crud).
179
+
180
+ ### Actions
181
+
182
+ Write `lb-action` on a button to ask the server to do something that is not
183
+ one of the three CRUD operations:
184
+
185
+ ```html
186
+ <button lb-action="mailRoster">Mail the roster</button>
187
+ ```
188
+
189
+ An action carries whatever binding is in scope at the element — `list` or
190
+ `row`, whichever scoped it, plus `key` and `cell` — and nothing else. There is
191
+ no argument list. A button carries no value, so the server computes the whole
192
+ of the new state and the page displays only what came back.
193
+
194
+ Write `lb-action` on a widget to have the widget decide what performing the
195
+ action means. Loadbare turns a click into a request for a native element
196
+ only, and leaves a widget to send its own — a `<select>` performs its action
197
+ on change, not on click.
198
+
199
+ ### Deleting a row
200
+
201
+ ```html
202
+ <button lb-action="lb-row-delete">Remove</button>
203
+ ```
204
+
205
+ `lb-row-delete` needs nothing declared. The row's `lb-list` and `lb-key-value`
206
+ are already in scope, and they are all the server needs to know which row is
207
+ meant and whether the list permits deleting it.
208
+
209
+ The hub sends it from a native element. A widget sends its own request.
210
+
211
+ ### Forms
212
+
213
+ A `<form>` performs its `lb-action` on submit. `lb-row-insert` and `lb-row-update`
214
+ both gather every `lb-cell` of the form into one values map, read from
215
+ the control each cell is or wraps. A cell inside a scope nested in the form
216
+ is that scope's and is not gathered, the same way a value landing on the
217
+ form's row does not reach it. They differ in one thing: `lb-row-update`
218
+ also carries the key of the row it is inside, and `lb-row-insert` carries none,
219
+ because there is no row yet. A declared name on a form sends that action on
220
+ submit, carrying the binding and no values.
221
+
222
+ Put an `lb-row-update` form inside the row it edits, so it has that row's key
223
+ from the same ancestor a delete button reads:
224
+
225
+ ```html
226
+ <template lb-key="id">
227
+ <li>
228
+ <form lb-action="lb-row-update">
229
+ <input lb-cell="name" />
230
+ <button type="submit">Save</button>
231
+ </form>
232
+ </li>
233
+ </template>
234
+ ```
235
+
236
+ When an `lb-row-insert` succeeds, the hub resets every control it gathered
237
+ from to its default, as `form.reset()` would, so the form is ready for the
238
+ next entry. A failed insert leaves the entry for the user to correct, and a
239
+ control the user changed while the request was in flight keeps the change.
240
+ An `lb-row-update` resets nothing: the row it sent lands back on its cells.
241
+
242
+ Give every `lb-cell` in a form a control to read. A cell that is neither an
243
+ `<input>`, `<select>`, or `<textarea>` nor wraps one is left out of the
244
+ values map, and a form with no cell to read sends nothing.
245
+
246
+ An insert or update gathers the row it belongs to: the nearest `<form>`,
247
+ `<tr>` or live row around the element that sends it, inside its scope. A
248
+ button submits its form the same way, whatever else sits beside it.
249
+
250
+ A form cannot go around a table row's controls, so there the row is the
251
+ form. Put the action on a button in the row, and it inserts every cell in
252
+ the row, wherever in the row the button and the cells are:
253
+
254
+ ```html
255
+ <table lb-list="roster">
256
+ <tbody>
257
+ <tr>
258
+ <td><input lb-cell="name" /></td>
259
+ <td>
260
+ <input lb-cell="role" />
261
+ <button lb-action="lb-row-insert">Add</button>
262
+ </td>
263
+ </tr>
264
+ </tbody>
265
+ </table>
266
+ ```
267
+
268
+ An `lb-row-update` button in a live row saves that row and no other, and a
269
+ form nested in a row gathers only the form. Anywhere else, write the form:
270
+ cells and a button in a `<div>` belong to no row, so the hub refuses the
271
+ request and says to put them in a `<form>`.
272
+
273
+ Put these two actions on a form or a button, not on an element that holds
274
+ the cells. A click into one of its inputs would send the row, so the hub
275
+ refuses it. A widget may dispatch either from any element, and the hub
276
+ gathers the same way; see
277
+ [Sending a request](./custom-elements.md#sending-a-request).
278
+
279
+ ### Committing one cell
280
+
281
+ Put `lb-row-update` on a widget that carries `lb-cell` to save that one cell
282
+ whenever it changes. The shipped `<lb-input>` sends it on `change`:
283
+
284
+ ```html
285
+ <template lb-key="id">
286
+ <tr>
287
+ <td><lb-input lb-cell="name" lb-action="lb-row-update"></lb-input></td>
288
+ <td><lb-input lb-cell="note" lb-action="lb-row-update"></lb-input></td>
289
+ </tr>
290
+ </template>
291
+ ```
292
+
293
+ An element carrying `lb-cell` is a record of one cell, the way a control has
294
+ a value and a form has values. Its update carries `values` holding that cell
295
+ alone, so an edit in one input never sends the other. It reaches the same
296
+ `rowUpdate` a form does. SQL has one UPDATE whether it sets one column or
297
+ many, and the application writes one handler for both.
298
+
299
+ The widget decides when the cell has changed. A native `<input>` carrying
300
+ `lb-cell` and `lb-row-update` has no such moment, since a click into it
301
+ would send it, so the hub refuses it and says so.
302
+
303
+ Leave `lb-action` off a widget inside an `lb-row-insert` or `lb-row-update`
304
+ form. The form reads every `lb-cell` in it on submit, so a widget that also
305
+ sent its own would write the same edit twice.
306
+
307
+ ## Conditional rendering
308
+
309
+ Loadbare ships static HTML and hydrates elements that are already in the
310
+ document. There is no `if`, and none is needed: write every possibility into
311
+ the page, and let a column decide which of them is present.
312
+
313
+ Write `lb-show` on an element, naming the column that decides it:
314
+
315
+ ```html
316
+ <template lb-key="id">
317
+ <tr>
318
+ <td lb-cell="name"></td>
319
+ <td><button lb-action="lb-row-delete" lb-show="removable">Remove</button></td>
320
+ </tr>
321
+ </template>
322
+ ```
323
+
324
+ ```sql
325
+ (ledger_count = 0 AND system_behavior IS NULL) AS removable
326
+ ```
327
+
328
+ A value of `null` or `false` takes the element out of the page, and any other
329
+ value puts it back. The hub never reads a string, so `"false"` is a value like
330
+ any other: have the query answer with a boolean or a null. A row that does not
331
+ carry the column leaves the element as it is, so a query that answers with
332
+ whole rows returns the column in every row.
333
+
334
+ `lb-show` binds the way `lb-cell` does, to the row on its nearest scoped
335
+ ancestor. On an element that is itself a scope, the column belongs to the
336
+ row around it, so this picker takes its choices from `groups` and whether it
337
+ is present from the account row:
338
+
339
+ ```html
340
+ <select lb-list="groups" lb-cell="group_id" lb-show="group_choice">
341
+ ```
342
+
343
+ An element may show a column and be decided by it, which shows a note only
344
+ when there is one:
345
+
346
+ ```html
347
+ <span lb-cell="note" lb-show="note"></span>
348
+ ```
349
+
350
+ An element that is not present cannot be clicked, but that is presentation:
351
+ the server still refuses what a request may not do.
352
+
353
+ ### Where an absent element is
354
+
355
+ An element whose column is off is moved into a `<template lb-show>` that
356
+ stands where it stood, and moved back out when the column turns on. The
357
+ developer never writes that template.
358
+
359
+ - Nothing renders it, whatever a stylesheet says, because a template's content
360
+ is not its children.
361
+ - It cannot be focused or clicked, assistive technology does not announce it,
362
+ and a form does not gather it.
363
+ - It is moved, never rebuilt, so a widget keeps its instance and a control
364
+ keeps what was typed into it.
365
+ - Values keep landing on it, and on every cell and scope inside it, while it
366
+ is away, so it returns current.
367
+
368
+ The builder ships every `lb-show` element already inside its template, so
369
+ nothing conditional shows until its row has landed.
370
+
371
+ A condition never changes the structure of a page. An absent element keeps
372
+ its place among its siblings, so a position selector (`:first-child`,
373
+ `:nth-child`, `:empty`, `+`, `~`) counts its template as a sibling. A selector
374
+ by tag, class or attribute is unaffected. Only a list changes a page's
375
+ structure.
376
+
377
+ A condition that is only a style is a class on an element that is present or
378
+ not:
379
+
380
+ ```html
381
+ <span lb-show="out_of_balance" class="danger">Out of balance</span>
382
+ ```
383
+
384
+ These are build errors:
385
+
386
+ - `lb-show` on a row template's root. A row that should not show is left out
387
+ by the query.
388
+ - `lb-show` with no row around it: outside every scope, on a scope with none
389
+ around it, or in a list scope outside its row template, where nothing lands.
390
+ - `lb-show` on a `<template>`.
391
+
392
+ A condition that is not data, such as a collapsed section or an open menu,
393
+ has no column. Use `<details>`, a stylesheet, or a widget. Every cell still
394
+ carries the value that landed on it as `lb-value`, for a widget to read or a
395
+ stylesheet to select on.
396
+
397
+ ### An empty list
398
+
399
+ The hub stamps every list scope with `lb-row-count`, the number of rows it is
400
+ showing. It is the one conditional a page cannot be sent, because the server
401
+ answers with rows and says nothing about how many survived. It makes an empty
402
+ list a stylesheet rule rather than code anywhere:
403
+
404
+ ```html
405
+ <div lb-list="roster">
406
+ <ul>
407
+ <template lb-key="id">
408
+ <li lb-cell="name"></li>
409
+ </template>
410
+ </ul>
411
+ <p class="roster-empty">No members yet.</p>
412
+ </div>
413
+ ```
414
+
415
+ ```css
416
+ .roster-empty {
417
+ display: none;
418
+ }
419
+ [lb-row-count="0"] .roster-empty {
420
+ display: revert;
421
+ }
422
+ ```
423
+
424
+ The scope is a `<div>` here rather than the `<ul>`, so that the empty message
425
+ is inside it and the same rule can reach both.
426
+
427
+ ## Request state
428
+
429
+ Loadbare stamps two attributes on the element a request came from — the
430
+ button, the form, or the widget itself:
431
+
432
+ | Attribute | Means |
433
+ |-------------------|-------------------------------------------|
434
+ | `lb-pending` | The request is in flight |
435
+ | `lb-error` | The last request from this element failed |
436
+
437
+ `lb-pending` is set when the request goes out and removed when it
438
+ settles. `lb-error` is set on a failed response, a network failure, or
439
+ a timeout alike, and cleared when that element sends its next request.
440
+
441
+ Neither one carries any meaning beyond the fact it states. Dim a pending
442
+ button in a stylesheet, or have a widget watch its own attributes and
443
+ disable itself. An application that styles neither behaves correctly and
444
+ shows nothing.
445
+
446
+ A native button or form pressed again while it carries `lb-pending` is
447
+ ignored, so a pending one is already disabled and the stylesheet only shows
448
+ it. A widget is not held back, since one that sends on change must send its
449
+ latest value. The hub also sets `aria-busy="true"` for as long as
450
+ `lb-pending` is present.
451
+
452
+ ## Sending a request from a widget
453
+
454
+ A widget can build and dispatch a request itself instead of carrying one of
455
+ the attributes above — which is what `<lb-input>` does, and what a widget
456
+ carrying `lb-action` must do. See
457
+ [Custom Elements](./custom-elements.md#code).
@@ -0,0 +1,38 @@
1
+ # The Elements of a Loadbare Application
2
+
3
+ This reference is organized into four groups that reflect how an application is
4
+ put together: the shell it lives in, the build that assembles it, the pages it
5
+ serves, and the widgets those pages are made of.
6
+
7
+ ## The application shell
8
+
9
+ | Topic | Description |
10
+ |--------------------------------------------------|-----------------------------------------------------|
11
+ | [`chrome.html`](./chrome.md) | The HTML document w/head, body, banner, main, etc. |
12
+ | [The Express server](./server.md) | Serves static assets and handles data channel calls |
13
+ | [The database layer](./server.md#database-layer) | Where the app's database access fits |
14
+
15
+ ## Building the app
16
+
17
+ | Topic | Description |
18
+ |----------------------------------------------------------|-------------------------------------------|
19
+ | [`imports.ts`](./builder.md#widgets-from-packages) | The packages this app takes widgets from |
20
+ | [`loadbare-app-build`](./builder.md#running-the-builder) | Configuring the build command |
21
+ | [CSS](./css.md) | How the builder packages css |
22
+
23
+ ## Application Pages
24
+
25
+ | Topic | Description |
26
+ |---------------------------------------------------------|--------------------------------|
27
+ | [`<name>.page.html`](./page-files.md#html) | The page's HTML |
28
+ | [`<name>.queries.ts`](./page-files.md#queries) | The data the page displays |
29
+ | [`<name>.requests.ts`](./page-files.md#requests-actions-crud) | Data Channel handlers |
30
+ | [Data Binding](./data-binding.md) | Connecting HTML to server data |
31
+
32
+ ## Widgets
33
+
34
+ | Topic | Description |
35
+ |------------------------------------------------|---------------------------------|
36
+ | [What a widget is](./custom-elements.md) | An HTML file, a script, or both |
37
+ | [`<tag-name>.html`](./custom-elements.md#html) | The markup the tag expands into |
38
+ | [`<tag-name>.ts`](./custom-elements.md#code) | The class the tag registers |
@@ -0,0 +1,194 @@
1
+ # Page Files
2
+
3
+ A page is a set of files sharing one base name. The application writes the
4
+ HTML, and adds queries and requests when the page shows data.
5
+
6
+ | File | Holds |
7
+ |----------------------|-------------------------------|
8
+ | `<name>.page.html` | The page's HTML |
9
+ | `<name>.queries.ts` | The data the page displays |
10
+ | `<name>.requests.ts` | What the page does when asked |
11
+
12
+ Name the landing page `index.page.html`. A bare `/` resolves to `index`, and
13
+ every other path names the page of the same name.
14
+
15
+ Put the files anywhere under `src/`. The builder pairs them by base name, not
16
+ by directory; `src/pages/` is the convention.
17
+
18
+ ## HTML
19
+
20
+ Write the page as a fragment. The fragment will land in `<main>`, which
21
+ is supplied by [chrome.html](./chrome.md).
22
+
23
+ ```html
24
+ <!-- src/pages/about.page.html -->
25
+ <h1>About</h1>
26
+ <p>This is the about page.</p>
27
+
28
+ <div lb-row="visits">
29
+ <p>This page has been visited <span lb-cell="count"></span> times.</p>
30
+ </div>
31
+ ```
32
+
33
+ Bind elements to data with `lb-list` or `lb-row`, `lb-cell`, and the rest of the
34
+ attribute vocabulary in [Data Binding](./data-binding.md).
35
+
36
+ A page that displays no data needs no other file.
37
+
38
+ ## Queries
39
+
40
+ Export `queries` from `<name>.queries.ts`. Each key is a name the HTML binds
41
+ to with `lb-list` or `lb-row`, and each value takes the request context and returns
42
+ that query's result:
43
+
44
+ ```ts
45
+ // src/pages/about.queries.ts
46
+ import { row, type Queries } from "@loadbare/app/server";
47
+
48
+ export const queries: Queries = {
49
+ visits: row(async (ctx) => ({ count: String(await ctx.db.visitCount()) })),
50
+ };
51
+ ```
52
+
53
+ Declare each query with `row()` or `list()`. Cardinality is a property of the
54
+ name rather than of any one answer, so one name answers with one shape,
55
+ always, and `createHub` refuses an answer that disagrees. A page that needs
56
+ the same data as one row and as a set declares two queries.
57
+
58
+ The hub hands each cell to the browser untouched and takes no position on
59
+ its type, so what a number, a date or a null looks like is decided here, in
60
+ the query. Formatting it here means the browser displays a value it never
61
+ computes.
62
+
63
+ Declare a query that answers with many rows using `list()`, and return the
64
+ array itself:
65
+
66
+ ```ts
67
+ // src/pages/directory.queries.ts
68
+ import { list, type Queries } from "@loadbare/app/server";
69
+
70
+ export const queries: Queries = {
71
+ directory: list((ctx) => ctx.db.directory()),
72
+ };
73
+ ```
74
+
75
+ Give every row a column that identifies it, and name that column with
76
+ `lb-key` in the HTML. Return the rows in the order the page shows them.
77
+
78
+ Return the full result every time. Sending only what changed is a request's job —
79
+ see [refresh and patch](#refresh-and-patch).
80
+
81
+ ## Requests, actions, CRUD
82
+
83
+ Export `requests` from `<name>.requests.ts`. It holds three keys, each optional:
84
+
85
+ | Key | Runs |
86
+ |---------------|------------------------------------------------------|
87
+ | `onPageEnter` | Before the page's queries, on entering the page |
88
+ | `actions` | What the page may be asked to do, by name |
89
+ | `crud` | The three operations a list permits on its rows |
90
+
91
+ ### onPageEnter
92
+
93
+ ```ts
94
+ // src/pages/about.requests.ts
95
+ import { type Requests } from "@loadbare/app/server";
96
+
97
+ export const requests: Requests = {
98
+ onPageEnter: (ctx) => ctx.db.recordVisit(),
99
+ };
100
+ ```
101
+
102
+ Declare no refresh set here. The page's queries run afterward.
103
+
104
+ ### actions
105
+
106
+ Declare an action under the name the HTML gives `lb-action`. Pair what it
107
+ does with the queries to re-run once it has:
108
+
109
+ ```ts
110
+ export const requests: Requests = {
111
+ actions: {
112
+ resetVisits: {
113
+ run: (ctx) => ctx.db.resetVisits(),
114
+ refresh: ["visits"],
115
+ },
116
+ },
117
+ };
118
+ ```
119
+
120
+ Declare every action the page allows. A name the page does not declare is
121
+ refused.
122
+
123
+ Read where the interaction happened from `run`'s second argument, which
124
+ carries `list` or `row`, whichever attribute scoped the element, plus `key`,
125
+ `cell` and `value` when the element that dispatched the request had them.
126
+
127
+ ### crud
128
+
129
+ Declare CRUD operations under `crud`, keyed by the list they operate on. All
130
+ three are list operations: each needs a key, and a key exists only on a live
131
+ row inside a list, so a single-row scope is read-only and a declared action is
132
+ the only thing it can send. Each operation takes the binding its trigger
133
+ supplies:
134
+
135
+ | Operation | The page writes | `run` receives |
136
+ |-------------|------------------------------------------------------------------------|-----------------|
137
+ | `rowDelete` | `lb-action="lb-row-delete"` | `key` |
138
+ | `rowInsert` | `<form lb-action="lb-row-insert">` | `values` |
139
+ | `rowUpdate` | `<form lb-action="lb-row-update">` or `<lb-input lb-action="lb-row-update">` | `key`, `values` |
140
+
141
+ Write `rowUpdate` to set the columns `values` names and leave every other
142
+ column as it is. A form sends the cells it holds, and a widget cell sends
143
+ itself alone. Check the names in `values` against the columns the list lets
144
+ the page edit.
145
+
146
+ The operation names are reserved: a name beginning with `lb-` cannot be
147
+ declared under `actions` or as a query, and `createHub` refuses a page that
148
+ tries.
149
+
150
+ ```ts
151
+ // src/pages/directory.requests.ts
152
+ import { patch, type Requests } from "@loadbare/app/server";
153
+
154
+ export const requests: Requests = {
155
+ crud: {
156
+ directory: {
157
+ rowInsert: {
158
+ run: async (ctx, { values }) => {
159
+ const entry = await ctx.db.addDirectoryEntry(values);
160
+ return { directory: patch({ rows: [entry] }) };
161
+ },
162
+ refresh: [],
163
+ },
164
+ },
165
+ },
166
+ };
167
+ ```
168
+
169
+ Declare every operation the list permits. An operation a list does not
170
+ declare is refused, and a name with no `crud` entry permits none.
171
+
172
+ ### refresh and patch
173
+
174
+ List in `refresh` every query whose whole answer the operation changed.
175
+
176
+ Return a result from `run` to state a narrower change than re-running a query
177
+ would. What `run` returns is laid over the refreshed queries:
178
+
179
+ | Result | States |
180
+ |--------------------------|---------------------------------------------|
181
+ | `[...]` | The entire set, and its order |
182
+ | `patch({ rows: [...] })` | These rows arrive or change; the rest stand |
183
+ | `patch({ drop: [...] })` | These keys are gone; the rest stand |
184
+
185
+ Return a patch for a change the operation knows the extent of — one row added,
186
+ one row dropped, one row edited — and leave `refresh` empty. Re-run the query
187
+ instead when membership or order changed in a way the operation cannot name:
188
+
189
+ ```ts
190
+ resetRoster: {
191
+ run: (ctx) => ctx.db.resetMembers(),
192
+ refresh: ["roster"],
193
+ },
194
+ ```