@loadbare/app 0.5.4 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build/expand.d.ts.map +1 -1
- package/dist/build/expand.js +12 -3
- package/dist/core/lb-constants.d.ts +3 -0
- package/dist/core/lb-constants.d.ts.map +1 -1
- package/dist/core/lb-constants.js +13 -0
- package/dist/hub/lb-hub.browser.d.ts.map +1 -1
- package/dist/hub/lb-hub.browser.js +51 -5
- package/dist/tests/expand.test.js +24 -0
- package/docs/reference/chrome.md +88 -6
- package/docs/reference/custom-elements.md +8 -0
- package/docs/reference/data-binding.md +5 -0
- package/docs/reference/widgets.md +21 -2
- package/docs/tutorials/000-getting-started.md +10 -1
- package/docs/tutorials/010-pages-and-navigation.md +10 -3
- package/docs/tutorials/020-css.md +2 -1
- package/docs/tutorials/030-html-decomposition.md +2 -1
- package/docs/tutorials/090-using-widget-libraries.md +22 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"expand.d.ts","sourceRoot":"","sources":["../../build/expand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;
|
|
1
|
+
{"version":3,"file":"expand.d.ts","sourceRoot":"","sources":["../../build/expand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAkDH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AA8BjD;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CAmB1E;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GACzB,OAAO,CAAC,WAAW,CAAC,CAStB;AA4CD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAS3D;AAmPD;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,CAIhE"}
|
package/dist/build/expand.js
CHANGED
|
@@ -29,7 +29,15 @@ const WIDGET_TAG = /^LB-/;
|
|
|
29
29
|
*/
|
|
30
30
|
const HUB_TAG = HUB_TAG_NAME.toUpperCase();
|
|
31
31
|
/** A placeholder is an entire attribute value or an entire text node. */
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* `{{name}}` or `{{name|default}}`. The default is a literal that stands in
|
|
34
|
+
* when the tag supplies nothing; it is not evaluated and nothing branches on
|
|
35
|
+
* it. Whitespace around the name and the default is not part of either, so
|
|
36
|
+
* `{{button-text | OK}}` is the same placeholder as `{{button-text|OK}}` —
|
|
37
|
+
* a spaced form that shipped as literal braces would be a silent failure
|
|
38
|
+
* an author would find in the browser.
|
|
39
|
+
*/
|
|
40
|
+
const PLACEHOLDER = /^\{\{\s*([A-Za-z][\w-]*)\s*(?:\|\s*([^}]*?)\s*)?\}\}$/;
|
|
33
41
|
/**
|
|
34
42
|
* Marks an attribute as input to expansion. Three namespaces share the tag:
|
|
35
43
|
* `lb-` is the hub's, `exp-` is expansion's, and everything unprefixed is
|
|
@@ -212,7 +220,8 @@ function checkAcyclic(defs) {
|
|
|
212
220
|
visit(tag);
|
|
213
221
|
}
|
|
214
222
|
/**
|
|
215
|
-
* Resolve a placeholder against what the author wrote
|
|
223
|
+
* Resolve a placeholder against what the author wrote: the supplied value,
|
|
224
|
+
* else the definition's default, else absent.
|
|
216
225
|
*
|
|
217
226
|
* Every name the definition asks for is recorded, whether or not it was
|
|
218
227
|
* supplied, so expansion can report a parameter the definition never declared.
|
|
@@ -223,7 +232,7 @@ function resolve(text, attrs, asked) {
|
|
|
223
232
|
return text;
|
|
224
233
|
const name = match[1];
|
|
225
234
|
asked.add(name);
|
|
226
|
-
return attrs.get(name) ?? null;
|
|
235
|
+
return attrs.get(name) ?? match[2] ?? null;
|
|
227
236
|
}
|
|
228
237
|
function substitute(root, attrs, asked) {
|
|
229
238
|
for (const scope of scopes(root)) {
|
|
@@ -14,6 +14,9 @@ export declare const ATTR_SLOT = "lb-slot";
|
|
|
14
14
|
export declare const ATTR_TEMPLATE = "lb-template";
|
|
15
15
|
export declare const ATTR_NAV_LINK = "lb-nav-link";
|
|
16
16
|
export declare const PAGE_TEMPLATE_PREFIX = "page-";
|
|
17
|
+
export declare const NAV_QUERY = "lb-navigation";
|
|
18
|
+
export declare const NAV_CELL_LABEL = "page-label";
|
|
19
|
+
export declare const NAV_CELL_URI = "page-uri";
|
|
17
20
|
export declare const ATTR_UNKNOWN_PAGE = "lb-unknown-page";
|
|
18
21
|
export declare const HUB_TAG_NAME = "lb-hub";
|
|
19
22
|
export declare const ATTR_PENDING = "data-lb-pending";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lb-constants.d.ts","sourceRoot":"","sources":["../../core/lb-constants.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,UAAU,aAAa,CAAC;AACrC,eAAO,MAAM,QAAQ,WAAW,CAAC;AACjC,eAAO,MAAM,SAAS,YAAY,CAAC;AACnC,eAAO,MAAM,UAAU,aAAa,CAAC;AAMrC,eAAO,MAAM,UAAU,aAAa,CAAC;AAKrC,eAAO,MAAM,SAAS,YAAY,CAAC;AAOnC,eAAO,MAAM,cAAc,cAAc,CAAC;AAK1C,eAAO,MAAM,aAAa,eAAe,CAAC;AAM1C,eAAO,MAAM,WAAW,cAAc,CAAC;AAMvC,eAAO,MAAM,WAAW,cAAc,CAAC;AAKvC,eAAO,MAAM,WAAW,cAAc,CAAC;AAOvC,eAAO,MAAM,WAAW,cAAc,CAAC;AAMvC,eAAO,MAAM,SAAS,YAAY,CAAC;AAYnC,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAK3C,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAG3C,eAAO,MAAM,oBAAoB,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"lb-constants.d.ts","sourceRoot":"","sources":["../../core/lb-constants.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,UAAU,aAAa,CAAC;AACrC,eAAO,MAAM,QAAQ,WAAW,CAAC;AACjC,eAAO,MAAM,SAAS,YAAY,CAAC;AACnC,eAAO,MAAM,UAAU,aAAa,CAAC;AAMrC,eAAO,MAAM,UAAU,aAAa,CAAC;AAKrC,eAAO,MAAM,SAAS,YAAY,CAAC;AAOnC,eAAO,MAAM,cAAc,cAAc,CAAC;AAK1C,eAAO,MAAM,aAAa,eAAe,CAAC;AAM1C,eAAO,MAAM,WAAW,cAAc,CAAC;AAMvC,eAAO,MAAM,WAAW,cAAc,CAAC;AAKvC,eAAO,MAAM,WAAW,cAAc,CAAC;AAOvC,eAAO,MAAM,WAAW,cAAc,CAAC;AAMvC,eAAO,MAAM,SAAS,YAAY,CAAC;AAYnC,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAK3C,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAG3C,eAAO,MAAM,oBAAoB,UAAU,CAAC;AAO5C,eAAO,MAAM,SAAS,kBAAkB,CAAC;AAKzC,eAAO,MAAM,cAAc,eAAe,CAAC;AAC3C,eAAO,MAAM,YAAY,aAAa,CAAC;AAUvC,eAAO,MAAM,iBAAiB,oBAAoB,CAAC;AAQnD,eAAO,MAAM,YAAY,WAAW,CAAC;AAQrC,eAAO,MAAM,YAAY,oBAAoB,CAAC;AAO9C,eAAO,MAAM,UAAU,kBAAkB,CAAC;AAG1C,eAAO,MAAM,gBAAgB,aAAa,CAAC;AAC3C,eAAO,MAAM,mBAAmB,QAAQ,CAAC;AAIzC,eAAO,MAAM,qBAAqB,QAAS,CAAC"}
|
|
@@ -67,9 +67,22 @@ export const ATTR_TEMPLATE = "lb-template";
|
|
|
67
67
|
export const ATTR_NAV_LINK = "lb-nav-link";
|
|
68
68
|
// A page host ships as <template id="page-<name>">.
|
|
69
69
|
export const PAGE_TEMPLATE_PREFIX = "page-";
|
|
70
|
+
// The hub's own query. Landed on every navigation, on any subtree inside
|
|
71
|
+
// the hub that names it, through the same applyData() a server answer goes
|
|
72
|
+
// through — so a chrome displays where the page is the way it displays
|
|
73
|
+
// anything else. The `lb-` prefix keeps it out of a server's namespace:
|
|
74
|
+
// no server answers a query so named.
|
|
75
|
+
export const NAV_QUERY = "lb-navigation";
|
|
76
|
+
// Its two cells. The label is the nav's: the text of the lb-nav-link anchor
|
|
77
|
+
// whose href names the current page, and empty when no anchor does. The
|
|
78
|
+
// URI is the path as the browser has it.
|
|
79
|
+
export const NAV_CELL_LABEL = "page-label";
|
|
80
|
+
export const NAV_CELL_URI = "page-uri";
|
|
70
81
|
// Marks the chrome's own <dialog> for a page name that resolves to no
|
|
71
82
|
// template — a routing miss discovered client-side, not an HTTP 404 (every
|
|
72
83
|
// route gets the same 200 response; see docs/tutorials/010-pages-and-navigation.md).
|
|
84
|
+
// The hub only opens it; what it says comes from NAV_QUERY, which the
|
|
85
|
+
// dialog consumes like any other subtree, by naming it in lb-query.
|
|
73
86
|
// Optional: an application that declares none gets today's console.error
|
|
74
87
|
// and nothing more. The builder requires the element it's on to be a
|
|
75
88
|
// <dialog> (build/assemble.ts), since the hub calls showModal() on it.
|
|
@@ -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":"AA2BA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
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_UNKNOWN_PAGE, 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
|
|
2
|
+
import { ATTR_ACTION, ATTR_CELL, ATTR_DELETE, ATTR_ERROR, ATTR_INSERT, ATTR_KEY, ATTR_NAV_LINK, ATTR_PENDING, ATTR_QUERY, ATTR_UNKNOWN_PAGE, ATTR_UPDATE, HUB_TAG_NAME, NAV_CELL_LABEL, NAV_CELL_URI, NAV_QUERY, 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
4
|
export { applyData, applyTuple } from "./lb-apply";
|
|
5
5
|
/**
|
|
6
6
|
* Where a click happened, in the binding vocabulary. An action carries
|
|
@@ -225,10 +225,12 @@ class LbHub extends HTMLElement {
|
|
|
225
225
|
*/
|
|
226
226
|
async navigate(path) {
|
|
227
227
|
const page = pageNameFor(path);
|
|
228
|
+
this.landNavigation(path, page);
|
|
228
229
|
const template = document.getElementById(PAGE_TEMPLATE_PREFIX + page);
|
|
229
230
|
if (!(template instanceof HTMLTemplateElement)) {
|
|
230
231
|
console.error(`lb-hub: no page host for '${page}'`);
|
|
231
|
-
this.reportUnknownPage(
|
|
232
|
+
this.reportUnknownPage();
|
|
233
|
+
this.reveal();
|
|
232
234
|
return;
|
|
233
235
|
}
|
|
234
236
|
this.page = page;
|
|
@@ -236,6 +238,7 @@ class LbHub extends HTMLElement {
|
|
|
236
238
|
// template is already in the document, so holding it back behind a
|
|
237
239
|
// network round trip only leaves <main> empty for no reason.
|
|
238
240
|
this.main.replaceChildren(template.content.cloneNode(true));
|
|
241
|
+
this.reveal();
|
|
239
242
|
let data = {};
|
|
240
243
|
try {
|
|
241
244
|
data = await fetchData(`${LB_DATA_ENDPOINT}?page=${encodeURIComponent(page)}`);
|
|
@@ -245,6 +248,47 @@ class LbHub extends HTMLElement {
|
|
|
245
248
|
}
|
|
246
249
|
applyData(this, data);
|
|
247
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* An app that ships `<body hidden>` (see docs/reference/chrome.md) is
|
|
253
|
+
* asking to stay invisible until there is something coherent to show,
|
|
254
|
+
* rather than flash the chrome before `<main>` has real content. This is
|
|
255
|
+
* a no-op for an app that doesn't use that convention.
|
|
256
|
+
*/
|
|
257
|
+
reveal() {
|
|
258
|
+
document.body.hidden = false;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The hub's own query: where the page is, in the vocabulary a server
|
|
262
|
+
* answer arrives in, so a chrome displays it the way it displays anything
|
|
263
|
+
* — `lb-query="lb-navigation"` on a subtree, `lb-cell` on what shows a
|
|
264
|
+
* value. Landed before the page host is looked up, so a miss has it too;
|
|
265
|
+
* the unknown-page dialog is one consumer among whatever others the
|
|
266
|
+
* chrome writes.
|
|
267
|
+
*
|
|
268
|
+
* The label is the nav's: the text of the first lb-nav-link anchor whose
|
|
269
|
+
* href names this page. Taken from the nav rather than from a click, so a
|
|
270
|
+
* click, a reload and the back button all land the same label — and a
|
|
271
|
+
* path no anchor names lands an empty one, explicitly, so a consumer is
|
|
272
|
+
* never left showing the previous page's.
|
|
273
|
+
*
|
|
274
|
+
* Landed only where a subtree names it. applyData() warns about a query
|
|
275
|
+
* with no scope, and a chrome that displays no navigation has nothing to
|
|
276
|
+
* be warned about.
|
|
277
|
+
*/
|
|
278
|
+
landNavigation(path, page) {
|
|
279
|
+
if (!this.querySelector(`[${ATTR_QUERY}="${NAV_QUERY}"]`))
|
|
280
|
+
return;
|
|
281
|
+
let label = "";
|
|
282
|
+
for (const a of this.querySelectorAll(`a[${ATTR_NAV_LINK}]`)) {
|
|
283
|
+
if (pageNameFor(new URL(a.href).pathname) !== page)
|
|
284
|
+
continue;
|
|
285
|
+
label = a.textContent?.trim() ?? "";
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
applyData(this, {
|
|
289
|
+
[NAV_QUERY]: { [NAV_CELL_LABEL]: label, [NAV_CELL_URI]: path },
|
|
290
|
+
});
|
|
291
|
+
}
|
|
248
292
|
/**
|
|
249
293
|
* A path that resolves to no page host is discovered client-side, after a
|
|
250
294
|
* successful 200 — every route gets the same document, so there is no
|
|
@@ -254,14 +298,16 @@ class LbHub extends HTMLElement {
|
|
|
254
298
|
* element it's on is a <dialog> (build/assemble.ts), so this can call
|
|
255
299
|
* showModal() without checking the tag here.
|
|
256
300
|
*
|
|
301
|
+
* Only opens it. What the dialog says it gets from landNavigation(),
|
|
302
|
+
* already run, by naming the hub's query in lb-query like any subtree.
|
|
303
|
+
*
|
|
257
304
|
* Scoped to the hub, like every other lookup here, so that one rule holds
|
|
258
305
|
* without exception: what the hub acts on is inside the hub.
|
|
259
306
|
*/
|
|
260
|
-
reportUnknownPage(
|
|
307
|
+
reportUnknownPage() {
|
|
261
308
|
const dialog = this.querySelector(`[${ATTR_UNKNOWN_PAGE}]`);
|
|
262
309
|
if (!dialog)
|
|
263
310
|
return;
|
|
264
|
-
applyTuple(dialog, { page });
|
|
265
311
|
const modal = dialog;
|
|
266
312
|
if (!modal.open)
|
|
267
313
|
modal.showModal();
|
|
@@ -101,6 +101,30 @@ describe("substitution", () => {
|
|
|
101
101
|
const html = expand(`<lb-thing></lb-thing>`, defs({ "lb-thing": `<label>{{label}}</label>` }));
|
|
102
102
|
assert.match(html, /<label><\/label>/);
|
|
103
103
|
});
|
|
104
|
+
it("falls back to a default the definition wrote", () => {
|
|
105
|
+
const html = expand(`<lb-thing></lb-thing>`, defs({ "lb-thing": `<button>{{text|OK}}</button>` }));
|
|
106
|
+
assert.match(html, /<button>OK<\/button>/);
|
|
107
|
+
});
|
|
108
|
+
it("lets a supplied value win over a default", () => {
|
|
109
|
+
const html = expand(`<lb-thing exp-text="Go"></lb-thing>`, defs({ "lb-thing": `<button>{{text|OK}}</button>` }));
|
|
110
|
+
assert.match(html, /<button>Go<\/button>/);
|
|
111
|
+
});
|
|
112
|
+
it("trims whitespace around a name and its default", () => {
|
|
113
|
+
// A spaced form that shipped as literal braces would be a silent failure
|
|
114
|
+
// found in the browser, so the space is not part of either side.
|
|
115
|
+
const html = expand(`<lb-thing></lb-thing>`, defs({
|
|
116
|
+
"lb-thing": `<button title="{{ tip | Press me }}">{{ text | OK }}</button>`,
|
|
117
|
+
}));
|
|
118
|
+
assert.match(html, /<button title="Press me">OK<\/button>/);
|
|
119
|
+
});
|
|
120
|
+
it("emits an attribute whose default is empty", () => {
|
|
121
|
+
// The one case a default distinguishes from no default: presence.
|
|
122
|
+
// readonly="{{readonly|}}" is a boolean attribute on unless a tag turns
|
|
123
|
+
// it off — which, since a supplied value is a string, a tag cannot do;
|
|
124
|
+
// the default is for a definition that wants the attribute there.
|
|
125
|
+
const html = expand(`<lb-thing></lb-thing>`, defs({ "lb-thing": `<input readonly="{{readonly|}}" />` }));
|
|
126
|
+
assert.match(html, /<input readonly="">/);
|
|
127
|
+
});
|
|
104
128
|
it("reaches placeholders inside template content", () => {
|
|
105
129
|
// lb-picker ships a row template of its own, so {{key}} written inside
|
|
106
130
|
// one has to be a placeholder rather than literal text that ships.
|
package/docs/reference/chrome.md
CHANGED
|
@@ -20,18 +20,22 @@ Here is a minimal but fully complaint chrome for a typical app:
|
|
|
20
20
|
<title>Membership Roster</title>
|
|
21
21
|
<script src="/client.js" defer></script>
|
|
22
22
|
<link rel="stylesheet" href="/app.css" />
|
|
23
|
+
<noscript><style>body[hidden] { display: block; }</style></noscript>
|
|
23
24
|
</head>
|
|
24
|
-
<body>
|
|
25
|
+
<body hidden>
|
|
25
26
|
<lb-hub>
|
|
26
|
-
<header
|
|
27
|
+
<header lb-query="lb-navigation">
|
|
28
|
+
<h1>Membership Roster</h1>
|
|
29
|
+
<h2 lb-cell="page-label"></h2>
|
|
30
|
+
</header>
|
|
27
31
|
<nav>
|
|
28
32
|
<a href="/" lb-nav-link>Home</a>
|
|
29
33
|
<a href="/members" lb-nav-link>Members</a>
|
|
30
34
|
<a href="https://example.org/">Our website</a>
|
|
31
35
|
</nav>
|
|
32
36
|
<main></main>
|
|
33
|
-
<dialog lb-unknown-page>
|
|
34
|
-
The
|
|
37
|
+
<dialog lb-unknown-page lb-query="lb-navigation">
|
|
38
|
+
The URL <span lb-cell="page-uri"></span> is not in this app.
|
|
35
39
|
</dialog>
|
|
36
40
|
</lb-hub>
|
|
37
41
|
</body>
|
|
@@ -56,8 +60,10 @@ Everything else is optional:
|
|
|
56
60
|
|-------------------------------------------|---------------------------------------------|
|
|
57
61
|
| `<link rel="stylesheet" href="/app.css">` | The bundled stylesheet |
|
|
58
62
|
| `<a lb-nav-link>` | Navigation between pages |
|
|
63
|
+
| `lb-query="lb-navigation"` | Where the page is — see below |
|
|
59
64
|
| `<dialog lb-unknown-page>` | A message when a URL matches no page |
|
|
60
65
|
| Custom elements | The chrome, decomposed into widget files |
|
|
66
|
+
| `<body hidden>` + a `<noscript>` fallback | Avoids a first-load blink — see below |
|
|
61
67
|
| Any other HTML | Header, footer, skip links, meta tags, etc. |
|
|
62
68
|
|
|
63
69
|
## Rules for writing chrome
|
|
@@ -71,5 +77,81 @@ A navigation anchor's `href` is a path, and the path names a page:
|
|
|
71
77
|
landing page is the one named `index.page.html`. An anchor without
|
|
72
78
|
`lb-nav-link` is left alone and behaves like any other link.
|
|
73
79
|
|
|
74
|
-
The `lb-unknown-page` attribute, if used, must appear on a `<dialog>`.
|
|
75
|
-
|
|
80
|
+
The `lb-unknown-page` attribute, if used, must appear on a `<dialog>`. The
|
|
81
|
+
hub opens it when a path names no page. What it says is up to the chrome:
|
|
82
|
+
the dialog is a subtree like any other, and it displays where the page is by
|
|
83
|
+
naming the hub's own query, described next.
|
|
84
|
+
|
|
85
|
+
## Where the page is
|
|
86
|
+
|
|
87
|
+
On every navigation the hub lands a query of its own, `lb-navigation`, on
|
|
88
|
+
any subtree inside the hub that names it. It arrives the way a server's
|
|
89
|
+
query arrives — `lb-query` on the subtree, `lb-cell` on each element that
|
|
90
|
+
shows a value — so a chrome displays the current page with no code at all:
|
|
91
|
+
|
|
92
|
+
```html
|
|
93
|
+
<header lb-query="lb-navigation">
|
|
94
|
+
<h1>Membership Roster</h1>
|
|
95
|
+
<h2 lb-cell="page-label"></h2>
|
|
96
|
+
</header>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
| Cell | Holds |
|
|
100
|
+
|--------------|-----------------------------------------------------------|
|
|
101
|
+
| `page-label` | The text of the `lb-nav-link` anchor for the path, or empty if none |
|
|
102
|
+
| `page-uri` | The path as the browser has it, such as `/members` |
|
|
103
|
+
|
|
104
|
+
The label is the nav's. The hub takes it from the first `lb-nav-link`
|
|
105
|
+
anchor whose `href` names the current page, so a click, a reload and the
|
|
106
|
+
back button all land the same text, and a path no anchor names lands an
|
|
107
|
+
empty label. A chrome that shows the label somewhere fixed should expect
|
|
108
|
+
that case for a page reachable only by URL.
|
|
109
|
+
|
|
110
|
+
The `lb-` prefix on the query name is what keeps it out of the server's
|
|
111
|
+
namespace: no server answers a query so named. It is landed only where a
|
|
112
|
+
subtree names it, so a chrome that displays no navigation is not warned
|
|
113
|
+
about a query with no scope.
|
|
114
|
+
|
|
115
|
+
The same tuple is what an unknown-page dialog has to work with. It lands
|
|
116
|
+
before the page host is looked up, so a miss has it too. Name the query on
|
|
117
|
+
the dialog and show whichever cell fits:
|
|
118
|
+
|
|
119
|
+
```html
|
|
120
|
+
<dialog lb-unknown-page lb-query="lb-navigation">
|
|
121
|
+
The URL <span lb-cell="page-uri"></span> is not in this app.
|
|
122
|
+
</dialog>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`@loadbare/widgets` ships this dialog as a widget, `<lb-unknown-page>`, for a
|
|
126
|
+
chrome that would rather write one tag — see
|
|
127
|
+
[The Basic Widget Library](./widgets.md#lb-unknown-page).
|
|
128
|
+
|
|
129
|
+
## Preventing the first-load blink
|
|
130
|
+
|
|
131
|
+
`client.js` loads with `defer`, so the browser can — and typically does —
|
|
132
|
+
paint the document before the script has run. Without help, that means a
|
|
133
|
+
visible flash: the chrome appears first, then `<main>`'s real content pops in
|
|
134
|
+
a moment later and shifts everything around it.
|
|
135
|
+
|
|
136
|
+
`<body hidden>` avoids this by hiding the whole document, not just `<main>`,
|
|
137
|
+
until the hub has something to show. `<lb-hub>` un-hides `<body>` itself the
|
|
138
|
+
first time `navigate()` finishes, so the chrome and the first page's content
|
|
139
|
+
always appear together, already in their final layout — there is no
|
|
140
|
+
intermediate state to flash.
|
|
141
|
+
|
|
142
|
+
Hiding `<main>` alone doesn't work: an empty `<main>` already renders at zero
|
|
143
|
+
height, so hiding it changes nothing visible. The pop-in comes from the
|
|
144
|
+
chrome being shown *before* `<main>` has real content, not from `<main>`
|
|
145
|
+
being visibly empty — so it's the whole document that needs to wait, not
|
|
146
|
+
just the piece that was empty.
|
|
147
|
+
|
|
148
|
+
The `<noscript>` block is the escape hatch for a visitor with JavaScript
|
|
149
|
+
disabled. `<lb-hub>` is what removes `hidden` from `<body>`, so a browser
|
|
150
|
+
that never runs `client.js` would otherwise be stuck looking at a
|
|
151
|
+
permanently blank page. `<noscript>` content is only rendered when scripting
|
|
152
|
+
is off, so the fallback rule only ever applies in exactly that case — it
|
|
153
|
+
never runs, and never races with the hub, on a normal visit.
|
|
154
|
+
|
|
155
|
+
Both are optional. An app that doesn't mind the blink, or has no chrome
|
|
156
|
+
complex enough for it to be noticeable, can leave `<body>` unhidden and skip
|
|
157
|
+
the `<noscript>` block entirely.
|
|
@@ -86,6 +86,14 @@ value is a placeholder is removed rather than shipped empty, which is how
|
|
|
86
86
|
placeholder in a text node resolves to nothing, and the whitespace around
|
|
87
87
|
it survives.
|
|
88
88
|
|
|
89
|
+
Give a placeholder a default with a pipe: `{{button-text|OK}}` reads
|
|
90
|
+
`exp-button-text` when the tag supplies it and `OK` when it does not. The
|
|
91
|
+
default is a literal, not an expression. Whitespace around the name and the
|
|
92
|
+
default is not part of either, so `{{ button-text | OK }}` is the same
|
|
93
|
+
placeholder. An empty default, `readonly="{{readonly|}}"`, is the one that
|
|
94
|
+
differs from no default at all: it emits the attribute, empty, rather than
|
|
95
|
+
dropping it, so a definition can ship a boolean attribute switched on.
|
|
96
|
+
|
|
89
97
|
Every attribute stays on the tag after expansion, `exp-` ones included.
|
|
90
98
|
|
|
91
99
|
Parameter values reach a definition through the DOM rather than through
|
|
@@ -56,6 +56,11 @@ an `<option>` — whose content model is text — displays the value it is.
|
|
|
56
56
|
Name the same query on more than one subtree to display it in more than one
|
|
57
57
|
place. Every subtree gets the result.
|
|
58
58
|
|
|
59
|
+
One query is the hub's rather than the server's: `lb-navigation`, landed on
|
|
60
|
+
every navigation with the cells `page-label` and `page-uri`. It binds the
|
|
61
|
+
same way, and the `lb-` prefix on its name is what marks it as Loadbare's —
|
|
62
|
+
see [Where the page is](./chrome.md#where-the-page-is).
|
|
63
|
+
|
|
59
64
|
`lb-key` holds one name in two positions. On a row template it names the
|
|
60
65
|
cell that identifies a row; on a row that is showing, it carries that row's
|
|
61
66
|
key value. A template is never a row, so the two never collide.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# The Basic Widget Library
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
`lb-picker`. Every one of them is written against the same two contracts
|
|
3
|
+
Seven widgets: `lb-input`, `lb-select`, `lb-list`, `lb-options`, `lb-table`,
|
|
4
|
+
`lb-picker`, `lb-unknown-page`. Every one of them is written against the same two contracts
|
|
5
5
|
documented elsewhere — [Custom Elements](./custom-elements.md#html) for its
|
|
6
6
|
definition, [Custom Elements](./custom-elements.md#code) for its class —
|
|
7
7
|
nothing here is special-cased machinery.
|
|
@@ -161,3 +161,22 @@ Behavior — grouping, key-as-value, the action on change — is inherited
|
|
|
161
161
|
whole from `lb-options`; a page author who needs a second element in the
|
|
162
162
|
row, or an option built from two columns, writes `lb-options` and its own
|
|
163
163
|
`<template>` instead.
|
|
164
|
+
|
|
165
|
+
## `lb-unknown-page`
|
|
166
|
+
|
|
167
|
+
The chrome's dialog for a URL that names no page, as one tag:
|
|
168
|
+
|
|
169
|
+
```html
|
|
170
|
+
<lb-hub>
|
|
171
|
+
<nav>...</nav>
|
|
172
|
+
<main></main>
|
|
173
|
+
<lb-unknown-page></lb-unknown-page>
|
|
174
|
+
</lb-hub>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
It expands to a `<dialog lb-unknown-page>` scoped to the hub's own
|
|
178
|
+
`lb-navigation` query, so `page-label` and `page-uri` land in it the way any
|
|
179
|
+
cell lands anywhere, and the hub opens it on a miss. It takes no parameters
|
|
180
|
+
and, so far, shows both cells rather than choosing between them. See
|
|
181
|
+
[`chrome.html`](./chrome.md#where-the-page-is) for the query, and for the same
|
|
182
|
+
dialog written by hand.
|
|
@@ -26,8 +26,9 @@ At minimum, the chrome must contain:
|
|
|
26
26
|
<meta charset="utf-8" />
|
|
27
27
|
<title>@Loadbare/app Tutorials</title>
|
|
28
28
|
<script src="/client.js" defer></script>
|
|
29
|
+
<noscript><style>body[hidden] { display: block; }</style></noscript>
|
|
29
30
|
</head>
|
|
30
|
-
<body>
|
|
31
|
+
<body hidden>
|
|
31
32
|
<lb-hub>
|
|
32
33
|
<main></main>
|
|
33
34
|
</lb-hub>
|
|
@@ -41,6 +42,14 @@ the custom elements used anywhere in `src/`. In this basic starting point,
|
|
|
41
42
|
we have `<lb-hub>` as the only custom element, so `client.js` will just
|
|
42
43
|
contain the Javascript class `LbHub`.
|
|
43
44
|
|
|
45
|
+
`<body hidden>` and the `<noscript>` rule next to it are optional, not part
|
|
46
|
+
of the minimum above. `<lb-hub>` removes `hidden` from `<body>` as soon as it
|
|
47
|
+
has something to show, so the document stays invisible for a moment rather
|
|
48
|
+
than flashing an empty page and then popping in real content — see
|
|
49
|
+
[chrome.html](../reference/chrome.md#preventing-the-first-load-blink) for
|
|
50
|
+
why. Every chrome in the rest of these tutorials carries this forward; drop
|
|
51
|
+
it if you'd rather not have it.
|
|
52
|
+
|
|
44
53
|
## The dev script
|
|
45
54
|
|
|
46
55
|
Add a command to package.json that builds the app.
|
|
@@ -46,8 +46,9 @@ display something to the user if they type a URL that has no matching page.
|
|
|
46
46
|
<meta charset="utf-8" />
|
|
47
47
|
<title>Pages and Navigation</title>
|
|
48
48
|
<script src="/client.js" defer></script>
|
|
49
|
+
<noscript><style>body[hidden] { display: block; }</style></noscript>
|
|
49
50
|
</head>
|
|
50
|
-
<body>
|
|
51
|
+
<body hidden>
|
|
51
52
|
<lb-hub>
|
|
52
53
|
<nav>
|
|
53
54
|
<!-- a bare link to "/" goes to page "index" -->
|
|
@@ -55,7 +56,9 @@ display something to the user if they type a URL that has no matching page.
|
|
|
55
56
|
<a href="/about" lb-nav-link>About</a>
|
|
56
57
|
</nav>
|
|
57
58
|
<main></main>
|
|
58
|
-
<dialog lb-unknown-page
|
|
59
|
+
<dialog lb-unknown-page lb-query="lb-navigation">
|
|
60
|
+
The URL <span lb-cell="page-uri"></span> is not in this app.
|
|
61
|
+
</dialog>
|
|
59
62
|
</lb-hub>
|
|
60
63
|
</body>
|
|
61
64
|
</html>
|
|
@@ -72,7 +75,11 @@ link.
|
|
|
72
75
|
|
|
73
76
|
If a `<dialog lb-unknown-page>` element is present in the HTML, it will be
|
|
74
77
|
displayed to the user when a URL is entered that has no matching page in the app.
|
|
75
|
-
The `lb-cell`
|
|
78
|
+
The `lb-query` and `lb-cell` attributes will be explained when we get to data
|
|
79
|
+
binding; for now, know that `lb-navigation` is a query the hub itself answers
|
|
80
|
+
on every navigation, and `page-uri` is the path that was asked for. The
|
|
81
|
+
widget library ships this dialog ready-made as `<lb-unknown-page>`, which
|
|
82
|
+
[Using Widget Libraries](./090-using-widget-libraries.md) covers.
|
|
76
83
|
|
|
77
84
|
## Serving every route
|
|
78
85
|
|
|
@@ -60,8 +60,9 @@ Add the stylesheet link to the chrome:
|
|
|
60
60
|
<script src="/client.js" defer></script>
|
|
61
61
|
<!-- Add a standard stylesheet link -->
|
|
62
62
|
<link rel="stylesheet" href="/app.css" />
|
|
63
|
+
<noscript><style>body[hidden] { display: block; }</style></noscript>
|
|
63
64
|
</head>
|
|
64
|
-
<body>
|
|
65
|
+
<body hidden>
|
|
65
66
|
<lb-hub>
|
|
66
67
|
<nav>
|
|
67
68
|
<a href="/" lb-nav-link>Home</a>
|
|
@@ -27,8 +27,9 @@ element, not yet defined:
|
|
|
27
27
|
<title>Pages and Navigation</title>
|
|
28
28
|
<script src="/client.js" defer></script>
|
|
29
29
|
<link rel="stylesheet" href="/app.css" />
|
|
30
|
+
<noscript><style>body[hidden] { display: block; }</style></noscript>
|
|
30
31
|
</head>
|
|
31
|
-
<body>
|
|
32
|
+
<body hidden>
|
|
32
33
|
<lb-hub>
|
|
33
34
|
<!-- replace this from the previous tutorial...
|
|
34
35
|
<nav>
|
|
@@ -60,6 +60,28 @@ writes the query result to `lb-value`, and the widget's own class —
|
|
|
60
60
|
imported from `@loadbare/widgets`, not written in this
|
|
61
61
|
application — reacts to it.
|
|
62
62
|
|
|
63
|
+
## The unknown-page dialog, as a widget
|
|
64
|
+
|
|
65
|
+
[Pages and Navigation](./010-pages-and-navigation.md) wrote a
|
|
66
|
+
`<dialog lb-unknown-page>` by hand. The library ships the same dialog as a
|
|
67
|
+
widget, so a chrome that lists the package can replace the dialog with one
|
|
68
|
+
tag:
|
|
69
|
+
|
|
70
|
+
```html
|
|
71
|
+
<lb-hub>
|
|
72
|
+
<nav>
|
|
73
|
+
<a href="/" lb-nav-link>Home</a>
|
|
74
|
+
<a href="/about" lb-nav-link>About</a>
|
|
75
|
+
</nav>
|
|
76
|
+
<main></main>
|
|
77
|
+
<lb-unknown-page></lb-unknown-page>
|
|
78
|
+
</lb-hub>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Its definition is the dialog, already scoped to the hub's `lb-navigation`
|
|
82
|
+
query and carrying both of its cells. Nothing else changes: the hub still
|
|
83
|
+
finds a `<dialog lb-unknown-page>` inside itself and opens it on a miss.
|
|
84
|
+
|
|
63
85
|
## Where this stops
|
|
64
86
|
|
|
65
87
|
This only covers consuming a published widget, not writing one for
|