@loadbare/app 0.8.1 → 0.9.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 (41) hide show
  1. package/dist/core/lb-constants.d.ts +3 -0
  2. package/dist/core/lb-constants.d.ts.map +1 -1
  3. package/dist/core/lb-constants.js +28 -4
  4. package/dist/core/lb-constants.js.map +1 -1
  5. package/dist/core/lb-types.d.ts +11 -0
  6. package/dist/core/lb-types.d.ts.map +1 -1
  7. package/dist/core/lb-types.js +20 -0
  8. package/dist/core/lb-types.js.map +1 -1
  9. package/dist/hub/lb-apply.d.ts +10 -0
  10. package/dist/hub/lb-apply.d.ts.map +1 -1
  11. package/dist/hub/lb-apply.js +15 -1
  12. package/dist/hub/lb-apply.js.map +1 -1
  13. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  14. package/dist/hub/lb-hub.browser.js +161 -38
  15. package/dist/hub/lb-hub.browser.js.map +1 -1
  16. package/dist/server/lb-express.d.ts +22 -7
  17. package/dist/server/lb-express.d.ts.map +1 -1
  18. package/dist/server/lb-express.js +79 -31
  19. package/dist/server/lb-express.js.map +1 -1
  20. package/dist/server/lb-server.d.ts +26 -0
  21. package/dist/server/lb-server.d.ts.map +1 -1
  22. package/dist/server/lb-server.js +53 -3
  23. package/dist/server/lb-server.js.map +1 -1
  24. package/docs/TECHREF-1.0.md +154 -22
  25. package/docs/comparison.md +8 -7
  26. package/docs/reference/chrome.md +5 -3
  27. package/docs/reference/data-binding.md +33 -0
  28. package/docs/reference/page-files.md +39 -0
  29. package/docs/reference/server.md +17 -0
  30. package/docs/reference/widgets.md +6 -2
  31. package/docs/roadmap.md +28 -0
  32. package/docs/theory.md +8 -1
  33. package/docs/tutorials/010-pages-and-navigation.md +2 -1
  34. package/package.json +1 -1
  35. package/skills/loadbare-app/SKILL.md +36 -8
  36. package/skills/loadbare-app/references/TECHREF-1.0.md +154 -22
  37. package/skills/loadbare-app/references/chrome.md +5 -3
  38. package/skills/loadbare-app/references/data-binding.md +33 -0
  39. package/skills/loadbare-app/references/page-files.md +39 -0
  40. package/skills/loadbare-app/references/server.md +17 -0
  41. package/skills/loadbare-app/references/widgets.md +6 -2
@@ -106,14 +106,6 @@ in the row.
106
106
  stays a flag, since it is what locates the file. State the precedence
107
107
  between a flag and a key once. Its key names join the permanent surface,
108
108
  so this lands before 1.0 or not at all.
