@loadbare/app 0.8.0 → 0.8.2
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 +2 -0
- package/dist/core/lb-constants.d.ts.map +1 -1
- package/dist/core/lb-constants.js +18 -4
- package/dist/core/lb-constants.js.map +1 -1
- package/dist/hub/lb-apply.d.ts +10 -0
- package/dist/hub/lb-apply.d.ts.map +1 -1
- package/dist/hub/lb-apply.js +15 -1
- package/dist/hub/lb-apply.js.map +1 -1
- package/dist/hub/lb-hub.browser.d.ts.map +1 -1
- package/dist/hub/lb-hub.browser.js +116 -35
- package/dist/hub/lb-hub.browser.js.map +1 -1
- package/dist/server/lb-express.d.ts +13 -5
- package/dist/server/lb-express.d.ts.map +1 -1
- package/dist/server/lb-express.js +26 -7
- package/dist/server/lb-express.js.map +1 -1
- package/dist/server/lb-server.d.ts +3 -0
- package/dist/server/lb-server.d.ts.map +1 -1
- package/dist/server/lb-server.js.map +1 -1
- package/docs/TECHREF-1.0.md +93 -22
- package/docs/comparison.md +8 -7
- package/docs/reference/chrome.md +5 -3
- package/docs/reference/data-binding.md +28 -0
- package/docs/reference/server.md +11 -0
- package/docs/reference/widgets.md +8 -4
- package/docs/roadmap.md +16 -0
- package/docs/theory.md +8 -1
- package/docs/tutorials/010-pages-and-navigation.md +2 -1
- package/package.json +8 -4
- package/skills/loadbare-app/SKILL.md +275 -0
- package/skills/loadbare-app/references/TECHREF-1.0.md +1260 -0
- package/skills/loadbare-app/references/builder.md +134 -0
- package/skills/loadbare-app/references/chrome.md +160 -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 +485 -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 +153 -0
- package/skills/loadbare-app/references/widgets.md +178 -0
|
@@ -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
|
+
```
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# The Express Server
|
|
2
|
+
|
|
3
|
+
Loadbare ships no server. The application writes an ordinary Express app and
|
|
4
|
+
serves four things from it: the client script, the stylesheet, the data
|
|
5
|
+
channel, and the one HTML document.
|
|
6
|
+
|
|
7
|
+
Express is a peer dependency. The application installs it.
|
|
8
|
+
|
|
9
|
+
## A complete server
|
|
10
|
+
|
|
11
|
+
Here is a minimal but complete server for a typical app:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// server.ts
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import express, { type Request } from "express";
|
|
18
|
+
import { hubRoutes } from "@loadbare/app/express";
|
|
19
|
+
import type { HubContext } from "@loadbare/app/server";
|
|
20
|
+
import { hub } from "./dist/pages";
|
|
21
|
+
import { openDb } from "./src/database";
|
|
22
|
+
|
|
23
|
+
const DIST = path.resolve("dist");
|
|
24
|
+
const app = express();
|
|
25
|
+
|
|
26
|
+
app.get("/client.js", (_req, res) => res.sendFile(path.join(DIST, "client.js")));
|
|
27
|
+
app.get("/app.css", (_req, res) => res.sendFile(path.join(DIST, "app.css")));
|
|
28
|
+
|
|
29
|
+
function contextFor(_req: Request): HubContext {
|
|
30
|
+
return { db: openDb() };
|
|
31
|
+
}
|
|
32
|
+
app.use(hubRoutes(hub, contextFor));
|
|
33
|
+
|
|
34
|
+
app.get(/.*/, (_req, res) =>
|
|
35
|
+
res.type("html").send(readFileSync(path.join(DIST, "app.html"), "utf-8")),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
app.listen(8787);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## What the server serves
|
|
42
|
+
|
|
43
|
+
| Required | Serves |
|
|
44
|
+
|------------------------------|-------------------------------|
|
|
45
|
+
| `/client.js` | `dist/client.js` |
|
|
46
|
+
| `/app.css` | `dist/app.css` |
|
|
47
|
+
| `hubRoutes(hub, contextFor)` | The hub's own route |
|
|
48
|
+
| Every other GET | `dist/app.html` |
|
|
49
|
+
|
|
50
|
+
Everything else is optional:
|
|
51
|
+
|
|
52
|
+
| Optional | Description |
|
|
53
|
+
|-------------------------------|----------------------------------------------|
|
|
54
|
+
| Middleware before `hubRoutes` | Sessions, authentication, logging |
|
|
55
|
+
| The application's own routes | Uploads, webhooks, anything outside Loadbare |
|
|
56
|
+
| An error handler | Express sends its own 500 without one |
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
## Rules for writing the server
|
|
60
|
+
|
|
61
|
+
Register the static routes and `hubRoutes` before the catch-all.
|
|
62
|
+
|
|
63
|
+
Register authentication before `hubRoutes`.
|
|
64
|
+
|
|
65
|
+
Serve `dist/app.html` for every route the application does not claim,
|
|
66
|
+
including a path that names no page. See [`chrome.html`](./chrome.md) for the
|
|
67
|
+
`<dialog lb-unknown-page>` that announces that case to the user.
|
|
68
|
+
|
|
69
|
+
Give the server the origin root. The hub reaches its own route by absolute
|
|
70
|
+
path, so the application cannot be hosted under a subpath such as
|
|
71
|
+
`example.com/myapp/`, and anything proxying in front of the server passes the
|
|
72
|
+
whole path space through unchanged.
|
|
73
|
+
|
|
74
|
+
Leave `express.json()` to `hubRoutes`, which mounts it on its own routes.
|
|
75
|
+
|
|
76
|
+
## Running the server
|
|
77
|
+
|
|
78
|
+
Run the server under a TypeScript-capable runner. The builder writes
|
|
79
|
+
`dist/pages.ts`, which exports `hub`, as TypeScript:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"scripts": {
|
|
84
|
+
"dev": "loadbare-app-build --watch & tsx server.ts"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Node 22.18 and later also runs it with no runner at all, since Node strips
|
|
90
|
+
TypeScript types itself. `dist/pages.ts` imports each `.requests.ts` and
|
|
91
|
+
`.queries.ts` file by its full name, extension included, because Node looks
|
|
92
|
+
for exactly the path an import names.
|
|
93
|
+
|
|
94
|
+
Enable `allowImportingTsExtensions` in the application's `tsconfig.json`
|
|
95
|
+
when `tsc` type-checks the server. Without it, `tsc` rejects the `.ts`
|
|
96
|
+
extensions in `dist/pages.ts`:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"compilerOptions": {
|
|
101
|
+
"noEmit": true,
|
|
102
|
+
"allowImportingTsExtensions": true
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Restart the server after adding or changing a `.requests.ts` or `.queries.ts`
|
|
108
|
+
file.
|
|
109
|
+
|
|
110
|
+
## Database layer
|
|
111
|
+
|
|
112
|
+
Loadbare ships no data layer. The application opens its own database and hands
|
|
113
|
+
it to Loadbare as the request context.
|
|
114
|
+
|
|
115
|
+
The application writes `contextFor` and passes it to `hubRoutes`. Loadbare
|
|
116
|
+
calls it on every data request:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
// server.ts
|
|
120
|
+
function contextFor(req: Request): HubContext {
|
|
121
|
+
return { db: openDb(req.session.userId) };
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Open the handle in `contextFor` rather than once at startup, so that each
|
|
126
|
+
request works through a database opened for the caller it authenticated.
|
|
127
|
+
|
|
128
|
+
Declare what the context holds, once, anywhere in the application's own
|
|
129
|
+
source. Next to the database module is the natural place:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
// src/database.ts
|
|
133
|
+
declare module "@loadbare/app/server" {
|
|
134
|
+
interface HubContext {
|
|
135
|
+
db: Db;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The query string the browser is showing arrives on every data request, so
|
|
141
|
+
`req.query` holds the page's query parms. Put on the context whatever a query
|
|
142
|
+
reads from them, and treat them as user input:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
function contextFor(req: Request): HubContext {
|
|
146
|
+
const { team } = req.query;
|
|
147
|
+
return { db: openDb(), team: typeof team === "string" ? team : "" };
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Add a field for anything else a request needs — the authenticated user, a
|
|
152
|
+
request id, a feature flag set. Queries and requests read them from `ctx`; see
|
|
153
|
+
[page files](./page-files.md).
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# The Basic Widget Library
|
|
2
|
+
|
|
3
|
+
Seven widgets: `lb-input`, `lb-select`, `lb-list`, `lb-options`, `lb-table`,
|
|
4
|
+
`lb-picker`, `lb-unknown-page`. Every one of them is written against the same two contracts
|
|
5
|
+
documented elsewhere — [Custom Elements](./custom-elements.md#html) for its
|
|
6
|
+
definition, [Custom Elements](./custom-elements.md#code) for its class —
|
|
7
|
+
nothing here is special-cased machinery.
|
|
8
|
+
|
|
9
|
+
They ship compiled, in `@loadbare/widgets`, a package the builder resolves the
|
|
10
|
+
way it resolves anyone else's — it declares `"loadbare": { "widgets": "./dist" }`
|
|
11
|
+
and the builder scans that. Install it and list it:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
npm install @loadbare/widgets
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// src/imports.ts
|
|
19
|
+
export default ["@loadbare/widgets"];
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
See [Widgets from packages](./builder.md#widgets-from-packages) for what
|
|
23
|
+
listing a package does, and [The Builder](./builder.md#where-the-builder-looks)
|
|
24
|
+
for where a listed package sits in the cascade.
|
|
25
|
+
|
|
26
|
+
## `lb-input`
|
|
27
|
+
|
|
28
|
+
Wraps an `<input>`. `lb-value` sets the input's `.value`. The widget sends
|
|
29
|
+
nothing on its own: `lb-action` names what the input's `change` sends. The
|
|
30
|
+
reserved `lb-row-update` saves the input's own cell; any other name sends that
|
|
31
|
+
action. Either carries the input's value, and the hub adds the scope the
|
|
32
|
+
input sits in.
|
|
33
|
+
|
|
34
|
+
An input inside an `lb-row-insert` or `lb-row-update` form leaves the attribute off.
|
|
35
|
+
The form reads every `lb-cell` in it on submit and sends one request for all
|
|
36
|
+
of them, so an input that also sent its own would write the same edit twice.
|
|
37
|
+
|
|
38
|
+
| Parameter | Fills |
|
|
39
|
+
| ---------- | ----- |
|
|
40
|
+
| `exp-label` | the visible `<label>` text |
|
|
41
|
+
| `exp-readonly` | the input's `readonly` attribute |
|
|
42
|
+
|
|
43
|
+
| Attribute | Asks for |
|
|
44
|
+
| ---------- | ----- |
|
|
45
|
+
| `lb-action` | what to send on `change`; `lb-row-update` to save the cell's own edit |
|
|
46
|
+
|
|
47
|
+
## `lb-select`
|
|
48
|
+
|
|
49
|
+
Wraps a `<select>` whose `<option>`s the author writes directly inside (via
|
|
50
|
+
`lb-slot`). `lb-value` sets the select's `.value`; a `change` sends the
|
|
51
|
+
action named by `lb-action`, with the select's `.value` as the request's
|
|
52
|
+
`value` — the choice is the interaction, so this is the case where an
|
|
53
|
+
action carries a value.
|
|
54
|
+
|
|
55
|
+
| Parameter | Fills |
|
|
56
|
+
| ---------- | ----- |
|
|
57
|
+
| `exp-label` | the visible `<label>` text |
|
|
58
|
+
|
|
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.
|
|
61
|
+
|
|
62
|
+
## `lb-options`
|
|
63
|
+
|
|
64
|
+
A `<select>` whose `<option>`s come from a query instead of being written by
|
|
65
|
+
hand. The author supplies the row template inside the widget (via
|
|
66
|
+
`lb-slot`), same as any list widget:
|
|
67
|
+
|
|
68
|
+
```html
|
|
69
|
+
<lb-options lb-list="statuses" exp-label="Status" lb-action="setStatus">
|
|
70
|
+
<template lb-key="id" data-group="category">
|
|
71
|
+
<option lb-cell="label"></option>
|
|
72
|
+
</template>
|
|
73
|
+
</lb-options>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
| Parameter | Fills |
|
|
77
|
+
| ---------- | ----- |
|
|
78
|
+
| `exp-label` | the visible `<label>` text |
|
|
79
|
+
|
|
80
|
+
- The row's key becomes the option's `value`: the `lb-key-value` the hub
|
|
81
|
+
stamps on each option is copied across, so the page names the key column
|
|
82
|
+
once, with `lb-key`.
|
|
83
|
+
- `data-group` on the row template sections the options into `<optgroup>`s,
|
|
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.
|
|
87
|
+
- A `change` sends the action named by `lb-action`, value from the
|
|
88
|
+
select's `.value`. With `lb-query-parm` instead, the hub writes the
|
|
89
|
+
choice into the URL.
|
|
90
|
+
|
|
91
|
+
`lb-picker` is this same class with its row template supplied by the
|
|
92
|
+
definition instead of the page — see below.
|
|
93
|
+
|
|
94
|
+
## `lb-table`
|
|
95
|
+
|
|
96
|
+
A `<table>` that supplies its own scaffolding; the author supplies the
|
|
97
|
+
heading row, the row template, and optionally a footer, each as a
|
|
98
|
+
`<template>` matched to a destination:
|
|
99
|
+
|
|
100
|
+
```html
|
|
101
|
+
<lb-table lb-list="ledger" exp-caption="Ledger">
|
|
102
|
+
<template lb-template="head">
|
|
103
|
+
<tr><th>Date</th><th>Amount</th></tr>
|
|
104
|
+
</template>
|
|
105
|
+
<template lb-key="id" data-sort="date" data-group="month">
|
|
106
|
+
<tr><td lb-cell="date"></td><td lb-cell="amount"></td></tr>
|
|
107
|
+
</template>
|
|
108
|
+
<template lb-template="foot">
|
|
109
|
+
<tr lb-row="ledger-total"><td>Total</td><td lb-cell="total"></td></tr>
|
|
110
|
+
</template>
|
|
111
|
+
</lb-table>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
| Parameter | Fills |
|
|
115
|
+
| ---------- | ----- |
|
|
116
|
+
| `exp-caption` | the `<caption>` text |
|
|
117
|
+
|
|
118
|
+
| Destination | Fills |
|
|
119
|
+
| ------------ | ----- |
|
|
120
|
+
| `head` (`lb-template="head"`) | the `<thead>` content |
|
|
121
|
+
| `foot` (`lb-template="foot"`) | the `<tfoot>` content |
|
|
122
|
+
| slot (no `lb-template`) | the row template, via `lb-slot` on `<tbody>` |
|
|
123
|
+
|
|
124
|
+
- `data-group` on the row template sections rows under a derived heading row,
|
|
125
|
+
one per distinct value, whose `colSpan` matches the row's own column
|
|
126
|
+
count. `data-sort` orders rows within a section (or the whole body, with no
|
|
127
|
+
grouping) by comparing each row's cell text.
|
|
128
|
+
- The `foot` destination is not delivered through `lbPlaceRow` — it's an
|
|
129
|
+
ordinary scope carrying its own `lb-row`, resolved by name like any
|
|
130
|
+
other on the page. A grand total is a second query over the same
|
|
131
|
+
data, not a row the hub hands the table.
|
|
132
|
+
|
|
133
|
+
## `lb-picker`
|
|
134
|
+
|
|
135
|
+
`lb-options`, with the row template supplied by the definition instead of
|
|
136
|
+
the page — for when every row is one option and nothing else varies:
|
|
137
|
+
|
|
138
|
+
```html
|
|
139
|
+
<lb-picker
|
|
140
|
+
lb-list="statuses"
|
|
141
|
+
exp-label="Status"
|
|
142
|
+
exp-key="id"
|
|
143
|
+
exp-cell="label"
|
|
144
|
+
exp-group="category"
|
|
145
|
+
lb-action="setStatus"
|
|
146
|
+
></lb-picker>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
| Parameter | Fills |
|
|
150
|
+
| ---------- | ----- |
|
|
151
|
+
| `exp-label` | the visible `<label>` text |
|
|
152
|
+
| `exp-key` | the row template's `lb-key` |
|
|
153
|
+
| `exp-cell` | the option's `lb-cell` |
|
|
154
|
+
| `exp-group` | the row template's `data-group` |
|
|
155
|
+
|
|
156
|
+
Behavior — grouping, key-as-value, the action on change — is inherited
|
|
157
|
+
whole from `lb-options`; a page author who needs a second element in the
|
|
158
|
+
row, or an option built from two columns, writes `lb-options` and its own
|
|
159
|
+
`<template>` instead.
|
|
160
|
+
|
|
161
|
+
## `lb-unknown-page`
|
|
162
|
+
|
|
163
|
+
The chrome's dialog for a URL that names no page, as one tag:
|
|
164
|
+
|
|
165
|
+
```html
|
|
166
|
+
<lb-hub>
|
|
167
|
+
<nav>...</nav>
|
|
168
|
+
<main></main>
|
|
169
|
+
<lb-unknown-page></lb-unknown-page>
|
|
170
|
+
</lb-hub>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
It expands to a `<dialog lb-unknown-page>` scoped to the hub's own
|
|
174
|
+
`lb-navigation` query, so `page-label` and `page-uri` land in it the way any
|
|
175
|
+
cell lands anywhere, and the hub opens it on a miss. It takes no parameters
|
|
176
|
+
and, so far, shows both cells rather than choosing between them. See
|
|
177
|
+
[`chrome.html`](./chrome.md#where-the-page-is) for the query, and for the same
|
|
178
|
+
dialog written by hand.
|