@loadbare/app 0.5.6 → 0.7.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 +3 -4
- package/dist/build/assemble.d.ts +1 -1
- package/dist/build/assemble.d.ts.map +1 -1
- package/dist/build/assemble.js +81 -7
- package/dist/build/assemble.js.map +1 -0
- package/dist/build/cli.d.ts +2 -2
- package/dist/build/cli.js +3 -2
- package/dist/build/cli.js.map +1 -0
- package/dist/build/elements.js +1 -0
- package/dist/build/elements.js.map +1 -0
- package/dist/build/expand.d.ts.map +1 -1
- package/dist/build/expand.js +20 -32
- package/dist/build/expand.js.map +1 -0
- package/dist/build/format.js +1 -0
- package/dist/build/format.js.map +1 -0
- package/dist/build/locations.d.ts +4 -4
- package/dist/build/locations.d.ts.map +1 -1
- package/dist/build/locations.js +16 -5
- package/dist/build/locations.js.map +1 -0
- package/dist/build/origins.d.ts +0 -13
- package/dist/build/origins.d.ts.map +1 -1
- package/dist/build/origins.js +33 -8
- package/dist/build/origins.js.map +1 -0
- package/dist/build/package-root.js +1 -0
- package/dist/build/package-root.js.map +1 -0
- package/dist/build/pages.d.ts +7 -3
- package/dist/build/pages.d.ts.map +1 -1
- package/dist/build/pages.js +14 -7
- package/dist/build/pages.js.map +1 -0
- package/dist/build/styles.js +1 -0
- package/dist/build/styles.js.map +1 -0
- package/dist/core/lb-constants.d.ts +15 -13
- package/dist/core/lb-constants.d.ts.map +1 -1
- package/dist/core/lb-constants.js +104 -53
- package/dist/core/lb-constants.js.map +1 -0
- package/dist/core/lb-types.d.ts +103 -62
- package/dist/core/lb-types.d.ts.map +1 -1
- package/dist/core/lb-types.js +12 -3
- package/dist/core/lb-types.js.map +1 -0
- package/dist/hub/lb-apply.d.ts +28 -4
- package/dist/hub/lb-apply.d.ts.map +1 -1
- package/dist/hub/lb-apply.js +227 -40
- package/dist/hub/lb-apply.js.map +1 -0
- package/dist/hub/lb-hub.browser.d.ts +1 -1
- package/dist/hub/lb-hub.browser.d.ts.map +1 -1
- package/dist/hub/lb-hub.browser.js +165 -110
- package/dist/hub/lb-hub.browser.js.map +1 -0
- package/dist/server/lb-express.d.ts +8 -5
- package/dist/server/lb-express.d.ts.map +1 -1
- package/dist/server/lb-express.js +46 -33
- package/dist/server/lb-express.js.map +1 -0
- package/dist/server/lb-server.d.ts +67 -45
- package/dist/server/lb-server.d.ts.map +1 -1
- package/dist/server/lb-server.js +56 -18
- package/dist/server/lb-server.js.map +1 -0
- package/docs/TECHREF-1.0.md +1107 -0
- package/docs/analysis-accidental-complexity.md +149 -0
- package/docs/reference/builder.md +4 -4
- package/docs/reference/chrome.md +10 -9
- package/docs/reference/custom-elements.md +54 -46
- package/docs/reference/data-binding.md +179 -97
- package/docs/reference/overview.md +1 -1
- package/docs/reference/page-files.md +64 -49
- package/docs/reference/server.md +25 -6
- package/docs/reference/widgets.md +22 -30
- package/docs/roadmap.md +68 -22
- package/docs/testing.md +47 -17
- package/docs/theory.md +116 -3
- package/docs/tutorials/010-pages-and-navigation.md +8 -8
- package/docs/tutorials/040-displaying-data.md +9 -9
- package/docs/tutorials/050-actions.md +5 -5
- package/docs/tutorials/060-custom-element-code.md +1 -1
- package/docs/tutorials/065-conditional-rendering.md +4 -4
- package/docs/tutorials/070-displaying-a-list.md +24 -47
- package/docs/tutorials/072-inserting-into-a-list.md +18 -15
- package/docs/tutorials/074-deleting-from-a-list.md +15 -17
- package/docs/tutorials/076-updating-a-list-item.md +20 -22
- package/docs/tutorials/080-widget-requests.md +22 -35
- package/docs/tutorials/090-using-widget-libraries.md +1 -1
- package/package.json +2 -3
- package/dist/hub/lb-rows.d.ts +0 -18
- package/dist/hub/lb-rows.d.ts.map +0 -1
- package/dist/hub/lb-rows.js +0 -106
- package/dist/tests/assemble.test.d.ts +0 -8
- package/dist/tests/assemble.test.d.ts.map +0 -1
- package/dist/tests/assemble.test.js +0 -58
- package/dist/tests/elements.test.d.ts +0 -8
- package/dist/tests/elements.test.d.ts.map +0 -1
- package/dist/tests/elements.test.js +0 -118
- package/dist/tests/expand.test.d.ts +0 -10
- package/dist/tests/expand.test.d.ts.map +0 -1
- package/dist/tests/expand.test.js +0 -250
- package/dist/tests/fixtures/elements/collision/imports.d.ts +0 -3
- package/dist/tests/fixtures/elements/collision/imports.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/collision/imports.js +0 -1
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.d.ts +0 -2
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.js +0 -1
- package/dist/tests/fixtures/elements/local/widgets/app-box.browser.d.ts +0 -2
- package/dist/tests/fixtures/elements/local/widgets/app-box.browser.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/local/widgets/app-box.browser.js +0 -1
- package/dist/tests/fixtures/elements/manifest/imports.d.ts +0 -3
- package/dist/tests/fixtures/elements/manifest/imports.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/manifest/imports.js +0 -1
- package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts +0 -3
- package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/manifest-bad-entry/imports.js +0 -1
- package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts +0 -5
- package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/manifest-not-array/imports.js +0 -1
- package/dist/tests/fixtures/elements/pkg/acme-widget.browser.d.ts +0 -2
- package/dist/tests/fixtures/elements/pkg/acme-widget.browser.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/pkg/acme-widget.browser.js +0 -1
- package/dist/tests/fixtures/elements/unmarked/widgets/app-box.d.ts +0 -6
- package/dist/tests/fixtures/elements/unmarked/widgets/app-box.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/unmarked/widgets/app-box.js +0 -1
- package/dist/tests/helpers/console.d.ts +0 -20
- package/dist/tests/helpers/console.d.ts.map +0 -1
- package/dist/tests/helpers/console.js +0 -28
- package/dist/tests/helpers/dom.d.ts +0 -18
- package/dist/tests/helpers/dom.d.ts.map +0 -1
- package/dist/tests/helpers/dom.js +0 -22
- package/dist/tests/lb-apply.test.d.ts +0 -8
- package/dist/tests/lb-apply.test.d.ts.map +0 -1
- package/dist/tests/lb-apply.test.js +0 -153
- package/dist/tests/lb-express.test.d.ts +0 -14
- package/dist/tests/lb-express.test.d.ts.map +0 -1
- package/dist/tests/lb-express.test.js +0 -238
- package/dist/tests/lb-rows.test.d.ts +0 -12
- package/dist/tests/lb-rows.test.d.ts.map +0 -1
- package/dist/tests/lb-rows.test.js +0 -336
- package/dist/tests/lb-server.test.d.ts +0 -9
- package/dist/tests/lb-server.test.d.ts.map +0 -1
- package/dist/tests/lb-server.test.js +0 -495
- package/dist/tests/origins.test.d.ts +0 -10
- package/dist/tests/origins.test.d.ts.map +0 -1
- package/dist/tests/origins.test.js +0 -369
- package/dist/tests/pages.test.d.ts +0 -6
- package/dist/tests/pages.test.d.ts.map +0 -1
- package/dist/tests/pages.test.js +0 -98
- package/dist/tests/styles.test.d.ts +0 -7
- package/dist/tests/styles.test.d.ts.map +0 -1
- package/dist/tests/styles.test.js +0 -76
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lb-hub.browser.d.ts","sourceRoot":"","sources":["../../hub/lb-hub.browser.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"lb-hub.browser.d.ts","sourceRoot":"","sources":["../../hub/lb-hub.browser.ts"],"names":[],"mappings":"AA8BA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -1,21 +1,100 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
|
-
import { ATTR_ACTION, ATTR_CELL,
|
|
3
|
-
import { applyData } from "./lb-apply";
|
|
4
|
-
export { applyData,
|
|
2
|
+
import { ACTION_CELL_CHANGE, ACTION_ROW_DELETE, ACTION_ROW_INSERT, ACTION_ROW_UPDATE, ATTR_ACTION, ATTR_CELL, ATTR_ERROR, ATTR_KEY_VALUE, ATTR_LIST, ATTR_NAV_LINK, ATTR_PAGE, ATTR_PENDING, ATTR_ROW, ATTR_UNKNOWN_PAGE, LB_ACTIONS, LB_RESERVED_PREFIX, HUB_TAG_NAME, NAV_CELL_LABEL, NAV_CELL_URI, NAV_ROW, LB_EVENT_NAME, LB_ENDPOINT, LB_REQUEST_TIMEOUT_MS, } from "../core/lb-constants.js";
|
|
3
|
+
import { applyData, isValueControl } from "./lb-apply.js";
|
|
4
|
+
export { applyData, applyRow } from "./lb-apply.js";
|
|
5
|
+
/** What each operation takes from scope, and what it cannot be sent without. */
|
|
6
|
+
const OPERATIONS = {
|
|
7
|
+
[ACTION_CELL_CHANGE]: {
|
|
8
|
+
fills: ["list", "key", "cell"],
|
|
9
|
+
requires: ["list", "key", "cell", "value"],
|
|
10
|
+
},
|
|
11
|
+
[ACTION_ROW_INSERT]: { fills: ["list"], requires: ["list", "values"] },
|
|
12
|
+
[ACTION_ROW_DELETE]: { fills: ["list", "key"], requires: ["list", "key"] },
|
|
13
|
+
[ACTION_ROW_UPDATE]: {
|
|
14
|
+
fills: ["list", "key"],
|
|
15
|
+
requires: ["list", "key", "values"],
|
|
16
|
+
},
|
|
17
|
+
};
|
|
5
18
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
19
|
+
* Complete a request from where its element sits — see docs/TECHREF-1.0.md,
|
|
20
|
+
* "Requests". Every request passes through here, whoever built it, so a
|
|
21
|
+
* widget states what it did and never where it is.
|
|
22
|
+
*
|
|
23
|
+
* The scope field is `list` or `row`, whichever attribute scoped the nearest
|
|
24
|
+
* ancestor, so a declared action fired inside a single-row scope says so and
|
|
25
|
+
* the wire never needs a noun for the addressable thing. A field the request
|
|
26
|
+
* already carries is the dispatcher's to decide and is left as it is.
|
|
27
|
+
*
|
|
28
|
+
* False when the request cannot be sent, having said why.
|
|
9
29
|
*/
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
30
|
+
function scope(request, el) {
|
|
31
|
+
if (!request?.action) {
|
|
32
|
+
console.warn(`lb-hub: event without an action, ignoring`, request);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const { action } = request;
|
|
36
|
+
const reserved = action.startsWith(LB_RESERVED_PREFIX);
|
|
37
|
+
// The reserved prefix is the whole of the discriminant, so a reserved
|
|
38
|
+
// name that is not one of the four operations is a typo rather than
|
|
39
|
+
// something to forward. Refused here, where the widget can see why: a
|
|
40
|
+
// server would only report that it declares no such action.
|
|
41
|
+
if (reserved && !LB_ACTIONS.includes(action)) {
|
|
42
|
+
console.error(`lb-hub: '${action}' begins with '${LB_RESERVED_PREFIX}' ` +
|
|
43
|
+
`but names no operation, ignoring`);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const fields = request;
|
|
47
|
+
// The element's own lb-list or lb-row is what it displays. Where it sits
|
|
48
|
+
// is its ancestors', the way a control belongs to the form around it.
|
|
49
|
+
const parent = el.parentElement;
|
|
50
|
+
const enclosing = parent?.closest(`[${ATTR_LIST}], [${ATTR_ROW}]`);
|
|
51
|
+
const row = parent?.closest(`[${ATTR_KEY_VALUE}]`);
|
|
52
|
+
const inScope = {
|
|
53
|
+
list: enclosing?.getAttribute(ATTR_LIST),
|
|
54
|
+
row: enclosing?.hasAttribute(ATTR_LIST)
|
|
55
|
+
? null
|
|
56
|
+
: enclosing?.getAttribute(ATTR_ROW),
|
|
57
|
+
// Only a row of that scope: a key from outside it names a row of some
|
|
58
|
+
// other list.
|
|
59
|
+
key: row && enclosing?.contains(row) ? row.getAttribute(ATTR_KEY_VALUE) : null,
|
|
60
|
+
cell: el.getAttribute(ATTR_CELL),
|
|
18
61
|
};
|
|
62
|
+
const fill = (name) => {
|
|
63
|
+
const value = inScope[name];
|
|
64
|
+
if (fields[name] === undefined && value !== null && value !== undefined) {
|
|
65
|
+
fields[name] = value;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const operation = OPERATIONS[action];
|
|
69
|
+
if (!operation) {
|
|
70
|
+
// A declared action names one scope or the other, never both.
|
|
71
|
+
if (fields.list === undefined && fields.row === undefined) {
|
|
72
|
+
fill("list");
|
|
73
|
+
fill("row");
|
|
74
|
+
}
|
|
75
|
+
fill("key");
|
|
76
|
+
fill("cell");
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
for (const name of operation.fills)
|
|
80
|
+
fill(name);
|
|
81
|
+
if (fields.values === undefined &&
|
|
82
|
+
el instanceof HTMLFormElement &&
|
|
83
|
+
operation.requires.includes("values")) {
|
|
84
|
+
fields.values = gatherValues(el);
|
|
85
|
+
}
|
|
86
|
+
const missing = operation.requires.filter((f) => fields[f] === undefined);
|
|
87
|
+
if (missing.length > 0) {
|
|
88
|
+
console.error(`lb-hub: '${action}' from <${el.localName}> has no ` +
|
|
89
|
+
`${missing.join(", ")} in scope, not sent`, el);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
/** Dispatch the bare action an element carries, for the hub to scope. */
|
|
95
|
+
function dispatchAction(el) {
|
|
96
|
+
const detail = { action: el.getAttribute(ATTR_ACTION) };
|
|
97
|
+
el.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
|
|
19
98
|
}
|
|
20
99
|
/**
|
|
21
100
|
* The value a cell is currently showing, for gathering rather than landing.
|
|
@@ -23,20 +102,19 @@ function binding(el) {
|
|
|
23
102
|
* A cell is always either a native form control itself (a bare
|
|
24
103
|
* `<input lb-cell>`) or a widget that wraps one (`<lb-input>`, `<lb-select>`)
|
|
25
104
|
* — that wrapping is structural, not incidental, which is what lets this be
|
|
26
|
-
* one rule instead of one per widget type.
|
|
105
|
+
* one rule instead of one per widget type. The controls are the ones a value
|
|
106
|
+
* lands on, so what is read back is what landed.
|
|
27
107
|
*/
|
|
28
108
|
function controlValue(el) {
|
|
29
|
-
const control = el
|
|
30
|
-
el instanceof HTMLSelectElement ||
|
|
31
|
-
el instanceof HTMLTextAreaElement
|
|
109
|
+
const control = isValueControl(el)
|
|
32
110
|
? el
|
|
33
|
-
: el.
|
|
111
|
+
: [...el.querySelectorAll("input, select, textarea")].find(isValueControl);
|
|
34
112
|
return control?.value;
|
|
35
113
|
}
|
|
36
114
|
/**
|
|
37
115
|
* 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
|
|
39
|
-
* whether a key comes with them.
|
|
116
|
+
* lb-row-insert and lb-row-update — both submit a batch of cells, and differ
|
|
117
|
+
* only in whether a key comes with them.
|
|
40
118
|
*/
|
|
41
119
|
function gatherValues(form) {
|
|
42
120
|
const values = {};
|
|
@@ -44,7 +122,8 @@ function gatherValues(form) {
|
|
|
44
122
|
const name = cell.getAttribute(ATTR_CELL);
|
|
45
123
|
const value = controlValue(cell);
|
|
46
124
|
if (value === undefined) {
|
|
47
|
-
console.warn(`lb-hub: lb-cell '${name}' has no
|
|
125
|
+
console.warn(`lb-hub: lb-cell '${name}' has no input, select or textarea to ` +
|
|
126
|
+
`read, ignoring it`, cell);
|
|
48
127
|
continue;
|
|
49
128
|
}
|
|
50
129
|
values[name] = value;
|
|
@@ -61,6 +140,29 @@ function pageNameFor(path) {
|
|
|
61
140
|
const name = path.replace(/^\/+|\/+$/g, "");
|
|
62
141
|
return name === "" ? "index" : name;
|
|
63
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* The `<template>` a page's markup ships in. The builder guarantees one per
|
|
145
|
+
* page name (build/locations.ts refuses two page files sharing a stub), so
|
|
146
|
+
* a second match here means the document was assembled by something else,
|
|
147
|
+
* and is worth saying out loud rather than resolving by document order.
|
|
148
|
+
*/
|
|
149
|
+
function pageHostFor(page) {
|
|
150
|
+
const hosts = [];
|
|
151
|
+
for (const el of document.querySelectorAll(`template[${ATTR_PAGE}]`)) {
|
|
152
|
+
if (el.getAttribute(ATTR_PAGE) === page) {
|
|
153
|
+
hosts.push(el);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (hosts.length === 0) {
|
|
157
|
+
console.error(`lb-hub: no page host for '${page}'`);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
if (hosts.length > 1) {
|
|
161
|
+
console.error(`lb-hub: ${hosts.length} page hosts carry ${ATTR_PAGE}="${page}", ` +
|
|
162
|
+
`using the first`);
|
|
163
|
+
}
|
|
164
|
+
return hosts[0];
|
|
165
|
+
}
|
|
64
166
|
/**
|
|
65
167
|
* Fetch one round trip against the client-side deadline. Throws — rather
|
|
66
168
|
* than swallowing — on a non-ok response, a network failure, or a timeout,
|
|
@@ -68,11 +170,18 @@ function pageNameFor(path) {
|
|
|
68
170
|
* only ever wants the freshest data, like navigate()) can tell the two
|
|
69
171
|
* apart from a genuinely empty result.
|
|
70
172
|
*/
|
|
71
|
-
async function fetchData(
|
|
173
|
+
async function fetchData(page, request) {
|
|
174
|
+
const url = `${LB_ENDPOINT}?page=${encodeURIComponent(page)}`;
|
|
72
175
|
const controller = new AbortController();
|
|
73
176
|
const timeout = setTimeout(() => controller.abort(), LB_REQUEST_TIMEOUT_MS);
|
|
74
177
|
try {
|
|
75
|
-
|
|
178
|
+
// An empty body is how the hub asks for the page's whole query set.
|
|
179
|
+
const res = await fetch(url, {
|
|
180
|
+
method: "POST",
|
|
181
|
+
headers: { "content-type": "application/json" },
|
|
182
|
+
body: JSON.stringify(request ?? {}),
|
|
183
|
+
signal: controller.signal,
|
|
184
|
+
});
|
|
76
185
|
if (!res.ok) {
|
|
77
186
|
throw new Error(`lb-hub: server responded ${res.status} for ${url}`);
|
|
78
187
|
}
|
|
@@ -89,31 +198,31 @@ async function fetchData(url, init) {
|
|
|
89
198
|
class LbHub extends HTMLElement {
|
|
90
199
|
main;
|
|
91
200
|
/**
|
|
92
|
-
* The page currently in <main>.
|
|
201
|
+
* The page currently in <main>. A page declares its own requests, so a request
|
|
93
202
|
* has to say which page it came from. The page rides on the URL rather
|
|
94
203
|
* than in the request, leaving the operation set closed.
|
|
95
204
|
*/
|
|
96
205
|
page = "";
|
|
97
206
|
connectedCallback() {
|
|
98
207
|
this.main = this.querySelector("main");
|
|
208
|
+
// Scoped in the capture phase, on the way down, so every ancestor that
|
|
209
|
+
// sees the request on its way back up sees it whole. A request that
|
|
210
|
+
// cannot be sent is stopped here, before anything sees it at all.
|
|
211
|
+
this.addEventListener(LB_EVENT_NAME, (e) => {
|
|
212
|
+
const origin = e.target;
|
|
213
|
+
const request = e.detail;
|
|
214
|
+
if (!scope(request, origin))
|
|
215
|
+
e.stopPropagation();
|
|
216
|
+
}, { capture: true });
|
|
99
217
|
this.addEventListener(LB_EVENT_NAME, (e) => {
|
|
100
218
|
const request = e.detail;
|
|
101
|
-
if (!request?.op) {
|
|
102
|
-
console.warn(`lb-hub: event without an op, ignoring`, request);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
219
|
// The element that dispatched the request, not e.currentTarget (the
|
|
106
220
|
// hub itself, since the event bubbles) — pending/error state belongs
|
|
107
221
|
// at the origin so a widget can watch its own attributes.
|
|
108
222
|
const origin = e.target;
|
|
109
223
|
origin?.removeAttribute(ATTR_ERROR);
|
|
110
224
|
origin?.setAttribute(ATTR_PENDING, "");
|
|
111
|
-
|
|
112
|
-
void fetchData(url, {
|
|
113
|
-
method: "POST",
|
|
114
|
-
headers: { "content-type": "application/json" },
|
|
115
|
-
body: JSON.stringify(request),
|
|
116
|
-
})
|
|
225
|
+
void fetchData(this.page, request)
|
|
117
226
|
.then((data) => applyData(this, data))
|
|
118
227
|
.catch((err) => {
|
|
119
228
|
console.error(`lb-hub: request failed`, err);
|
|
@@ -122,11 +231,11 @@ class LbHub extends HTMLElement {
|
|
|
122
231
|
.finally(() => origin?.removeAttribute(ATTR_PENDING));
|
|
123
232
|
});
|
|
124
233
|
/**
|
|
125
|
-
* A native element carrying an action. The hub
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* confirm. A native action button and a
|
|
129
|
-
* produce identical events.
|
|
234
|
+
* A native element carrying an action. The hub dispatches the bare action
|
|
235
|
+
* from that element as the ordinary bubbling event, rather than sending
|
|
236
|
+
* it directly, so it is scoped exactly as a widget's is and an ancestor
|
|
237
|
+
* widget can still stop it and confirm. A native action button and a
|
|
238
|
+
* hand-written widget therefore produce identical events.
|
|
130
239
|
*
|
|
131
240
|
* A widget carrying an action is left alone — it owns its own interaction
|
|
132
241
|
* and decides what counts as performing it, which for a <select> is a
|
|
@@ -137,73 +246,19 @@ class LbHub extends HTMLElement {
|
|
|
137
246
|
const el = e.target?.closest(`[${ATTR_ACTION}]`);
|
|
138
247
|
if (!el || el.localName.includes("-"))
|
|
139
248
|
return;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
op: "action",
|
|
143
|
-
name: el.getAttribute(ATTR_ACTION),
|
|
144
|
-
...binding(el),
|
|
145
|
-
};
|
|
146
|
-
el.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
|
|
147
|
-
});
|
|
148
|
-
// The CRUD counterpart to the lb-action delegation above: a native
|
|
149
|
-
// element needs no declared name to delete the row it is in, only its
|
|
150
|
-
// own query/key binding.
|
|
151
|
-
this.addEventListener("click", (e) => {
|
|
152
|
-
const el = e.target?.closest(`[${ATTR_DELETE}]`);
|
|
153
|
-
if (!el || el.localName.includes("-"))
|
|
154
|
-
return;
|
|
155
|
-
e.preventDefault();
|
|
156
|
-
const { query, key } = binding(el);
|
|
157
|
-
if (!query || !key) {
|
|
158
|
-
console.warn(`lb-hub: lb-delete with no query/key binding, ignoring`, el);
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
const detail = { op: "tuple-delete", query, key };
|
|
162
|
-
el.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
|
|
163
|
-
});
|
|
164
|
-
// The CRUD op that gathers rather than binds a single cell: every
|
|
165
|
-
// lb-cell inside the form becomes one entry of the values map. There is
|
|
166
|
-
// no key, because there is no row yet — only the query, found the same
|
|
167
|
-
// way binding() finds one for a click, says what it is inserting
|
|
168
|
-
// into.
|
|
169
|
-
this.addEventListener("submit", (e) => {
|
|
170
|
-
const form = e.target?.closest(`[${ATTR_INSERT}]`);
|
|
171
|
-
if (!form)
|
|
249
|
+
// A form performs its action on submit, whatever was clicked in it.
|
|
250
|
+
if (el instanceof HTMLFormElement)
|
|
172
251
|
return;
|
|
173
252
|
e.preventDefault();
|
|
174
|
-
|
|
175
|
-
if (!query) {
|
|
176
|
-
console.warn(`lb-hub: lb-insert with no lb-query, ignoring`, form);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
const detail = {
|
|
180
|
-
op: "tuple-insert",
|
|
181
|
-
query,
|
|
182
|
-
values: gatherValues(form),
|
|
183
|
-
};
|
|
184
|
-
form.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
|
|
253
|
+
dispatchAction(el);
|
|
185
254
|
});
|
|
186
|
-
// The
|
|
187
|
-
// with the key of the row it is a form for — a list's row template root,
|
|
188
|
-
// or a form inside one, has it from the same ancestor a delete button
|
|
189
|
-
// reads.
|
|
255
|
+
// The same delegation for the event a form owns.
|
|
190
256
|
this.addEventListener("submit", (e) => {
|
|
191
|
-
const form = e.target
|
|
192
|
-
if (!form)
|
|
257
|
+
const form = e.target;
|
|
258
|
+
if (!form || !form.hasAttribute(ATTR_ACTION))
|
|
193
259
|
return;
|
|
194
260
|
e.preventDefault();
|
|
195
|
-
|
|
196
|
-
if (!query || !key) {
|
|
197
|
-
console.warn(`lb-hub: lb-update with no query/key binding, ignoring`, form);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
const detail = {
|
|
201
|
-
op: "tuple-update",
|
|
202
|
-
query,
|
|
203
|
-
key,
|
|
204
|
-
values: gatherValues(form),
|
|
205
|
-
};
|
|
206
|
-
form.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
|
|
261
|
+
dispatchAction(form);
|
|
207
262
|
});
|
|
208
263
|
// Only anchors that opted in. Everything else is a real link.
|
|
209
264
|
this.addEventListener("click", (e) => {
|
|
@@ -226,9 +281,8 @@ class LbHub extends HTMLElement {
|
|
|
226
281
|
async navigate(path) {
|
|
227
282
|
const page = pageNameFor(path);
|
|
228
283
|
this.landNavigation(path, page);
|
|
229
|
-
const template =
|
|
230
|
-
if (!
|
|
231
|
-
console.error(`lb-hub: no page host for '${page}'`);
|
|
284
|
+
const template = pageHostFor(page);
|
|
285
|
+
if (!template) {
|
|
232
286
|
this.reportUnknownPage();
|
|
233
287
|
this.reveal();
|
|
234
288
|
return;
|
|
@@ -241,7 +295,7 @@ class LbHub extends HTMLElement {
|
|
|
241
295
|
this.reveal();
|
|
242
296
|
let data = {};
|
|
243
297
|
try {
|
|
244
|
-
data = await fetchData(
|
|
298
|
+
data = await fetchData(page);
|
|
245
299
|
}
|
|
246
300
|
catch (err) {
|
|
247
301
|
console.error(`lb-hub: failed to load data for '${page}'`, err);
|
|
@@ -258,9 +312,9 @@ class LbHub extends HTMLElement {
|
|
|
258
312
|
document.body.hidden = false;
|
|
259
313
|
}
|
|
260
314
|
/**
|
|
261
|
-
* The hub's own
|
|
315
|
+
* The hub's own row: where the page is, in the vocabulary a server
|
|
262
316
|
* answer arrives in, so a chrome displays it the way it displays anything
|
|
263
|
-
* — `lb-
|
|
317
|
+
* — `lb-row="lb-navigation"` on a subtree, `lb-cell` on what shows a
|
|
264
318
|
* value. Landed before the page host is looked up, so a miss has it too;
|
|
265
319
|
* the unknown-page dialog is one consumer among whatever others the
|
|
266
320
|
* chrome writes.
|
|
@@ -276,7 +330,7 @@ class LbHub extends HTMLElement {
|
|
|
276
330
|
* be warned about.
|
|
277
331
|
*/
|
|
278
332
|
landNavigation(path, page) {
|
|
279
|
-
if (!this.querySelector(`[${
|
|
333
|
+
if (!this.querySelector(`[${ATTR_ROW}="${NAV_ROW}"]`))
|
|
280
334
|
return;
|
|
281
335
|
let label = "";
|
|
282
336
|
for (const a of this.querySelectorAll(`a[${ATTR_NAV_LINK}]`)) {
|
|
@@ -286,7 +340,7 @@ class LbHub extends HTMLElement {
|
|
|
286
340
|
break;
|
|
287
341
|
}
|
|
288
342
|
applyData(this, {
|
|
289
|
-
[
|
|
343
|
+
[NAV_ROW]: { [NAV_CELL_LABEL]: label, [NAV_CELL_URI]: path },
|
|
290
344
|
});
|
|
291
345
|
}
|
|
292
346
|
/**
|
|
@@ -299,7 +353,7 @@ class LbHub extends HTMLElement {
|
|
|
299
353
|
* showModal() without checking the tag here.
|
|
300
354
|
*
|
|
301
355
|
* Only opens it. What the dialog says it gets from landNavigation(),
|
|
302
|
-
* already run, by naming the hub's
|
|
356
|
+
* already run, by naming the hub's row in lb-row like any subtree.
|
|
303
357
|
*
|
|
304
358
|
* Scoped to the hub, like every other lookup here, so that one rule holds
|
|
305
359
|
* without exception: what the hub acts on is inside the hub.
|
|
@@ -314,3 +368,4 @@ class LbHub extends HTMLElement {
|
|
|
314
368
|
}
|
|
315
369
|
}
|
|
316
370
|
customElements.define(HUB_TAG_NAME, LbHub);
|
|
371
|
+
//# sourceMappingURL=lb-hub.browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lb-hub.browser.js","sourceRoot":"","sources":["../../hub/lb-hub.browser.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport {\n ACTION_CELL_CHANGE,\n ACTION_ROW_DELETE,\n ACTION_ROW_INSERT,\n ACTION_ROW_UPDATE,\n ATTR_ACTION,\n ATTR_CELL,\n ATTR_ERROR,\n ATTR_KEY_VALUE,\n ATTR_LIST,\n ATTR_NAV_LINK,\n ATTR_PAGE,\n ATTR_PENDING,\n ATTR_ROW,\n ATTR_UNKNOWN_PAGE,\n LB_ACTIONS,\n LB_RESERVED_PREFIX,\n HUB_TAG_NAME,\n NAV_CELL_LABEL,\n NAV_CELL_URI,\n NAV_ROW,\n LB_EVENT_NAME,\n LB_ENDPOINT,\n LB_REQUEST_TIMEOUT_MS,\n} from \"../core/lb-constants.js\";\nimport type { HubData, HubRequest } from \"../core/lb-types.js\";\nimport { applyData, isValueControl } from \"./lb-apply.js\";\n\nexport { applyData, applyRow } from \"./lb-apply.js\";\n\n/** What each operation takes from scope, and what it cannot be sent without. */\nconst OPERATIONS: Record<\n string,\n { fills: readonly string[]; requires: readonly string[] }\n> = {\n [ACTION_CELL_CHANGE]: {\n fills: [\"list\", \"key\", \"cell\"],\n requires: [\"list\", \"key\", \"cell\", \"value\"],\n },\n [ACTION_ROW_INSERT]: { fills: [\"list\"], requires: [\"list\", \"values\"] },\n [ACTION_ROW_DELETE]: { fills: [\"list\", \"key\"], requires: [\"list\", \"key\"] },\n [ACTION_ROW_UPDATE]: {\n fills: [\"list\", \"key\"],\n requires: [\"list\", \"key\", \"values\"],\n },\n};\n\n/**\n * Complete a request from where its element sits — see docs/TECHREF-1.0.md,\n * \"Requests\". Every request passes through here, whoever built it, so a\n * widget states what it did and never where it is.\n *\n * The scope field is `list` or `row`, whichever attribute scoped the nearest\n * ancestor, so a declared action fired inside a single-row scope says so and\n * the wire never needs a noun for the addressable thing. A field the request\n * already carries is the dispatcher's to decide and is left as it is.\n *\n * False when the request cannot be sent, having said why.\n */\nfunction scope(request: HubRequest | undefined, el: Element): boolean {\n if (!request?.action) {\n console.warn(`lb-hub: event without an action, ignoring`, request);\n return false;\n }\n const { action } = request;\n const reserved = action.startsWith(LB_RESERVED_PREFIX);\n // The reserved prefix is the whole of the discriminant, so a reserved\n // name that is not one of the four operations is a typo rather than\n // something to forward. Refused here, where the widget can see why: a\n // server would only report that it declares no such action.\n if (reserved && !(LB_ACTIONS as readonly string[]).includes(action)) {\n console.error(\n `lb-hub: '${action}' begins with '${LB_RESERVED_PREFIX}' ` +\n `but names no operation, ignoring`,\n );\n return false;\n }\n\n const fields = request as unknown as Record<string, unknown>;\n // The element's own lb-list or lb-row is what it displays. Where it sits\n // is its ancestors', the way a control belongs to the form around it.\n const parent = el.parentElement;\n const enclosing = parent?.closest(`[${ATTR_LIST}], [${ATTR_ROW}]`);\n const row = parent?.closest(`[${ATTR_KEY_VALUE}]`);\n const inScope: Record<string, string | null | undefined> = {\n list: enclosing?.getAttribute(ATTR_LIST),\n row: enclosing?.hasAttribute(ATTR_LIST)\n ? null\n : enclosing?.getAttribute(ATTR_ROW),\n // Only a row of that scope: a key from outside it names a row of some\n // other list.\n key:\n row && enclosing?.contains(row) ? row.getAttribute(ATTR_KEY_VALUE) : null,\n cell: el.getAttribute(ATTR_CELL),\n };\n const fill = (name: string) => {\n const value = inScope[name];\n if (fields[name] === undefined && value !== null && value !== undefined) {\n fields[name] = value;\n }\n };\n\n const operation = OPERATIONS[action];\n if (!operation) {\n // A declared action names one scope or the other, never both.\n if (fields.list === undefined && fields.row === undefined) {\n fill(\"list\");\n fill(\"row\");\n }\n fill(\"key\");\n fill(\"cell\");\n return true;\n }\n\n for (const name of operation.fills) fill(name);\n if (\n fields.values === undefined &&\n el instanceof HTMLFormElement &&\n operation.requires.includes(\"values\")\n ) {\n fields.values = gatherValues(el);\n }\n const missing = operation.requires.filter((f) => fields[f] === undefined);\n if (missing.length > 0) {\n console.error(\n `lb-hub: '${action}' from <${el.localName}> has no ` +\n `${missing.join(\", \")} in scope, not sent`,\n el,\n );\n return false;\n }\n return true;\n}\n\n/** Dispatch the bare action an element carries, for the hub to scope. */\nfunction dispatchAction(el: Element): void {\n const detail = { action: el.getAttribute(ATTR_ACTION)! } as HubRequest;\n el.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));\n}\n\n/**\n * The value a cell is currently showing, for gathering rather than landing.\n *\n * A cell is always either a native form control itself (a bare\n * `<input lb-cell>`) or a widget that wraps one (`<lb-input>`, `<lb-select>`)\n * — that wrapping is structural, not incidental, which is what lets this be\n * one rule instead of one per widget type. The controls are the ones a value\n * lands on, so what is read back is what landed.\n */\nfunction controlValue(el: Element): string | undefined {\n const control = isValueControl(el)\n ? el\n : [...el.querySelectorAll(\"input, select, textarea\")].find(isValueControl);\n return control?.value;\n}\n\n/**\n * Every lb-cell inside a form, gathered into one values map. Shared by\n * lb-row-insert and lb-row-update — both submit a batch of cells, and differ\n * only in whether a key comes with them.\n */\nfunction gatherValues(form: Element): Record<string, string> {\n const values: Record<string, string> = {};\n for (const cell of form.querySelectorAll(`[${ATTR_CELL}]`)) {\n const name = cell.getAttribute(ATTR_CELL)!;\n const value = controlValue(cell);\n if (value === undefined) {\n console.warn(\n `lb-hub: lb-cell '${name}' has no input, select or textarea to ` +\n `read, ignoring it`,\n cell,\n );\n continue;\n }\n values[name] = value;\n }\n return values;\n}\n\n/**\n * A path names a page host, and nothing finer. The bare path resolves to\n * `index`, the same convention a static file server follows — an\n * application names its landing page that way rather than the hub singling\n * one out.\n */\nfunction pageNameFor(path: string): string {\n const name = path.replace(/^\\/+|\\/+$/g, \"\");\n return name === \"\" ? \"index\" : name;\n}\n\n/**\n * The `<template>` a page's markup ships in. The builder guarantees one per\n * page name (build/locations.ts refuses two page files sharing a stub), so\n * a second match here means the document was assembled by something else,\n * and is worth saying out loud rather than resolving by document order.\n */\nfunction pageHostFor(page: string): HTMLTemplateElement | null {\n const hosts: HTMLTemplateElement[] = [];\n for (const el of document.querySelectorAll(`template[${ATTR_PAGE}]`)) {\n if (el.getAttribute(ATTR_PAGE) === page) {\n hosts.push(el as HTMLTemplateElement);\n }\n }\n if (hosts.length === 0) {\n console.error(`lb-hub: no page host for '${page}'`);\n return null;\n }\n if (hosts.length > 1) {\n console.error(\n `lb-hub: ${hosts.length} page hosts carry ${ATTR_PAGE}=\"${page}\", ` +\n `using the first`,\n );\n }\n return hosts[0]!;\n}\n\n/**\n * Fetch one round trip against the client-side deadline. Throws — rather\n * than swallowing — on a non-ok response, a network failure, or a timeout,\n * so a caller that needs to know the request failed (as opposed to one that\n * only ever wants the freshest data, like navigate()) can tell the two\n * apart from a genuinely empty result.\n */\nasync function fetchData(page: string, request?: HubRequest): Promise<HubData> {\n const url = `${LB_ENDPOINT}?page=${encodeURIComponent(page)}`;\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), LB_REQUEST_TIMEOUT_MS);\n try {\n // An empty body is how the hub asks for the page's whole query set.\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(request ?? {}),\n signal: controller.signal,\n });\n if (!res.ok) {\n throw new Error(`lb-hub: server responded ${res.status} for ${url}`);\n }\n return (await res.json()) as HubData;\n } finally {\n clearTimeout(timeout);\n }\n}\n\n/**\n * The hub is a singleton outside <main>. It survives every page change,\n * so it needs no id to disambiguate it.\n */\nclass LbHub extends HTMLElement {\n private main!: HTMLElement;\n\n /**\n * The page currently in <main>. A page declares its own requests, so a request\n * has to say which page it came from. The page rides on the URL rather\n * than in the request, leaving the operation set closed.\n */\n private page = \"\";\n\n connectedCallback() {\n this.main = this.querySelector(\"main\")!;\n\n // Scoped in the capture phase, on the way down, so every ancestor that\n // sees the request on its way back up sees it whole. A request that\n // cannot be sent is stopped here, before anything sees it at all.\n this.addEventListener(\n LB_EVENT_NAME,\n (e: Event) => {\n const origin = e.target as Element;\n const request = (e as CustomEvent<HubRequest>).detail;\n if (!scope(request, origin)) e.stopPropagation();\n },\n { capture: true },\n );\n\n this.addEventListener(LB_EVENT_NAME, (e: Event) => {\n const request = (e as CustomEvent<HubRequest>).detail;\n // The element that dispatched the request, not e.currentTarget (the\n // hub itself, since the event bubbles) — pending/error state belongs\n // at the origin so a widget can watch its own attributes.\n const origin = e.target as Element | null;\n origin?.removeAttribute(ATTR_ERROR);\n origin?.setAttribute(ATTR_PENDING, \"\");\n void fetchData(this.page, request)\n .then((data) => applyData(this, data))\n .catch((err) => {\n console.error(`lb-hub: request failed`, err);\n origin?.setAttribute(ATTR_ERROR, \"\");\n })\n .finally(() => origin?.removeAttribute(ATTR_PENDING));\n });\n\n /**\n * A native element carrying an action. The hub dispatches the bare action\n * from that element as the ordinary bubbling event, rather than sending\n * it directly, so it is scoped exactly as a widget's is and an ancestor\n * widget can still stop it and confirm. A native action button and a\n * hand-written widget therefore produce identical events.\n *\n * A widget carrying an action is left alone — it owns its own interaction\n * and decides what counts as performing it, which for a <select> is a\n * change rather than a click. The test is the same lexical one applyData\n * uses: a hyphen in the tag name.\n */\n this.addEventListener(\"click\", (e: MouseEvent) => {\n const el = (e.target as Element | null)?.closest(`[${ATTR_ACTION}]`);\n if (!el || el.localName.includes(\"-\")) return;\n // A form performs its action on submit, whatever was clicked in it.\n if (el instanceof HTMLFormElement) return;\n e.preventDefault();\n dispatchAction(el);\n });\n\n // The same delegation for the event a form owns.\n this.addEventListener(\"submit\", (e: SubmitEvent) => {\n const form = e.target as Element | null;\n if (!form || !form.hasAttribute(ATTR_ACTION)) return;\n e.preventDefault();\n dispatchAction(form);\n });\n\n // Only anchors that opted in. Everything else is a real link.\n this.addEventListener(\"click\", (e: MouseEvent) => {\n const link = (e.target as Element | null)?.closest(`a[${ATTR_NAV_LINK}]`);\n if (!link) return;\n e.preventDefault();\n const path = new URL((link as HTMLAnchorElement).href).pathname;\n history.pushState(null, \"\", path);\n void this.navigate(path);\n });\n\n addEventListener(\"popstate\", () => void this.navigate(location.pathname));\n\n void this.navigate(location.pathname);\n }\n\n /**\n * Navigation is the one operation that replaces host DOM. The host is\n * already in the document, so selecting a page is getElementById() and\n * there is no fetching mechanism on the host channel.\n */\n private async navigate(path: string): Promise<void> {\n const page = pageNameFor(path);\n this.landNavigation(path, page);\n const template = pageHostFor(page);\n if (!template) {\n this.reportUnknownPage();\n this.reveal();\n return;\n }\n this.page = page;\n // Insert the page shell before fetching its data, not after: the\n // template is already in the document, so holding it back behind a\n // network round trip only leaves <main> empty for no reason.\n this.main.replaceChildren(template.content.cloneNode(true));\n this.reveal();\n let data: HubData = {};\n try {\n data = await fetchData(page);\n } catch (err) {\n console.error(`lb-hub: failed to load data for '${page}'`, err);\n }\n applyData(this, data);\n }\n\n /**\n * An app that ships `<body hidden>` (see docs/reference/chrome.md) is\n * asking to stay invisible until there is something coherent to show,\n * rather than flash the chrome before `<main>` has real content. This is\n * a no-op for an app that doesn't use that convention.\n */\n private reveal(): void {\n document.body.hidden = false;\n }\n\n /**\n * The hub's own row: where the page is, in the vocabulary a server\n * answer arrives in, so a chrome displays it the way it displays anything\n * — `lb-row=\"lb-navigation\"` on a subtree, `lb-cell` on what shows a\n * value. Landed before the page host is looked up, so a miss has it too;\n * the unknown-page dialog is one consumer among whatever others the\n * chrome writes.\n *\n * The label is the nav's: the text of the first lb-nav-link anchor whose\n * href names this page. Taken from the nav rather than from a click, so a\n * click, a reload and the back button all land the same label — and a\n * path no anchor names lands an empty one, explicitly, so a consumer is\n * never left showing the previous page's.\n *\n * Landed only where a subtree names it. applyData() warns about a query\n * with no scope, and a chrome that displays no navigation has nothing to\n * be warned about.\n */\n private landNavigation(path: string, page: string): void {\n if (!this.querySelector(`[${ATTR_ROW}=\"${NAV_ROW}\"]`)) return;\n let label = \"\";\n for (const a of this.querySelectorAll<HTMLAnchorElement>(\n `a[${ATTR_NAV_LINK}]`,\n )) {\n if (pageNameFor(new URL(a.href).pathname) !== page) continue;\n label = a.textContent?.trim() ?? \"\";\n break;\n }\n applyData(this, {\n [NAV_ROW]: { [NAV_CELL_LABEL]: label, [NAV_CELL_URI]: path },\n });\n }\n\n /**\n * A path that resolves to no page host is discovered client-side, after a\n * successful 200 — every route gets the same document, so there is no\n * HTTP 404 to report (see docs/tutorials/010-pages-and-navigation.md).\n * lb-unknown-page is optional: an application that declares none gets\n * nothing beyond navigate()'s console.error. The builder guarantees the\n * element it's on is a <dialog> (build/assemble.ts), so this can call\n * showModal() without checking the tag here.\n *\n * Only opens it. What the dialog says it gets from landNavigation(),\n * already run, by naming the hub's row in lb-row like any subtree.\n *\n * Scoped to the hub, like every other lookup here, so that one rule holds\n * without exception: what the hub acts on is inside the hub.\n */\n private reportUnknownPage(): void {\n const dialog = this.querySelector(`[${ATTR_UNKNOWN_PAGE}]`);\n if (!dialog) return;\n const modal = dialog as HTMLDialogElement;\n if (!modal.open) modal.showModal();\n }\n}\n\ncustomElements.define(HUB_TAG_NAME, LbHub);\n"]}
|
|
@@ -6,16 +6,19 @@
|
|
|
6
6
|
* Express itself is a peer dependency: nothing outside this file imports it,
|
|
7
7
|
* so an application that hosts Hub some other way never pays for it.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* One endpoint, and no second:
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* POST /lb?page=<name> an empty body asks for the page's whole query
|
|
12
|
+
* set; a body carrying an action runs that action
|
|
13
|
+
* and answers with its refresh set
|
|
13
14
|
*
|
|
14
15
|
* The page rides on the query string rather than in the body, which is what
|
|
15
|
-
* lets the operation set stay closed.
|
|
16
|
+
* lets the operation set stay closed. Every hub call is a POST, so an
|
|
17
|
+
* application's own GET routes never collide with this one and no page name
|
|
18
|
+
* is reserved.
|
|
16
19
|
*/
|
|
17
20
|
import { type Request, type Router } from "express";
|
|
18
|
-
import type { Hub, HubContext } from "./lb-server";
|
|
21
|
+
import type { Hub, HubContext } from "./lb-server.js";
|
|
19
22
|
/**
|
|
20
23
|
* Building the context is the application's job — it is where an
|
|
21
24
|
* authenticated database handle comes from — so it is injected rather than
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lb-express.d.ts","sourceRoot":"","sources":["../../server/lb-express.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"lb-express.d.ts","sourceRoot":"","sources":["../../server/lb-express.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAgB,EAAE,KAAK,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAS7D,OAAO,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEtD;;;;;;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,CAkFlE"}
|
|
@@ -6,72 +6,85 @@
|
|
|
6
6
|
* Express itself is a peer dependency: nothing outside this file imports it,
|
|
7
7
|
* so an application that hosts Hub some other way never pays for it.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* One endpoint, and no second:
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* POST /lb?page=<name> an empty body asks for the page's whole query
|
|
12
|
+
* set; a body carrying an action runs that action
|
|
13
|
+
* and answers with its refresh set
|
|
13
14
|
*
|
|
14
15
|
* The page rides on the query string rather than in the body, which is what
|
|
15
|
-
* lets the operation set stay closed.
|
|
16
|
+
* lets the operation set stay closed. Every hub call is a POST, so an
|
|
17
|
+
* application's own GET routes never collide with this one and no page name
|
|
18
|
+
* is reserved.
|
|
16
19
|
*/
|
|
17
20
|
import express from "express";
|
|
18
|
-
import {
|
|
21
|
+
import { ACTION_CELL_CHANGE, ACTION_ROW_DELETE, ACTION_ROW_INSERT, ACTION_ROW_UPDATE, LB_ENDPOINT, } from "../core/lb-constants.js";
|
|
22
|
+
import { isOperation } from "../core/lb-types.js";
|
|
19
23
|
export function hubRoutes(hub, contextFor) {
|
|
20
24
|
const router = express.Router();
|
|
21
25
|
// Mounted here rather than on the app so that Hub's need for a parsed
|
|
22
26
|
// body is stated where the routes that need it are.
|
|
23
27
|
router.use(express.json());
|
|
24
|
-
router.
|
|
28
|
+
router.post(LB_ENDPOINT, async (req, res) => {
|
|
25
29
|
const page = String(req.query.page ?? "");
|
|
26
|
-
res.json(await hub.dataForPage(page, contextFor(req)));
|
|
27
|
-
});
|
|
28
|
-
router.post(LB_REQUEST_ENDPOINT, async (req, res) => {
|
|
29
|
-
const page = String(req.query.page ?? "");
|
|
30
|
-
// The body is unknown until op says which shape it is, so it is read
|
|
31
|
-
// once through this loose view for the case that does not match one.
|
|
32
|
-
const body = req.body;
|
|
33
30
|
const request = req.body;
|
|
34
31
|
const ctx = contextFor(req);
|
|
32
|
+
// No body at all is the page load: the browser is asking for this
|
|
33
|
+
// page's whole query set, which is every request it makes that names no
|
|
34
|
+
// action. A body that carries something but not an action is malformed.
|
|
35
|
+
if (request === undefined || Object.keys(request).length === 0) {
|
|
36
|
+
res.json(await hub.dataForPage(page, ctx));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (!request.action) {
|
|
40
|
+
console.warn(`loadbare: request without an action`);
|
|
41
|
+
res.status(400).json({});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
35
44
|
// A thrown error is how application code (a query, an action, a CRUD
|
|
36
45
|
// op) says "this request is invalid" or hits an unexpected failure —
|
|
37
46
|
// it resolves to a real response rather than an unhandled rejection, so
|
|
38
47
|
// the browser side has something to catch.
|
|
39
48
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
// The reserved prefix is the whole of the discriminant: a name so
|
|
50
|
+
// prefixed is one of the four operations, and anything else is a name
|
|
51
|
+
// the page declared. Nothing else tells them apart, here or anywhere.
|
|
52
|
+
if (!isOperation(request)) {
|
|
53
|
+
const { action, list, row, key, cell, value } = request;
|
|
54
|
+
res.json(await hub.runAction(page, action, { list, row, key, cell, value }, ctx));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
switch (request.action) {
|
|
58
|
+
case ACTION_CELL_CHANGE: {
|
|
59
|
+
const { list, key, cell, value } = request;
|
|
60
|
+
res.json(await hub.runCellChange(page, { list, key, cell, value }, ctx));
|
|
49
61
|
return;
|
|
50
62
|
}
|
|
51
|
-
case
|
|
52
|
-
const {
|
|
53
|
-
res.json(await hub.
|
|
63
|
+
case ACTION_ROW_DELETE: {
|
|
64
|
+
const { list, key } = request;
|
|
65
|
+
res.json(await hub.runRowDelete(page, { list, key }, ctx));
|
|
54
66
|
return;
|
|
55
67
|
}
|
|
56
|
-
case
|
|
57
|
-
const {
|
|
58
|
-
res.json(await hub.
|
|
68
|
+
case ACTION_ROW_INSERT: {
|
|
69
|
+
const { list, values } = request;
|
|
70
|
+
res.json(await hub.runRowInsert(page, { list, values }, ctx));
|
|
59
71
|
return;
|
|
60
72
|
}
|
|
61
|
-
case
|
|
62
|
-
const {
|
|
63
|
-
res.json(await hub.
|
|
73
|
+
case ACTION_ROW_UPDATE: {
|
|
74
|
+
const { list, key, values } = request;
|
|
75
|
+
res.json(await hub.runRowUpdate(page, { list, key, values }, ctx));
|
|
64
76
|
return;
|
|
65
77
|
}
|
|
66
78
|
default:
|
|
67
|
-
console.warn(`loadbare: operation '${
|
|
79
|
+
console.warn(`loadbare: operation '${request.action}' not implemented`);
|
|
68
80
|
res.status(400).json({});
|
|
69
81
|
}
|
|
70
82
|
}
|
|
71
83
|
catch (err) {
|
|
72
|
-
console.error(`loadbare:
|
|
84
|
+
console.error(`loadbare: '${request.action}' threw`, err);
|
|
73
85
|
res.status(500).json({});
|
|
74
86
|
}
|
|
75
87
|
});
|
|
76
88
|
return router;
|
|
77
89
|
}
|
|
90
|
+
//# sourceMappingURL=lb-express.js.map
|