@loadbare/app 0.4.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/LICENSE +201 -0
- package/README.md +95 -0
- package/dist/build/assemble.d.ts +31 -0
- package/dist/build/assemble.d.ts.map +1 -0
- package/dist/build/assemble.js +53 -0
- package/dist/build/cli.d.ts +30 -0
- package/dist/build/cli.d.ts.map +1 -0
- package/dist/build/cli.js +107 -0
- package/dist/build/elements.d.ts +61 -0
- package/dist/build/elements.d.ts.map +1 -0
- package/dist/build/elements.js +158 -0
- package/dist/build/expand.d.ts +45 -0
- package/dist/build/expand.d.ts.map +1 -0
- package/dist/build/expand.js +386 -0
- package/dist/build/format.d.ts +28 -0
- package/dist/build/format.d.ts.map +1 -0
- package/dist/build/format.js +42 -0
- package/dist/build/locations.d.ts +87 -0
- package/dist/build/locations.d.ts.map +1 -0
- package/dist/build/locations.js +173 -0
- package/dist/build/package-root.d.ts +9 -0
- package/dist/build/package-root.d.ts.map +1 -0
- package/dist/build/package-root.js +24 -0
- package/dist/build/pages.d.ts +25 -0
- package/dist/build/pages.d.ts.map +1 -0
- package/dist/build/pages.js +54 -0
- package/dist/build/styles.d.ts +13 -0
- package/dist/build/styles.d.ts.map +1 -0
- package/dist/build/styles.js +18 -0
- package/dist/client.js +522 -0
- package/dist/core/lb-constants.d.ts +23 -0
- package/dist/core/lb-constants.d.ts.map +1 -0
- package/dist/core/lb-constants.js +95 -0
- package/dist/core/lb-types.d.ts +88 -0
- package/dist/core/lb-types.d.ts.map +1 -0
- package/dist/core/lb-types.js +5 -0
- package/dist/demo-static/src/widgets/app-box.d.ts +15 -0
- package/dist/demo-static/src/widgets/app-box.d.ts.map +1 -0
- package/dist/demo-static/src/widgets/app-box.js +19 -0
- package/dist/hub/lb-apply.d.ts +13 -0
- package/dist/hub/lb-apply.d.ts.map +1 -0
- package/dist/hub/lb-apply.js +77 -0
- package/dist/hub/lb-hub.d.ts +2 -0
- package/dist/hub/lb-hub.d.ts.map +1 -0
- package/dist/hub/lb-hub.js +242 -0
- package/dist/hub/lb-rows.d.ts +18 -0
- package/dist/hub/lb-rows.d.ts.map +1 -0
- package/dist/hub/lb-rows.js +106 -0
- package/dist/server/lb-express.d.ts +28 -0
- package/dist/server/lb-express.d.ts.map +1 -0
- package/dist/server/lb-express.js +77 -0
- package/dist/server/lb-server.d.ts +174 -0
- package/dist/server/lb-server.d.ts.map +1 -0
- package/dist/server/lb-server.js +79 -0
- package/dist/tests/assemble.test.d.ts +8 -0
- package/dist/tests/assemble.test.d.ts.map +1 -0
- package/dist/tests/assemble.test.js +51 -0
- package/dist/tests/elements.test.d.ts +8 -0
- package/dist/tests/elements.test.d.ts.map +1 -0
- package/dist/tests/elements.test.js +111 -0
- package/dist/tests/expand.test.d.ts +10 -0
- package/dist/tests/expand.test.d.ts.map +1 -0
- package/dist/tests/expand.test.js +226 -0
- package/dist/tests/fixtures/elements/collision/elements.d.ts +5 -0
- package/dist/tests/fixtures/elements/collision/elements.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/collision/elements.js +3 -0
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.d.ts +2 -0
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.js +1 -0
- package/dist/tests/fixtures/elements/local/widgets/app-box.d.ts +2 -0
- package/dist/tests/fixtures/elements/local/widgets/app-box.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/local/widgets/app-box.js +1 -0
- package/dist/tests/fixtures/elements/manifest/elements.d.ts +5 -0
- package/dist/tests/fixtures/elements/manifest/elements.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/manifest/elements.js +3 -0
- package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts +5 -0
- package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/manifest-bad-tag/elements.js +3 -0
- package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts +5 -0
- package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/manifest-bad-value/elements.js +3 -0
- package/dist/tests/golden.test.d.ts +19 -0
- package/dist/tests/golden.test.d.ts.map +1 -0
- package/dist/tests/golden.test.js +60 -0
- package/dist/tests/helpers/console.d.ts +20 -0
- package/dist/tests/helpers/console.d.ts.map +1 -0
- package/dist/tests/helpers/console.js +28 -0
- package/dist/tests/helpers/dom.d.ts +18 -0
- package/dist/tests/helpers/dom.d.ts.map +1 -0
- package/dist/tests/helpers/dom.js +22 -0
- package/dist/tests/helpers/window.d.ts +43 -0
- package/dist/tests/helpers/window.d.ts.map +1 -0
- package/dist/tests/helpers/window.js +78 -0
- package/dist/tests/lb-apply.test.d.ts +8 -0
- package/dist/tests/lb-apply.test.d.ts.map +1 -0
- package/dist/tests/lb-apply.test.js +153 -0
- package/dist/tests/lb-express.test.d.ts +14 -0
- package/dist/tests/lb-express.test.d.ts.map +1 -0
- package/dist/tests/lb-express.test.js +238 -0
- package/dist/tests/lb-input.test.d.ts +9 -0
- package/dist/tests/lb-input.test.d.ts.map +1 -0
- package/dist/tests/lb-input.test.js +78 -0
- package/dist/tests/lb-list.test.d.ts +12 -0
- package/dist/tests/lb-list.test.d.ts.map +1 -0
- package/dist/tests/lb-list.test.js +44 -0
- package/dist/tests/lb-options.test.d.ts +10 -0
- package/dist/tests/lb-options.test.d.ts.map +1 -0
- package/dist/tests/lb-options.test.js +121 -0
- package/dist/tests/lb-picker.test.d.ts +14 -0
- package/dist/tests/lb-picker.test.d.ts.map +1 -0
- package/dist/tests/lb-picker.test.js +59 -0
- package/dist/tests/lb-rows.test.d.ts +12 -0
- package/dist/tests/lb-rows.test.d.ts.map +1 -0
- package/dist/tests/lb-rows.test.js +336 -0
- package/dist/tests/lb-select.test.d.ts +9 -0
- package/dist/tests/lb-select.test.d.ts.map +1 -0
- package/dist/tests/lb-select.test.js +71 -0
- package/dist/tests/lb-server.test.d.ts +9 -0
- package/dist/tests/lb-server.test.d.ts.map +1 -0
- package/dist/tests/lb-server.test.js +495 -0
- package/dist/tests/lb-table.test.d.ts +15 -0
- package/dist/tests/lb-table.test.d.ts.map +1 -0
- package/dist/tests/lb-table.test.js +205 -0
- package/dist/tests/pages.test.d.ts +6 -0
- package/dist/tests/pages.test.d.ts.map +1 -0
- package/dist/tests/pages.test.js +98 -0
- package/dist/tests/styles.test.d.ts +7 -0
- package/dist/tests/styles.test.d.ts.map +1 -0
- package/dist/tests/styles.test.js +73 -0
- package/dist/widgets/index.d.ts +7 -0
- package/dist/widgets/index.d.ts.map +1 -0
- package/dist/widgets/index.js +6 -0
- package/dist/widgets/lb-input.d.ts +2 -0
- package/dist/widgets/lb-input.d.ts.map +1 -0
- package/dist/widgets/lb-input.js +48 -0
- package/dist/widgets/lb-list.d.ts +2 -0
- package/dist/widgets/lb-list.d.ts.map +1 -0
- package/dist/widgets/lb-list.js +17 -0
- package/dist/widgets/lb-options.d.ts +26 -0
- package/dist/widgets/lb-options.d.ts.map +1 -0
- package/dist/widgets/lb-options.js +72 -0
- package/dist/widgets/lb-picker.d.ts +2 -0
- package/dist/widgets/lb-picker.d.ts.map +1 -0
- package/dist/widgets/lb-picker.js +25 -0
- package/dist/widgets/lb-select.d.ts +2 -0
- package/dist/widgets/lb-select.d.ts.map +1 -0
- package/dist/widgets/lb-select.js +43 -0
- package/dist/widgets/lb-table.d.ts +2 -0
- package/dist/widgets/lb-table.d.ts.map +1 -0
- package/dist/widgets/lb-table.js +113 -0
- package/docs/application-chrome.md +36 -0
- package/docs/building-html-pages.md +130 -0
- package/docs/getting-started.md +120 -0
- package/docs/guide.md +1164 -0
- package/docs/hosting.md +218 -0
- package/docs/latent-risks.md +20 -0
- package/docs/theory.md +226 -0
- package/package.json +85 -0
- package/widgets/index.ts +6 -0
- package/widgets/lb-input.html +1 -0
- package/widgets/lb-input.ts +64 -0
- package/widgets/lb-list.html +1 -0
- package/widgets/lb-list.ts +21 -0
- package/widgets/lb-options.html +4 -0
- package/widgets/lb-options.ts +88 -0
- package/widgets/lb-picker.html +7 -0
- package/widgets/lb-picker.ts +27 -0
- package/widgets/lb-select.html +4 -0
- package/widgets/lb-select.ts +55 -0
- package/widgets/lb-table.html +8 -0
- package/widgets/lb-table.ts +126 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** docs/requests.md — the operation set is closed. */
|
|
2
|
+
export type HubRequest = {
|
|
3
|
+
op: "cell-change";
|
|
4
|
+
query: string;
|
|
5
|
+
key: string;
|
|
6
|
+
cell: string;
|
|
7
|
+
value: string;
|
|
8
|
+
} | {
|
|
9
|
+
op: "tuple-insert";
|
|
10
|
+
query: string;
|
|
11
|
+
values: Record<string, string>;
|
|
12
|
+
} | {
|
|
13
|
+
op: "tuple-update";
|
|
14
|
+
query: string;
|
|
15
|
+
key: string;
|
|
16
|
+
values: Record<string, string>;
|
|
17
|
+
} | {
|
|
18
|
+
op: "tuple-delete";
|
|
19
|
+
query: string;
|
|
20
|
+
key: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Everything that is not a CRUD operation.
|
|
24
|
+
*
|
|
25
|
+
* The name is the application's and is looked up in the page's declared
|
|
26
|
+
* actions. What keeps this from being an RPC endpoint is that the name must
|
|
27
|
+
* already appear in the page's hooks: the browser cannot reach anything the
|
|
28
|
+
* page has not published, and there is no argument list — only where the
|
|
29
|
+
* interaction happened and, where a control has one, its value.
|
|
30
|
+
*
|
|
31
|
+
* A button carries no value, so the server computes the whole of the new
|
|
32
|
+
* state and the browser never displays a number it has not confirmed. A
|
|
33
|
+
* `<select>` carries one, because the choice is the interaction.
|
|
34
|
+
*/
|
|
35
|
+
| {
|
|
36
|
+
op: "action";
|
|
37
|
+
name: string;
|
|
38
|
+
query?: string;
|
|
39
|
+
key?: string;
|
|
40
|
+
cell?: string;
|
|
41
|
+
value?: string;
|
|
42
|
+
};
|
|
43
|
+
/** One query's result: the cells of a single tuple. Every value is a string. */
|
|
44
|
+
export type QueryResult = Record<string, string>;
|
|
45
|
+
/**
|
|
46
|
+
* A query that returns many tuples — see docs/guide.md, "List processing".
|
|
47
|
+
*
|
|
48
|
+
* Two results, because a list widget must be told the difference. `rows` is
|
|
49
|
+
* the entire set and therefore also the order; `patch` names only what
|
|
50
|
+
* changed and leaves everything it does not name alone. Add and remove are
|
|
51
|
+
* one result rather than two because the interesting cases are both at once
|
|
52
|
+
* — a row whose sort key changed has to move, a swap is one out and one in —
|
|
53
|
+
* and two messages would paint the intermediate state.
|
|
54
|
+
*
|
|
55
|
+
* `op` is the same discriminant word the request union uses, so one
|
|
56
|
+
* vocabulary runs in both directions. It is therefore a reserved cell name:
|
|
57
|
+
* a tuple with a cell called `op` whose value is `rows` or `patch` would be
|
|
58
|
+
* read as a projection.
|
|
59
|
+
*/
|
|
60
|
+
export type Projection = {
|
|
61
|
+
op: "rows";
|
|
62
|
+
rows: QueryResult[];
|
|
63
|
+
} | {
|
|
64
|
+
op: "patch";
|
|
65
|
+
rows?: QueryResult[];
|
|
66
|
+
drop?: string[];
|
|
67
|
+
};
|
|
68
|
+
/** A tuple is the sugar and the common case; a projection says so. */
|
|
69
|
+
export type HubResult = QueryResult | Projection;
|
|
70
|
+
export declare function isProjection(result: HubResult): result is Projection;
|
|
71
|
+
/**
|
|
72
|
+
* Every response on the data channel is the same shape, whether it is a
|
|
73
|
+
* cold start or the narrowest refresh. The hub does not know which case
|
|
74
|
+
* it is in.
|
|
75
|
+
*/
|
|
76
|
+
export type HubData = Record<string, HubResult>;
|
|
77
|
+
/**
|
|
78
|
+
* What a list widget implements. The hub delivers a projection by calling
|
|
79
|
+
* this and stops; where a row goes is the widget's, because only the widget
|
|
80
|
+
* knows whether it sorts, groups, or appends.
|
|
81
|
+
*
|
|
82
|
+
* One protocol name, not a table of tag names — the hub tests for the method,
|
|
83
|
+
* never for the element.
|
|
84
|
+
*/
|
|
85
|
+
export interface HubRowHost {
|
|
86
|
+
acceptRows(result: Projection): void;
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=lb-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lb-types.d.ts","sourceRoot":"","sources":["../../core/lb-types.ts"],"names":[],"mappings":"AAEA,sDAAsD;AACtD,MAAM,MAAM,UAAU,GAClB;IACE,EAAE,EAAE,aAAa,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf,GACD;IAAE,EAAE,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GACrE;IACE,EAAE,EAAE,cAAc,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC,GACD;IAAE,EAAE,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE;AACpD;;;;;;;;;;;;GAYG;GACD;IACE,EAAE,EAAE,QAAQ,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEN,gFAAgF;AAChF,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEjD;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,UAAU,GAClB;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,EAAE,CAAA;CAAE,GACnC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,WAAW,EAAE,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAE3D,sEAAsE;AACtE,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG,UAAU,CAAC;AAEjD,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,IAAI,UAAU,CAGpE;AAED;;;;GAIG;AACH,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAEhD;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAAC;CACtC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A plain custom element with no tie to the framework: no lb- prefix, no
|
|
3
|
+
* expansion definition, nothing but customElements.define. It exists to
|
|
4
|
+
* prove the builder resolves and bundles a tag by directory convention
|
|
5
|
+
* alone, exactly as it does for a widget the framework ships itself.
|
|
6
|
+
*
|
|
7
|
+
* Its look comes from app-box.css, a plain class rather than the style
|
|
8
|
+
* attribute — a style attribute is inline script by another name, and the
|
|
9
|
+
* CSP this framework targets refuses it exactly as it refuses inline
|
|
10
|
+
* <script>.
|
|
11
|
+
*/
|
|
12
|
+
declare class AppBox extends HTMLElement {
|
|
13
|
+
connectedCallback(): void;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=app-box.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-box.d.ts","sourceRoot":"","sources":["../../../../demo-static/src/widgets/app-box.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AACH,cAAM,MAAO,SAAQ,WAAW;IAC9B,iBAAiB;CAGlB"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/// <reference lib="dom" />
|
|
3
|
+
/**
|
|
4
|
+
* A plain custom element with no tie to the framework: no lb- prefix, no
|
|
5
|
+
* expansion definition, nothing but customElements.define. It exists to
|
|
6
|
+
* prove the builder resolves and bundles a tag by directory convention
|
|
7
|
+
* alone, exactly as it does for a widget the framework ships itself.
|
|
8
|
+
*
|
|
9
|
+
* Its look comes from app-box.css, a plain class rather than the style
|
|
10
|
+
* attribute — a style attribute is inline script by another name, and the
|
|
11
|
+
* CSP this framework targets refuses it exactly as it refuses inline
|
|
12
|
+
* <script>.
|
|
13
|
+
*/
|
|
14
|
+
class AppBox extends HTMLElement {
|
|
15
|
+
connectedCallback() {
|
|
16
|
+
this.classList.add("app-box");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
customElements.define("app-box", AppBox);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type QueryResult, type HubData } from "../core/lb-types";
|
|
2
|
+
/**
|
|
3
|
+
* Fill one scope from one tuple.
|
|
4
|
+
*
|
|
5
|
+
* The root counts as a cell if it carries one. A `<tr>` holds its cells in
|
|
6
|
+
* `<td>` children, but `<option>`'s content model is text, so an option row
|
|
7
|
+
* has to be the cell it displays. Requiring a wrapper there would require an
|
|
8
|
+
* element HTML does not allow.
|
|
9
|
+
*/
|
|
10
|
+
export declare function applyTuple(root: Element, cells: QueryResult): void;
|
|
11
|
+
/** Land a whole response. Every query result arrives through here. */
|
|
12
|
+
export declare function applyData(root: ParentNode, data: HubData): void;
|
|
13
|
+
//# sourceMappingURL=lb-apply.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lb-apply.d.ts","sourceRoot":"","sources":["../../hub/lb-apply.ts"],"names":[],"mappings":"AAcA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,OAAO,EAEb,MAAM,kBAAkB,CAAC;AAkB1B;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAMlE;AAmBD,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAY/D"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Landing data on a live host — see docs/guide.md, "Applying a response".
|
|
4
|
+
*
|
|
5
|
+
* This is one file rather than part of the hub because a list widget needs
|
|
6
|
+
* the same operation. It clones a row and fills it with a tuple, and filling
|
|
7
|
+
* a row is filling a scope: the only difference is how much of the page the
|
|
8
|
+
* root covers. A widget imports `applyTuple` the way it imports `ATTR_VALUE`,
|
|
9
|
+
* so there is one implementation of what a cell means and nobody reaches into
|
|
10
|
+
* anybody's children.
|
|
11
|
+
*/
|
|
12
|
+
import { ATTR_CELL, ATTR_QUERY, ATTR_VALUE } from "../core/lb-constants";
|
|
13
|
+
import { isProjection, } from "../core/lb-types";
|
|
14
|
+
/**
|
|
15
|
+
* A cell lands one of two ways. A custom element owns whatever control it
|
|
16
|
+
* wraps, so it receives the value as an attribute and renders it itself.
|
|
17
|
+
* Because the browser runs `attributeChangedCallback` for attributes already
|
|
18
|
+
* present when a widget upgrades, that is the same operation whether the host
|
|
19
|
+
* was inserted a microsecond ago or an hour ago.
|
|
20
|
+
*
|
|
21
|
+
* A native element has no behavior of its own, so its value is its text.
|
|
22
|
+
* The test is lexical — a hyphen in the tag name — and the hub holds no
|
|
23
|
+
* knowledge of any particular element.
|
|
24
|
+
*/
|
|
25
|
+
function land(el, value) {
|
|
26
|
+
if (el.localName.includes("-"))
|
|
27
|
+
el.setAttribute(ATTR_VALUE, value);
|
|
28
|
+
else
|
|
29
|
+
el.textContent = value;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Fill one scope from one tuple.
|
|
33
|
+
*
|
|
34
|
+
* The root counts as a cell if it carries one. A `<tr>` holds its cells in
|
|
35
|
+
* `<td>` children, but `<option>`'s content model is text, so an option row
|
|
36
|
+
* has to be the cell it displays. Requiring a wrapper there would require an
|
|
37
|
+
* element HTML does not allow.
|
|
38
|
+
*/
|
|
39
|
+
export function applyTuple(root, cells) {
|
|
40
|
+
for (const [cell, value] of Object.entries(cells)) {
|
|
41
|
+
const selector = `[${ATTR_CELL}="${cell}"]`;
|
|
42
|
+
if (root.matches(selector))
|
|
43
|
+
land(root, value);
|
|
44
|
+
for (const el of root.querySelectorAll(selector))
|
|
45
|
+
land(el, value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A projection has to reach a widget. A native element has one destination
|
|
50
|
+
* for a value and no way to acquire children, so a list is not something it
|
|
51
|
+
* can be asked to show.
|
|
52
|
+
*/
|
|
53
|
+
function landRows(scope, query, result) {
|
|
54
|
+
const host = scope;
|
|
55
|
+
if (typeof host.acceptRows !== "function") {
|
|
56
|
+
console.error(`lb-hub: query '${query}' returned rows, but <${scope.localName}> ` +
|
|
57
|
+
`is not a list widget`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
host.acceptRows(result);
|
|
61
|
+
}
|
|
62
|
+
/** Land a whole response. Every query result arrives through here. */
|
|
63
|
+
export function applyData(root, data) {
|
|
64
|
+
for (const [query, result] of Object.entries(data)) {
|
|
65
|
+
const scopes = root.querySelectorAll(`[${ATTR_QUERY}="${query}"]`);
|
|
66
|
+
if (scopes.length === 0) {
|
|
67
|
+
console.warn(`lb-hub: no scope for query '${query}', skipping`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
for (const scope of scopes) {
|
|
71
|
+
if (isProjection(result))
|
|
72
|
+
landRows(scope, query, result);
|
|
73
|
+
else
|
|
74
|
+
applyTuple(scope, result);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lb-hub.d.ts","sourceRoot":"","sources":["../../hub/lb-hub.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
import { ATTR_ACTION, ATTR_CELL, ATTR_DELETE, ATTR_ERROR, ATTR_INSERT, ATTR_KEY, ATTR_NAV_LINK, ATTR_PENDING, ATTR_QUERY, ATTR_UPDATE, HUB_TAG_NAME, PAGE_TEMPLATE_PREFIX, LB_DATA_ENDPOINT, LB_EVENT_NAME, LB_REQUEST_ENDPOINT, LB_REQUEST_TIMEOUT_MS, } from "../core/lb-constants";
|
|
3
|
+
import { applyData } from "./lb-apply";
|
|
4
|
+
export { applyData, applyTuple } from "./lb-apply";
|
|
5
|
+
/**
|
|
6
|
+
* Where a click happened, in the addressing vocabulary. An action carries
|
|
7
|
+
* these and nothing else, so there is no channel for a browser-supplied
|
|
8
|
+
* argument.
|
|
9
|
+
*/
|
|
10
|
+
function coordinates(el) {
|
|
11
|
+
const query = el.closest(`[${ATTR_QUERY}]`)?.getAttribute(ATTR_QUERY);
|
|
12
|
+
const key = el.closest(`[${ATTR_KEY}]`)?.getAttribute(ATTR_KEY);
|
|
13
|
+
const cell = el.getAttribute(ATTR_CELL);
|
|
14
|
+
return {
|
|
15
|
+
...(query === null || query === undefined ? {} : { query }),
|
|
16
|
+
...(key === null || key === undefined ? {} : { key }),
|
|
17
|
+
...(cell === null ? {} : { cell }),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The value a cell is currently showing, for gathering rather than landing.
|
|
22
|
+
*
|
|
23
|
+
* A cell is always either a native form control itself (a bare
|
|
24
|
+
* `<input lb-cell>`) or a widget that wraps one (`<lb-input>`, `<lb-select>`)
|
|
25
|
+
* — that wrapping is structural, not incidental, which is what lets this be
|
|
26
|
+
* one rule instead of one per widget type.
|
|
27
|
+
*/
|
|
28
|
+
function controlValue(el) {
|
|
29
|
+
const control = el instanceof HTMLInputElement ||
|
|
30
|
+
el instanceof HTMLSelectElement ||
|
|
31
|
+
el instanceof HTMLTextAreaElement
|
|
32
|
+
? el
|
|
33
|
+
: el.querySelector("input, select, textarea");
|
|
34
|
+
return control?.value;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Every lb-cell inside a form, gathered into one values map. Shared by
|
|
38
|
+
* lb-insert and lb-update — both submit a batch of cells, and differ only in
|
|
39
|
+
* whether a key comes with them.
|
|
40
|
+
*/
|
|
41
|
+
function gatherValues(form) {
|
|
42
|
+
const values = {};
|
|
43
|
+
for (const cell of form.querySelectorAll(`[${ATTR_CELL}]`)) {
|
|
44
|
+
const name = cell.getAttribute(ATTR_CELL);
|
|
45
|
+
const value = controlValue(cell);
|
|
46
|
+
if (value === undefined) {
|
|
47
|
+
console.warn(`lb-hub: lb-cell '${name}' has no control to read, ignoring it`, cell);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
values[name] = value;
|
|
51
|
+
}
|
|
52
|
+
return values;
|
|
53
|
+
}
|
|
54
|
+
/** A path names a page host, and nothing finer. */
|
|
55
|
+
function pageNameFor(path) {
|
|
56
|
+
const name = path.replace(/^\/+|\/+$/g, "");
|
|
57
|
+
return name === "" ? "counter" : name;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Fetch one round trip against the client-side deadline. Throws — rather
|
|
61
|
+
* than swallowing — on a non-ok response, a network failure, or a timeout,
|
|
62
|
+
* so a caller that needs to know the request failed (as opposed to one that
|
|
63
|
+
* only ever wants the freshest data, like navigate()) can tell the two
|
|
64
|
+
* apart from a genuinely empty result.
|
|
65
|
+
*/
|
|
66
|
+
async function fetchData(url, init) {
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
const timeout = setTimeout(() => controller.abort(), LB_REQUEST_TIMEOUT_MS);
|
|
69
|
+
try {
|
|
70
|
+
const res = await fetch(url, { ...init, signal: controller.signal });
|
|
71
|
+
if (!res.ok) {
|
|
72
|
+
throw new Error(`lb-hub: server responded ${res.status} for ${url}`);
|
|
73
|
+
}
|
|
74
|
+
return (await res.json());
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
clearTimeout(timeout);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The hub is a singleton outside <main>. It survives every page change,
|
|
82
|
+
* so it needs no id to disambiguate it.
|
|
83
|
+
*/
|
|
84
|
+
class LbHub extends HTMLElement {
|
|
85
|
+
main;
|
|
86
|
+
/**
|
|
87
|
+
* The page currently in <main>. Hooks are declared per page, so a request
|
|
88
|
+
* has to say which page it came from. The page rides on the URL rather
|
|
89
|
+
* than in the request, leaving the operation set closed.
|
|
90
|
+
*/
|
|
91
|
+
page = "";
|
|
92
|
+
connectedCallback() {
|
|
93
|
+
this.main = this.querySelector("main");
|
|
94
|
+
this.addEventListener(LB_EVENT_NAME, (e) => {
|
|
95
|
+
const request = e.detail;
|
|
96
|
+
if (!request?.op) {
|
|
97
|
+
console.warn(`lb-hub: event without an op, ignoring`, request);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// The element that dispatched the request, not e.currentTarget (the
|
|
101
|
+
// hub itself, since the event bubbles) — pending/error state belongs
|
|
102
|
+
// at the origin so a widget can watch its own attributes.
|
|
103
|
+
const origin = e.target;
|
|
104
|
+
origin?.removeAttribute(ATTR_ERROR);
|
|
105
|
+
origin?.setAttribute(ATTR_PENDING, "");
|
|
106
|
+
const url = `${LB_REQUEST_ENDPOINT}?page=${encodeURIComponent(this.page)}`;
|
|
107
|
+
void fetchData(url, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: { "content-type": "application/json" },
|
|
110
|
+
body: JSON.stringify(request),
|
|
111
|
+
})
|
|
112
|
+
.then((data) => applyData(this.main, data))
|
|
113
|
+
.catch((err) => {
|
|
114
|
+
console.error(`lb-hub: request failed`, err);
|
|
115
|
+
origin?.setAttribute(ATTR_ERROR, "");
|
|
116
|
+
})
|
|
117
|
+
.finally(() => origin?.removeAttribute(ATTR_PENDING));
|
|
118
|
+
});
|
|
119
|
+
/**
|
|
120
|
+
* A native element carrying an action. The hub builds the request and
|
|
121
|
+
* dispatches it from that element as the ordinary bubbling event, rather
|
|
122
|
+
* than sending it directly, so an ancestor widget can still stop it and
|
|
123
|
+
* confirm. A native action button and a hand-written widget therefore
|
|
124
|
+
* produce identical events.
|
|
125
|
+
*
|
|
126
|
+
* A widget carrying an action is left alone — it owns its own interaction
|
|
127
|
+
* and decides what counts as performing it, which for a <select> is a
|
|
128
|
+
* change rather than a click. The test is the same lexical one applyData
|
|
129
|
+
* uses: a hyphen in the tag name.
|
|
130
|
+
*/
|
|
131
|
+
this.addEventListener("click", (e) => {
|
|
132
|
+
const el = e.target?.closest(`[${ATTR_ACTION}]`);
|
|
133
|
+
if (!el || el.localName.includes("-"))
|
|
134
|
+
return;
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
const detail = {
|
|
137
|
+
op: "action",
|
|
138
|
+
name: el.getAttribute(ATTR_ACTION),
|
|
139
|
+
...coordinates(el),
|
|
140
|
+
};
|
|
141
|
+
el.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
|
|
142
|
+
});
|
|
143
|
+
// The CRUD counterpart to the lb-action delegation above: a native
|
|
144
|
+
// element needs no declared name to delete the row it is in, only its
|
|
145
|
+
// own query/key coordinates.
|
|
146
|
+
this.addEventListener("click", (e) => {
|
|
147
|
+
const el = e.target?.closest(`[${ATTR_DELETE}]`);
|
|
148
|
+
if (!el || el.localName.includes("-"))
|
|
149
|
+
return;
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
const { query, key } = coordinates(el);
|
|
152
|
+
if (!query || !key) {
|
|
153
|
+
console.warn(`lb-hub: lb-delete with no query/key coordinates, ignoring`, el);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const detail = { op: "tuple-delete", query, key };
|
|
157
|
+
el.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
|
|
158
|
+
});
|
|
159
|
+
// The CRUD op that gathers rather than addresses a single cell: every
|
|
160
|
+
// lb-cell inside the form becomes one entry of the values map. There is
|
|
161
|
+
// no key, because there is no row yet — only the query, found the same
|
|
162
|
+
// way coordinates() finds one for a click, says what it is inserting
|
|
163
|
+
// into.
|
|
164
|
+
this.addEventListener("submit", (e) => {
|
|
165
|
+
const form = e.target?.closest(`[${ATTR_INSERT}]`);
|
|
166
|
+
if (!form)
|
|
167
|
+
return;
|
|
168
|
+
e.preventDefault();
|
|
169
|
+
const { query } = coordinates(form);
|
|
170
|
+
if (!query) {
|
|
171
|
+
console.warn(`lb-hub: lb-insert with no lb-query, ignoring`, form);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const detail = {
|
|
175
|
+
op: "tuple-insert",
|
|
176
|
+
query,
|
|
177
|
+
values: gatherValues(form),
|
|
178
|
+
};
|
|
179
|
+
form.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
|
|
180
|
+
});
|
|
181
|
+
// The batch counterpart to cell-change: same gathering as lb-insert, but
|
|
182
|
+
// with the key of the row it is a form for — a list's row template root,
|
|
183
|
+
// or a form inside one, has it from the same ancestor a delete button
|
|
184
|
+
// reads.
|
|
185
|
+
this.addEventListener("submit", (e) => {
|
|
186
|
+
const form = e.target?.closest(`[${ATTR_UPDATE}]`);
|
|
187
|
+
if (!form)
|
|
188
|
+
return;
|
|
189
|
+
e.preventDefault();
|
|
190
|
+
const { query, key } = coordinates(form);
|
|
191
|
+
if (!query || !key) {
|
|
192
|
+
console.warn(`lb-hub: lb-update with no query/key coordinates, ignoring`, form);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const detail = {
|
|
196
|
+
op: "tuple-update",
|
|
197
|
+
query,
|
|
198
|
+
key,
|
|
199
|
+
values: gatherValues(form),
|
|
200
|
+
};
|
|
201
|
+
form.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
|
|
202
|
+
});
|
|
203
|
+
// Only anchors that opted in. Everything else is a real link.
|
|
204
|
+
this.addEventListener("click", (e) => {
|
|
205
|
+
const link = e.target?.closest(`a[${ATTR_NAV_LINK}]`);
|
|
206
|
+
if (!link)
|
|
207
|
+
return;
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
const path = new URL(link.href).pathname;
|
|
210
|
+
history.pushState(null, "", path);
|
|
211
|
+
void this.navigate(path);
|
|
212
|
+
});
|
|
213
|
+
addEventListener("popstate", () => void this.navigate(location.pathname));
|
|
214
|
+
void this.navigate(location.pathname);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Navigation is the one operation that replaces host DOM. The host is
|
|
218
|
+
* already in the document, so selecting a page is getElementById() and
|
|
219
|
+
* there is no fetching mechanism on the host channel.
|
|
220
|
+
*/
|
|
221
|
+
async navigate(path) {
|
|
222
|
+
const page = pageNameFor(path);
|
|
223
|
+
const template = document.getElementById(PAGE_TEMPLATE_PREFIX + page);
|
|
224
|
+
if (!(template instanceof HTMLTemplateElement)) {
|
|
225
|
+
console.error(`lb-hub: no page host for '${page}'`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
let data = {};
|
|
229
|
+
try {
|
|
230
|
+
data = await fetchData(`${LB_DATA_ENDPOINT}?page=${encodeURIComponent(page)}`);
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
console.error(`lb-hub: failed to load data for '${page}'`, err);
|
|
234
|
+
}
|
|
235
|
+
this.page = page;
|
|
236
|
+
// Insert and hydrate in one synchronous block: the browser does not
|
|
237
|
+
// paint mid-task, so there is no empty flash.
|
|
238
|
+
this.main.replaceChildren(template.content.cloneNode(true));
|
|
239
|
+
applyData(this.main, data);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
customElements.define(HUB_TAG_NAME, LbHub);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Projection, QueryResult } from "../core/lb-types";
|
|
2
|
+
/**
|
|
3
|
+
* Where a row belongs, called with the row detached on its first appearance.
|
|
4
|
+
*
|
|
5
|
+
* The default puts it immediately before the template, so rows accumulate in
|
|
6
|
+
* the order they arrive and the template stays put as the insertion marker.
|
|
7
|
+
*/
|
|
8
|
+
export type Place = (row: Element, tuple: QueryResult, template: HTMLTemplateElement) => void;
|
|
9
|
+
/**
|
|
10
|
+
* Land a projection in a widget's subtree.
|
|
11
|
+
*
|
|
12
|
+
* `rows` is the whole set, so it decides membership and order: every row is
|
|
13
|
+
* placed in the order given, and a row whose key did not arrive is gone.
|
|
14
|
+
* `patch` disturbs only what it names — a row it did not mention keeps its
|
|
15
|
+
* contents and its position.
|
|
16
|
+
*/
|
|
17
|
+
export declare function applyRows(scope: Element, result: Projection, place?: Place): void;
|
|
18
|
+
//# sourceMappingURL=lb-rows.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lb-rows.d.ts","sourceRoot":"","sources":["../../hub/lb-rows.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGhE;;;;;GAKG;AACH,MAAM,MAAM,KAAK,GAAG,CAClB,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,mBAAmB,KAC1B,IAAI,CAAC;AAwCV;;;;;;;GAOG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,UAAU,EAClB,KAAK,GAAE,KAAc,GACpB,IAAI,CAkDN"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* The row machinery a list widget shares — see docs/guide.md, "List
|
|
4
|
+
* processing".
|
|
5
|
+
*
|
|
6
|
+
* A widget owns where a row goes and nothing else. Cloning the template,
|
|
7
|
+
* matching a tuple to the row already showing it, and filling that row are
|
|
8
|
+
* the same in every list, so they are here, and a widget that sorts or groups
|
|
9
|
+
* supplies one function rather than a second implementation of all of it.
|
|
10
|
+
*/
|
|
11
|
+
import { ATTR_KEY, ATTR_ROW_COUNT } from "../core/lb-constants";
|
|
12
|
+
import { applyTuple } from "./lb-apply";
|
|
13
|
+
const before = (row, _tuple, template) => {
|
|
14
|
+
template.parentElement.insertBefore(row, template);
|
|
15
|
+
};
|
|
16
|
+
/** The row template: the one the page author wrote inside this widget. */
|
|
17
|
+
function templateIn(scope) {
|
|
18
|
+
const template = scope.querySelector("template");
|
|
19
|
+
if (!template) {
|
|
20
|
+
console.error(`lb-rows: <${scope.localName}> has no <template> to clone`);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
if (!template.getAttribute(ATTR_KEY)) {
|
|
24
|
+
console.error(`lb-rows: the <template> in <${scope.localName}> has no ${ATTR_KEY} ` +
|
|
25
|
+
`naming the column that identifies a row`);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return template;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The rows already showing, by the key value each carries.
|
|
32
|
+
*
|
|
33
|
+
* `lb-key` names the key column on the template and carries the key value on
|
|
34
|
+
* a live row. One name in two positions, and never ambiguous, because a
|
|
35
|
+
* template is not a row.
|
|
36
|
+
*/
|
|
37
|
+
function showing(scope, template) {
|
|
38
|
+
const rows = new Map();
|
|
39
|
+
for (const el of scope.querySelectorAll(`[${ATTR_KEY}]`)) {
|
|
40
|
+
if (el !== template)
|
|
41
|
+
rows.set(el.getAttribute(ATTR_KEY), el);
|
|
42
|
+
}
|
|
43
|
+
return rows;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Land a projection in a widget's subtree.
|
|
47
|
+
*
|
|
48
|
+
* `rows` is the whole set, so it decides membership and order: every row is
|
|
49
|
+
* placed in the order given, and a row whose key did not arrive is gone.
|
|
50
|
+
* `patch` disturbs only what it names — a row it did not mention keeps its
|
|
51
|
+
* contents and its position.
|
|
52
|
+
*/
|
|
53
|
+
export function applyRows(scope, result, place = before) {
|
|
54
|
+
const template = templateIn(scope);
|
|
55
|
+
if (!template)
|
|
56
|
+
return;
|
|
57
|
+
const keyCell = template.getAttribute(ATTR_KEY);
|
|
58
|
+
const rows = showing(scope, template);
|
|
59
|
+
const upsert = (tuple) => {
|
|
60
|
+
const key = tuple[keyCell];
|
|
61
|
+
if (key === undefined) {
|
|
62
|
+
console.error(`lb-rows: a tuple for <${scope.localName}> has no '${keyCell}' cell`);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
let row = rows.get(key);
|
|
66
|
+
const fresh = row === undefined;
|
|
67
|
+
if (!row) {
|
|
68
|
+
row = template.content.firstElementChild.cloneNode(true);
|
|
69
|
+
row.setAttribute(ATTR_KEY, key);
|
|
70
|
+
rows.set(key, row);
|
|
71
|
+
}
|
|
72
|
+
// Fill before insertion. The attributes are already there when the row
|
|
73
|
+
// upgrades, which is the same thing that makes hydration and refresh one
|
|
74
|
+
// operation everywhere else.
|
|
75
|
+
applyTuple(row, tuple);
|
|
76
|
+
if (fresh || result.op === "rows")
|
|
77
|
+
place(row, tuple, template);
|
|
78
|
+
return key;
|
|
79
|
+
};
|
|
80
|
+
if (result.op === "rows") {
|
|
81
|
+
const arrived = new Set();
|
|
82
|
+
for (const tuple of result.rows) {
|
|
83
|
+
const key = upsert(tuple);
|
|
84
|
+
if (key !== null)
|
|
85
|
+
arrived.add(key);
|
|
86
|
+
}
|
|
87
|
+
for (const [key, row] of rows)
|
|
88
|
+
if (!arrived.has(key))
|
|
89
|
+
row.remove();
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
for (const tuple of result.rows ?? [])
|
|
93
|
+
upsert(tuple);
|
|
94
|
+
for (const key of result.drop ?? [])
|
|
95
|
+
rows.get(key)?.remove();
|
|
96
|
+
}
|
|
97
|
+
// How many rows are showing, counted from the DOM rather than from either
|
|
98
|
+
// branch above, so a set and a patch report the same fact the same way.
|
|
99
|
+
//
|
|
100
|
+
// It is stamped here because only this function knows the count: it is the
|
|
101
|
+
// one conditional a page cannot be sent, since the server answers with rows
|
|
102
|
+
// and says nothing about how many survived reconciliation. A page says what
|
|
103
|
+
// an empty list looks like in a stylesheet, and no list widget carries code
|
|
104
|
+
// for it. See docs/guide.md, "Conditional rendering".
|
|
105
|
+
scope.setAttribute(ATTR_ROW_COUNT, String(showing(scope, template).size));
|
|
106
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Express adapter — the standard hand-off from an Express router to a
|
|
3
|
+
* Hub built by createHub(). Every Hub application wires this same router,
|
|
4
|
+
* which is why it ships from here rather than being copied into each one.
|
|
5
|
+
*
|
|
6
|
+
* Express itself is a peer dependency: nothing outside this file imports it,
|
|
7
|
+
* so an application that hosts Hub some other way never pays for it.
|
|
8
|
+
*
|
|
9
|
+
* Two endpoints, and no third:
|
|
10
|
+
*
|
|
11
|
+
* GET /hub/data?page=<name> the page's whole query set
|
|
12
|
+
* POST /hub?page=<name> one operation, then its refresh set
|
|
13
|
+
*
|
|
14
|
+
* The page rides on the query string rather than in the body, which is what
|
|
15
|
+
* lets the operation set stay closed.
|
|
16
|
+
*/
|
|
17
|
+
import { type Request, type Router } from "express";
|
|
18
|
+
import type { Hub, HubContext } from "./lb-server";
|
|
19
|
+
/**
|
|
20
|
+
* Building the context is the application's job — it is where an
|
|
21
|
+
* authenticated database handle comes from — so it is injected rather than
|
|
22
|
+
* assumed. Whatever established identity has already run by the time this
|
|
23
|
+
* router is reached; where that happens is the application's ordering
|
|
24
|
+
* decision, not Hub's.
|
|
25
|
+
*/
|
|
26
|
+
export type ContextFor = (req: Request) => HubContext;
|
|
27
|
+
export declare function hubRoutes(hub: Hub, contextFor: ContextFor): Router;
|
|
28
|
+
//# sourceMappingURL=lb-express.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lb-express.d.ts","sourceRoot":"","sources":["../../server/lb-express.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAgB,EAAE,KAAK,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAG7D,OAAO,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEnD;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,UAAU,CAAC;AAEtD,wBAAgB,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,UAAU,GAAG,MAAM,CA8DlE"}
|