@loadbare/app 0.13.0 → 0.14.0

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 (42) hide show
  1. package/dist/build/assemble.d.ts.map +1 -1
  2. package/dist/build/assemble.js +31 -1
  3. package/dist/build/assemble.js.map +1 -1
  4. package/dist/core/lb-constants.d.ts +11 -1
  5. package/dist/core/lb-constants.d.ts.map +1 -1
  6. package/dist/core/lb-constants.js +34 -0
  7. package/dist/core/lb-constants.js.map +1 -1
  8. package/dist/core/lb-types.d.ts +27 -0
  9. package/dist/core/lb-types.d.ts.map +1 -1
  10. package/dist/core/lb-types.js.map +1 -1
  11. package/dist/hub/lb-apply.d.ts +20 -3
  12. package/dist/hub/lb-apply.d.ts.map +1 -1
  13. package/dist/hub/lb-apply.js +64 -8
  14. package/dist/hub/lb-apply.js.map +1 -1
  15. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  16. package/dist/hub/lb-hub.browser.js +341 -21
  17. package/dist/hub/lb-hub.browser.js.map +1 -1
  18. package/dist/server/lb-express.d.ts +13 -3
  19. package/dist/server/lb-express.d.ts.map +1 -1
  20. package/dist/server/lb-express.js +30 -12
  21. package/dist/server/lb-express.js.map +1 -1
  22. package/dist/server/lb-server.d.ts +31 -1
  23. package/dist/server/lb-server.d.ts.map +1 -1
  24. package/dist/server/lb-server.js +112 -2
  25. package/dist/server/lb-server.js.map +1 -1
  26. package/docs/TECHREF-1.0.md +294 -21
  27. package/docs/comparison.md +6 -4
  28. package/docs/possible-ideas.md +146 -26
  29. package/docs/reference/custom-elements.md +97 -1
  30. package/docs/reference/data-binding.md +303 -4
  31. package/docs/reference/page-files.md +76 -3
  32. package/docs/reference/server.md +20 -0
  33. package/docs/reference/widgets.md +33 -10
  34. package/docs/roadmap.md +9 -2
  35. package/package.json +1 -1
  36. package/skills/loadbare-app/SKILL.md +50 -7
  37. package/skills/loadbare-app/references/TECHREF-1.0.md +294 -21
  38. package/skills/loadbare-app/references/custom-elements.md +97 -1
  39. package/skills/loadbare-app/references/data-binding.md +303 -4
  40. package/skills/loadbare-app/references/page-files.md +76 -3
  41. package/skills/loadbare-app/references/server.md +20 -0
  42. package/skills/loadbare-app/references/widgets.md +33 -10
@@ -137,10 +137,14 @@ name:
137
137
  | `name` | The request name, from `lb-request` |
138
138
  | `query` | The query the request is for |
139
139
  | `key` | The key of the row the request is for |
140
+ | `keys` | The keys of the rows selected, in place of `key` |
141
+ | `rows` | A form's nested rows, by query: each `key` and `values` |
140
142
  | `values` | The gathered control values, by column |
141
143
 
142
144
  The hub sends `query`, `key` and `values` when it finds them; see
