@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
|
@@ -9,6 +9,7 @@ import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { resolveLocations } from "../build/locations";
|
|
12
|
+
import { cssFrom, scanAll } from "../build/origins";
|
|
12
13
|
import { stylesSource } from "../build/styles";
|
|
13
14
|
const CHROME = `<!doctype html><body><lb-hub><main></main></lb-hub></body>`;
|
|
14
15
|
async function tree(files) {
|
|
@@ -23,12 +24,13 @@ async function tree(files) {
|
|
|
23
24
|
describe("discovering CSS", () => {
|
|
24
25
|
it("finds a .css file anywhere under src", async () => {
|
|
25
26
|
const src = await tree({
|
|
26
|
-
"chrome.
|
|
27
|
+
"chrome.html": CHROME,
|
|
27
28
|
"widgets/app-box.css": ".app-box {}",
|
|
28
29
|
"styles/00-reset.css": "* { margin: 0; }",
|
|
29
30
|
});
|
|
30
31
|
try {
|
|
31
|
-
const
|
|
32
|
+
const locations = await resolveLocations({ src, out: `${src}/dist` });
|
|
33
|
+
const cssFiles = cssFrom(await scanAll(locations, src, locations.importsFile));
|
|
32
34
|
assert.equal(cssFiles.length, 2);
|
|
33
35
|
}
|
|
34
36
|
finally {
|
|
@@ -37,12 +39,13 @@ describe("discovering CSS", () => {
|
|
|
37
39
|
});
|
|
38
40
|
it("orders by filename, not by directory, so a global sorts first by naming itself 00-", async () => {
|
|
39
41
|
const src = await tree({
|
|
40
|
-
"chrome.
|
|
42
|
+
"chrome.html": CHROME,
|
|
41
43
|
"widgets/app-box.css": ".app-box {}",
|
|
42
44
|
"00-reset.css": "* { margin: 0; }",
|
|
43
45
|
});
|
|
44
46
|
try {
|
|
45
|
-
const
|
|
47
|
+
const locations = await resolveLocations({ src, out: `${src}/dist` });
|
|
48
|
+
const cssFiles = cssFrom(await scanAll(locations, src, locations.importsFile));
|
|
46
49
|
assert.deepEqual(cssFiles.map((f) => path.basename(f)), ["00-reset.css", "app-box.css"]);
|
|
47
50
|
}
|
|
48
51
|
finally {
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# The Builder
|
|
2
|
+
|
|
3
|
+
The application runs `loadbare-app-build`. The builder reads the application's
|
|
4
|
+
source tree and writes the files the server serves.
|
|
5
|
+
|
|
6
|
+
## Running the builder
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
loadbare-app-build [--src src] [--out dist] [--watch] [--minify]
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
| Option | Effect |
|
|
13
|
+
|------------|------------------------------------------------------|
|
|
14
|
+
| `--src` | The tree to scan, default `src` |
|
|
15
|
+
| `--out` | Where output lands, default `dist` |
|
|
16
|
+
| `--watch` | Build once, then again on every change under `--src` |
|
|
17
|
+
| `--minify` | Minify `client.js` and `app.css` |
|
|
18
|
+
|
|
19
|
+
Run it from the application's own `package.json`:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"scripts": {
|
|
24
|
+
"dev": "loadbare-app-build --watch & tsx server.ts"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`--watch` watches every `.html`, `.ts`, and `.css` file under `--src`, and
|
|
30
|
+
reports a failed build to the console without stopping. It runs no dev server
|
|
31
|
+
and reloads no browser.
|
|
32
|
+
|
|
33
|
+
`--minify` leaves `app.html` alone.
|
|
34
|
+
|
|
35
|
+
## What the builder reads
|
|
36
|
+
|
|
37
|
+
The builder classifies by name, not location.
|
|
38
|
+
|
|
39
|
+
| Name | Role |
|
|
40
|
+
|----------------------------|---------------------------------------------------|
|
|
41
|
+
| `chrome.html` | The chrome — see [`chrome.html`](./chrome.md) |
|
|
42
|
+
| `*.page.html` | A page |
|
|
43
|
+
| `*.hooks.ts` | A page's hooks, matched by base name |
|
|
44
|
+
| `*.queries.ts` | A page's queries, matched by base name |
|
|
45
|
+
| `imports.ts` | The packages this app takes widgets from |
|
|
46
|
+
| `*.css` | A stylesheet — see [CSS](./css.md) |
|
|
47
|
+
| Any other `.html` | A widget definition, named for the tag it defines |
|
|
48
|
+
| Any other tag-shaped `.ts` | A widget script, named for the tag it registers |
|
|
49
|
+
|
|
50
|
+
Give the application exactly one `chrome.html` and at most one `imports.ts`.
|
|
51
|
+
Give every `.hooks.ts` and `.queries.ts` a `.page.html` of the same base name.
|
|
52
|
+
|
|
53
|
+
## Widgets from packages
|
|
54
|
+
|
|
55
|
+
List a package in `imports.ts` to take widgets from it:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
// src/imports.ts
|
|
59
|
+
export default ["@acme/widgets"];
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The builder scans one directory of that package the same way it scans `--src`,
|
|
63
|
+
finding a widget by its filename. Nothing names a tag: `acme-widget.html` and
|
|
64
|
+
`acme-widget.js` there define and register `<acme-widget>`.
|
|
65
|
+
|
|
66
|
+
Which directory is the package's own to declare, in its `package.json`:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"loadbare": { "widgets": "./dist" }
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
A package that declares nothing has its whole installed directory scanned.
|
|
75
|
+
That is what a package of hand-written widgets wants and it needs no field at
|
|
76
|
+
all; a package that compiles wants the field, because its source and its
|
|
77
|
+
compiled output both carry `acme-widget`, and two files claiming one tag in one
|
|
78
|
+
origin is an error. Declaring the directory that ships settles which one the
|
|
79
|
+
builder means, and leaves the rest of the package — the README, the docs, the
|
|
80
|
+
tests — out of the question entirely.
|
|
81
|
+
|
|
82
|
+
The path must be inside the package and must exist in it once installed;
|
|
83
|
+
either failure names the package and stops the build.
|
|
84
|
+
|
|
85
|
+
An application author writes nothing for this and installs the package as they
|
|
86
|
+
would any other. The declaration is the package author's, made once.
|
|
87
|
+
|
|
88
|
+
## Where the builder looks
|
|
89
|
+
|
|
90
|
+
Definitions, scripts and stylesheets all come from the same ordered origins:
|
|
91
|
+
|
|
92
|
+
| Order | Origin |
|
|
93
|
+
|-------|----------------------------------------------------|
|
|
94
|
+
| 1 | `@loadbare/app` itself, which supplies `lb-hub` |
|
|
95
|
+
| 2 | Each package in `imports.ts`, in the order listed |
|
|
96
|
+
| 3 | The application's own `--src` tree |
|
|
97
|
+
|
|
98
|
+
The first origin holds one tag. Every application has a hub whether or not it
|
|
99
|
+
says so, so the builder supplies that one and nothing else; a widget comes
|
|
100
|
+
from a package the application lists, including
|
|
101
|
+
[`@loadbare/widgets`](./widgets.md).
|
|
102
|
+
|
|
103
|
+
Two files claiming one tag within a single origin is an error, naming both.
|
|
104
|
+
Across origins the later one wins, so the application overrides a package,
|
|
105
|
+
and a package overrides `lb-hub`. Stylesheets follow the same order — see
|
|
106
|
+
[CSS](./css.md).
|
|
107
|
+
|
|
108
|
+
A tag with neither a script nor a definition in any origin is an error.
|
|
109
|
+
|
|
110
|
+
## What the builder writes
|
|
111
|
+
|
|
112
|
+
| File | Contents |
|
|
113
|
+
|-------------|-----------------------------------------------------|
|
|
114
|
+
| `app.html` | The chrome, with every page inside a `<template>` |
|
|
115
|
+
| `client.js` | Every widget class the document uses, in one bundle |
|
|
116
|
+
| `app.css` | Every stylesheet, concatenated |
|
|
117
|
+
| `pages.ts` | The `hub` the server passes to `hubRoutes` |
|
|
118
|
+
|
|
119
|
+
The builder expands the chrome and every page against the available widget
|
|
120
|
+
definitions — see [Custom Elements](./custom-elements.md#html) for the
|
|
121
|
+
substitution rules — wraps each expanded page in
|
|
122
|
+
`<template id="page-<name>">`, and splices them into the chrome's `<body>`. It formats the result with
|
|
123
|
+
Prettier when the application has it installed.
|
|
124
|
+
|
|
125
|
+
The builder writes `app.css` only when it finds a stylesheet, and `pages.ts`
|
|
126
|
+
only when some page has a `.hooks.ts` or a `.queries.ts` file. Import `hub`
|
|
127
|
+
from `pages.ts` — see [The Express Server](./server.md) for the rest of the
|
|
128
|
+
wiring.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# chrome.html
|
|
2
|
+
|
|
3
|
+
The chrome is the application's one HTML document. Banner, navigation,
|
|
4
|
+
footer, dialogs — everything that is not a page lives in the chrome, written
|
|
5
|
+
once.
|
|
6
|
+
|
|
7
|
+
The builder finds the chrome by name: exactly one file called `chrome.html`,
|
|
8
|
+
anywhere in the `src/` tree. Any directory will do.
|
|
9
|
+
|
|
10
|
+
## A complete chrome
|
|
11
|
+
|
|
12
|
+
Here is a minimal but fully complaint chrome for a typical app:
|
|
13
|
+
|
|
14
|
+
```html
|
|
15
|
+
<!-- src/chrome.html -->
|
|
16
|
+
<!doctype html>
|
|
17
|
+
<html lang="en">
|
|
18
|
+
<head>
|
|
19
|
+
<meta charset="utf-8" />
|
|
20
|
+
<title>Membership Roster</title>
|
|
21
|
+
<script src="/client.js" defer></script>
|
|
22
|
+
<link rel="stylesheet" href="/app.css" />
|
|
23
|
+
</head>
|
|
24
|
+
<body>
|
|
25
|
+
<lb-hub>
|
|
26
|
+
<header><h1>Membership Roster</h1></header>
|
|
27
|
+
<nav>
|
|
28
|
+
<a href="/" lb-nav-link>Home</a>
|
|
29
|
+
<a href="/members" lb-nav-link>Members</a>
|
|
30
|
+
<a href="https://example.org/">Our website</a>
|
|
31
|
+
</nav>
|
|
32
|
+
<main></main>
|
|
33
|
+
<dialog lb-unknown-page>
|
|
34
|
+
The page <span lb-cell="page"></span> is not in this app.
|
|
35
|
+
</dialog>
|
|
36
|
+
</lb-hub>
|
|
37
|
+
</body>
|
|
38
|
+
</html>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The chrome is plain HTML; nothing in it is generated or templated.
|
|
42
|
+
|
|
43
|
+
## The parts of a chrome
|
|
44
|
+
|
|
45
|
+
| Required | Description |
|
|
46
|
+
|-------------------------------------|-------------------------------------------|
|
|
47
|
+
| Exactly one `chrome.html` in `src/` | The one chrome file, found by name |
|
|
48
|
+
| A complete HTML document | Doctype, `<html>`, `<head>`, `<body>` |
|
|
49
|
+
| `<lb-hub>` inside `<body>` | The application's live element |
|
|
50
|
+
| An empty `<main>` inside `<lb-hub>` | Holds the current page |
|
|
51
|
+
| `<script src="/client.js" defer>` | Defines `<lb-hub>` and every other widget |
|
|
52
|
+
|
|
53
|
+
Everything else is optional:
|
|
54
|
+
|
|
55
|
+
| Optional | Description |
|
|
56
|
+
|-------------------------------------------|---------------------------------------------|
|
|
57
|
+
| `<link rel="stylesheet" href="/app.css">` | The bundled stylesheet |
|
|
58
|
+
| `<a lb-nav-link>` | Navigation between pages |
|
|
59
|
+
| `<dialog lb-unknown-page>` | A message when a URL matches no page |
|
|
60
|
+
| Custom elements | The chrome, decomposed into widget files |
|
|
61
|
+
| Any other HTML | Header, footer, skip links, meta tags, etc. |
|
|
62
|
+
|
|
63
|
+
## Rules for writing chrome
|
|
64
|
+
|
|
65
|
+
Anything the user interacts with must be inside `<lb-hub>`. The hub normally
|
|
66
|
+
sits directly inside `<body>`, with banner, nav, footer and `<main>` inside
|
|
67
|
+
it, so Loadbare can act on all of them.
|
|
68
|
+
|
|
69
|
+
A navigation anchor's `href` is a path, and the path names a page:
|
|
70
|
+
`/members` shows `members.page.html`. A bare `/` resolves to `index`, so the
|
|
71
|
+
landing page is the one named `index.page.html`. An anchor without
|
|
72
|
+
`lb-nav-link` is left alone and behaves like any other link.
|
|
73
|
+
|
|
74
|
+
The `lb-unknown-page` attribute, if used, must appear on a `<dialog>`.
|
|
75
|
+
Inside it, `lb-cell="page"` shows the name of the page that was asked for.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# CSS
|
|
2
|
+
|
|
3
|
+
The builder concatenates every `.css` file it finds into one stylesheet,
|
|
4
|
+
`<out>/app.css`. Nothing is scoped, renamed, or removed.
|
|
5
|
+
|
|
6
|
+
Write ordinary CSS against ordinary markup. Loadbare uses light DOM, so
|
|
7
|
+
every selector reaches every element, in the chrome and in a widget alike.
|
|
8
|
+
|
|
9
|
+
Put stylesheets anywhere under `src/`. The builder finds them by extension
|
|
10
|
+
and pairs them with nothing — a `.css` file is not a widget's, a page's, or
|
|
11
|
+
the chrome's.
|
|
12
|
+
|
|
13
|
+
## Order
|
|
14
|
+
|
|
15
|
+
The bundle is assembled in partitions, one per origin:
|
|
16
|
+
|
|
17
|
+
| Partition | Holds |
|
|
18
|
+
|----------------------------------|-------------------------------------------|
|
|
19
|
+
| Each package in `imports.ts` | Every `.css` file inside that package |
|
|
20
|
+
| The application's own `src` tree | Every `.css` file under `--src` |
|
|
21
|
+
|
|
22
|
+
Package partitions come first, in the order `imports.ts` first mentions each
|
|
23
|
+
package. The application's own partition is always last, so its rules have
|
|
24
|
+
the final say over any imported package's.
|
|
25
|
+
|
|
26
|
+
A package's partition excludes that package's own nested `node_modules`.
|
|
27
|
+
|
|
28
|
+
Within a partition, files sort by filename. The directory does not affect
|
|
29
|
+
the order, and the full path only breaks a tie. Name a global stylesheet
|
|
30
|
+
`00-reset.css`, or similar, to sort it to the front of its partition.
|
|
31
|
+
|
|
32
|
+
## What ships
|
|
33
|
+
|
|
34
|
+
The builder writes `app.css` only when it finds at least one stylesheet.
|
|
35
|
+
Serve it at `/app.css` and link it from the chrome:
|
|
36
|
+
|
|
37
|
+
```html
|
|
38
|
+
<link rel="stylesheet" href="/app.css" />
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
See [`chrome.html`](./chrome.md) for the link, [The Express
|
|
42
|
+
server](./server.md) for the route, and
|
|
43
|
+
[`loadbare-app-build`](./builder.md#running-the-builder) for `--minify`.
|
|
44
|
+
</content>
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# Custom Elements
|
|
2
|
+
|
|
3
|
+
A widget is a custom element, written as a set of files sharing one tag
|
|
4
|
+
name. It serves two purposes, and an application uses it for either or for
|
|
5
|
+
both at once.
|
|
6
|
+
|
|
7
|
+
| File | Holds |
|
|
8
|
+
|------------------|----------------------------------|
|
|
9
|
+
| `<tag-name>.html` | The markup the tag expands into |
|
|
10
|
+
| `<tag-name>.ts` | The class the tag registers |
|
|
11
|
+
|
|
12
|
+
Write one of the two, or both, but write at least one. A tag with neither is
|
|
13
|
+
a build error.
|
|
14
|
+
|
|
15
|
+
Custom elements are the only mechanism Loadbare offers for either purpose.
|
|
16
|
+
An application decomposes its HTML by defining a tag, and delivers behavior
|
|
17
|
+
to the browser by registering one; the builder recognizes no other way to do
|
|
18
|
+
either.
|
|
19
|
+
|
|
20
|
+
Put the files anywhere under `src/`. The builder finds them by name, the
|
|
21
|
+
same way it finds pages — see [The Builder](./builder.md).
|
|
22
|
+
|
|
23
|
+
Loadbare uses light DOM throughout. A widget's markup is ordinary markup in
|
|
24
|
+
the document, visible in view-source, reachable by `closest()` and by the
|
|
25
|
+
application's stylesheets.
|
|
26
|
+
|
|
27
|
+
## HTML
|
|
28
|
+
|
|
29
|
+
An `.html` file named for a tag defines that tag. When the builder finds the
|
|
30
|
+
tag in the chrome, in a page, or in another definition, it inserts the
|
|
31
|
+
definition's content as children of the tag. We call this process
|
|
32
|
+
'HTML expansion'.
|
|
33
|
+
|
|
34
|
+
A package listed in `imports.ts` loads first and an application's own
|
|
35
|
+
definitions load after, so a tag defined in both resolves to the
|
|
36
|
+
application's.
|
|
37
|
+
|
|
38
|
+
### Parameters
|
|
39
|
+
|
|
40
|
+
A definition takes parameters. Three namespaces share the attributes of a
|
|
41
|
+
widget tag, and the prefix says which namespace an attribute is in:
|
|
42
|
+
|
|
43
|
+
| Prefix | Belongs to | Read |
|
|
44
|
+
|-------------|------------|-----------------------------|
|
|
45
|
+
| `exp-` | Expansion | By the builder, at build time |
|
|
46
|
+
| `lb-` | The hub | By Loadbare, in the browser |
|
|
47
|
+
| No prefix | HTML | By the browser, as HTML says |
|
|
48
|
+
|
|
49
|
+
Pass a parameter by writing `exp-<name>` on the tag. Read it in the
|
|
50
|
+
definition by writing `{{<name>}}`. The prefix marks the attribute as
|
|
51
|
+
expansion's input and is not part of the parameter's name, so `exp-label`
|
|
52
|
+
supplies `{{label}}`:
|
|
53
|
+
|
|
54
|
+
```html
|
|
55
|
+
<!-- definition: src/note-field.html -->
|
|
56
|
+
<label>{{label}} <input readonly="{{readonly}}" /></label>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```html
|
|
60
|
+
<!-- authored -->
|
|
61
|
+
<note-field lb-cell="name" exp-label="Name"></note-field>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Write `class`, `title`, or any other unprefixed attribute on a widget
|
|
65
|
+
freely. They are HTML's, and never collide with a definition's parameters.
|
|
66
|
+
|
|
67
|
+
Write a placeholder as an entire attribute value or an entire text node.
|
|
68
|
+
Nothing inside one is evaluated, and expansion has no data to branch on —
|
|
69
|
+
see [Conditional rendering](./data-binding.md#conditional-rendering) for
|
|
70
|
+
what to write instead of a branch.
|
|
71
|
+
|
|
72
|
+
Name a parameter in lowercase. HTML lowercases attribute names before
|
|
73
|
+
expansion sees them, so `exp-inputClass` arrives as `exp-inputclass` and
|
|
74
|
+
could never fill `{{inputClass}}`. Write `{{input-class}}` instead; a
|
|
75
|
+
definition declaring such a name is rejected when it loads.
|
|
76
|
+
|
|
77
|
+
Leave a parameter unsupplied to drop what reads it. An attribute whose
|
|
78
|
+
value is a placeholder is removed rather than shipped empty, which is how
|
|
79
|
+
`readonly="{{readonly}}"` works as an ordinary boolean attribute. A
|
|
80
|
+
placeholder in a text node resolves to nothing, and the whitespace around
|
|
81
|
+
it survives.
|
|
82
|
+
|
|
83
|
+
Every attribute stays on the tag after expansion, `exp-` ones included.
|
|
84
|
+
|
|
85
|
+
Parameter values reach a definition through the DOM rather than through
|
|
86
|
+
string substitution, so a value is never reparsed as markup and needs no
|
|
87
|
+
escaping.
|
|
88
|
+
|
|
89
|
+
### The slot
|
|
90
|
+
|
|
91
|
+
`lb-slot` marks the one element in a definition whose children receive
|
|
92
|
+
whatever the author wrote inside the tag:
|
|
93
|
+
|
|
94
|
+
```html
|
|
95
|
+
<!-- definition: src/note-card.html -->
|
|
96
|
+
<article class="card"><div lb-slot></div></article>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```html
|
|
100
|
+
<!-- authored -->
|
|
101
|
+
<note-card>
|
|
102
|
+
<h3>Meeting notes</h3>
|
|
103
|
+
<p>Bring the roster.</p>
|
|
104
|
+
</note-card>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```html
|
|
108
|
+
<!-- ships -->
|
|
109
|
+
<note-card>
|
|
110
|
+
<article class="card">
|
|
111
|
+
<div>
|
|
112
|
+
<h3>Meeting notes</h3>
|
|
113
|
+
<p>Bring the roster.</p>
|
|
114
|
+
</div>
|
|
115
|
+
</article>
|
|
116
|
+
</note-card>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The tag stays and the definition becomes its children. `lb-slot` itself is
|
|
120
|
+
gone from what ships, having done its job at build time.
|
|
121
|
+
|
|
122
|
+
Give a definition at most one `lb-slot`. A definition with a slot and
|
|
123
|
+
nothing written inside it leaves the slot empty.
|
|
124
|
+
|
|
125
|
+
`lb-slot` is an attribute rather than an element because HTML's content
|
|
126
|
+
model discards foreign elements inside `<select>` and `<table>`. A slot
|
|
127
|
+
marker has to survive on an element the surrounding tag already permits.
|
|
128
|
+
|
|
129
|
+
### Destinations
|
|
130
|
+
|
|
131
|
+
`lb-template` names a destination in a definition. On an authored
|
|
132
|
+
`<template lb-template="name">`, it names the destination that template
|
|
133
|
+
fills:
|
|
134
|
+
|
|
135
|
+
```html
|
|
136
|
+
<!-- definition: src/ledger-table.html -->
|
|
137
|
+
<table>
|
|
138
|
+
<caption>{{caption}}</caption>
|
|
139
|
+
<thead lb-template="head"></thead>
|
|
140
|
+
<tbody lb-slot></tbody>
|
|
141
|
+
</table>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```html
|
|
145
|
+
<!-- authored -->
|
|
146
|
+
<ledger-table exp-caption="Ledger">
|
|
147
|
+
<template lb-template="head">
|
|
148
|
+
<tr><th>Date</th><th>Amount</th></tr>
|
|
149
|
+
</template>
|
|
150
|
+
<tbody>
|
|
151
|
+
<!-- row template -->
|
|
152
|
+
</tbody>
|
|
153
|
+
</ledger-table>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Declare as many destinations as the definition needs, one per name. An
|
|
157
|
+
author fills none, some, or all of them, and a destination nobody fills
|
|
158
|
+
stays and is empty.
|
|
159
|
+
|
|
160
|
+
Wrap authored content for a destination in a `<template>`, which exists only
|
|
161
|
+
to survive the parser. A `<thead>` written directly inside `<ledger-table>`
|
|
162
|
+
is not inside a `<table>`, so the tokenizer drops the tag before expansion
|
|
163
|
+
sees it. A `<template>` survives anywhere and parses its contents as though
|
|
164
|
+
already in place. What lands in the destination is the template's contents —
|
|
165
|
+
an ordinary `<thead>` in view-source — not the template element.
|
|
166
|
+
|
|
167
|
+
### Recursion
|
|
168
|
+
|
|
169
|
+
A definition may use other widgets. Expansion repeats until nothing new
|
|
170
|
+
appears, so a widget built out of other widgets needs nothing declared for
|
|
171
|
+
it.
|
|
172
|
+
|
|
173
|
+
Keep the definition graph acyclic. A cycle is a build error, reported as the
|
|
174
|
+
path that closes it.
|
|
175
|
+
|
|
176
|
+
Expansion runs over `<template>` contents wherever they occur, so a widget's
|
|
177
|
+
row template ships already expanded, `{{placeholder}}` included.
|
|
178
|
+
|
|
179
|
+
### What fails at build time
|
|
180
|
+
|
|
181
|
+
Each of these is reported with the tag and file name, and nothing is
|
|
182
|
+
shipped:
|
|
183
|
+
|
|
184
|
+
- a tag in the `LB-*` namespace with no definition
|
|
185
|
+
- a cycle in the definition graph
|
|
186
|
+
- an `exp-` attribute the definition never declared
|
|
187
|
+
- a `{{placeholder}}` spelled so that no `exp-` attribute could supply it
|
|
188
|
+
- content written inside a tag whose definition has no `lb-slot`
|
|
189
|
+
- more than one `lb-slot` in one definition
|
|
190
|
+
- a `<template lb-template="name">` naming a destination the definition
|
|
191
|
+
does not have
|
|
192
|
+
- two templates for the same destination
|
|
193
|
+
|
|
194
|
+
An unfilled `{{placeholder}}` is not among these. That is presence
|
|
195
|
+
propagation working as intended, indistinguishable from an author who left
|
|
196
|
+
an optional parameter out.
|
|
197
|
+
|
|
198
|
+
## Code
|
|
199
|
+
|
|
200
|
+
A `.ts` file named for a tag registers that tag's class. A widget is an
|
|
201
|
+
ordinary custom element — Loadbare imposes no base class — and it takes part
|
|
202
|
+
in data binding through three contracts: it receives a value, it sends a
|
|
203
|
+
request, and it accepts a set of rows.
|
|
204
|
+
|
|
205
|
+
Import every attribute name from `@loadbare/app/constants`. Never write one
|
|
206
|
+
as a string literal.
|
|
207
|
+
|
|
208
|
+
| Constant | Value |
|
|
209
|
+
|------------------|------------|
|
|
210
|
+
| `ATTR_VALUE` | `lb-value` |
|
|
211
|
+
| `ATTR_CELL` | `lb-cell` |
|
|
212
|
+
| `ATTR_QUERY` | `lb-query` |
|
|
213
|
+
| `ATTR_KEY` | `lb-key` |
|
|
214
|
+
| `ATTR_GROUP` | `lb-group` |
|
|
215
|
+
| `ATTR_SORT` | `lb-sort` |
|
|
216
|
+
| `ATTR_ACTION` | `lb-action`|
|
|
217
|
+
| `ATTR_ROW_COUNT` | `data-rows`|
|
|
218
|
+
| `LB_EVENT_NAME` | `lb-request` |
|
|
219
|
+
|
|
220
|
+
### Receiving a value
|
|
221
|
+
|
|
222
|
+
A bound cell lands on a widget as the `lb-value` attribute rather than as
|
|
223
|
+
text — see [Data Binding](./data-binding.md#where-a-bound-value-lands).
|
|
224
|
+
Observe it, and render the value however the widget renders things:
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
// src/visit-count.ts
|
|
228
|
+
import { ATTR_VALUE } from "@loadbare/app/constants";
|
|
229
|
+
|
|
230
|
+
class VisitCount extends HTMLElement {
|
|
231
|
+
static observedAttributes = [ATTR_VALUE];
|
|
232
|
+
|
|
233
|
+
attributeChangedCallback(_name: string, _old: string, value: string) {
|
|
234
|
+
this.textContent = `visited ${value} times`;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
customElements.define("visit-count", VisitCount);
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
List `ATTR_VALUE` in `observedAttributes`, or `attributeChangedCallback`
|
|
242
|
+
never fires. The browser calls it for an attribute already present when an
|
|
243
|
+
element upgrades, not only for one that changes afterward, so a widget's
|
|
244
|
+
first render and every later refresh go through the one callback. There is
|
|
245
|
+
no separate hydration path to write.
|
|
246
|
+
|
|
247
|
+
### Sending a request
|
|
248
|
+
|
|
249
|
+
A widget that owns its own interaction — a `<select>`'s choice rather than a
|
|
250
|
+
click — builds its own request and dispatches it as a bubbling
|
|
251
|
+
`CustomEvent` named `LB_EVENT_NAME`, carrying one of the `HubRequest` shapes
|
|
252
|
+
as its `detail`:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
import { ATTR_CELL, ATTR_KEY, ATTR_QUERY, LB_EVENT_NAME } from "@loadbare/app/constants";
|
|
256
|
+
import type { HubRequest } from "@loadbare/app/types";
|
|
257
|
+
|
|
258
|
+
const detail: HubRequest = {
|
|
259
|
+
op: "cell-change",
|
|
260
|
+
query: this.closest(`[${ATTR_QUERY}]`)!.getAttribute(ATTR_QUERY)!,
|
|
261
|
+
key: this.closest(`[${ATTR_KEY}]`)!.getAttribute(ATTR_KEY)!,
|
|
262
|
+
cell: this.getAttribute(ATTR_CELL)!,
|
|
263
|
+
value: input.value,
|
|
264
|
+
};
|
|
265
|
+
this.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Find `query` and `key` by walking up with `closest()`, the same way the hub
|
|
269
|
+
finds the binding for a native element. See
|
|
270
|
+
[Data Binding](./data-binding.md#requests) for the request vocabulary and
|
|
271
|
+
what each operation carries.
|
|
272
|
+
|
|
273
|
+
Let the event bubble, so an ancestor widget can intercept and stop it before
|
|
274
|
+
the hub sees it. A hand-written widget and a native element carrying
|
|
275
|
+
`lb-action` produce the same event.
|
|
276
|
+
|
|
277
|
+
### Accepting rows
|
|
278
|
+
|
|
279
|
+
A query answering with many rows is delivered to whichever element carries
|
|
280
|
+
an `acceptRows` method. It is a protocol, not a tag name — the hub tests for
|
|
281
|
+
the method and never for the element:
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
import { applyRows } from "@loadbare/app/rows";
|
|
285
|
+
import type { Projection, HubRowHost } from "@loadbare/app/types";
|
|
286
|
+
|
|
287
|
+
class UpdateList extends HTMLElement implements HubRowHost {
|
|
288
|
+
acceptRows(result: Projection) {
|
|
289
|
+
applyRows(this, result);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Call `applyRows(scope, result, place?)` rather than reimplementing rows.
|
|
295
|
+
Every built-in list widget uses it, and it settles four things:
|
|
296
|
+
|
|
297
|
+
| Concern | What `applyRows` does |
|
|
298
|
+
|-------------|---------------------------------------------------------|
|
|
299
|
+
| Cloning | Clones the `<template lb-key="...">` in the widget |
|
|
300
|
+
| Matching | Updates the row already showing that key, or clones one |
|
|
301
|
+
| Reconciling | Removes the rows the response says are gone |
|
|
302
|
+
| Counting | Stamps `data-rows` with the number of rows showing |
|
|
303
|
+
|
|
304
|
+
`rows` is the whole set, so it decides membership and order, and a key
|
|
305
|
+
absent from it is removed. `patch` touches only the rows it names and leaves
|
|
306
|
+
every other row's contents and position alone.
|
|
307
|
+
|
|
308
|
+
Supply a `place` function to decide where a row goes — `(row, tuple,
|
|
309
|
+
template) => void`, called with a fresh or reordered row. The default
|
|
310
|
+
inserts immediately before the template, so rows accumulate in arrival
|
|
311
|
+
order. A widget that groups or sorts supplies its own `place` instead of
|
|
312
|
+
reimplementing matching and cloning around it; `lb-options.ts` and
|
|
313
|
+
`lb-table.ts` in [`@loadbare/widgets`](./widgets.md) are two different
|
|
314
|
+
`place` functions over the same `applyRows`.
|
|
315
|
+
|
|
316
|
+
Style an empty list against `data-rows` rather than carrying an empty-state
|
|
317
|
+
conditional in the widget — see
|
|
318
|
+
[Conditional rendering](./data-binding.md#conditional-rendering).
|
|
319
|
+
|
|
320
|
+
### Filling a scope by hand
|
|
321
|
+
|
|
322
|
+
`@loadbare/app/rows` also exports `applyTuple(root, cells)`, the same
|
|
323
|
+
tuple-landing operation a page host uses. Call it in a widget that builds
|
|
324
|
+
its own rows or scopes rather than relying on `acceptRows`. It fills `root`
|
|
325
|
+
itself when `root` carries a matching `lb-cell`, and every matching
|
|
326
|
+
descendant.
|
|
327
|
+
</content>
|