@loadbare/app 0.4.0 → 0.5.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.
- package/README.md +53 -82
- package/dist/build/assemble.d.ts +7 -5
- package/dist/build/assemble.d.ts.map +1 -1
- package/dist/build/assemble.js +29 -9
- package/dist/build/cli.d.ts +20 -12
- package/dist/build/cli.d.ts.map +1 -1
- package/dist/build/cli.js +34 -16
- package/dist/build/elements.d.ts +15 -29
- package/dist/build/elements.d.ts.map +1 -1
- package/dist/build/elements.js +25 -111
- package/dist/build/expand.d.ts +1 -1
- package/dist/build/expand.js +1 -1
- package/dist/build/locations.d.ts +14 -37
- package/dist/build/locations.d.ts.map +1 -1
- package/dist/build/locations.js +24 -67
- package/dist/build/origins.d.ts +109 -0
- package/dist/build/origins.d.ts.map +1 -0
- package/dist/build/origins.js +270 -0
- package/dist/core/lb-constants.d.ts +1 -0
- package/dist/core/lb-constants.d.ts.map +1 -1
- package/dist/core/lb-constants.js +15 -8
- package/dist/core/lb-types.d.ts +2 -2
- package/dist/core/lb-types.d.ts.map +1 -1
- package/dist/hub/lb-apply.js +1 -1
- package/dist/hub/lb-hub.d.ts.map +1 -1
- package/dist/hub/lb-hub.js +44 -17
- package/dist/hub/lb-rows.js +3 -3
- package/dist/server/lb-server.d.ts +5 -4
- package/dist/server/lb-server.d.ts.map +1 -1
- package/dist/tests/assemble.test.js +11 -4
- package/dist/tests/elements.test.js +47 -51
- package/dist/tests/expand.test.d.ts +1 -1
- package/dist/tests/expand.test.js +2 -2
- package/dist/tests/fixtures/elements/collision/imports.d.ts +3 -0
- package/dist/tests/fixtures/elements/collision/imports.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/collision/imports.js +1 -0
- package/dist/tests/fixtures/elements/manifest/imports.d.ts +3 -0
- package/dist/tests/fixtures/elements/manifest/imports.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/manifest/imports.js +1 -0
- package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts +3 -0
- package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/manifest-bad-entry/imports.js +1 -0
- package/dist/tests/fixtures/elements/{collision/elements.d.ts → manifest-not-array/imports.d.ts} +1 -1
- package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/manifest-not-array/imports.js +1 -0
- package/dist/tests/fixtures/elements/pkg/acme-widget.d.ts +2 -0
- package/dist/tests/fixtures/elements/pkg/acme-widget.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/pkg/acme-widget.js +1 -0
- package/dist/tests/lb-express.test.js +1 -1
- package/dist/tests/origins.test.d.ts +10 -0
- package/dist/tests/origins.test.d.ts.map +1 -0
- package/dist/tests/origins.test.js +326 -0
- package/dist/tests/pages.test.js +3 -3
- package/dist/tests/styles.test.js +7 -4
- package/docs/reference/builder.md +128 -0
- package/docs/reference/chrome.md +75 -0
- package/docs/reference/css.md +44 -0
- package/docs/reference/custom-elements.md +327 -0
- package/docs/reference/data-binding.md +240 -0
- package/docs/reference/overview.md +38 -0
- package/docs/reference/page-files.md +175 -0
- package/docs/reference/server.md +123 -0
- package/docs/reference/widgets.md +163 -0
- package/docs/roadmap.md +130 -0
- package/docs/testing.md +228 -0
- package/docs/theory.md +344 -223
- package/docs/tutorials/000-getting-started.md +86 -0
- package/docs/tutorials/010-pages-and-navigation.md +129 -0
- package/docs/tutorials/020-css.md +103 -0
- package/docs/tutorials/030-html-decomposition.md +79 -0
- package/docs/tutorials/040-displaying-data.md +169 -0
- package/docs/tutorials/050-actions.md +77 -0
- package/docs/tutorials/060-custom-element-code.md +73 -0
- package/docs/tutorials/065-conditional-rendering.md +161 -0
- package/docs/tutorials/070-displaying-a-list.md +137 -0
- package/docs/tutorials/072-inserting-into-a-list.md +88 -0
- package/docs/tutorials/074-deleting-from-a-list.md +77 -0
- package/docs/tutorials/076-updating-a-list-item.md +86 -0
- package/docs/tutorials/080-widget-requests.md +124 -0
- package/docs/tutorials/090-using-widget-libraries.md +75 -0
- package/package.json +4 -12
- package/dist/client.js +0 -522
- package/dist/demo-static/src/widgets/app-box.d.ts +0 -15
- package/dist/demo-static/src/widgets/app-box.d.ts.map +0 -1
- package/dist/demo-static/src/widgets/app-box.js +0 -19
- package/dist/tests/fixtures/elements/collision/elements.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/collision/elements.js +0 -3
- package/dist/tests/fixtures/elements/manifest/elements.d.ts +0 -5
- package/dist/tests/fixtures/elements/manifest/elements.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/manifest/elements.js +0 -3
- package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts +0 -5
- package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/manifest-bad-tag/elements.js +0 -3
- package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts +0 -5
- package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/manifest-bad-value/elements.js +0 -3
- package/dist/tests/golden.test.d.ts +0 -19
- package/dist/tests/golden.test.d.ts.map +0 -1
- package/dist/tests/golden.test.js +0 -60
- package/dist/tests/helpers/window.d.ts +0 -43
- package/dist/tests/helpers/window.d.ts.map +0 -1
- package/dist/tests/helpers/window.js +0 -78
- package/dist/tests/lb-input.test.d.ts +0 -9
- package/dist/tests/lb-input.test.d.ts.map +0 -1
- package/dist/tests/lb-input.test.js +0 -78
- package/dist/tests/lb-list.test.d.ts +0 -12
- package/dist/tests/lb-list.test.d.ts.map +0 -1
- package/dist/tests/lb-list.test.js +0 -44
- package/dist/tests/lb-options.test.d.ts +0 -10
- package/dist/tests/lb-options.test.d.ts.map +0 -1
- package/dist/tests/lb-options.test.js +0 -121
- package/dist/tests/lb-picker.test.d.ts +0 -14
- package/dist/tests/lb-picker.test.d.ts.map +0 -1
- package/dist/tests/lb-picker.test.js +0 -59
- package/dist/tests/lb-select.test.d.ts +0 -9
- package/dist/tests/lb-select.test.d.ts.map +0 -1
- package/dist/tests/lb-select.test.js +0 -71
- package/dist/tests/lb-table.test.d.ts +0 -15
- package/dist/tests/lb-table.test.d.ts.map +0 -1
- package/dist/tests/lb-table.test.js +0 -205
- package/dist/widgets/index.d.ts +0 -7
- package/dist/widgets/index.d.ts.map +0 -1
- package/dist/widgets/index.js +0 -6
- package/dist/widgets/lb-input.d.ts +0 -2
- package/dist/widgets/lb-input.d.ts.map +0 -1
- package/dist/widgets/lb-input.js +0 -48
- package/dist/widgets/lb-list.d.ts +0 -2
- package/dist/widgets/lb-list.d.ts.map +0 -1
- package/dist/widgets/lb-list.js +0 -17
- package/dist/widgets/lb-options.d.ts +0 -26
- package/dist/widgets/lb-options.d.ts.map +0 -1
- package/dist/widgets/lb-options.js +0 -72
- package/dist/widgets/lb-picker.d.ts +0 -2
- package/dist/widgets/lb-picker.d.ts.map +0 -1
- package/dist/widgets/lb-picker.js +0 -25
- package/dist/widgets/lb-select.d.ts +0 -2
- package/dist/widgets/lb-select.d.ts.map +0 -1
- package/dist/widgets/lb-select.js +0 -43
- package/dist/widgets/lb-table.d.ts +0 -2
- package/dist/widgets/lb-table.d.ts.map +0 -1
- package/dist/widgets/lb-table.js +0 -113
- package/docs/application-chrome.md +0 -36
- package/docs/building-html-pages.md +0 -130
- package/docs/getting-started.md +0 -120
- package/docs/guide.md +0 -1164
- package/docs/hosting.md +0 -218
- package/docs/latent-risks.md +0 -20
- package/widgets/index.ts +0 -6
- package/widgets/lb-input.html +0 -1
- package/widgets/lb-input.ts +0 -64
- package/widgets/lb-list.html +0 -1
- package/widgets/lb-list.ts +0 -21
- package/widgets/lb-options.html +0 -4
- package/widgets/lb-options.ts +0 -88
- package/widgets/lb-picker.html +0 -7
- package/widgets/lb-picker.ts +0 -27
- package/widgets/lb-select.html +0 -4
- package/widgets/lb-select.ts +0 -55
- package/widgets/lb-table.html +0 -8
- package/widgets/lb-table.ts +0 -126
package/docs/hosting.md
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
# Hosting a Loadbare App Application
|
|
2
|
-
|
|
3
|
-
Everything here is written once for an application and then left alone. None
|
|
4
|
-
of it is per-page knowledge — for that, see the [Programmer's
|
|
5
|
-
Guide](./guide.md), which builds a page and stops at the point where the page
|
|
6
|
-
has to be served.
|
|
7
|
-
|
|
8
|
-
Loadbare App ships no HTTP server, no router and no data layer. The engine in
|
|
9
|
-
`@loadbare/app/server` imports one thing — its own wire types — and never
|
|
10
|
-
sees a request object. So hosting is an adapter you write once, and this
|
|
11
|
-
document is that adapter.
|
|
12
|
-
|
|
13
|
-
Status: runs, except where marked.
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## Register the page
|
|
18
|
-
|
|
19
|
-
Status: partial. Nothing loads `pages/<name>.queries.ts` or
|
|
20
|
-
`pages/<name>.hooks.ts` by name yet, so an application names its pages in a
|
|
21
|
-
registry. `demo/pages.ts` is the demo's:
|
|
22
|
-
|
|
23
|
-
```ts
|
|
24
|
-
import { createHub } from "@loadbare/app/server";
|
|
25
|
-
|
|
26
|
-
export const hub = createHub({
|
|
27
|
-
hello: { queries: helloQueries, hooks: helloHooks },
|
|
28
|
-
});
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
`createHub` returns two functions, `dataForPage` and `runAction`. The pages
|
|
32
|
-
are fixed at startup; the context is not, and arrives with each call.
|
|
33
|
-
|
|
34
|
-
When the loader ships, this file disappears and the directory is the registry.
|
|
35
|
-
|
|
36
|
-
## The shell
|
|
37
|
-
|
|
38
|
-
The shell is the one document the server sends for every route and every user.
|
|
39
|
-
It carries the chrome — whatever surrounds the page, such as a header and a
|
|
40
|
-
nav — an empty `<main>`, and every page host in the application, each wrapped
|
|
41
|
-
in a `<template>`:
|
|
42
|
-
|
|
43
|
-
```html
|
|
44
|
-
<lb-hub>
|
|
45
|
-
<header><h1>Loadbare App Demo</h1></header>
|
|
46
|
-
<nav>
|
|
47
|
-
<a href="/hello" lb-nav-link>Hello</a>
|
|
48
|
-
<a href="/counter" lb-nav-link>Counter</a>
|
|
49
|
-
</nav>
|
|
50
|
-
<main></main>
|
|
51
|
-
</lb-hub>
|
|
52
|
-
<template id="page-hello">…</template>
|
|
53
|
-
<template id="page-counter">…</template>
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
`<lb-hub>` is the hub: a single custom element that sits above every page,
|
|
57
|
-
outside `<main>`. It survives navigation, so it needs no id. It receives every
|
|
58
|
-
request a widget sends, POSTs it, and applies the response.
|
|
59
|
-
|
|
60
|
-
Because the shell carries no data, it is identical for every user and every
|
|
61
|
-
route, which is what makes it cacheable indefinitely. Values arrive later, on
|
|
62
|
-
the data channel, and land on the tree as attributes.
|
|
63
|
-
|
|
64
|
-
Assembling it is a build step: read each page host, expand its widgets, wrap
|
|
65
|
-
each in `<template id="page-<name>">`, and concatenate. `demo/shell.ts` does
|
|
66
|
-
this at startup rather than at build time, which is a convenience of the demo
|
|
67
|
-
and not the design.
|
|
68
|
-
|
|
69
|
-
## Navigation
|
|
70
|
-
|
|
71
|
-
Every page host already ships in the document, so navigation moves markup that
|
|
72
|
-
is already there. Mark an anchor to have the hub intercept it:
|
|
73
|
-
|
|
74
|
-
```html
|
|
75
|
-
<a href="/entity" lb-nav-link>Entity</a>
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
The hub calls `history.pushState`, selects the host with
|
|
79
|
-
`getElementById("page-" + name)`, fetches that page's data, then inserts and
|
|
80
|
-
hydrates in one synchronous block. The browser does not paint mid-task, so
|
|
81
|
-
there is no empty flash.
|
|
82
|
-
|
|
83
|
-
Anchors without the attribute are left alone. There is no fetch on the host
|
|
84
|
-
channel, no route table, and no in-flight state.
|
|
85
|
-
|
|
86
|
-
The path-to-page rule is one segment. An empty path currently resolves to
|
|
87
|
-
`counter`, which is demo leakage in `hub/lb-hub.ts` and should be
|
|
88
|
-
configuration.
|
|
89
|
-
|
|
90
|
-
---
|
|
91
|
-
|
|
92
|
-
## Serving it with Express
|
|
93
|
-
|
|
94
|
-
The demo runs on Express, because Loadbare App has no opinion about which server you
|
|
95
|
-
bring and the community has a settled answer. The demo splits the work along
|
|
96
|
-
the line that matters:
|
|
97
|
-
|
|
98
|
-
| file | whose it is |
|
|
99
|
-
| ---------------------- | ----------------------------------------------- |
|
|
100
|
-
| `demo/lb-routes.ts` | Loadbare App's. Every application writes this same file. |
|
|
101
|
-
| `demo/server.ts` | the demo's. Its store, its bundle, its shell. |
|
|
102
|
-
|
|
103
|
-
### Two endpoints, and no third
|
|
104
|
-
|
|
105
|
-
```
|
|
106
|
-
GET /lb/data?page=<name> the page's whole query set
|
|
107
|
-
POST /lb?page=<name> one operation, then its refresh set
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
Both names are constants — `LB_DATA_ENDPOINT` and
|
|
111
|
-
`LB_REQUEST_ENDPOINT`. The page rides on the query string rather than in
|
|
112
|
-
the body, which is what lets the operation set stay closed.
|
|
113
|
-
|
|
114
|
-
They mount as an ordinary router:
|
|
115
|
-
|
|
116
|
-
```ts
|
|
117
|
-
export function hubRoutes(hub: Hub, contextFor: ContextFor): Router {
|
|
118
|
-
const router = express.Router();
|
|
119
|
-
|
|
120
|
-
// Scoped here, not on the app: the routes that need a parsed body are the
|
|
121
|
-
// routes that say so.
|
|
122
|
-
router.use(express.json());
|
|
123
|
-
|
|
124
|
-
router.get(LB_DATA_ENDPOINT, async (req, res) => {
|
|
125
|
-
const page = String(req.query.page ?? "");
|
|
126
|
-
res.json(await hub.dataForPage(page, contextFor(req)));
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
router.post(LB_REQUEST_ENDPOINT, async (req, res) => {
|
|
130
|
-
const { name, query, key, cell, value } = req.body as HubRequest;
|
|
131
|
-
const page = String(req.query.page ?? "");
|
|
132
|
-
res.json(
|
|
133
|
-
await hub.runAction(
|
|
134
|
-
page,
|
|
135
|
-
name,
|
|
136
|
-
{ query, key, cell, value },
|
|
137
|
-
contextFor(req),
|
|
138
|
-
),
|
|
139
|
-
);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
return router;
|
|
143
|
-
}
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
Note what the POST handler does not do. It does not look up a function by a
|
|
147
|
-
name from the wire, and it does not read a payload. It passes a declared name
|
|
148
|
-
and the addressing coordinates to `runAction`, which refuses any name the page
|
|
149
|
-
did not declare.
|
|
150
|
-
|
|
151
|
-
### The context is injected, because it is yours
|
|
152
|
-
|
|
153
|
-
`contextFor` is a parameter rather than something the router builds, because
|
|
154
|
-
building it is the one part of this that is genuinely the application's:
|
|
155
|
-
|
|
156
|
-
```ts
|
|
157
|
-
export type ContextFor = (req: Request) => HubContext;
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
The demo's opens the same store every time. A real one opens it for the
|
|
161
|
-
authenticated caller — which is the whole reason the engine takes a context
|
|
162
|
-
per request rather than holding one for the life of the process:
|
|
163
|
-
|
|
164
|
-
```ts
|
|
165
|
-
function contextFor(req: Request): HubContext {
|
|
166
|
-
return { db: openDb(req.user) };
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
### Ordering is yours, and Loadbare App asks for one thing
|
|
171
|
-
|
|
172
|
-
Express keeps one ordered list of middleware and walks it top to bottom.
|
|
173
|
-
Nothing about `app.use` makes authentication run first; it runs first because
|
|
174
|
-
you registered it first. Loadbare App's only requirement is the obvious one: the
|
|
175
|
-
context must be built per request, after whatever establishes identity, and
|
|
176
|
-
before a Loadbare App route runs.
|
|
177
|
-
|
|
178
|
-
```ts
|
|
179
|
-
const app = express();
|
|
180
|
-
|
|
181
|
-
app.use(session(...));
|
|
182
|
-
app.use(authenticate); // sets req.user, or 401s
|
|
183
|
-
app.use(hubRoutes(hub, contextFor)); // now req.user exists
|
|
184
|
-
|
|
185
|
-
app.get("/client.js", serveBundle);
|
|
186
|
-
app.get(/.*/, serveShell); // last, or it swallows everything
|
|
187
|
-
app.use(errors); // four arguments, last of all
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
You can scope instead of sequence — `app.use("/lb", authenticate)` covers
|
|
191
|
-
both endpoints, since `use` matches on prefix and they share one. Either way
|
|
192
|
-
the decision is yours; Loadbare App never sees the request.
|
|
193
|
-
|
|
194
|
-
### The demo's own routes
|
|
195
|
-
|
|
196
|
-
Everything below the Loadbare App mount is this application's, and would differ in
|
|
197
|
-
yours:
|
|
198
|
-
|
|
199
|
-
```ts
|
|
200
|
-
app.get(/.*/, async (_req, res) => {
|
|
201
|
-
res.type("html").send(await buildShell());
|
|
202
|
-
});
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
Every remaining route gets the same shell. The demo rebuilds it per request
|
|
206
|
-
only so that editing a page host shows up on reload; it is a build step
|
|
207
|
-
wearing a route's clothes.
|
|
208
|
-
|
|
209
|
-
### Three things that will bite you, none of them Loadbare App's
|
|
210
|
-
|
|
211
|
-
- **The catch-all must be last.** It is a route like any other, and Express
|
|
212
|
-
takes the first match. Put it above the Loadbare App mount and it answers
|
|
213
|
-
`/lb/data` with HTML.
|
|
214
|
-
- **Express 5 changed the wildcard.** A bare `"*"` is no longer a valid path;
|
|
215
|
-
use a regex or `"/*splat"`.
|
|
216
|
-
- **Express 4 does not catch async rejections.** An `await` that throws inside
|
|
217
|
-
a handler hangs the request rather than reaching your error middleware.
|
|
218
|
-
Express 5 forwards it. The demo is on 5 and has no `try`/`catch` anywhere.
|
package/docs/latent-risks.md
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# Latent Risks
|
|
2
|
-
|
|
3
|
-
Issues that a single developer iterating on one app is unlikely to hit by
|
|
4
|
-
accident, but that are expensive to retrofit once real usage exposes them.
|
|
5
|
-
Not being built speculatively — read this when the symptom below actually
|
|
6
|
-
shows up, as a prompt to come back and decide the shape deliberately.
|
|
7
|
-
|
|
8
|
-
## Staleness and concurrent writers
|
|
9
|
-
|
|
10
|
-
Two tabs, or two users, updating the same projection at once. A solo
|
|
11
|
-
developer testing in one browser will not produce this by accident, and
|
|
12
|
-
retrofitting a version or conflict check onto every tuple after the fact
|
|
13
|
-
touches every widget that writes.
|
|
14
|
-
|
|
15
|
-
## Nesting
|
|
16
|
-
|
|
17
|
-
Whether a tuple may contain a projection (master-detail, an expanding row).
|
|
18
|
-
`theory.md` already flags this as possibly load-bearing if disallowed. Worth
|
|
19
|
-
a decision-in-principle the first time a master-detail page is built, even
|
|
20
|
-
before the mechanism is needed elsewhere.
|
package/widgets/index.ts
DELETED
package/widgets/lb-input.html
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<label>{{label}} <input readonly="{{readonly}}" /></label>
|
package/widgets/lb-input.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/// <reference lib="dom" />
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
ATTR_CELL,
|
|
5
|
-
ATTR_KEY,
|
|
6
|
-
ATTR_QUERY,
|
|
7
|
-
ATTR_VALUE,
|
|
8
|
-
LB_EVENT_NAME,
|
|
9
|
-
} from "../core/lb-constants";
|
|
10
|
-
import type { HubRequest } from "../core/lb-types";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* A leaf cell wrapping an <input>. The hub writes one attribute name for
|
|
14
|
-
* every widget type, so the value lands in `lb-value` and the widget
|
|
15
|
-
* forwards it to the control it owns.
|
|
16
|
-
*
|
|
17
|
-
* The other direction of the same cell: on change it sends `cell-change`,
|
|
18
|
-
* addressed by the same lb-query/lb-key/lb-cell coordinates the value
|
|
19
|
-
* arrived on. A `readonly` input never fires `change` from user input, so a
|
|
20
|
-
* readonly cell sends nothing on its own.
|
|
21
|
-
*/
|
|
22
|
-
class LbInput extends HTMLElement {
|
|
23
|
-
static observedAttributes = [ATTR_VALUE];
|
|
24
|
-
|
|
25
|
-
attributeChangedCallback(_name: string, _old: string, value: string) {
|
|
26
|
-
const input = this.querySelector("input");
|
|
27
|
-
if (!input) {
|
|
28
|
-
console.error(`lb-input: no <input> to receive the value`);
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
input.value = value;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
connectedCallback() {
|
|
35
|
-
this.addEventListener("change", () => {
|
|
36
|
-
const input = this.querySelector("input");
|
|
37
|
-
if (!input) {
|
|
38
|
-
console.error(`lb-input: change with no <input>, ignoring`);
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
const query = this.closest(`[${ATTR_QUERY}]`)?.getAttribute(ATTR_QUERY);
|
|
42
|
-
const key = this.closest(`[${ATTR_KEY}]`)?.getAttribute(ATTR_KEY);
|
|
43
|
-
const cell = this.getAttribute(ATTR_CELL);
|
|
44
|
-
if (!query || !key || !cell) {
|
|
45
|
-
console.error(
|
|
46
|
-
`lb-input: change with no query/key/cell coordinates, ignoring`,
|
|
47
|
-
);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
const detail: HubRequest = {
|
|
51
|
-
op: "cell-change",
|
|
52
|
-
query,
|
|
53
|
-
key,
|
|
54
|
-
cell,
|
|
55
|
-
value: input.value,
|
|
56
|
-
};
|
|
57
|
-
this.dispatchEvent(
|
|
58
|
-
new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }),
|
|
59
|
-
);
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
customElements.define("lb-input", LbInput);
|
package/widgets/lb-list.html
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<div lb-slot></div>
|
package/widgets/lb-list.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/// <reference lib="dom" />
|
|
2
|
-
|
|
3
|
-
import type { Projection, HubRowHost } from "../core/lb-types";
|
|
4
|
-
import { applyRows } from "../hub/lb-rows";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The plain repeater: whatever the page author wrote inside is repeated once
|
|
8
|
-
* per tuple, in the order the server sent.
|
|
9
|
-
*
|
|
10
|
-
* It exists because a projection has to land on a widget, and because a
|
|
11
|
-
* `<table>` cannot be one — a custom element written inside `<tbody>` is
|
|
12
|
-
* discarded by the parser, so the widget goes around the table and the
|
|
13
|
-
* template goes inside it, where the content model already allows it.
|
|
14
|
-
*/
|
|
15
|
-
class LbList extends HTMLElement implements HubRowHost {
|
|
16
|
-
acceptRows(result: Projection) {
|
|
17
|
-
applyRows(this, result);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
customElements.define("lb-list", LbList);
|
package/widgets/lb-options.html
DELETED
package/widgets/lb-options.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/// <reference lib="dom" />
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
ATTR_ACTION,
|
|
5
|
-
ATTR_GROUP,
|
|
6
|
-
ATTR_KEY,
|
|
7
|
-
LB_EVENT_NAME,
|
|
8
|
-
} from "../core/lb-constants";
|
|
9
|
-
import type {
|
|
10
|
-
Projection,
|
|
11
|
-
HubRequest,
|
|
12
|
-
HubRowHost,
|
|
13
|
-
} from "../core/lb-types";
|
|
14
|
-
import { applyRows, type Place } from "../hub/lb-rows";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* A `<select>` whose choices come from a query, and the case that shows why
|
|
18
|
-
* placement belongs to the widget rather than to the framework.
|
|
19
|
-
*
|
|
20
|
-
* The rows arrive flat and keyed. Nothing on the wire knows about
|
|
21
|
-
* `<optgroup>`, and nothing needs to: a group is a column like any other, and
|
|
22
|
-
* turning a repeated column value into an element is exactly the local
|
|
23
|
-
* knowledge a widget has and the hub does not. A table wanting section
|
|
24
|
-
* headings would read the same `lb-group` and build something else.
|
|
25
|
-
*
|
|
26
|
-
* The identity of a row is the value of the option, so `lb-key` supplies
|
|
27
|
-
* both and the page declares it once.
|
|
28
|
-
*
|
|
29
|
-
* Exported because `lb-picker` is this widget with its row template
|
|
30
|
-
* supplied by the definition instead of by the page. Nothing about the
|
|
31
|
-
* behavior changes, so nothing about the behavior is written twice.
|
|
32
|
-
*/
|
|
33
|
-
export class LbOptions extends HTMLElement implements HubRowHost {
|
|
34
|
-
acceptRows(result: Projection) {
|
|
35
|
-
applyRows(this, result, this.place);
|
|
36
|
-
// The identity of the row is the value of the option, so nothing on the
|
|
37
|
-
// page has to say so twice.
|
|
38
|
-
for (const option of this.querySelectorAll(`option[${ATTR_KEY}]`)) {
|
|
39
|
-
option.setAttribute("value", option.getAttribute(ATTR_KEY)!);
|
|
40
|
-
}
|
|
41
|
-
// A group is derived, so it goes when its last row does.
|
|
42
|
-
for (const group of this.querySelectorAll("optgroup")) {
|
|
43
|
-
if (!group.querySelector("option")) group.remove();
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
private place: Place = (row, tuple, template) => {
|
|
48
|
-
const select = this.querySelector("select")!;
|
|
49
|
-
const groupCell = template.getAttribute(ATTR_GROUP);
|
|
50
|
-
if (!groupCell) {
|
|
51
|
-
select.insertBefore(row, template);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
this.groupFor(select, template, tuple[groupCell] ?? "").append(row);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/** The group with this label, or a new one in front of the template. */
|
|
58
|
-
private groupFor(select: Element, template: Element, label: string): Element {
|
|
59
|
-
for (const group of select.querySelectorAll("optgroup")) {
|
|
60
|
-
if (group.label === label) return group;
|
|
61
|
-
}
|
|
62
|
-
const group = document.createElement("optgroup");
|
|
63
|
-
group.label = label;
|
|
64
|
-
select.insertBefore(group, template);
|
|
65
|
-
return group;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
connectedCallback() {
|
|
69
|
-
this.addEventListener("change", () => {
|
|
70
|
-
const select = this.querySelector("select");
|
|
71
|
-
if (!select) {
|
|
72
|
-
console.error(`lb-options: change with no <select>, ignoring`);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
const name = this.getAttribute(ATTR_ACTION);
|
|
76
|
-
if (!name) {
|
|
77
|
-
console.error(`lb-options: no ${ATTR_ACTION}, nothing to send`);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
const detail: HubRequest = { op: "action", name, value: select.value };
|
|
81
|
-
this.dispatchEvent(
|
|
82
|
-
new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }),
|
|
83
|
-
);
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
customElements.define("lb-options", LbOptions);
|
package/widgets/lb-picker.html
DELETED
package/widgets/lb-picker.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/// <reference lib="dom" />
|
|
2
|
-
|
|
3
|
-
import { LbOptions } from "./lb-options";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* The same `<select>` as `lb-options`, with the row template supplied by
|
|
7
|
-
* the definition instead of by the page.
|
|
8
|
-
*
|
|
9
|
-
* A row template is markup, so writing it is the page author's job — until
|
|
10
|
-
* every row is one option and the only thing that varies is which columns it
|
|
11
|
-
* reads. Those are words, and words are parameters: `exp-key`, `exp-cell` and
|
|
12
|
-
* `exp-group` fill the placeholders in the template the definition already
|
|
13
|
-
* has, at build time, and the page writes one tag with no children.
|
|
14
|
-
*
|
|
15
|
-
* It is the same trade as anywhere else in expansion. A page author who wants
|
|
16
|
-
* a second element in the row, or an option built from two columns, writes
|
|
17
|
-
* `lb-options` and the template themselves. This one is for when they do
|
|
18
|
-
* not.
|
|
19
|
-
*
|
|
20
|
-
* The behavior is inherited whole, because it is the same behavior: rows land
|
|
21
|
-
* in groups, the key becomes the value, and a change sends the declared
|
|
22
|
-
* action. A distinct class exists only because `customElements.define`
|
|
23
|
-
* requires one constructor per name.
|
|
24
|
-
*/
|
|
25
|
-
class LbPicker extends LbOptions {}
|
|
26
|
-
|
|
27
|
-
customElements.define("lb-picker", LbPicker);
|
package/widgets/lb-select.html
DELETED
package/widgets/lb-select.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/// <reference lib="dom" />
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
ATTR_ACTION,
|
|
5
|
-
ATTR_VALUE,
|
|
6
|
-
LB_EVENT_NAME,
|
|
7
|
-
} from "../core/lb-constants";
|
|
8
|
-
import type { HubRequest } from "../core/lb-types";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* A leaf cell wrapping a <select>. It receives its value like any other cell,
|
|
12
|
-
* and on change it sends the action the page declared for it — the request
|
|
13
|
-
* vocabulary and the addressing vocabulary running in both directions on one
|
|
14
|
-
* widget.
|
|
15
|
-
*
|
|
16
|
-
* The choice is the interaction, so this is the case where an action carries
|
|
17
|
-
* a value. The server still decides what that choice means.
|
|
18
|
-
*/
|
|
19
|
-
class LbSelect extends HTMLElement {
|
|
20
|
-
static observedAttributes = [ATTR_VALUE];
|
|
21
|
-
|
|
22
|
-
attributeChangedCallback(_name: string, _old: string, value: string) {
|
|
23
|
-
const select = this.querySelector("select");
|
|
24
|
-
if (!select) {
|
|
25
|
-
console.error(`lb-select: no <select> to receive the value`);
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
select.value = value;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
connectedCallback() {
|
|
32
|
-
this.addEventListener("change", () => {
|
|
33
|
-
const select = this.querySelector("select");
|
|
34
|
-
if (!select) {
|
|
35
|
-
console.error(`lb-select: change with no <select>, ignoring`);
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
const name = this.getAttribute(ATTR_ACTION);
|
|
39
|
-
if (!name) {
|
|
40
|
-
console.error(`lb-select: no ${ATTR_ACTION}, nothing to send`);
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
const detail: HubRequest = {
|
|
44
|
-
op: "action",
|
|
45
|
-
name,
|
|
46
|
-
value: select.value,
|
|
47
|
-
};
|
|
48
|
-
this.dispatchEvent(
|
|
49
|
-
new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }),
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
customElements.define("lb-select", LbSelect);
|
package/widgets/lb-table.html
DELETED
package/widgets/lb-table.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
/// <reference lib="dom" />
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
ATTR_CELL,
|
|
5
|
-
ATTR_GROUP,
|
|
6
|
-
ATTR_KEY,
|
|
7
|
-
ATTR_SORT,
|
|
8
|
-
} from "../core/lb-constants";
|
|
9
|
-
import type { Projection, HubRowHost } from "../core/lb-types";
|
|
10
|
-
import { applyRows, type Place } from "../hub/lb-rows";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* A table that owns its own scaffolding, and the case that shows what a list
|
|
14
|
-
* widget is for once the repeater exists.
|
|
15
|
-
*
|
|
16
|
-
* `lb-list` goes around a table the page author wrote. This one supplies
|
|
17
|
-
* the table, and the page writes what only the page knows: the heading row
|
|
18
|
-
* and the row template. That division is forced rather than chosen — a
|
|
19
|
-
* `<thead>` written inside a custom element is not inside a table, so the
|
|
20
|
-
* parser drops the tag and keeps the text. A `<template>` survives anywhere,
|
|
21
|
-
* so the heading row arrives wrapped and expansion unwraps it into the
|
|
22
|
-
* `lb-template` destination beside the slot.
|
|
23
|
-
*
|
|
24
|
-
* What it adds at run time is placement, and only placement. `lb-sort` names
|
|
25
|
-
* the column rows are ordered by; `lb-group` names the column they are
|
|
26
|
-
* sectioned by, and is the same attribute a grouped `<select>` reads, because
|
|
27
|
-
* a table with section headings and an `<optgroup>` are asking the same thing
|
|
28
|
-
* of the same data.
|
|
29
|
-
*
|
|
30
|
-
* The `foot` destination is the head's argument a second time and needs no
|
|
31
|
-
* code here. A `<tfoot>` is a place on the page, not a row the hub delivers,
|
|
32
|
-
* so what lands in it is an ordinary scope carrying its own `lb-query` — a
|
|
33
|
-
* grand total is a second projection of the same table, and the hub resolves
|
|
34
|
-
* it by name like any other. See demo/pages/ledger.html.
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* The heading row of a section, carrying the value it stands for.
|
|
39
|
-
*
|
|
40
|
-
* The widget's own scaffolding, so it is HTML's namespace rather than
|
|
41
|
-
* Loadbare App's: it is derived from rows the hub knows and is invisible to the hub
|
|
42
|
-
* itself, exactly as the `<optgroup>` in `lb-options` is. Nothing
|
|
43
|
-
* addresses it, so it carries no `lb-key` and no name from the vocabulary.
|
|
44
|
-
*/
|
|
45
|
-
const GROUP_ROW = "data-group";
|
|
46
|
-
|
|
47
|
-
class LbTable extends HTMLElement implements HubRowHost {
|
|
48
|
-
acceptRows(result: Projection) {
|
|
49
|
-
applyRows(this, result, this.place);
|
|
50
|
-
// A section is derived, so it goes when its last row does.
|
|
51
|
-
for (const heading of this.querySelectorAll(`tr[${GROUP_ROW}]`)) {
|
|
52
|
-
if (this.section(heading).rows.length === 0) heading.remove();
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Group first, then sort within the group.
|
|
58
|
-
*
|
|
59
|
-
* Inserting a row before the first one that sorts after it keeps the
|
|
60
|
-
* section ordered, whatever else is in it — which is what lets a whole set
|
|
61
|
-
* and a single patched row take the same path.
|
|
62
|
-
*/
|
|
63
|
-
private place: Place = (row, tuple, template) => {
|
|
64
|
-
const groupCell = template.getAttribute(ATTR_GROUP);
|
|
65
|
-
const sortCell = template.getAttribute(ATTR_SORT);
|
|
66
|
-
const heading = groupCell
|
|
67
|
-
? this.headingFor(template, tuple[groupCell] ?? "")
|
|
68
|
-
: null;
|
|
69
|
-
const { rows, end } = this.section(heading);
|
|
70
|
-
|
|
71
|
-
const value = sortCell ? (tuple[sortCell] ?? "") : "";
|
|
72
|
-
const after = sortCell
|
|
73
|
-
? rows.find((other) => other !== row && cellOf(other, sortCell) > value)
|
|
74
|
-
: undefined;
|
|
75
|
-
template.parentElement!.insertBefore(row, after ?? end);
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* The rows of one section, and the node it ends at.
|
|
80
|
-
*
|
|
81
|
-
* A section runs from its heading to the next heading, or to the template
|
|
82
|
-
* if there is none after it. Without grouping there is one section and it
|
|
83
|
-
* is the whole body — the heading row the page wrote is skipped along the
|
|
84
|
-
* way, because it carries no key and is therefore not a row.
|
|
85
|
-
*/
|
|
86
|
-
private section(heading: Element | null): { rows: Element[]; end: Element } {
|
|
87
|
-
const template = this.querySelector("template")!;
|
|
88
|
-
const rows: Element[] = [];
|
|
89
|
-
let node = heading
|
|
90
|
-
? heading.nextElementSibling
|
|
91
|
-
: template.parentElement!.firstElementChild;
|
|
92
|
-
while (node && node !== template && !node.hasAttribute(GROUP_ROW)) {
|
|
93
|
-
if (node.hasAttribute(ATTR_KEY)) rows.push(node);
|
|
94
|
-
node = node.nextElementSibling;
|
|
95
|
-
}
|
|
96
|
-
return { rows, end: node ?? template };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** The section with this label, or a new one in front of the template. */
|
|
100
|
-
private headingFor(template: HTMLTemplateElement, label: string): Element {
|
|
101
|
-
const body = template.parentElement!;
|
|
102
|
-
for (const heading of body.querySelectorAll(`tr[${GROUP_ROW}]`)) {
|
|
103
|
-
if (heading.getAttribute(GROUP_ROW) === label) return heading;
|
|
104
|
-
}
|
|
105
|
-
const heading = document.createElement("tr");
|
|
106
|
-
heading.setAttribute(GROUP_ROW, label);
|
|
107
|
-
const cell = document.createElement("th");
|
|
108
|
-
cell.setAttribute("scope", "rowgroup");
|
|
109
|
-
// How wide the table is, from the row the page author wrote. Nothing
|
|
110
|
-
// else knows, and nothing had to be told twice.
|
|
111
|
-
cell.colSpan = template.content.firstElementChild?.childElementCount ?? 1;
|
|
112
|
-
cell.textContent = label;
|
|
113
|
-
heading.append(cell);
|
|
114
|
-
body.insertBefore(heading, template);
|
|
115
|
-
return heading;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** What a live row is showing in one column. */
|
|
120
|
-
function cellOf(row: Element, cell: string): string {
|
|
121
|
-
const selector = `[${ATTR_CELL}="${cell}"]`;
|
|
122
|
-
const el = row.matches(selector) ? row : row.querySelector(selector);
|
|
123
|
-
return el?.textContent ?? "";
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
customElements.define("lb-table", LbTable);
|