143
- [Gathering](./data-binding.md#gathering).
145
+ [Gathering](./data-binding.md#gathering). It sends `keys` for a request that
146
+ acts on the rows the user selected; see
147
+ [Selecting rows](./data-binding.md#selecting-rows).
144
148
 
145
149
  ### onPageEnter
146
150
 
@@ -191,10 +195,34 @@ the query they operate on:
191
195
 
192
196
  | Request name | Runs | `run` receives |
193
197
  |-----------------|-------------|------------------|
194
- | `lb-row-insert` | `rowInsert` | `query`, `values` |
195
- | `lb-row-update` | `rowUpdate` | `query`, `key`, `values` |
198
+ | `lb-row-insert` | `rowInsert` | `query`, `values`, and `rows` from a form |
199
+ | `lb-row-update` | `rowUpdate` | `query`, `key`, `values`, and `rows` from a form |
196
200
  | `lb-row-delete` | `rowDelete` | `query`, `key` |
197
201
 
202
+ An `lb-row-update` or `lb-row-delete` carrying `keys` runs `rowUpdate` or
203
+ `rowDelete` once for each key, with that key, so a handler written for one
204
+ row serves several. The patches the runs return are laid together, and the
205
+ refresh set runs once. A `url()` returned for one of several rows is
206
+ reported and ignored. A declared request receives `keys` as it is.
207
+
208
+ `rows` holds the rows of a query nested in the form the request came from,
209
+ by query name, each with its `key` and the `values` of its own controls; see
210
+ [A form over a set of rows](./data-binding.md#a-form-over-a-set-of-rows).
211
+ The handler writes the master and its details together, deciding what each
212
+ row's values mean, such as an empty amount being no row at all:
213
+
214
+ ```ts
215
+ rowInsert: {
216
+ run: async (ctx, { values, rows }) => {
217
+ const id = await ctx.db.insertPaycheck(values);
218
+ for (const { key, values: row } of rows?.deduction_types ?? []) {
219
+ if (row.amount !== "") await ctx.db.insertDeduction(id, key, row.amount);
220
+ }
221
+ },
222
+ refresh: ["paychecks"],
223
+ },
224
+ ```
225
+
198
226
  ```ts
199
227
  // src/pages/directory.requests.ts
200
228
  import { patch, type Requests } from "@loadbare/app/server";
@@ -333,3 +361,48 @@ Post/Redirect/Get without the redirect. The refresh set does not run, and
333
361
  nothing else `run` returned is sent, since both answered for the URL the page
334
362
  left. The loaded page reads the new parms through `contextFor`; see
335
363
  [the Express server](./server.md#database-layer).
364
+
365
+ ### Refusing a request
366
+
367
+ A handler refuses a request by throwing `refusal()`, with words written for
368
+ the user:
369
+
370
+ ```ts
371
+ import { refusal, type Requests } from "@loadbare/app/server";
372
+
373
+ export const requests: Requests = {
374
+ handlers: {
375
+ enterPaycheck: {
376
+ run: async (ctx, { values }) => {
377
+ if (Number(values!.amount) <= 0) {
378
+ throw refusal({ amount: "Must be more than zero" });
379
+ }
380
+ const overlap = await ctx.db.overlappingSeries(values!);
381
+ if (overlap) {
382
+ throw refusal(
383
+ `That series already has paychecks from ${overlap.from} through ${overlap.to}.`,
384
+ );
385
+ }
386
+ await ctx.db.enterPaycheck(values!);
387
+ },
388
+ refresh: ["paychecks"],
389
+ },
390
+ },
391
+ };
392
+ ```
393
+
394
+ | Call | The browser shows |
395
+ |----------------------------------|--------------------------------------------|
396
+ | `refusal("message")` | The message, for the request as a whole |
397
+ | `refusal({ column: "message" })` | Each message on the control for its column |
398
+ | `refusal("message", { ... })` | Both |
399
+
400
+ Throw it, from the handler or from anything the handler calls, and whatever
401
+ the application has open rolls back as for any throw. A column names a
402
+ control the request gathered. See [Refusals](./data-binding.md#refusals)
403
+ for how the browser shows them.
404
+
405
+ A refusal is the only error whose words reach the browser. Anything else a
406
+ handler throws is a failure, answered with nothing in it, so a stack or a
407
+ database message never reaches a page.
408
+
@@ -72,6 +72,26 @@ path, so the application cannot be hosted under a subpath such as
72
72
  whole path space through unchanged.
73
73
 
74
74
  Leave `express.json()` to `hubRoutes`, which mounts it on its own routes.
75
+ Pass its options through `hubRoutes` instead.
76
+
77
+ ## Options
78
+
79
+ `hubRoutes` takes an optional third argument, an object with one key per
80
+ concern:
81
+
82
+ | Option | Description |
83
+ |--------|----------------------------------------------------------|
84
+ | `json` | Options for `express.json()`, passed to it unchanged |
85
+
86
+ A request body over 100 KB, the `express.json()` default, is refused with a
87
+ 413 before any handler runs, and the server log names the option that raises
88
+ it. Raise it for an application whose pages send a file's text as a value:
89
+
90
+ ```ts
91
+ app.use(hubRoutes(hub, contextFor, { json: { limit: "5mb" } }));
92
+ ```
93
+
94
+ The limit covers every page's requests.
75
95
 
76
96
  ## Running the server
77
97
 
@@ -1,8 +1,8 @@
1
1
  # The Basic Widget Library
2
2
 
3
- Ten widgets: `lb-input`, `lb-select`, `lb-options`, `lb-picker`, `lb-table`,
4
- `lb-unknown-page`, `lb-confirm`, `lb-form`, `lb-insert-dialog` and
5
- `lb-master`. Every one of them is an ordinary custom element, written
3
+ Eleven widgets: `lb-input`, `lb-select`, `lb-options`, `lb-picker`,
4
+ `lb-table`, `lb-unknown-page`, `lb-confirm`, `lb-form`, `lb-insert-dialog`,
5
+ `lb-master` and `lb-checkbox`. Every one of them is an ordinary custom element, written
6
6
  against the contracts in [Custom Elements](./custom-elements.md#html) for its
7
7
  definition and [Custom Elements](./custom-elements.md#code) for its class.
8
8
 
@@ -26,7 +26,9 @@ for where a listed package sits in the cascade.
26
26
  `lb-input`, `lb-select`, `lb-options` and `lb-picker` are controls:
27
27
  form-associated custom elements with a `value` property that fire `change`.
28
28
  Each carries `lb-column` and `lb-request` itself, the way an `<input>` does;
29
- see [Being a control](./custom-elements.md#being-a-control).
29
+ see [Being a control](./custom-elements.md#being-a-control). Each passes a
30
+ refusal's message for its column to the native control inside it; see
31
+ [Refusals](./data-binding.md#refusals).
30
32
 
31
33
  ## `lb-input`
32
34
 
@@ -186,8 +188,8 @@ succeeded, and leaves it open on a refusal.
186
188
  ## `lb-form`
187
189
 
188
190
  A `<form>` over one row's controls. A row with a key saves each control's
189
- column on `change`; a row whose key is empty is new, and the widget stops an
190
- `lb-row-update` that carries no key, so its controls wait for a submit.
191
+ column on `change`; a row whose key is empty is new, and the hub sends no
192
+ update for it, so its controls wait for a submit. A definition only.
191
193
 
192
194
  | Parameter | Fills |
193
195
  |-----------|-----------------|
@@ -210,16 +212,37 @@ answers with a patch holding the new row.
210
212
 
211
213
  ## `lb-master`
212
214
 
213
- A set, one of its rows, and the buttons that act on the set: Copy, Delete,
214
- Save new and Cancel, each shown only for a row on record or a new one. A
215
- definition only, built from `lb-form` and `lb-confirm`. Copy sends `copy`, a
216
- request the page declares.
215
+ A set, one of its rows, and the buttons that act on the set: New, Copy,
216
+ Delete, Save new and Cancel, each shown only for a row on record or a new
217
+ one. A definition only, built from `lb-form` and `lb-confirm`. New sends
218
+ `new` and Copy sends `copy`, requests the page declares; `new` returns
219
+ `url()` emptying the parm that names the row.
217
220
 
218
221
  | Parameter | Fills |
219
222
  |---------------|--------------------------------------------|
220
223
  | `exp-row` | The `lb-query` of the row the form shows |
221
224
  | `exp-form-id` | The form's `id`, `master-form` when absent |
222
225
 
226
+ ## `lb-checkbox`
227
+
228
+ A checkbox and its label as one element, for an application to style. A
229
+ definition only: a native checkbox is already a control (see
230
+ [Checkboxes](./data-binding.md#checkboxes)), so the widget is the markup, a
231
+ `<label>` holding the `<input type="checkbox">` and a `<span>` with the
232
+ text. The column and request go on the checkbox inside, so they arrive as
233
+ expansion attributes.
234
+
235
+ Unlike `lb-input`, it has no script making the tag the control. A value
236
+ that lands before a widget's script has loaded arrives only as the string
237
+ stamp `lb-column-value`, and a script checkbox would read `"false"` as
238
+ checked. The hub sets a native checkbox's `checked` directly.
239
+
240
+ | Parameter | Fills |
241
+ |---------------|-----------------------------------------------|
242
+ | `exp-column` | The checkbox's `lb-column` |
243
+ | `exp-label` | The label's text |
244
+ | `exp-request` | The checkbox's `lb-request`, none when absent |
245
+
223
246
  ## `lb-unknown-page`
224
247
 
225
248
  The chrome's dialog for a URL that names no page, as one tag:
package/docs/roadmap.md CHANGED
@@ -102,6 +102,14 @@ Most valuable against a large imported widget library, where an app uses a
102
102
  small fraction of what ships. Revisit when a real app's `app.css` is big
103
103
  enough to measure.
104
104
 
105
+ ### Options on `createHub`
106
+
107
+ Global hooks, transaction wrapping, error handling and a CSRF token each
108
+ want an option on `createHub`, `hubRoutes`, or both. `hubRoutes` already
109
+ takes a trailing options object, one key per concern. `createHub` takes
110
+ the same shape when its first option arrives, which adds a parameter and
111
+ breaks nothing.
112
+
105
113
  ### Queries stated by the chrome
106
114
 
107
115
  A custom element in the chrome naming a query forces every page to declare
@@ -109,8 +117,7 @@ that query, since the page load covers one page's set. `lb-url` shows the
109
117
  pattern from the framework side, and an application has no equivalent.
110
118
 
111
119
  A chrome-level query set on `createHub` would close this, and it is additive.
112
- It takes its place in the options argument whose shape is a 1.0 blocker in
113
- [TECHREF-1.0](./TECHREF-1.0.md#the-server-api).
120
+ It takes its place in the [options on `createHub`](#options-on-createhub).
114
121
 
115
122
  ### Data binding utilities
116
123
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@loadbare/app",
3
3
  "description": "High performance web app framework for server-bound applications",
4
- "version": "0.13.0",
4
+ "version": "0.14.0",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: loadbare-app
3
- description: Build server-bound web applications with Loadbare/app. Covers the file conventions the builder finds by name (`chrome.html`, `*.page.html`, `*.queries.ts`, `*.requests.ts`, `imports.ts`, `<tag>.html`, `<tag>.browser.ts`); the `lb-*` attribute vocabulary that lands query rows on HTML and issues requests (`lb-query`, `lb-column`, `lb-show`, `lb-request`); queries declared with `row(key, run)` and `rows(key, run)`; the URL as the hub's own query `lb-url`, with `lb-url-link`, `lb-url-push` and `lb-url-unknown`; build-time expansion with `exp-*` parameters, `lb-exp-slot` and `lb-exp-template`; custom elements as form-associated controls and row hosts; the Express wiring with `hubRoutes`; and the `@loadbare/widgets` library. Use whenever a task involves `@loadbare/app`, `@loadbare/widgets`, `loadbare-app-build`, an `lb-` attribute, or a page, query, request or custom element file in a Loadbare application. The model is not React, htmx or REST, and cannot be inferred from them.
3
+ description: Build server-bound web applications with Loadbare/app. Covers the file conventions the builder finds by name (`chrome.html`, `*.page.html`, `*.queries.ts`, `*.requests.ts`, `imports.ts`, `<tag>.html`, `<tag>.browser.ts`); the `lb-*` attribute vocabulary that lands query rows on HTML and issues requests (`lb-query`, `lb-column`, `lb-show`, `lb-request`); queries declared with `row(key, run)` and `rows(key, run)`; the URL as the hub's own query `lb-url`, with `lb-url-link`, `lb-url-push` and `lb-url-unknown`; one form pointed at a row by the `--lb-edit`, `--lb-new` and `--lb-release` commands; build-time expansion with `exp-*` parameters, `lb-exp-slot` and `lb-exp-template`; custom elements as form-associated controls and row hosts; the Express wiring with `hubRoutes`; and the `@loadbare/widgets` library. Use whenever a task involves `@loadbare/app`, `@loadbare/widgets`, `loadbare-app-build`, an `lb-` attribute, or a page, query, request or custom element file in a Loadbare application. The model is not React, htmx or REST, and cannot be inferred from them.
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  package: "@loadbare/app"
@@ -178,7 +178,11 @@ from it.
178
178
  query under two names. Many masters with their details is one `rows` query
179
179
  of joined rows, ordered by the master first and shown in groups. A
180
180
  query nested inside another query's row template receives the same rows in
181
- every outer row; use it for a picker, never for per-row detail. A new outer
181
+ every outer row; use it for a picker, never for per-row detail. Inside a
182
+ form, the live rows of a query nested in the form's own are gathered as
183
+ `rows: { <query>: [{ key, values }] }` beside `values`, so a master and a
184
+ subtable of its details (one input per type, say) are entered in one
185
+ submit, and the master's handler writes both. A new outer
182
186
  row is filled from the nested query's last answer, whatever order they land
183
187
  in.
184
188
 
@@ -206,7 +210,8 @@ leaving row or group `lb-row-leaving` or `lb-group-leaving`, removing it once
206
210
  its animations end. Scrolling to a row is script, in `lbRowsLanded`.
207
211
 
208
212
  **Every request has one shape.** `lb-request` names the request, and the
209
- hub sends `{ name, query, key, values }` as present: `query` from the
213
+ hub sends `{ name, query, key, values }` as present, with `keys` in place
214
+ of `key` for the rows the user selected: `query` from the
210
215
  nearest ancestor `lb-query`, `key` from the nearest ancestor row of that
211
216
  query, and `values` gathered from controls by their `lb-column`.
212
217
 
@@ -224,9 +229,10 @@ with `lb-column` gathers its own value alone. A `<form>` gathers every
224
229
  control whose form owner it is. An element in a live row gathers that live
225
230
  row. Anything else gathers its form owner's controls. A `<form>` cannot
226
231
  wrap a `<tr>`, so an insert row in a table puts its controls in a form
227
- elsewhere with the HTML `form` attribute. Checkboxes, radio buttons and
228
- file inputs are not controls here: they receive no value and are not
229
- gathered.
232
+ elsewhere with the HTML `form` attribute. A checkbox is a control: checked
233
+ unless its column is `false` or null, and gathered as `"true"` or `"false"`.
234
+ Radio buttons and file inputs are not controls here: they receive no value
235
+ and are not gathered.
230
236
 
231
237
  **A form that adds to a set chosen elsewhere is written once, outside every
232
238
  row.** A form cannot sit inside another form or in a table row, so a dialog
@@ -234,6 +240,17 @@ that creates an account while a posting's picker is choosing one goes at the
234
240
  end of the page, and each row opens it with `commandfor`. The dialog learns
235
241
  the new key from `lb-request-done`.
236
242
 
243
+ **A set the user edits has one form, pointed at a row.** Never put a form
244
+ in every row. Write one form naming the set's query, outside every row,
245
+ often in a `<dialog>`, and give it an `id`. A `type="button"` with
246
+ `commandfor="<id>"` and `command="--lb-edit"` in a live row fills it from
247
+ that row with no round trip; `--lb-new` starts it empty for an insert;
248
+ `--lb-release` lets it go. The form's `lb-key-value` is the row's key, empty
249
+ for a new row, absent when not in use, and a stylesheet shows Save or Save
250
+ new by it. A successful request from the form, or its dialog closing, lets
251
+ it go. Which row is being edited is never a query parm. A row that must be
252
+ fetched to be edited is another page, with that row as its master.
253
+
237
254
  **Three requests are Loadbare's.** `lb-row-insert` needs `query` and
238
255
  `values`, `lb-row-update` needs `query`, `key` and `values`, and
239
256
  `lb-row-delete` needs `query` and `key`. The page permits each under
@@ -242,6 +259,32 @@ Each works on a `row` query as well as a `rows` query, since the element a
242
259
  `row` lands on carries its key. Every other request name is a declared
243
260
  request, under `handlers`. Anything undeclared is refused.
244
261
 
262
+ **An empty key is a row not yet entered.** A `row` query answering with an
263
+ empty key, or a form `--lb-new` pointed, carries `lb-key-value=""`. The hub
264
+ sends inserts and declared requests from it with no `key`, and sends no
265
+ update or delete at all, silently. So a control with
266
+ `lb-request="lb-row-update"` saves on a row on record and only holds its
267
+ value on a new one. Never write script to stop such an update.
268
+
269
+ **Select rows with `lb-select`, never with a column.** An
270
+ `<input type="checkbox" lb-select>` in a row template selects its row; the
271
+ hub stamps `lb-row-selected`. A request for that query naming no one row,
272
+ such as a form outside the rows, carries the ticked rows as `keys`.
273
+ `rowUpdate` and `rowDelete` then run once per key with no new code; a
274
+ declared request gets `keys` itself. With none ticked an update or delete
275
+ is not sent. Success clears the selection, and so does `lb-show` hiding a
276
+ selector. A selecting mode kept in the URL reaches the rows as a column the
277
+ query computes from the parm.
278
+
279
+ **Refuse with `refusal()`, never with a plain throw.** `throw
280
+ refusal("message")`, `throw refusal({ column: "message" })` or both, from
281
+ `@loadbare/app/server`. The whole-request message lands as the
282
+ `lb-request-refused` stamp on the element that committed, shown by CSS
283
+ `attr()`; each column's message goes on its control by
284
+ `setCustomValidity`, and the first is reported. Editing anything the
285
+ request gathered withdraws it all. Any other throw is a failure with no
286
+ message, so a database error never reaches the page.
287
+
245
288
  **Write `rowUpdate` as a partial update.** A form sends every control it
246
289
  owns; a single control sends its one column. Set the columns `values`
247
290
  names, leave the rest alone, and check the names against the columns the
@@ -395,7 +438,7 @@ own, or to act on rows once they land through the hook the hub calls,
395
438
 
396
439
  Before writing one, check `@loadbare/widgets`: `lb-input`, `lb-select`,
397
440
  `lb-options`, `lb-picker`, `lb-table`, `lb-unknown-page`, `lb-confirm`,
398
- `lb-form`, `lb-insert-dialog`, `lb-master`. The first four are
441
+ `lb-form`, `lb-insert-dialog`, `lb-master`, `lb-checkbox`. The first four are
399
442
  form-associated controls that carry `lb-column` and `lb-request`
400
443
  themselves. Install the package and list it in `src/imports.ts`:
401
444