109
- - **Decide whether a URL carries view parameters.** A URL names a page and
110
- nothing finer — see [What a URL names](#what-a-url-names) — and a query
111
- takes no argument from the browser. So a page has nowhere to keep which
112
- record it shows, a filter, a sort, or a collapsed section: a reload or a
113
- shared link loses them, and a filter is lost whenever an action re-runs its
114
- query. Parameters on a page URL, as `<form method="get">` puts its fields
115
- in the query string, are one answer. Deciding against them, and leaving
116
- view state to the application, is another. Write whichever one down.
117
109
  - **Decide CSS pairing.** Naming a stylesheet `<tag>.css` beside its
118
110
  definition would let the builder ship only what survives expansion.
119
111
  Recommend deferring the mechanism and reserving the configuration key, so
@@ -507,8 +499,11 @@ A list nested in another list's rows receives the same rows in every outer
507
499
  row. That serves a picker offering the same choices on every row, and is
508
500
  not a way to show a different detail per row.
509
501
 
510
- Which master a page shows is server state reached through `ctx`, since a
511
- query takes no argument from the browser — see the blocker on view parameters.
502
+ Which master a page shows is a query parm, which a query reads off `ctx`
503
+ since it takes no argument from the browser — see [Query parms](#query-parms).
504
+ When a write creates or removes the master, the server response changes that
505
+ parm — see
506
+ [When a server response changes the query parms](#when-a-server-response-changes-the-query-parms).
512
507
 
513
508
  ### Requests
514
509
 
@@ -638,15 +633,16 @@ A failed insert resets nothing, so the entry can be corrected.
638
633
 
639
634
  A navigation that fails to load its data sets nothing. No element
640
635
  dispatched it, so there is nothing to stamp, and the hub reports it to the
641
- console.
636
+ console. A load started by a control writing a query parm stamps that
637
+ control, as a request stamps its origin.
642
638
 
643
639
  ### Links
644
640
 
645
641
  ---- UNEDITED ----
646
642
 
647
643
  An anchor carrying `lb-nav-link` navigates inside the application: the hub
648
- catches the click, pushes the anchor's path onto history, and swaps the page
649
- host in `<main>`. An anchor without it is left alone and behaves like any
644
+ catches the click, pushes the anchor's path and query string onto history,
645
+ and swaps the page host in `<main>`. An anchor without it is left alone and behaves like any
650
646
  other link, so leaving the application is the default and staying in it is
651
647
  the opt-in.
652
648
 
@@ -660,6 +656,11 @@ link to determine the path.
660
656
  Path space is flat. A path such as `/members` links to the `members.*` files
661
657
  on the server.
662
658
 
659
+ The query string is kept. `/transactions?date_begin=2026-09-01` opens the
660
+ transactions page narrowed to those dates, and a link to the page it is
661
+ already on loads that page again at the new URL without replacing its DOM —
662
+ see [Query parms](#query-parms).
663
+
663
664
  The bare path `/` resolves to `index`.
664
665
 
665
666
  A path that names no page is detected in the browser, after a successful
@@ -680,19 +681,122 @@ See also [Navigation Row lb-navigation](#lb-navigation).
680
681
 
681
682
  #### What a URL names
682
683
 
683
- A URL names a page, a place in the application. It never names a resource,
684
- and Loadbare/app does not reproduce REST: `/accounts/42` is not a way to
685
- reach account 42.
684
+ The path names a page, a place in the application. It never names a
685
+ resource, and Loadbare/app does not reproduce REST: `/accounts/42` is not a
686
+ way to reach account 42.
687
+
688
+ The query string describes what that page has on screen:
689
+ `/accounts?acct=23&from=2026-09-09`. It never commands, and the server
690
+ remembers nothing, so a URL is a reproducible view. Reload it, bookmark it,
691
+ or mail it to someone, and what they see is what the sender saw.
686
692
 
687
693
  A page declares what it shows, its queries, and what it allows, its actions.
688
694
  Loading a page runs its queries. A request performs an action at a position.
689
695
  A row is addressed by `list` and `key`, taken from where the element sits,
690
696
  which is how the database already names it. Nothing is fetched by URL, so an
691
- application designs no endpoints, and the browser reaches only what a page
692
- publishes.
697
+ application designs no endpoints.
698
+
699
+ That last is where query parms move Loadbare's position. A query still takes
700
+ no argument from the browser, and a parm arrives the way the session cookie
701
+ does, as part of the request the context is built from. But a user who
702
+ types `?acct=99999` now influences what a query returns. A query parm is
703
+ user input, validated like any other where the application reads it, and a
704
+ per-session database role means an id outside the caller's reach finds
705
+ nothing.
706
+
707
+ Loadbare is for applications, not sites. Every route is answered with the
708
+ same document, and a path that names no page is found out in the browser —
709
+ see [Links](#links).
710
+
711
+ #### Query parms
712
+
713
+ A control that narrows what a page shows writes its value into the query
714
+ string, rather than sending it:
715
+
716
+ ```html
717
+ <lb-options lb-list="teams" lb-query-parm="team" exp-label="Team:">
718
+ <option value="">Every team</option>
719
+ <template lb-key="id"><option lb-cell="name"></option></template>
720
+ </lb-options>
721
+ ```
722
+
723
+ | Attribute | Written by | Behavior |
724
+ | -------------------- | ---------- | --------------------------------------------------------------- |
725
+ | `lb-query-parm` | Developer | On a control: its `change` writes this parm and reloads the page |
726
+ | `lb-query-parm-push` | Developer | With `lb-query-parm`: the write pushes a history entry |
727
+
728
+ On `change`, the hub reads the control's value the way a form reads a cell,
729
+ and sets that one parm in the URL. Every other parm is left as it is, since
730
+ it may have arrived by link with no control on screen to say it again. An
731
+ empty value takes the parm out, so a URL is as long as the user has narrowed
732
+ the page. A value the URL already carries does nothing.
733
+
734
+ The write replaces the current history entry: changing what a page shows is
735
+ not going anywhere, so Back leaves the page rather than walking back through
736
+ every choice. `lb-query-parm-push` makes the write push an entry instead.
737
+
738
+ The page then loads at the new URL exactly as a cold load of that URL would:
739
+ `onPageEnter`, then every query. The page's DOM is kept, and the answer
740
+ lands by key, so a list that gets its rows back keeps them and its scroll
741
+ position.
742
+
743
+ After every load — cold, by link, by Back, by a write — the hub lands each
744
+ parm on the control that writes it, and an absent parm lands empty. A
745
+ control therefore shows what the address bar says.
746
+
747
+ A control that writes a query parm sends no request. One that also carries
748
+ `lb-action` has that request refused, since the choice would otherwise be
749
+ sent twice.
750
+
751
+ Every round trip carries the query string the browser is showing, a page
752
+ load and an action alike. The server hands its parms to `contextFor` — see
753
+ [The Express server](#the-express-server). Nothing in Loadbare assigns a
754
+ parm a meaning.
693
755
 
694
- Whether a page URL may carry parameters, so that a link can open the page on
695
- one record, is undecided — see the blockers.
756
+ #### When a server response changes the query parms
757
+
758
+ Some query parms can only be known once a write has run. After an insert,
759
+ the key of the new row exists only on the server. After a delete, only the
760
+ server knows that the row the page was showing is gone.
761
+
762
+ The usual web answer is Post/Redirect/Get: the server answers the write with
763
+ a redirect to a URL naming the result, and the browser makes a second request
764
+ to load it. Loadbare/app does the same work in one round trip, and never
765
+ changes the path.
766
+
767
+ A request's `run` returns `queryParms()`, naming the parms the write decided.
768
+ The server loads the page with those parms set, as a cold load of the
769
+ resulting URL would: `onPageEnter`, then every query. The server response
770
+ carries the parms and that load together. The hub sets the parms in the URL,
771
+ replacing the history entry, and then lands the load.
772
+
773
+ ```ts
774
+ rowInsert: {
775
+ run: async (ctx, { values }) => {
776
+ const id = await ctx.db.addAccount(values);
777
+ return queryParms({ acct: String(id) });
778
+ },
779
+ refresh: [],
780
+ },
781
+ ```
782
+
783
+ - Only query parms change. A server response cannot send the browser to
784
+ another page.
785
+ - Parms the response does not name are left as they are. An empty value
786
+ removes its parm.
787
+ - At least one parm is named, and every value is a string. Otherwise the
788
+ server warns, ignores the parms, and runs the refresh set as usual.
789
+ - The refresh set does not run, and anything else `run` returned is dropped.
790
+ Both were answers for the query string the page is leaving.
791
+ - If loading the page fails, the write has still happened. The response
792
+ carries the parms alone, and the hub loads the page itself. A failure
793
+ there is a page load's, and is not stamped on the element that sent the
794
+ request.
795
+ - A user who has left the page by the time the response arrives keeps the
796
+ URL they are on.
797
+
798
+ The page is loaded with a second context, built by `contextFor` from the new
799
+ parms — see [The Express server](#the-express-server).
696
800
 
697
801
  ### lb-navigation
698
802
 
@@ -706,10 +810,11 @@ like a server-produced result.
706
810
  | Cell | Holds |
707
811
  | ------------ | ------------------------------------------------------------------- |
708
812
  | `page-label` | The text of the link to the current page, empty if no link names it |
709
- | `page-uri` | The path as the browser has it, such as `/members` |
813
+ | `page-uri` | The path and query string as the browser has them, such as `/members?team=Engines` |
710
814
 
711
815
  The `page-uri` is taken from the current URL. The `page-label` is taken from the first
712
- `lb-nav-link` link in the document (presumably in a nav bar) that matches the URI.
816
+ `lb-nav-link` link in the document (presumably in a nav bar) whose path matches the
817
+ current path, so the query string does not change it.
713
818
 
714
819
  The row lands before the page is looked up, so a path that names no page has
715
820
  it too, and an `lb-unknown-page` dialog, where the chrome has one, displays
@@ -871,6 +976,24 @@ Explaining Express is beyond the scope of this technical reference. The
871
976
  only real requirement is that the catch-all for app.html is at the end,
872
977
  so it does not catch any other files.
873
978
 
979
+ `hubRoutes` answers `POST /lb/<page>`, with the query string the browser is
980
+ showing after it, verbatim. It hands `contextFor` that query string's parms
981
+ as a second argument, and `contextFor` puts on the context whatever a query
982
+ reads from them. They are user input:
983
+
984
+ ```ts
985
+ function contextFor(req: Request, parms: URLSearchParams): HubContext {
986
+ return { db: openDb(), acct: parms.get("acct") ?? "" };
987
+ }
988
+ ```
989
+
990
+ Read the parms from that argument, not from `req.query`. After a request
991
+ whose `run` returned `queryParms()`, `contextFor` is called a second time for
992
+ the same request, with the new parms, to load the page at them. So it must
993
+ be safe to call twice, and a write must be visible to the second context by
994
+ the time its `run` returns. A handle opened per request without a
995
+ transaction around it is both.
996
+
874
997
  Give the server the origin root. The hub reaches its own endpoints by
875
998
  absolute path, so an application cannot be hosted under a subpath such as
876
999
  `example.com/myapp/`, and anything proxying in front of the server passes
@@ -941,6 +1064,13 @@ interaction happened, as `list`, `row`, `key`, `cell` and `value`. Under
941
1064
  ones. That is how a delta reaches the browser: wrap it in `patch()`, naming
942
1065
  the rows that arrived or changed and the keys that went.
943
1066
 
1067
+ `run` may instead return `queryParms()`, naming query parms only the write
1068
+ can know, such as the key of a row it inserted. The page then loads at them
1069
+ in the same round trip, in place of the refresh set — see
1070
+ [When a server response changes the query parms](#when-a-server-response-changes-the-query-parms).
1071
+ A `rowDelete` that removes the row on screen returns `queryParms({ acct: "" })`
1072
+ and leaves any other delete to its refresh set.
1073
+
944
1074
  ```ts
945
1075
  // members.requests.ts
946
1076
  import { patch, type Requests } from "@loadbare/app/server";
@@ -1148,6 +1278,8 @@ Each attribute is defined in one section, and this table says which.
1148
1278
  | `lb-value` | Hub | [How a value lands](#how-a-value-lands) |
1149
1279
  | `lb-action` | Developer | [Requests](#requests) |
1150
1280
  | `lb-nav-link` | Developer | [Links](#links) |
1281
+ | `lb-query-parm` | Developer | [Query parms](#query-parms) |
1282
+ | `lb-query-parm-push` | Developer | [Query parms](#query-parms) |
1151
1283
  | `lb-pending` | Hub | [Request state](#request-state) |
1152
1284
  | `lb-error` | Hub | [Request state](#request-state) |
1153
1285
  | `lb-row-count` | Hub | [Request state](#request-state) |
@@ -73,7 +73,9 @@ sits directly inside `<body>`, with banner, nav, footer and `<main>` inside
73
73
  it, so Loadbare can act on all of them.
74
74
 
75
75
  A navigation anchor's `href` is a path, and the path names a page:
76
- `/members` shows `members.page.html`. A bare `/` resolves to `index`, so the
76
+ `/members` shows `members.page.html`. A query string on it is kept, so
77
+ `/members?team=Engines` opens the page narrowed; see
78
+ [Query parms](./data-binding.md#query-parms). A bare `/` resolves to `index`, so the
77
79
  landing page is the one named `index.page.html`. An anchor without
78
80
  `lb-nav-link` is left alone and behaves like any other link.
79
81
 
@@ -100,10 +102,10 @@ shows a value — so a chrome displays the current page with no code at all:
100
102
  | Cell | Holds |
101
103
  |--------------|-----------------------------------------------------------|
102
104
  | `page-label` | The text of the `lb-nav-link` anchor for the path, or empty if none |
103
- | `page-uri` | The path as the browser has it, such as `/members` |
105
+ | `page-uri` | The path and query string as the browser has them, such as `/members?team=Engines` |
104
106
 
105
107
  The label is the nav's. The hub takes it from the first `lb-nav-link`
106
- anchor whose `href` names the current page, so a click, a reload and the
108
+ anchor whose path names the current page, whatever its query string, so a click, a reload and the
107
109
  back button all land the same text, and a path no anchor names lands an
108
110
  empty label. A chrome that shows the label somewhere fixed should expect
109
111
  that case for a page reachable only by URL.
@@ -304,6 +304,39 @@ Leave `lb-action` off a widget inside an `lb-row-insert` or `lb-row-update`
304
304
  form. The form reads every `lb-cell` in it on submit, so a widget that also
305
305
  sent its own would write the same edit twice.
306
306
 
307
+ ## Query parms
308
+
309
+ A control that narrows what the page shows writes its value into the query
310
+ string instead of sending a request. `lb-query-parm` names the parm:
311
+
312
+ ```html
313
+ <select lb-query-parm="team">
314
+ <option value="">Every team</option>
315
+ <option value="Engines">Engines</option>
316
+ </select>
317
+ ```
318
+
319
+ On `change` the hub sets that one parm in the URL, leaving every other parm
320
+ alone, and takes it out when the value is empty. The write replaces the
321
+ current history entry; add `lb-query-parm-push` to push one instead. The page
322
+ then loads at the new URL, exactly as a cold load of it would, without
323
+ replacing its DOM.
324
+
325
+ After every load the hub lands each parm on the control that writes it, and
326
+ an absent parm lands empty, so the control shows what the address bar says.
327
+
328
+ A control that writes a query parm sends no request, and one that also
329
+ carries `lb-action` has that request refused.
330
+
331
+ A request can write parms too, when only the write knows their value, such as
332
+ the key of a row it inserted; see
333
+ [refresh and patch](./page-files.md#refresh-and-patch). The page loads at
334
+ them in the same round trip, and they land on their controls as above.
335
+
336
+ The server hands the parms to `contextFor`; see
337
+ [the Express server](./server.md#database-layer). See
338
+ [Query parms](./TECHREF-1.0.md#query-parms) for the whole rule.
339
+
307
340
  ## Conditional rendering
308
341
 
309
342
  Loadbare ships static HTML and hydrates elements that are already in the
@@ -192,3 +192,42 @@ resetRoster: {
192
192
  refresh: ["roster"],
193
193
  },
194
194
  ```
195
+
196
+ Return `queryParms()` instead when the server response changes the query
197
+ parms: after an insert, only the server knows the new key, and after a delete,
198
+ only the server knows the parm should go. The page loads at the query string
199
+ with those parms set, in the same round trip, and the hub writes them into the
200
+ URL, replacing the history entry. This is Post/Redirect/Get without the
201
+ redirect. The refresh set does not run, and nothing else `run` returned is
202
+ sent, since both answered for the query string the page left. Name at least
203
+ one parm, give each a string, and use an empty string to take one out:
204
+
205
+ ```ts
206
+ // src/pages/accounts.requests.ts
207
+ import { queryParms, type Requests } from "@loadbare/app/server";
208
+
209
+ export const requests: Requests = {
210
+ crud: {
211
+ accounts: {
212
+ rowInsert: {
213
+ run: async (ctx, { values }) => {
214
+ const id = await ctx.db.addAccount(values);
215
+ return queryParms({ acct: String(id) });
216
+ },
217
+ refresh: [],
218
+ },
219
+ rowDelete: {
220
+ run: async (ctx, { key }) => {
221
+ await ctx.db.deleteAccount(key);
222
+ if (key === ctx.acct) return queryParms({ acct: "" });
223
+ },
224
+ refresh: ["accounts"],
225
+ },
226
+ },
227
+ },
228
+ };
229
+ ```
230
+
231
+ Only parms: `run` cannot send the browser to another page. The loaded page
232
+ reads the new parms through `contextFor` like any others; see
233
+ [the Express server](./server.md#database-layer).
@@ -137,6 +137,23 @@ declare module "@loadbare/app/server" {
137
137
  }
138
138
  ```
139
139
 
140
+ The query string the browser is showing arrives on every data request, and
141
+ `contextFor` gets its parms as a second argument. Put on the context whatever
142
+ a query reads from them, and treat them as user input:
143
+
144
+ ```ts
145
+ function contextFor(req: Request, parms: URLSearchParams): HubContext {
146
+ return { db: openDb(), team: parms.get("team") ?? "" };
147
+ }
148
+ ```
149
+
150
+ Read the parms from that argument, not from `req.query`. When a server
151
+ response changes the query parms, `contextFor` is called a second time for
152
+ that request, with the new parms, to load the page at them; see [refresh and patch](./page-files.md#refresh-and-patch). Write it to
153
+ be safe to call twice, and make a write visible to the second context by the
154
+ time `run` returns: a handle opened per request, with no transaction held
155
+ open across the two, is both.
156
+
140
157
  Add a field for anything else a request needs — the authenticated user, a
141
158
  request id, a feature flag set. Queries and requests read them from `ctx`; see
142
159
  [page files](./page-files.md).
@@ -56,7 +56,8 @@ action carries a value.
56
56
  | ---------- | ----- |
57
57
  | `exp-label` | the visible `<label>` text |
58
58
 
59
- Requires `lb-action` — a change with none logs and sends nothing.
59
+ Requires `lb-action` or `lb-query-parm` — a change with neither logs and
60
+ sends nothing. With `lb-query-parm` the hub writes the choice into the URL.
60
61
 
61
62
  ## `lb-options`
62
63
 
@@ -81,8 +82,11 @@ hand. The author supplies the row template inside the widget (via
81
82
  once, with `lb-key`.
82
83
  - `data-group` on the row template sections the options into `<optgroup>`s,
83
84
  one per distinct value, created and removed as rows arrive and leave.
85
+ - `lb-value` selects the option with that key, including one that arrives
86
+ after the value did.
84
87
  - A `change` sends the action named by `lb-action`, value from the
85
- select's `.value`.
88
+ select's `.value`. With `lb-query-parm` instead, the hub writes the
89
+ choice into the URL.
86
90
 
87
91
  `lb-picker` is this same class with its row template supplied by the
88
92
  definition instead of the page — see below.