@bodil/dom 0.1.10 → 0.2.1
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/component.d.ts +91 -17
- package/dist/component.js +110 -39
- package/dist/component.js.map +1 -1
- package/dist/decorators/attribute.d.ts +15 -15
- package/dist/decorators/attribute.js +27 -8
- package/dist/decorators/attribute.js.map +1 -1
- package/dist/decorators/attribute.test.js +58 -6
- package/dist/decorators/attribute.test.js.map +1 -1
- package/dist/decorators/connect.d.ts +2 -0
- package/dist/decorators/connect.js.map +1 -1
- package/dist/decorators/require.test.js +1 -1
- package/dist/decorators/require.test.js.map +1 -1
- package/dist/dom.d.ts +12 -2
- package/dist/dom.js +34 -18
- package/dist/dom.js.map +1 -1
- package/dist/dom.test.d.ts +1 -0
- package/dist/dom.test.js +20 -0
- package/dist/dom.test.js.map +1 -0
- package/dist/emitter.d.ts +6 -0
- package/dist/emitter.js +4 -0
- package/dist/emitter.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/test.d.ts +17 -0
- package/dist/test.js +34 -0
- package/dist/test.js.map +1 -0
- package/package.json +25 -19
- package/src/component.ts +146 -47
- package/src/decorators/attribute.test.ts +41 -14
- package/src/decorators/attribute.ts +56 -28
- package/src/decorators/require.test.ts +1 -1
- package/src/dom.test.ts +23 -0
- package/src/dom.ts +55 -16
- package/src/emitter.ts +6 -0
- package/src/index.ts +2 -1
- package/src/test.ts +38 -0
- package/src/decorators/connect.test.ts +0 -119
- package/src/decorators/connect.ts +0 -85
package/dist/emitter.d.ts
CHANGED
|
@@ -67,6 +67,8 @@ export declare class EmitterElement extends HTMLElement {
|
|
|
67
67
|
* class MyElement extends EmitterElement {
|
|
68
68
|
* emits!: Emits<"my-event" | "my-other-event";
|
|
69
69
|
* }
|
|
70
|
+
*
|
|
71
|
+
* @category Events
|
|
70
72
|
*/
|
|
71
73
|
emits: {
|
|
72
74
|
[K in keyof HTMLElementEventMap]?: never;
|
|
@@ -75,6 +77,8 @@ export declare class EmitterElement extends HTMLElement {
|
|
|
75
77
|
* Emit a custom event with the given name and detail.
|
|
76
78
|
*
|
|
77
79
|
* Event init options default to `{ bubbles: true, composed: true }`.
|
|
80
|
+
*
|
|
81
|
+
* @category Events
|
|
78
82
|
*/
|
|
79
83
|
emit<M extends CustomEventTypes<keyof this["emits"] & keyof HTMLElementEventMap>, K extends Extract<keyof M, string>, D extends M[K]>(name: K, detail?: D, options?: EventInit): CustomEvent<D>;
|
|
80
84
|
/**
|
|
@@ -86,6 +90,8 @@ export declare class EmitterElement extends HTMLElement {
|
|
|
86
90
|
* @example
|
|
87
91
|
* this.emitEvent("click", MouseEvent, { button: 2 });
|
|
88
92
|
* // emits: new MouseEvent("click", { button: 2 });
|
|
93
|
+
*
|
|
94
|
+
* @category Events
|
|
89
95
|
*/
|
|
90
96
|
emitEvent<K extends keyof this["emits"] & keyof HTMLElementEventMap, E extends HTMLElementEventMap[K], C extends new (type: string, ...args: Args) => E, Args extends Array<any>>(name: K, constructor: C, ...args: Args): E;
|
|
91
97
|
}
|
package/dist/emitter.js
CHANGED
|
@@ -27,6 +27,8 @@ export class EmitterElement extends HTMLElement {
|
|
|
27
27
|
* Emit a custom event with the given name and detail.
|
|
28
28
|
*
|
|
29
29
|
* Event init options default to `{ bubbles: true, composed: true }`.
|
|
30
|
+
*
|
|
31
|
+
* @category Events
|
|
30
32
|
*/
|
|
31
33
|
emit(name, detail, options) {
|
|
32
34
|
const event = new CustomEvent(name, {
|
|
@@ -47,6 +49,8 @@ export class EmitterElement extends HTMLElement {
|
|
|
47
49
|
* @example
|
|
48
50
|
* this.emitEvent("click", MouseEvent, { button: 2 });
|
|
49
51
|
* // emits: new MouseEvent("click", { button: 2 });
|
|
52
|
+
*
|
|
53
|
+
* @category Events
|
|
50
54
|
*/
|
|
51
55
|
emitEvent(name, constructor, ...args) {
|
|
52
56
|
const event = new constructor(name, ...args);
|
package/dist/emitter.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"emitter.js","sourceRoot":"","sources":["../src/emitter.ts"],"names":[],"mappings":"AAuCA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,OAAO,cAAe,SAAQ,WAAW;
|
|
1
|
+
{"version":3,"file":"emitter.js","sourceRoot":"","sources":["../src/emitter.ts"],"names":[],"mappings":"AAuCA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,OAAO,cAAe,SAAQ,WAAW;IAa3C;;;;;;OAMG;IACH,IAAI,CAIF,IAAO,EAAE,MAAU,EAAE,OAAmB;QACtC,MAAM,KAAK,GAAG,IAAI,WAAW,CAAC,IAAI,EAAE;YAChC,OAAO,EAAE,IAAI;YACb,QAAQ,EAAE,IAAI;YACd,MAAM;YACN,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC;SACrB,CAAC,CAAC;QACH,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC1B,OAAO,KAAK,CAAC;IACjB,CAAC;IAED;;;;;;;;;;;OAWG;IACH,SAAS,CAKP,IAAO,EAAE,WAAc,EAAE,GAAG,IAAU;QACpC,MAAM,KAAK,GAAG,IAAI,WAAW,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,CAAC;QAC7C,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC1B,OAAO,KAAK,CAAC;IACjB,CAAC;CACJ"}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,4 +8,5 @@ import * as dom from "./dom";
|
|
|
8
8
|
import * as event from "./event";
|
|
9
9
|
import * as geometry from "./geometry";
|
|
10
10
|
import * as signal from "./signal";
|
|
11
|
-
|
|
11
|
+
import * as test from "./test";
|
|
12
|
+
export { component, css, dom, event, geometry, signal, test };
|
package/dist/index.js
CHANGED
|
@@ -8,5 +8,6 @@ import * as dom from "./dom";
|
|
|
8
8
|
import * as event from "./event";
|
|
9
9
|
import * as geometry from "./geometry";
|
|
10
10
|
import * as signal from "./signal";
|
|
11
|
-
|
|
11
|
+
import * as test from "./test";
|
|
12
|
+
export { component, css, dom, event, geometry, signal, test };
|
|
12
13
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,SAAS,MAAM,aAAa,CAAC;AACzC,OAAO,KAAK,GAAG,MAAM,OAAO,CAAC;AAC7B,OAAO,KAAK,GAAG,MAAM,OAAO,CAAC;AAC7B,OAAO,KAAK,KAAK,MAAM,SAAS,CAAC;AACjC,OAAO,KAAK,QAAQ,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,MAAM,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,SAAS,MAAM,aAAa,CAAC;AACzC,OAAO,KAAK,GAAG,MAAM,OAAO,CAAC;AAC7B,OAAO,KAAK,GAAG,MAAM,OAAO,CAAC;AAC7B,OAAO,KAAK,KAAK,MAAM,SAAS,CAAC;AACjC,OAAO,KAAK,QAAQ,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,MAAM,MAAM,UAAU,CAAC;AACnC,OAAO,KAAK,IAAI,MAAM,QAAQ,CAAC;AAE/B,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC"}
|
package/dist/test.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools for testing web components.
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Render a Lit template and wait for any {@link Component}s inside it to
|
|
7
|
+
* stabilise before returning the {@link HTMLElement} containing the rendered
|
|
8
|
+
* template.
|
|
9
|
+
*
|
|
10
|
+
* The root element is also a {@link Disposable} which will remove itself from
|
|
11
|
+
* the DOM and dispose its {@link Component}s when disposed.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* using root = await testHTML(html`<my-component></my-component>`);
|
|
15
|
+
* expect(root.querySelector("my-component")).toBeInstanceOf(MyComponent);
|
|
16
|
+
*/
|
|
17
|
+
export declare function testHTML(template: unknown): Promise<HTMLElement & Disposable>;
|
package/dist/test.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools for testing web components.
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
import { render } from "lit";
|
|
6
|
+
import { Component } from "./component";
|
|
7
|
+
/**
|
|
8
|
+
* Render a Lit template and wait for any {@link Component}s inside it to
|
|
9
|
+
* stabilise before returning the {@link HTMLElement} containing the rendered
|
|
10
|
+
* template.
|
|
11
|
+
*
|
|
12
|
+
* The root element is also a {@link Disposable} which will remove itself from
|
|
13
|
+
* the DOM and dispose its {@link Component}s when disposed.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* using root = await testHTML(html`<my-component></my-component>`);
|
|
17
|
+
* expect(root.querySelector("my-component")).toBeInstanceOf(MyComponent);
|
|
18
|
+
*/
|
|
19
|
+
export async function testHTML(template) {
|
|
20
|
+
const root = document.createElement("section");
|
|
21
|
+
document.body.append(root);
|
|
22
|
+
render(template, root, { host: root });
|
|
23
|
+
root[Symbol.dispose] = () => {
|
|
24
|
+
Iterator.from(root.children)
|
|
25
|
+
.filter((el) => el instanceof Component)
|
|
26
|
+
.forEach((comp) => comp[Symbol.dispose]());
|
|
27
|
+
root.remove();
|
|
28
|
+
};
|
|
29
|
+
await Promise.all(Iterator.from(root.children)
|
|
30
|
+
.filter((el) => el instanceof Component)
|
|
31
|
+
.map((el) => el.hasStabilised));
|
|
32
|
+
return root;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=test.js.map
|
package/dist/test.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test.js","sourceRoot":"","sources":["../src/test.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,KAAK,CAAC;AAE7B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,QAAiB;IAC5C,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,SAAS,CAA6B,CAAC;IAC3E,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC3B,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IACvC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,GAAG,EAAE;QACxB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC;aACvB,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,YAAY,SAAS,CAAC;aACvC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC/C,IAAI,CAAC,MAAM,EAAE,CAAC;IAClB,CAAC,CAAC;IACF,MAAM,OAAO,CAAC,GAAG,CACb,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC;SACvB,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,YAAY,SAAS,CAAC;SACvC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,aAAa,CAAC,CACrC,CAAC;IACF,OAAO,IAAI,CAAC;AAChB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bodil/dom",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "DOM and web component tools",
|
|
5
5
|
"homepage": "https://codeberg.org/bodil/dom",
|
|
6
6
|
"repository": {
|
|
@@ -56,40 +56,46 @@
|
|
|
56
56
|
"types": "./dist/signal.d.ts",
|
|
57
57
|
"import": "./dist/signal.js"
|
|
58
58
|
}
|
|
59
|
+
},
|
|
60
|
+
"./test": {
|
|
61
|
+
"import": {
|
|
62
|
+
"types": "./dist/test.d.ts",
|
|
63
|
+
"import": "./dist/test.js"
|
|
64
|
+
}
|
|
59
65
|
}
|
|
60
66
|
},
|
|
61
67
|
"publishConfig": {
|
|
62
68
|
"access": "public"
|
|
63
69
|
},
|
|
64
70
|
"dependencies": {
|
|
65
|
-
"@bodil/core": "^0.5.
|
|
66
|
-
"@bodil/opt": "^0.
|
|
67
|
-
"@bodil/signal": "^0.5.
|
|
71
|
+
"@bodil/core": "^0.5.3",
|
|
72
|
+
"@bodil/opt": "^1.0.0",
|
|
73
|
+
"@bodil/signal": "^0.5.2",
|
|
68
74
|
"lit": "^3.3.2",
|
|
69
|
-
"type-fest": "^5.
|
|
75
|
+
"type-fest": "^5.4.4"
|
|
70
76
|
},
|
|
71
77
|
"devDependencies": {
|
|
72
78
|
"@eslint/eslintrc": "^3.3.3",
|
|
73
|
-
"@eslint/js": "^
|
|
74
|
-
"@ianvs/prettier-plugin-sort-imports": "^4.7.
|
|
75
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
76
|
-
"@typescript-eslint/parser": "^8.
|
|
79
|
+
"@eslint/js": "^10.0.1",
|
|
80
|
+
"@ianvs/prettier-plugin-sort-imports": "^4.7.1",
|
|
81
|
+
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
|
82
|
+
"@typescript-eslint/parser": "^8.56.0",
|
|
77
83
|
"@typhonjs-typedoc/ts-lib-docs": "^2024.12.25",
|
|
78
|
-
"@vitest/browser-playwright": "^4.0.
|
|
79
|
-
"@vitest/coverage-v8": "^4.0.
|
|
80
|
-
"eslint": "^
|
|
84
|
+
"@vitest/browser-playwright": "^4.0.18",
|
|
85
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
86
|
+
"eslint": "^10.0.1",
|
|
81
87
|
"eslint-config-prettier": "^10.1.8",
|
|
82
|
-
"eslint-plugin-jsdoc": "^
|
|
83
|
-
"globals": "^17.
|
|
84
|
-
"happy-dom": "^20.0
|
|
88
|
+
"eslint-plugin-jsdoc": "^62.7.0",
|
|
89
|
+
"globals": "^17.3.0",
|
|
90
|
+
"happy-dom": "^20.7.0",
|
|
85
91
|
"npm-run-all2": "^8.0.4",
|
|
86
|
-
"prettier": "^3.
|
|
87
|
-
"typedoc": "^0.28.
|
|
92
|
+
"prettier": "^3.8.1",
|
|
93
|
+
"typedoc": "^0.28.17",
|
|
88
94
|
"typedoc-plugin-extras": "^4.0.1",
|
|
89
|
-
"typedoc-plugin-mdn-links": "^5.
|
|
95
|
+
"typedoc-plugin-mdn-links": "^5.1.1",
|
|
90
96
|
"typedoc-theme-fresh": "^0.2.3",
|
|
91
97
|
"typescript": "^5.9.3",
|
|
92
|
-
"vitest": "^4.0.
|
|
98
|
+
"vitest": "^4.0.18"
|
|
93
99
|
},
|
|
94
100
|
"prettier": {
|
|
95
101
|
"editorconfig": true,
|
package/src/component.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @module
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { isNullish, present } from "@bodil/core/assert";
|
|
7
7
|
import { DisposableContext, toDisposable, type Disposifiable } from "@bodil/core/disposable";
|
|
8
8
|
import { Signal } from "@bodil/signal";
|
|
9
9
|
import {
|
|
@@ -29,10 +29,9 @@ import {
|
|
|
29
29
|
toAttribute,
|
|
30
30
|
type AttributeConfig,
|
|
31
31
|
} from "./decorators/attribute";
|
|
32
|
-
import { connectedJobs } from "./decorators/connect";
|
|
33
32
|
import { reactiveFields, signalForObject } from "./decorators/reactive";
|
|
34
33
|
import { requiredProperties } from "./decorators/require";
|
|
35
|
-
import {
|
|
34
|
+
import { findDescendants } from "./dom";
|
|
36
35
|
import { EmitterElement } from "./emitter";
|
|
37
36
|
import { eventListener } from "./event";
|
|
38
37
|
import { scheduler } from "./scheduler";
|
|
@@ -54,12 +53,6 @@ export {
|
|
|
54
53
|
type AttributeGetterSetterOptions,
|
|
55
54
|
type AttributeType,
|
|
56
55
|
} from "./decorators/attribute";
|
|
57
|
-
export {
|
|
58
|
-
connect,
|
|
59
|
-
connectEffect,
|
|
60
|
-
type ConnectFunction,
|
|
61
|
-
type ConnectFunctionReturnValue,
|
|
62
|
-
} from "./decorators/connect";
|
|
63
56
|
export { reactive } from "./decorators/reactive";
|
|
64
57
|
export { require } from "./decorators/require";
|
|
65
58
|
export { EmitterElement } from "./emitter";
|
|
@@ -74,6 +67,8 @@ export type UpdateConfig = {
|
|
|
74
67
|
|
|
75
68
|
const finalised = Symbol("finalised");
|
|
76
69
|
|
|
70
|
+
export type Disposables = Disposifiable | Iterable<Disposifiable | undefined> | undefined | void;
|
|
71
|
+
|
|
77
72
|
export type Deps = Array<typeof HTMLElement>;
|
|
78
73
|
|
|
79
74
|
export type CSSStyleSpec = CSSResult | CSSStyleSheet | string;
|
|
@@ -112,6 +107,8 @@ export abstract class Component
|
|
|
112
107
|
* class MyComponent extends Component {
|
|
113
108
|
* static deps: Deps = [ MySubcomponent, MyOtherSubcomponent ];
|
|
114
109
|
* }
|
|
110
|
+
*
|
|
111
|
+
* @category Declarations
|
|
115
112
|
*/
|
|
116
113
|
static deps: Deps = [];
|
|
117
114
|
|
|
@@ -132,9 +129,14 @@ export abstract class Component
|
|
|
132
129
|
* }
|
|
133
130
|
* `;
|
|
134
131
|
* }
|
|
132
|
+
*
|
|
133
|
+
* @category Declarations
|
|
135
134
|
*/
|
|
136
135
|
static styles?: CSSStyleSpecDeclaration;
|
|
137
136
|
|
|
137
|
+
/**
|
|
138
|
+
* @category Advanced
|
|
139
|
+
*/
|
|
138
140
|
static shadowRootOptions: ShadowRootInit = { mode: "open" };
|
|
139
141
|
|
|
140
142
|
private static initialisers = new Set<() => void>();
|
|
@@ -147,11 +149,20 @@ export abstract class Component
|
|
|
147
149
|
protected static requiredProperties = new Set<string | symbol>();
|
|
148
150
|
private static [finalised] = true;
|
|
149
151
|
|
|
152
|
+
/**
|
|
153
|
+
* @category Advanced
|
|
154
|
+
*/
|
|
150
155
|
renderOptions: RenderOptions = { host: this };
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* {@inheritDoc EmitterElement.emits}
|
|
159
|
+
* @category Declarations
|
|
160
|
+
*/
|
|
151
161
|
emits!: object;
|
|
152
162
|
|
|
153
163
|
readonly #controllers = new Set<ReactiveController>();
|
|
154
164
|
readonly #connectedContext = new DisposableContext();
|
|
165
|
+
readonly #finalCleanupContext = new DisposableContext();
|
|
155
166
|
#dirty = false;
|
|
156
167
|
#isUpdatePending = false;
|
|
157
168
|
#updateSignal?: Signal.Computed<void>;
|
|
@@ -164,15 +175,23 @@ export abstract class Component
|
|
|
164
175
|
#viewTransitionRequested = false;
|
|
165
176
|
#ignoreAttributeUpdates: number;
|
|
166
177
|
|
|
178
|
+
/**
|
|
179
|
+
* @category Advanced
|
|
180
|
+
*/
|
|
167
181
|
readonly renderRoot: ShadowRoot | HTMLElement = this.createRenderRoot();
|
|
168
182
|
|
|
169
183
|
/**
|
|
170
184
|
* True if this component had scheduled an update which has not yet started.
|
|
185
|
+
*
|
|
186
|
+
* @category Render
|
|
171
187
|
*/
|
|
172
188
|
get isUpdatePending(): boolean {
|
|
173
189
|
return this.#isUpdatePending;
|
|
174
190
|
}
|
|
175
191
|
|
|
192
|
+
/**
|
|
193
|
+
* @category Render
|
|
194
|
+
*/
|
|
176
195
|
get updateComplete(): Promise<boolean> {
|
|
177
196
|
return this.#updateResult.promise;
|
|
178
197
|
}
|
|
@@ -184,6 +203,8 @@ export abstract class Component
|
|
|
184
203
|
*
|
|
185
204
|
* By default, a component considers itself stabilised when all of its
|
|
186
205
|
* children which are also {@link Component}s are reporting as stabilised.
|
|
206
|
+
*
|
|
207
|
+
* @category Render
|
|
187
208
|
*/
|
|
188
209
|
get hasStabilised(): Promise<void> {
|
|
189
210
|
return this.#stabilised.promise;
|
|
@@ -196,6 +217,8 @@ export abstract class Component
|
|
|
196
217
|
* This can be passed along to asynchronous tasks such as {@link fetch}
|
|
197
218
|
* initiated by this component, which will then be aborted automatically if
|
|
198
219
|
* the component is removed from the DOM.
|
|
220
|
+
*
|
|
221
|
+
* @category Advanced
|
|
199
222
|
*/
|
|
200
223
|
get abortSignal(): AbortSignal {
|
|
201
224
|
return this.#abortController.signal;
|
|
@@ -205,6 +228,8 @@ export abstract class Component
|
|
|
205
228
|
/**
|
|
206
229
|
* Register a function which will be executed whenever an instance of this
|
|
207
230
|
* class is constructed.
|
|
231
|
+
*
|
|
232
|
+
* @category Advanced
|
|
208
233
|
*/
|
|
209
234
|
static addInitialiser(init: (this: Component) => void) {
|
|
210
235
|
if (!Object.hasOwn(this, "initialisers")) {
|
|
@@ -307,27 +332,12 @@ export abstract class Component
|
|
|
307
332
|
}
|
|
308
333
|
}
|
|
309
334
|
|
|
310
|
-
|
|
311
|
-
* This lifecycle callback is called when the component is attached to the
|
|
312
|
-
* DOM.
|
|
313
|
-
*
|
|
314
|
-
* Generally, prefer using methods with @{@link connect} decorators to
|
|
315
|
-
* overriding this method.
|
|
316
|
-
*/
|
|
317
|
-
protected connectedCallback() {
|
|
335
|
+
private connectedCallback() {
|
|
318
336
|
this.#connectedContext.dispose();
|
|
319
337
|
this.#controllers.forEach((c) => c.hostConnected?.());
|
|
320
338
|
this.#rootPart?.setConnected(true);
|
|
321
339
|
|
|
322
|
-
|
|
323
|
-
const result = job.call(this);
|
|
324
|
-
const items = isNullish(result)
|
|
325
|
-
? Iterator.from<Disposifiable>([])
|
|
326
|
-
: isIterable(result)
|
|
327
|
-
? Iterator.from(result)
|
|
328
|
-
: Iterator.from([result]);
|
|
329
|
-
items.filter((i) => i !== undefined).forEach(this.useWhileConnected.bind(this));
|
|
330
|
-
}
|
|
340
|
+
this.connected();
|
|
331
341
|
|
|
332
342
|
this.useWhileConnected(
|
|
333
343
|
Signal.effect(() => {
|
|
@@ -345,19 +355,13 @@ export abstract class Component
|
|
|
345
355
|
this.requestUpdate();
|
|
346
356
|
}
|
|
347
357
|
|
|
348
|
-
|
|
349
|
-
* This lifecycle callback is called when the component is removed from the
|
|
350
|
-
* DOM.
|
|
351
|
-
*
|
|
352
|
-
* Generally, prefer using methods with @{@link connect} decorators to
|
|
353
|
-
* overriding this method.
|
|
354
|
-
*/
|
|
355
|
-
protected disconnectedCallback() {
|
|
358
|
+
private disconnectedCallback() {
|
|
356
359
|
if (this.#updateSignal !== undefined) {
|
|
357
360
|
this.#updateSignalWatcher.unwatch(this.#updateSignal);
|
|
358
361
|
this.#updateSignal = undefined;
|
|
359
362
|
}
|
|
360
363
|
this.#isUpdatePending = false;
|
|
364
|
+
this.disconnected();
|
|
361
365
|
this.#abortController.abort(
|
|
362
366
|
new DOMException(`${this.tagName} element disconnected`, "AbortError"),
|
|
363
367
|
);
|
|
@@ -394,7 +398,10 @@ export abstract class Component
|
|
|
394
398
|
const attrConfig = (this.constructor as typeof Component).attributeConfig.get(name);
|
|
395
399
|
if (attrConfig !== undefined) {
|
|
396
400
|
const value = fromAttribute(valueString, attrConfig.type);
|
|
397
|
-
|
|
401
|
+
try {
|
|
402
|
+
this[attrConfig.property as keyof this] = value as any;
|
|
403
|
+
// Silently swallow any write errors.
|
|
404
|
+
} catch (_e) {}
|
|
398
405
|
}
|
|
399
406
|
}
|
|
400
407
|
|
|
@@ -410,6 +417,11 @@ export abstract class Component
|
|
|
410
417
|
}) as Signal.Computed<V>;
|
|
411
418
|
}
|
|
412
419
|
|
|
420
|
+
/**
|
|
421
|
+
* Adds a controller to the host, which sets up the controller's lifecycle
|
|
422
|
+
* methods to be called with the host's lifecycle.
|
|
423
|
+
* @category Advanced
|
|
424
|
+
*/
|
|
413
425
|
addController(controller: ReactiveController) {
|
|
414
426
|
this.#controllers.add(controller);
|
|
415
427
|
if (this.renderRoot !== undefined && this.isConnected) {
|
|
@@ -417,6 +429,10 @@ export abstract class Component
|
|
|
417
429
|
}
|
|
418
430
|
}
|
|
419
431
|
|
|
432
|
+
/**
|
|
433
|
+
* Removes a controller from the host.
|
|
434
|
+
* @category Advanced
|
|
435
|
+
*/
|
|
420
436
|
removeController(controller: ReactiveController) {
|
|
421
437
|
this.#controllers.delete(controller);
|
|
422
438
|
}
|
|
@@ -430,6 +446,8 @@ export abstract class Component
|
|
|
430
446
|
* If you don't want to use a shadow DOM, you can override this method to
|
|
431
447
|
* just `return this`, which causes the component's contents to render as
|
|
432
448
|
* direct children of the component itself.
|
|
449
|
+
*
|
|
450
|
+
* @category Advanced
|
|
433
451
|
*/
|
|
434
452
|
protected createRenderRoot(): HTMLElement | ShadowRoot {
|
|
435
453
|
const renderRoot =
|
|
@@ -440,8 +458,27 @@ export abstract class Component
|
|
|
440
458
|
return renderRoot;
|
|
441
459
|
}
|
|
442
460
|
|
|
461
|
+
/**
|
|
462
|
+
* Add a style sheet to the component's shadow root.
|
|
463
|
+
*
|
|
464
|
+
* @category Advanced
|
|
465
|
+
*/
|
|
466
|
+
addStyleSheet(spec: CSSStyleSpecDeclaration) {
|
|
467
|
+
if (!(this.renderRoot instanceof ShadowRoot)) {
|
|
468
|
+
throw new Error("Component needs a shadow root in order to add a style sheet");
|
|
469
|
+
}
|
|
470
|
+
const sheets = (
|
|
471
|
+
Array.isArray(spec)
|
|
472
|
+
? ((spec as Array<unknown>).flat(Infinity) as Array<CSSStyleSpec>)
|
|
473
|
+
: [spec]
|
|
474
|
+
).map(processCSSStyleSpec);
|
|
475
|
+
adoptStyles(this.renderRoot, sheets);
|
|
476
|
+
}
|
|
477
|
+
|
|
443
478
|
/**
|
|
444
479
|
* Ask this component to update itself.
|
|
480
|
+
*
|
|
481
|
+
* @category Render
|
|
445
482
|
*/
|
|
446
483
|
requestUpdate(opts?: UpdateConfig) {
|
|
447
484
|
this.#dirty = true;
|
|
@@ -466,9 +503,7 @@ export abstract class Component
|
|
|
466
503
|
() => {
|
|
467
504
|
return requiredProperties.size === 0
|
|
468
505
|
? true
|
|
469
|
-
: requiredProperties
|
|
470
|
-
.keys()
|
|
471
|
-
.every((key) => (this as any)[key] !== undefined);
|
|
506
|
+
: requiredProperties.keys().every((key) => !isNullish((this as any)[key]));
|
|
472
507
|
},
|
|
473
508
|
// every signal update is relevant, even if the signal value doesn't
|
|
474
509
|
// change, because the required values probably did.
|
|
@@ -553,16 +588,13 @@ export abstract class Component
|
|
|
553
588
|
/**
|
|
554
589
|
* Return an iterator over this component's children which are also
|
|
555
590
|
* {@link Component}s.
|
|
591
|
+
*
|
|
592
|
+
* @category Queries
|
|
556
593
|
*/
|
|
557
594
|
protected findChildComponents(
|
|
558
595
|
root: Element | ShadowRoot = this.renderRoot,
|
|
559
596
|
): IteratorObject<Component> {
|
|
560
|
-
|
|
561
|
-
const top = [...all];
|
|
562
|
-
for (const el of top) {
|
|
563
|
-
all = [...all, ...this.findChildComponents(el)];
|
|
564
|
-
}
|
|
565
|
-
return Iterator.from(all).filter((el) => el instanceof Component);
|
|
597
|
+
return findDescendants(root, (el) => el instanceof Component);
|
|
566
598
|
}
|
|
567
599
|
|
|
568
600
|
/**
|
|
@@ -571,6 +603,8 @@ export abstract class Component
|
|
|
571
603
|
*
|
|
572
604
|
* The default implementation waits for any children which are also
|
|
573
605
|
* {@link Component}s to report that they have also stabilised.
|
|
606
|
+
*
|
|
607
|
+
* @category Render
|
|
574
608
|
*/
|
|
575
609
|
protected async stabilise(): Promise<void> {
|
|
576
610
|
// wait for children to stabilise
|
|
@@ -579,8 +613,27 @@ export abstract class Component
|
|
|
579
613
|
await Promise.all(children.map((child) => child.hasStabilised)).catch(console.error);
|
|
580
614
|
}
|
|
581
615
|
|
|
616
|
+
/**
|
|
617
|
+
* This lifecycle callback is called each time this component is connected
|
|
618
|
+
* to the DOM.
|
|
619
|
+
* @category Lifecycle
|
|
620
|
+
*/
|
|
621
|
+
protected connected(): void {
|
|
622
|
+
//
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* This lifecycle callback is called each time this component is
|
|
627
|
+
* disconnected from the DOM.
|
|
628
|
+
* @category Lifecycle
|
|
629
|
+
*/
|
|
630
|
+
protected disconnected(): void {
|
|
631
|
+
//
|
|
632
|
+
}
|
|
633
|
+
|
|
582
634
|
/**
|
|
583
635
|
* This lifecycle callback is called each time an update has completed.
|
|
636
|
+
* @category Lifecycle
|
|
584
637
|
*/
|
|
585
638
|
protected updated(): void {
|
|
586
639
|
//
|
|
@@ -589,6 +642,7 @@ export abstract class Component
|
|
|
589
642
|
/**
|
|
590
643
|
* This lifecycle callback is called after the component's first update has
|
|
591
644
|
* completed, and before {@link Component.updated}.
|
|
645
|
+
* @category Lifecycle
|
|
592
646
|
*/
|
|
593
647
|
protected firstUpdated() {
|
|
594
648
|
//
|
|
@@ -599,6 +653,7 @@ export abstract class Component
|
|
|
599
653
|
* stabilised after its first update.
|
|
600
654
|
*
|
|
601
655
|
* @see {@link Component.hasStabilised}
|
|
656
|
+
* @category Lifecycle
|
|
602
657
|
*/
|
|
603
658
|
protected stabilised() {
|
|
604
659
|
//
|
|
@@ -608,6 +663,7 @@ export abstract class Component
|
|
|
608
663
|
* This lifecycle callback is called every time a property decorated with
|
|
609
664
|
* the @{@link require} decorator has changed, but only when every property
|
|
610
665
|
* marked as such is not `undefined`.
|
|
666
|
+
* @category Lifecycle
|
|
611
667
|
*/
|
|
612
668
|
protected initialised() {
|
|
613
669
|
//
|
|
@@ -617,6 +673,7 @@ export abstract class Component
|
|
|
617
673
|
* This lifecycle callback is called the first time every property decorated
|
|
618
674
|
* with @{@link require} has been defined, and before
|
|
619
675
|
* {@link Component.initialised}.
|
|
676
|
+
* @category Lifecycle
|
|
620
677
|
*/
|
|
621
678
|
protected firstInitialised() {
|
|
622
679
|
//
|
|
@@ -637,19 +694,42 @@ export abstract class Component
|
|
|
637
694
|
* `;
|
|
638
695
|
* }
|
|
639
696
|
* }
|
|
697
|
+
*
|
|
698
|
+
* @category Render
|
|
640
699
|
*/
|
|
641
700
|
protected render(): unknown {
|
|
642
701
|
return nothing;
|
|
643
702
|
}
|
|
644
703
|
|
|
645
|
-
|
|
704
|
+
/** @internal */
|
|
705
|
+
[Symbol.dispose](): void {
|
|
646
706
|
for (const child of this.findChildComponents()) {
|
|
647
707
|
child[Symbol.dispose]();
|
|
648
708
|
}
|
|
709
|
+
this.remove();
|
|
649
710
|
this.disconnectedCallback();
|
|
650
711
|
(this.#rootPart as any)?._$clear?.();
|
|
651
712
|
this.#rootPart = undefined;
|
|
652
|
-
this.
|
|
713
|
+
this.#finalCleanupContext.dispose();
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Register a {@link Disposable} for automatic disposal when this component
|
|
718
|
+
* is disposed.
|
|
719
|
+
* @category Resources
|
|
720
|
+
*/
|
|
721
|
+
use(disposifiable: undefined): undefined;
|
|
722
|
+
use(disposifiable: null): null;
|
|
723
|
+
use(disposifiable: Disposifiable): Disposable;
|
|
724
|
+
use(disposifiable: Disposifiable | undefined): Disposable | undefined;
|
|
725
|
+
use(disposifiable: Disposifiable | null | undefined): Disposable | null | undefined;
|
|
726
|
+
use(disposifiable: Disposifiable | null | undefined): Disposable | null | undefined {
|
|
727
|
+
if (isNullish(disposifiable)) {
|
|
728
|
+
return disposifiable;
|
|
729
|
+
}
|
|
730
|
+
const disposable = toDisposable(disposifiable);
|
|
731
|
+
this.#finalCleanupContext.use(disposable);
|
|
732
|
+
return disposable;
|
|
653
733
|
}
|
|
654
734
|
|
|
655
735
|
/**
|
|
@@ -667,7 +747,13 @@ export abstract class Component
|
|
|
667
747
|
* super.connectedCallback();
|
|
668
748
|
* this.useWhileConnected(this.on("click", this.handleClick.bind(this)));
|
|
669
749
|
* }
|
|
750
|
+
* @category Resources
|
|
670
751
|
*/
|
|
752
|
+
useWhileConnected(
|
|
753
|
+
disposifiable: Disposifiable,
|
|
754
|
+
secondDisposable: Disposifiable,
|
|
755
|
+
...disposifiables: Array<Disposifiable>
|
|
756
|
+
): Array<Disposable>;
|
|
671
757
|
useWhileConnected(disposifiable: undefined): undefined;
|
|
672
758
|
useWhileConnected(disposifiable: null): null;
|
|
673
759
|
useWhileConnected(disposifiable: Disposifiable): Disposable;
|
|
@@ -677,7 +763,15 @@ export abstract class Component
|
|
|
677
763
|
): Disposable | null | undefined;
|
|
678
764
|
useWhileConnected(
|
|
679
765
|
disposifiable: Disposifiable | null | undefined,
|
|
680
|
-
|
|
766
|
+
...disposifiables: Array<Disposifiable>
|
|
767
|
+
): Array<Disposable> | Disposable | null | undefined {
|
|
768
|
+
if (disposifiables.length > 0) {
|
|
769
|
+
return [disposifiable, ...disposifiables].map((d) => {
|
|
770
|
+
const disposable = toDisposable(present(d));
|
|
771
|
+
this.#connectedContext.use(disposable);
|
|
772
|
+
return disposable;
|
|
773
|
+
});
|
|
774
|
+
}
|
|
681
775
|
if (isNullish(disposifiable)) {
|
|
682
776
|
return disposifiable;
|
|
683
777
|
}
|
|
@@ -690,6 +784,7 @@ export abstract class Component
|
|
|
690
784
|
* Attach an event listener to an event on this component.
|
|
691
785
|
*
|
|
692
786
|
* @returns A {@link Disposable} to subsequently detach the event listener.
|
|
787
|
+
* @category Events
|
|
693
788
|
*/
|
|
694
789
|
on<K extends keyof HTMLElementEventMap>(
|
|
695
790
|
type: K,
|
|
@@ -703,6 +798,7 @@ export abstract class Component
|
|
|
703
798
|
* If an element that is either inside this element's shadow root or is a
|
|
704
799
|
* child of this element (ie. slotted) currently has focus, return that
|
|
705
800
|
* element. Otherwise, return null.
|
|
801
|
+
* @category Queries
|
|
706
802
|
*/
|
|
707
803
|
getFocusedElement(): Element | null {
|
|
708
804
|
return this.shadowRoot?.activeElement ?? this.querySelector(":focus");
|
|
@@ -728,6 +824,7 @@ export abstract class Component
|
|
|
728
824
|
* return html`<button>I am a button</button>`;
|
|
729
825
|
* }
|
|
730
826
|
* }
|
|
827
|
+
* @category Queries
|
|
731
828
|
*/
|
|
732
829
|
query<El extends keyof HTMLElementTagNameMap>(selector: El): HTMLElementTagNameMap[El] | null;
|
|
733
830
|
query<El extends keyof SVGElementTagNameMap>(selector: El): SVGElementTagNameMap[El] | null;
|
|
@@ -766,6 +863,7 @@ export abstract class Component
|
|
|
766
863
|
* `;
|
|
767
864
|
* }
|
|
768
865
|
* }
|
|
866
|
+
* @category Queries
|
|
769
867
|
*/
|
|
770
868
|
queryAll<El extends keyof HTMLElementTagNameMap>(
|
|
771
869
|
selector: El,
|
|
@@ -791,6 +889,7 @@ export abstract class Component
|
|
|
791
889
|
*
|
|
792
890
|
* If you include `reactive: true` in your query, the result will be a
|
|
793
891
|
* signal which updates with the contents of the slot.
|
|
892
|
+
* @category Queries
|
|
794
893
|
*/
|
|
795
894
|
querySlot(
|
|
796
895
|
options: QuerySlotOptions & { nodes: true; reactive: true },
|