@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.
- package/dist/build/skills-cli.d.ts +12 -0
- package/dist/build/skills-cli.d.ts.map +1 -0
- package/dist/build/skills-cli.js +81 -0
- package/dist/build/skills-cli.js.map +1 -0
- package/dist/build/skills.d.ts +47 -0
- package/dist/build/skills.d.ts.map +1 -0
- package/dist/build/skills.js +124 -0
- package/dist/build/skills.js.map +1 -0
- package/dist/core/lb-constants.d.ts +1 -2
- package/dist/core/lb-constants.d.ts.map +1 -1
- package/dist/core/lb-constants.js +13 -12
- package/dist/core/lb-constants.js.map +1 -1
- package/dist/core/lb-types.d.ts +4 -10
- package/dist/core/lb-types.d.ts.map +1 -1
- package/dist/core/lb-types.js.map +1 -1
- package/dist/hub/lb-hub.browser.d.ts.map +1 -1
- package/dist/hub/lb-hub.browser.js +47 -24
- package/dist/hub/lb-hub.browser.js.map +1 -1
- package/dist/server/lb-express.d.ts.map +1 -1
- package/dist/server/lb-express.js +2 -7
- package/dist/server/lb-express.js.map +1 -1
- package/dist/server/lb-server.d.ts +8 -15
- package/dist/server/lb-server.d.ts.map +1 -1
- package/dist/server/lb-server.js +0 -3
- package/dist/server/lb-server.js.map +1 -1
- package/docs/TECHREF-1.0.md +21 -13
- package/docs/analysis-closed-set.md +16 -9
- package/docs/comparison.md +7 -6
- package/docs/reference/custom-elements.md +11 -6
- package/docs/reference/data-binding.md +36 -12
- package/docs/reference/page-files.md +13 -9
- package/docs/reference/widgets.md +4 -4
- package/docs/roadmap.md +1 -1
- package/docs/testing.md +5 -1
- package/docs/theory.md +1 -1
- package/docs/tutorials/080-widget-requests.md +9 -28
- package/package.json +8 -4
- package/skills/loadbare-app/SKILL.md +258 -0
- package/skills/loadbare-app/references/TECHREF-1.0.md +1189 -0
- package/skills/loadbare-app/references/builder.md +134 -0
- package/skills/loadbare-app/references/chrome.md +158 -0
- package/skills/loadbare-app/references/css.md +44 -0
- package/skills/loadbare-app/references/custom-elements.md +397 -0
- package/skills/loadbare-app/references/data-binding.md +457 -0
- package/skills/loadbare-app/references/overview.md +38 -0
- package/skills/loadbare-app/references/page-files.md +194 -0
- package/skills/loadbare-app/references/server.md +142 -0
- 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
|
+
```
|