@webjsdev/core 0.7.65 → 0.9.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 +5 -0
- package/dist/webjs-core-browser.js +6 -5
- package/dist/webjs-core-browser.js.map +3 -3
- package/dist/webjs-core-testing.js +22 -21
- package/dist/webjs-core-testing.js.map +3 -3
- package/dist/webjs-core.js +7 -6
- package/dist/webjs-core.js.map +3 -3
- package/index-browser.js +18 -9
- package/index.d.ts +26 -10
- package/index.js +21 -9
- package/package.json +15 -2
- package/src/action-effects-client.d.ts +5 -0
- package/src/action-effects-client.js +84 -0
- package/src/action-seed-client.js +5 -5
- package/src/asset-url.js +12 -13
- package/src/async-directive.d.ts +18 -0
- package/src/async-directive.js +88 -0
- package/src/attribute-directives.d.ts +6 -0
- package/src/attribute-directives.js +49 -0
- package/src/attribute-reader.js +213 -25
- package/src/binding-prefixes.js +10 -53
- package/src/component/lifecycle.js +921 -252
- package/src/component/reactive.js +7 -14
- package/src/component/server-element.js +23 -1
- package/src/component.d.ts +160 -44
- package/src/context.d.ts +94 -18
- package/src/context.js +341 -217
- package/src/converters.d.ts +7 -0
- package/src/converters.js +38 -0
- package/src/css.d.ts +32 -6
- package/src/css.js +178 -14
- package/src/directive.d.ts +58 -0
- package/src/directive.js +119 -0
- package/src/directives.d.ts +40 -1
- package/src/directives.js +160 -1
- package/src/form-action.d.ts +13 -0
- package/src/form-action.js +139 -4
- package/src/hot.js +23 -0
- package/src/html.d.ts +8 -0
- package/src/html.js +55 -2
- package/src/is-server.d.ts +2 -0
- package/src/is-server.js +18 -0
- package/src/jsx-guard.js +130 -0
- package/src/metadata.d.ts +22 -3
- package/src/nav.d.ts +4 -1
- package/src/nav.js +44 -11
- package/src/optimistic.js +2 -2
- package/src/refresh-header.js +52 -0
- package/src/registry.js +10 -1
- package/src/render-client/adopt-slots.js +250 -0
- package/src/render-client/parts.js +790 -42
- package/src/render-client/reconciler.js +84 -23
- package/src/render-client/template-compiler.js +152 -43
- package/src/render-client.d.ts +6 -1
- package/src/render-server/attrs.js +2 -2
- package/src/render-server/compile.js +50 -2
- package/src/render-server/errors.js +37 -0
- package/src/render-server/expand.js +100 -20
- package/src/render-server/served-attrs.js +78 -0
- package/src/render-server/walk.js +177 -5
- package/src/render-server.d.ts +3 -0
- package/src/render-server.js +2 -0
- package/src/repeat.d.ts +7 -2
- package/src/repeat.js +20 -6
- package/src/router-client/anchors.js +3 -1
- package/src/router-client/boundaries.js +15 -1
- package/src/router-client/constants.js +30 -0
- package/src/router-client/dom-differ.js +17 -0
- package/src/router-client/fetch-apply.js +107 -5
- package/src/router-client/frames.js +6 -3
- package/src/router-client/head-merge.js +58 -10
- package/src/router-client/navigator.js +49 -9
- package/src/router-client/owned-styles.js +372 -0
- package/src/router-client/prefetch.js +6 -4
- package/src/router-client/root-attrs.js +43 -0
- package/src/router-client/stream.js +7 -0
- package/src/router-client/stylesheets.js +191 -0
- package/src/router-client/swap.js +53 -10
- package/src/routes.d.ts +75 -18
- package/src/sentinels.d.ts +2 -0
- package/src/sentinels.js +4 -0
- package/src/serialize.d.ts +2 -0
- package/src/serialize.js +35 -4
- package/src/signal.d.ts +7 -0
- package/src/signal.js +26 -6
- package/src/slot/polyfills.js +3 -3
- package/src/slot.js +4 -8
- package/src/static-html.d.ts +22 -0
- package/src/static-html.js +144 -0
- package/src/task.d.ts +63 -20
- package/src/task.js +326 -293
- package/src/webjs-config.d.ts +2 -9
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lit's `defaultConverter` and `notEqual` (#1686), exported under the same
|
|
3
|
+
* names so a component written from Lit's docs (a custom converter that
|
|
4
|
+
* delegates to the default, or `hasChanged: (v, old) => notEqual(v, old)`)
|
|
5
|
+
* imports them from `@webjsdev/core` as it would from `lit`.
|
|
6
|
+
*
|
|
7
|
+
* The semantics are Lit's, line for line: a Boolean attribute is on when
|
|
8
|
+
* present, a Number parses with `Number()`, an Object or Array round-trips
|
|
9
|
+
* through JSON (an unparseable value reads `null`, as in Lit), and anything
|
|
10
|
+
* else is the string.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** @type {{ toAttribute(value: unknown, type?: unknown): unknown, fromAttribute(value: string | null, type?: unknown): unknown }} */
|
|
16
|
+
export const defaultConverter = {
|
|
17
|
+
toAttribute(value, type) {
|
|
18
|
+
if (type === Boolean) return value ? '' : null;
|
|
19
|
+
if (type === Object || type === Array) return value == null ? value : JSON.stringify(value);
|
|
20
|
+
return value;
|
|
21
|
+
},
|
|
22
|
+
fromAttribute(value, type) {
|
|
23
|
+
if (type === Boolean) return value !== null;
|
|
24
|
+
if (type === Number) return value === null ? null : Number(value);
|
|
25
|
+
if (type === Object || type === Array) {
|
|
26
|
+
try { return JSON.parse(/** @type {string} */ (value)); } catch { return null; }
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Lit's default change check: true when `value` differs from `old`, by
|
|
34
|
+
* `Object.is` (so `NaN` equals `NaN`).
|
|
35
|
+
* @param {unknown} value @param {unknown} old
|
|
36
|
+
* @returns {boolean}
|
|
37
|
+
*/
|
|
38
|
+
export const notEqual = (value, old) => !Object.is(value, old);
|
package/src/css.d.ts
CHANGED
|
@@ -1,9 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/** The value `css` and `unsafeCSS` return (Lit's `CSSResult`). */
|
|
2
|
+
export declare class CSSResult {
|
|
3
|
+
private constructor(cssText: string);
|
|
4
|
+
readonly _$webjsCss: true;
|
|
5
|
+
/** The style text. */
|
|
6
|
+
readonly cssText: string;
|
|
7
|
+
/** The style text, an alias of `cssText`. */
|
|
8
|
+
readonly text: string;
|
|
9
|
+
/** A shared constructed stylesheet in the browser; undefined on the server. */
|
|
10
|
+
readonly styleSheet: CSSStyleSheet | undefined;
|
|
11
|
+
toString(): string;
|
|
4
12
|
}
|
|
5
13
|
|
|
6
|
-
|
|
14
|
+
/** A `css` result or a constructed stylesheet (Lit's `CSSResultOrNative`). */
|
|
15
|
+
export type CSSResultOrNative = CSSResult | CSSStyleSheet;
|
|
16
|
+
|
|
17
|
+
/** Arbitrarily nested arrays of styles (Lit's `CSSResultArray`). */
|
|
18
|
+
export interface CSSResultArray extends Array<CSSResultOrNative | CSSResultArray> {}
|
|
19
|
+
|
|
20
|
+
/** What `static styles` accepts (Lit's `CSSResultGroup`). */
|
|
21
|
+
export type CSSResultGroup = CSSResultOrNative | CSSResultArray;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Tagged template producing a `CSSResult`. A hole may hold another `css`
|
|
25
|
+
* result or a number; anything else throws (wrap a trusted string in
|
|
26
|
+
* `unsafeCSS()`).
|
|
27
|
+
*/
|
|
28
|
+
export function css(strings: TemplateStringsArray | string[], ...values: (CSSResult | number)[]): CSSResult;
|
|
29
|
+
/** Wrap a trusted string as a `CSSResult`. Never pass user input. */
|
|
30
|
+
export function unsafeCSS(value: unknown): CSSResult;
|
|
7
31
|
export function isCSS(x: unknown): x is CSSResult;
|
|
8
|
-
|
|
9
|
-
export function
|
|
32
|
+
/** Flatten `static styles` into the list both renderers apply (Lit's `finalizeStyles`). */
|
|
33
|
+
export function finalizeStyles(styles?: CSSResultGroup | null): CSSResultOrNative[];
|
|
34
|
+
export function adoptStyles(root: ShadowRoot | Document, styles: CSSResultOrNative[]): void;
|
|
35
|
+
export function stylesToString(styles: CSSResultOrNative[]): string;
|
package/src/css.js
CHANGED
|
@@ -1,7 +1,88 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The `css` tag and the style plumbing both renderers share.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Portions derived from Lit (`@lit/reactive-element` css-tag.ts and
|
|
5
|
+
* reactive-element.ts `finalizeStyles`), Copyright Google LLC,
|
|
6
|
+
* BSD-3-Clause.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The value `css` and `unsafeCSS` return, Lit's `CSSResult`. `cssText` is the
|
|
11
|
+
* style text; `styleSheet` is a constructed `CSSStyleSheet` built on first
|
|
12
|
+
* read in the browser (undefined on the server), cached per result so every
|
|
13
|
+
* element adopting it shares one sheet.
|
|
14
|
+
*
|
|
15
|
+
* The `_$webjsCss` brand, not `instanceof`, is what identifies a result:
|
|
16
|
+
* `@webjsdev/core` resolves to a bundle while a subpath resolves to source, so
|
|
17
|
+
* two copies of this class can meet in one page.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Held by `css` and `unsafeCSS` only, Lit's `constructionToken`. */
|
|
21
|
+
const constructionToken = Symbol();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The shared sheet per hole-free `css` template, keyed by its strings array.
|
|
25
|
+
* @type {WeakMap<readonly string[], CSSStyleSheet>}
|
|
26
|
+
*/
|
|
27
|
+
const sheetCache = new WeakMap();
|
|
28
|
+
|
|
29
|
+
export class CSSResult {
|
|
30
|
+
/**
|
|
31
|
+
* Not constructable from outside this module, as in Lit: `css` and
|
|
32
|
+
* `unsafeCSS` are the only ways to make one, so arbitrary text cannot pass
|
|
33
|
+
* for a checked `css` result without going through `unsafeCSS` by name.
|
|
34
|
+
* @param {string} cssText
|
|
35
|
+
* @param {readonly string[] | undefined} strings the `css` template, keying the shared sheet
|
|
36
|
+
* @param {symbol} safeToken
|
|
37
|
+
*/
|
|
38
|
+
constructor(cssText, strings, safeToken) {
|
|
39
|
+
if (safeToken !== constructionToken) {
|
|
40
|
+
throw new Error('[webjs] CSSResult is not constructable. Use `css` or `unsafeCSS` instead.');
|
|
41
|
+
}
|
|
42
|
+
/** @type {true} */
|
|
43
|
+
this._$webjsCss = true;
|
|
44
|
+
/** @type {string} */
|
|
45
|
+
this.cssText = cssText;
|
|
46
|
+
/** @type {CSSStyleSheet | undefined} */
|
|
47
|
+
this._styleSheet = undefined;
|
|
48
|
+
this._strings = strings;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The style text, also readable as `cssText` (Lit's name). */
|
|
52
|
+
get text() {
|
|
53
|
+
return this.cssText;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A constructed stylesheet for `adoptedStyleSheets`, or undefined where
|
|
58
|
+
* there is none (the server). A `css` template with no holes always yields
|
|
59
|
+
* the same text, so every result from it shares one sheet, as in Lit.
|
|
60
|
+
*/
|
|
61
|
+
get styleSheet() {
|
|
62
|
+
if (this._styleSheet === undefined && typeof CSSStyleSheet !== 'undefined') {
|
|
63
|
+
const strings = this._strings;
|
|
64
|
+
const cacheable = strings !== undefined && strings.length === 1;
|
|
65
|
+
let sheet = cacheable ? sheetCache.get(strings) : undefined;
|
|
66
|
+
if (sheet === undefined) {
|
|
67
|
+
sheet = new CSSStyleSheet();
|
|
68
|
+
sheet.replaceSync(this.cssText);
|
|
69
|
+
if (cacheable) sheetCache.set(strings, sheet);
|
|
70
|
+
}
|
|
71
|
+
this._styleSheet = sheet;
|
|
72
|
+
}
|
|
73
|
+
return this._styleSheet;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
toString() {
|
|
77
|
+
return this.cssText;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Tagged template literal producing a {@link CSSResult}, Lit's `css`. A hole
|
|
83
|
+
* may hold another `css` result (composed in as text) or a number. Anything
|
|
84
|
+
* else throws: a string in a hole is how untrusted text reaches a stylesheet,
|
|
85
|
+
* so it has to go through `unsafeCSS()` on purpose, as in Lit.
|
|
5
86
|
*
|
|
6
87
|
* @param {TemplateStringsArray | string[]} strings
|
|
7
88
|
* @param {...unknown} values
|
|
@@ -10,9 +91,39 @@
|
|
|
10
91
|
export function css(strings, ...values) {
|
|
11
92
|
let text = strings[0];
|
|
12
93
|
for (let i = 1; i < strings.length; i++) {
|
|
13
|
-
text +=
|
|
94
|
+
text += cssValue(values[i - 1]) + strings[i];
|
|
14
95
|
}
|
|
15
|
-
return
|
|
96
|
+
return new CSSResult(text, strings, constructionToken);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** @param {unknown} value */
|
|
100
|
+
function cssValue(value) {
|
|
101
|
+
if (isCSS(value)) return /** @type {any} */ (value).cssText ?? /** @type {any} */ (value).text;
|
|
102
|
+
if (typeof value === 'number') return String(value);
|
|
103
|
+
throw new Error(
|
|
104
|
+
`[webjs] Value passed to 'css' must be a 'css' result or a number, got ${describe(value)}. ` +
|
|
105
|
+
`Compose another css\`...\` result, or wrap a trusted string in unsafeCSS() from '@webjsdev/core'. ` +
|
|
106
|
+
`Never pass user input through unsafeCSS().`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @param {unknown} value */
|
|
111
|
+
function describe(value) {
|
|
112
|
+
if (value === null) return 'null';
|
|
113
|
+
if (typeof value === 'string') return JSON.stringify(value.length > 40 ? value.slice(0, 40) + '...' : value);
|
|
114
|
+
return typeof value;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Wrap a string (or any value, stringified) as a {@link CSSResult}, Lit's
|
|
119
|
+
* `unsafeCSS`. The text is used as is, so only pass trusted values: CSS from
|
|
120
|
+
* user input can exfiltrate data through `url()` and attribute selectors.
|
|
121
|
+
*
|
|
122
|
+
* @param {unknown} value
|
|
123
|
+
* @returns {CSSResult}
|
|
124
|
+
*/
|
|
125
|
+
export function unsafeCSS(value) {
|
|
126
|
+
return new CSSResult(typeof value === 'string' ? value : String(value), undefined, constructionToken);
|
|
16
127
|
}
|
|
17
128
|
|
|
18
129
|
/** @param {unknown} x @returns {x is CSSResult} */
|
|
@@ -21,34 +132,87 @@ export function isCSS(x) {
|
|
|
21
132
|
}
|
|
22
133
|
|
|
23
134
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
135
|
+
* Lit's `finalizeStyles`: turn a class's `static styles` (a result, a sheet,
|
|
136
|
+
* or arbitrarily nested arrays of them) into the flat list both renderers
|
|
137
|
+
* apply. A style included twice keeps its LAST position, so a later override
|
|
138
|
+
* still wins the cascade, as in Lit.
|
|
139
|
+
*
|
|
140
|
+
* @param {unknown} styles
|
|
141
|
+
* @returns {(CSSResult | CSSStyleSheet)[]}
|
|
142
|
+
*/
|
|
143
|
+
export function finalizeStyles(styles) {
|
|
144
|
+
/** @type {(CSSResult | CSSStyleSheet)[]} */
|
|
145
|
+
const out = [];
|
|
146
|
+
if (Array.isArray(styles)) {
|
|
147
|
+
const unique = new Set(styles.flat(Infinity).reverse());
|
|
148
|
+
for (const s of unique) if (isStyle(s)) out.unshift(/** @type {any} */ (s));
|
|
149
|
+
} else if (isStyle(styles)) {
|
|
150
|
+
out.push(/** @type {any} */ (styles));
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** @param {unknown} s */
|
|
156
|
+
function isStyle(s) {
|
|
157
|
+
return isCSS(s) || (typeof CSSStyleSheet !== 'undefined' && s instanceof CSSStyleSheet);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Apply styles to a shadow root (client-side only). Uses adoptedStyleSheets
|
|
162
|
+
* when available, falls back to a <style> element.
|
|
26
163
|
*
|
|
27
164
|
* @param {ShadowRoot | Document} root
|
|
28
|
-
* @param {CSSResult[]} styles
|
|
165
|
+
* @param {(CSSResult | CSSStyleSheet)[]} styles
|
|
29
166
|
*/
|
|
30
167
|
export function adoptStyles(root, styles) {
|
|
31
168
|
if (!styles || !styles.length) return;
|
|
32
169
|
if ('adoptedStyleSheets' in root && typeof CSSStyleSheet !== 'undefined') {
|
|
33
|
-
|
|
170
|
+
/** @type {any} */ (root).adoptedStyleSheets = styles.map((s) => {
|
|
171
|
+
if (s instanceof CSSStyleSheet) return s;
|
|
172
|
+
const shared = /** @type {any} */ (s).styleSheet;
|
|
173
|
+
if (shared) return shared;
|
|
34
174
|
const sheet = new CSSStyleSheet();
|
|
35
|
-
sheet.replaceSync(s
|
|
175
|
+
sheet.replaceSync(styleText(s));
|
|
36
176
|
return sheet;
|
|
37
177
|
});
|
|
38
|
-
/** @type {any} */ (root).adoptedStyleSheets = sheets;
|
|
39
178
|
} else {
|
|
40
179
|
const host = /** @type {any} */ (root);
|
|
41
180
|
const el = document.createElement('style');
|
|
42
|
-
el.textContent = styles.map(
|
|
181
|
+
el.textContent = styles.map(styleText).join('\n');
|
|
43
182
|
host.appendChild(el);
|
|
44
183
|
}
|
|
45
184
|
}
|
|
46
185
|
|
|
186
|
+
/** @param {any} s @returns {string} */
|
|
187
|
+
function styleText(s) {
|
|
188
|
+
if (isCSS(s)) return s.cssText ?? s.text;
|
|
189
|
+
if (s && s.cssRules) return Array.from(s.cssRules, (r) => /** @type {any} */ (r).cssText).join('\n');
|
|
190
|
+
return '';
|
|
191
|
+
}
|
|
192
|
+
|
|
47
193
|
/**
|
|
48
|
-
* Serialise styles to a <style> tag string (server-side).
|
|
49
|
-
*
|
|
194
|
+
* Serialise styles to a <style> tag string (server-side). A constructed
|
|
195
|
+
* `CSSStyleSheet` has no server equivalent, so only `css` results render.
|
|
196
|
+
* @param {(CSSResult | CSSStyleSheet)[]} styles
|
|
50
197
|
*/
|
|
51
198
|
export function stylesToString(styles) {
|
|
52
199
|
if (!styles || !styles.length) return '';
|
|
53
|
-
|
|
200
|
+
const text = styles.filter(isCSS).map(styleText);
|
|
201
|
+
return text.length ? `<style>${text.join('\n')}</style>` : '';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The error a light-DOM component with `static styles` gets: the styles are
|
|
206
|
+
* adopted into a shadow root, and the component has none. One string, used by
|
|
207
|
+
* the server renderer (logged during SSR) and by the browser element, so the
|
|
208
|
+
* two cannot drift. Internal: not re-exported from the package.
|
|
209
|
+
* @param {string} tag
|
|
210
|
+
* @returns {string}
|
|
211
|
+
*/
|
|
212
|
+
export function lightDomStylesMessage(tag) {
|
|
213
|
+
return (
|
|
214
|
+
`[webjs] <${tag}> declares static styles but renders into light DOM, so the styles are never applied. ` +
|
|
215
|
+
`Add \`static shadow = true\` to scope them in a shadow root, or style the light-DOM markup with Tailwind ` +
|
|
216
|
+
`utilities or a global stylesheet (prefix custom class selectors with the tag name).`
|
|
217
|
+
);
|
|
54
218
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** lit's `PartType`: the kind of part a directive is bound to. */
|
|
2
|
+
export const PartType: {
|
|
3
|
+
readonly ATTRIBUTE: 1;
|
|
4
|
+
readonly CHILD: 2;
|
|
5
|
+
readonly PROPERTY: 3;
|
|
6
|
+
readonly BOOLEAN_ATTRIBUTE: 4;
|
|
7
|
+
readonly EVENT: 5;
|
|
8
|
+
readonly ELEMENT: 6;
|
|
9
|
+
};
|
|
10
|
+
export type PartType = (typeof PartType)[keyof typeof PartType];
|
|
11
|
+
|
|
12
|
+
/** lit's `PartInfo`: where a directive instance is used. */
|
|
13
|
+
export interface PartInfo {
|
|
14
|
+
readonly type: PartType;
|
|
15
|
+
readonly name?: string;
|
|
16
|
+
readonly tagName?: string;
|
|
17
|
+
readonly strings?: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The part handed to `update()` on the client (a lit-shaped facade). */
|
|
21
|
+
export interface Part {
|
|
22
|
+
readonly type: PartType;
|
|
23
|
+
readonly options: { readonly host: unknown };
|
|
24
|
+
/** Element, attribute, property, boolean and event parts. */
|
|
25
|
+
readonly element?: Element;
|
|
26
|
+
readonly name?: string;
|
|
27
|
+
readonly strings?: readonly string[];
|
|
28
|
+
/** Child parts. */
|
|
29
|
+
readonly parentNode?: Node | null;
|
|
30
|
+
readonly startNode?: Node | null;
|
|
31
|
+
readonly endNode?: Node | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** lit's `Directive`: subclass it, implement `render()`, optionally `update()`. */
|
|
35
|
+
export abstract class Directive {
|
|
36
|
+
constructor(partInfo: PartInfo);
|
|
37
|
+
abstract render(...props: unknown[]): unknown;
|
|
38
|
+
update(part: Part, props: unknown[]): unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The value a `directive()` function returns; rendered by both renderers. */
|
|
42
|
+
export interface DirectiveResult<C extends abstract new (...args: any[]) => Directive = abstract new (...args: any[]) => Directive> {
|
|
43
|
+
readonly _$webjs: 'directive';
|
|
44
|
+
readonly ctor: C;
|
|
45
|
+
readonly values: unknown[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The arguments of a directive class's `render()`. */
|
|
49
|
+
export type DirectiveParameters<C extends Directive> = Parameters<C['render']>;
|
|
50
|
+
|
|
51
|
+
/** lit's `directive()`: the template-facing function for a directive class. */
|
|
52
|
+
export function directive<C extends new (partInfo: PartInfo) => Directive>(
|
|
53
|
+
c: C,
|
|
54
|
+
): (...values: DirectiveParameters<InstanceType<C>>) => DirectiveResult<C>;
|
|
55
|
+
|
|
56
|
+
export function isDirectiveResult(x: unknown): x is DirectiveResult;
|
|
57
|
+
/** lit's `getDirectiveClass`. */
|
|
58
|
+
export function getDirectiveClass(x: unknown): (abstract new (...args: any[]) => Directive) | undefined;
|
package/src/directive.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom directive authoring, lit's `lit/directive.js` under a WebJs path:
|
|
3
|
+
* `@webjsdev/core/directive`.
|
|
4
|
+
*
|
|
5
|
+
* Portions derived from Lit, Copyright Google LLC, BSD-3-Clause.
|
|
6
|
+
*
|
|
7
|
+
* ```js
|
|
8
|
+
* import { directive, Directive, PartType } from '@webjsdev/core/directive';
|
|
9
|
+
*
|
|
10
|
+
* class Uppercase extends Directive {
|
|
11
|
+
* constructor(partInfo) {
|
|
12
|
+
* super(partInfo);
|
|
13
|
+
* if (partInfo.type !== PartType.CHILD) throw new Error('uppercase() is a child directive');
|
|
14
|
+
* }
|
|
15
|
+
* render(text) { return String(text).toUpperCase(); }
|
|
16
|
+
* }
|
|
17
|
+
* export const uppercase = directive(Uppercase);
|
|
18
|
+
*
|
|
19
|
+
* html`<p>${uppercase(name)}</p>`;
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* The contract is lit's. One directive INSTANCE lives per template part (per
|
|
23
|
+
* piece of an interpolated attribute) for as long as the same directive class
|
|
24
|
+
* renders there; a different class (or a plain value) replaces it. On the
|
|
25
|
+
* client every render calls `update(part, values)`, whose default forwards to
|
|
26
|
+
* `render(...values)`; on the server only `render(...values)` runs, because
|
|
27
|
+
* there is no DOM to hand `update`. Whatever either returns is rendered at the
|
|
28
|
+
* part like any other value (`noChange` keeps the last commit, `nothing`
|
|
29
|
+
* clears it), so a directive may return a template or another directive.
|
|
30
|
+
*
|
|
31
|
+
* The result is a plain object branded `_$webjs: 'directive'`. Both renderers
|
|
32
|
+
* recognise it by that brand, never by `instanceof`, so a directive built
|
|
33
|
+
* against one loaded copy of this module renders through another (this
|
|
34
|
+
* subpath is source while the root import may be the built bundle).
|
|
35
|
+
*
|
|
36
|
+
* @module directive
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The kind of template part a directive instance is bound to, lit's values.
|
|
41
|
+
* @type {Readonly<{ ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 }>}
|
|
42
|
+
*/
|
|
43
|
+
export const PartType = Object.freeze({
|
|
44
|
+
ATTRIBUTE: 1,
|
|
45
|
+
CHILD: 2,
|
|
46
|
+
PROPERTY: 3,
|
|
47
|
+
BOOLEAN_ATTRIBUTE: 4,
|
|
48
|
+
EVENT: 5,
|
|
49
|
+
ELEMENT: 6,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Base class for a custom directive. Subclass it, implement `render()`, and
|
|
54
|
+
* optionally override `update()` for DOM access on the client.
|
|
55
|
+
*/
|
|
56
|
+
export class Directive {
|
|
57
|
+
/**
|
|
58
|
+
* @param {{ type: number, name?: string, tagName?: string, strings?: readonly string[] }} _partInfo
|
|
59
|
+
* Where the directive is used: `type` is a {@link PartType}; an attribute
|
|
60
|
+
* part also carries `name`, `tagName` and, when the attribute is
|
|
61
|
+
* interpolated, its static `strings`.
|
|
62
|
+
*/
|
|
63
|
+
constructor(_partInfo) {}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Produce the value to render. Called on the server, and on the client by
|
|
67
|
+
* the default `update()`.
|
|
68
|
+
* @param {...unknown} _values the arguments the directive function was called with
|
|
69
|
+
* @returns {unknown}
|
|
70
|
+
*/
|
|
71
|
+
render(..._values) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Called on the client on every render with the part (a lit-shaped facade:
|
|
77
|
+
* `type`, `options.host`, and `element` / `name` / `strings` for an element
|
|
78
|
+
* or attribute part, `parentNode` / `startNode` / `endNode` for a child
|
|
79
|
+
* part) and the arguments as an array. The default calls `render()`.
|
|
80
|
+
* @param {unknown} _part
|
|
81
|
+
* @param {unknown[]} values
|
|
82
|
+
* @returns {unknown}
|
|
83
|
+
*/
|
|
84
|
+
update(_part, values) {
|
|
85
|
+
return this.render(...values);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Turn a {@link Directive} subclass into the function templates call, lit's
|
|
91
|
+
* `directive()`. Each call returns a marker carrying the class and its
|
|
92
|
+
* arguments; the renderer owns the instance.
|
|
93
|
+
*
|
|
94
|
+
* @template {new (partInfo: any) => Directive} C
|
|
95
|
+
* @param {C} c
|
|
96
|
+
* @returns {(...values: unknown[]) => { _$webjs: 'directive', ctor: C, values: unknown[] }}
|
|
97
|
+
*/
|
|
98
|
+
export function directive(c) {
|
|
99
|
+
return (...values) => ({ _$webjs: 'directive', ctor: c, values });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Whether `x` is the result of calling a `directive()` function.
|
|
104
|
+
* @param {unknown} x
|
|
105
|
+
* @returns {x is { _$webjs: 'directive', ctor: any, values: unknown[] }}
|
|
106
|
+
*/
|
|
107
|
+
export function isDirectiveResult(x) {
|
|
108
|
+
return !!x && typeof x === 'object' && /** @type {any} */ (x)._$webjs === 'directive';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The directive class a directive result was made from, lit's
|
|
113
|
+
* `getDirectiveClass`.
|
|
114
|
+
* @param {unknown} x
|
|
115
|
+
* @returns {Function | undefined}
|
|
116
|
+
*/
|
|
117
|
+
export function getDirectiveClass(x) {
|
|
118
|
+
return isDirectiveResult(x) ? x.ctor : undefined;
|
|
119
|
+
}
|
package/src/directives.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import type { nothing, noChange } from './sentinels.js';
|
|
1
2
|
export { repeat, isRepeat } from './repeat.js';
|
|
3
|
+
export type { RepeatDirective } from './repeat.js';
|
|
2
4
|
|
|
3
5
|
export interface UnsafeHTMLDirective {
|
|
4
6
|
_$webjs: 'unsafe-html';
|
|
5
7
|
value: string;
|
|
8
|
+
/** Set by `unsafeSVG` / `unsafeMathML`: the namespace the client parses the markup in. */
|
|
9
|
+
ns?: 'svg' | 'mathml';
|
|
6
10
|
}
|
|
7
11
|
|
|
8
12
|
export interface LiveDirective<T = unknown> {
|
|
@@ -63,8 +67,12 @@ export interface WatchDirective<T = unknown> {
|
|
|
63
67
|
signal: { get(): T; __isSignal: true };
|
|
64
68
|
}
|
|
65
69
|
|
|
66
|
-
export function unsafeHTML(htmlString: string | null | undefined): UnsafeHTMLDirective;
|
|
70
|
+
export function unsafeHTML(htmlString: string | null | undefined | typeof nothing | typeof noChange): UnsafeHTMLDirective;
|
|
67
71
|
export function isUnsafeHTML(x: unknown): x is UnsafeHTMLDirective;
|
|
72
|
+
/** lit's `unsafeSVG`: trusted SVG markup, parsed in the SVG namespace on the client. */
|
|
73
|
+
export function unsafeSVG(svgString: string | null | undefined | typeof nothing | typeof noChange): UnsafeHTMLDirective;
|
|
74
|
+
/** lit's `unsafeMathML`: trusted MathML markup, parsed in the MathML namespace on the client. */
|
|
75
|
+
export function unsafeMathML(mathString: string | null | undefined | typeof nothing | typeof noChange): UnsafeHTMLDirective;
|
|
68
76
|
export function live<T>(value: T): LiveDirective<T>;
|
|
69
77
|
export function isLive(x: unknown): x is LiveDirective;
|
|
70
78
|
export function keyed<T>(key: unknown, template: T): KeyedDirective<T>;
|
|
@@ -96,3 +104,34 @@ export function asyncReplace<T>(
|
|
|
96
104
|
export function isAsyncReplace(x: unknown): x is AsyncReplaceDirective;
|
|
97
105
|
export function watch<T>(sig: { get(): T; __isSignal: true }): WatchDirective<T>;
|
|
98
106
|
export function isWatch(x: unknown): x is WatchDirective;
|
|
107
|
+
|
|
108
|
+
type Falsy = null | undefined | false | 0 | -0 | 0n | '';
|
|
109
|
+
|
|
110
|
+
/** lit's `when`: the case functions receive the condition value. */
|
|
111
|
+
export function when<C, T, F = undefined>(
|
|
112
|
+
condition: C,
|
|
113
|
+
trueCase: (c: Exclude<C, Falsy>) => T,
|
|
114
|
+
falseCase?: (c: Extract<C, Falsy>) => F,
|
|
115
|
+
): C extends Falsy ? F : T;
|
|
116
|
+
/** lit's `choose`: the case whose key is `===` to `value`, else the default. */
|
|
117
|
+
export function choose<T, V, K extends T = T>(
|
|
118
|
+
value: T,
|
|
119
|
+
cases: Array<[K, () => V]>,
|
|
120
|
+
defaultCase?: () => V,
|
|
121
|
+
): V | undefined;
|
|
122
|
+
/** lit's `map`: a lazy generator over `items`, renderable anywhere an array is. */
|
|
123
|
+
export function map<T, U>(
|
|
124
|
+
items: Iterable<T> | undefined,
|
|
125
|
+
f: (value: T, index: number) => U,
|
|
126
|
+
): Generator<U, void, unknown>;
|
|
127
|
+
/** lit's `join`: `items` with `joiner` (or `joiner(index)`) between each pair. */
|
|
128
|
+
export function join<I, J>(
|
|
129
|
+
items: Iterable<I> | undefined,
|
|
130
|
+
joiner: J | ((index: number) => J),
|
|
131
|
+
): Generator<I | J, void, unknown>;
|
|
132
|
+
/** lit's `range`: `range(end)`, `range(start, end)` or `range(start, end, step)`. */
|
|
133
|
+
export function range(end: number): Generator<number, void, unknown>;
|
|
134
|
+
export function range(start: number, end: number, step?: number): Generator<number, void, unknown>;
|
|
135
|
+
|
|
136
|
+
export { classMap, styleMap, ifDefined } from './attribute-directives.js';
|
|
137
|
+
export type { ClassMapDirective, StyleMapDirective } from './attribute-directives.js';
|