@bodil/dom 0.1.4 → 0.1.5
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/package.json +9 -7
- package/src/component.test.ts +13 -10
- package/src/component.ts +23 -1
- package/src/decorators/attribute.test.ts +57 -8
- package/src/decorators/attribute.ts +14 -13
- package/src/decorators/require.test.ts +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bodil/dom",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "DOM and web component tools",
|
|
5
5
|
"homepage": "https://codeberg.org/bodil/dom",
|
|
6
6
|
"repository": {
|
|
@@ -59,16 +59,17 @@
|
|
|
59
59
|
"@bodil/core": "^0.4.9",
|
|
60
60
|
"@bodil/opt": "^0.4.2",
|
|
61
61
|
"@bodil/signal": "^0.3.3",
|
|
62
|
-
"lit": "^3.3.
|
|
62
|
+
"lit": "^3.3.2",
|
|
63
63
|
"type-fest": "^5.3.1"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@eslint/eslintrc": "^3.3.3",
|
|
67
67
|
"@eslint/js": "^9.39.2",
|
|
68
68
|
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
|
|
69
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
70
|
-
"@typescript-eslint/parser": "^8.
|
|
71
|
-
"@vitest/
|
|
69
|
+
"@typescript-eslint/eslint-plugin": "^8.51.0",
|
|
70
|
+
"@typescript-eslint/parser": "^8.51.0",
|
|
71
|
+
"@vitest/browser-playwright": "^4.0.16",
|
|
72
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
72
73
|
"eslint": "^9.39.2",
|
|
73
74
|
"eslint-config-prettier": "^10.1.8",
|
|
74
75
|
"eslint-plugin-jsdoc": "^54.7.0",
|
|
@@ -80,7 +81,7 @@
|
|
|
80
81
|
"typedoc-plugin-extras": "^4.0.1",
|
|
81
82
|
"typedoc-plugin-mdn-links": "^5.0.10",
|
|
82
83
|
"typescript": "^5.9.3",
|
|
83
|
-
"vitest": "^4.0.
|
|
84
|
+
"vitest": "^4.0.16"
|
|
84
85
|
},
|
|
85
86
|
"prettier": {
|
|
86
87
|
"editorconfig": true,
|
|
@@ -106,11 +107,12 @@
|
|
|
106
107
|
"scripts": {
|
|
107
108
|
"build": "tsc",
|
|
108
109
|
"test": "vitest --run",
|
|
110
|
+
"test:browser": "vitest --run --browser",
|
|
109
111
|
"check:eslint": "eslint src",
|
|
110
112
|
"check:tsc": "tsc --noEmit",
|
|
111
113
|
"check": "run-p check:tsc check:eslint",
|
|
112
114
|
"doc": "typedoc",
|
|
113
115
|
"doc:readthedocs": "typedoc --out $READTHEDOCS_OUTPUT/html",
|
|
114
|
-
"prepublish": "
|
|
116
|
+
"prepublish": "run-p check test:browser"
|
|
115
117
|
}
|
|
116
118
|
}
|
package/src/component.test.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sleep } from "@bodil/core/async";
|
|
1
2
|
import type { Signal } from "@bodil/signal";
|
|
2
3
|
import { html } from "lit";
|
|
3
4
|
import { customElement } from "lit/decorators.js";
|
|
@@ -63,28 +64,30 @@ test("Component.querySlot", async () => {
|
|
|
63
64
|
|
|
64
65
|
await t.updateComplete;
|
|
65
66
|
|
|
66
|
-
expect(t.querySlot()).
|
|
67
|
-
expect(t.querySlot({ slot: "fixes-the-bug" })).
|
|
67
|
+
expect(t.querySlot()).toEqual([joe, mike]);
|
|
68
|
+
expect(t.querySlot({ slot: "fixes-the-bug" })).toEqual([robert]);
|
|
68
69
|
|
|
69
70
|
const slot = t.querySlot({ slot: "fixes-the-bug", reactive: true });
|
|
70
71
|
expectTypeOf(slot).toEqualTypeOf<Signal.Computed<Array<Element>>>();
|
|
71
|
-
expect(slot.get()).
|
|
72
|
+
expect(slot.get()).toEqual([robert]);
|
|
72
73
|
joe.slot = "fixes-the-bug"; // for argument's sake
|
|
73
|
-
|
|
74
|
-
expect(
|
|
74
|
+
await sleep(1);
|
|
75
|
+
expect(slot.get()).toEqual([joe, robert]);
|
|
76
|
+
expect(t.querySlot()).toEqual([mike]);
|
|
75
77
|
robert.removeAttribute("slot");
|
|
76
|
-
|
|
77
|
-
expect(
|
|
78
|
+
await sleep(1);
|
|
79
|
+
expect(slot.get()).toEqual([joe]);
|
|
80
|
+
expect(t.querySlot()).toEqual([mike, robert]);
|
|
78
81
|
|
|
79
82
|
const divsByInstance = t.querySlot({ instanceOf: HTMLDivElement });
|
|
80
83
|
expectTypeOf(divsByInstance).toEqualTypeOf<Array<HTMLDivElement>>();
|
|
81
|
-
expect(divsByInstance).
|
|
84
|
+
expect(divsByInstance).toEqual([mike, robert]);
|
|
82
85
|
|
|
83
86
|
const divsBySelector = t.querySlot({ selector: "div" });
|
|
84
87
|
expectTypeOf(divsBySelector).toEqualTypeOf<Array<HTMLDivElement>>();
|
|
85
|
-
expect(divsBySelector).
|
|
88
|
+
expect(divsBySelector).toEqual([mike, robert]);
|
|
86
89
|
|
|
87
|
-
expect(t.querySlot({ selector: "div.robert" })).
|
|
90
|
+
expect(t.querySlot({ selector: "div.robert" })).toEqual([robert]);
|
|
88
91
|
|
|
89
92
|
expectTypeOf(t.querySlot({ nodes: true })).toEqualTypeOf<Array<Node>>();
|
|
90
93
|
});
|
package/src/component.ts
CHANGED
|
@@ -16,7 +16,12 @@ import {
|
|
|
16
16
|
} from "lit";
|
|
17
17
|
import type { Constructor } from "type-fest";
|
|
18
18
|
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
attributeConfig,
|
|
21
|
+
fromAttribute,
|
|
22
|
+
toAttribute,
|
|
23
|
+
type AttributeConfig,
|
|
24
|
+
} from "./decorators/attribute";
|
|
20
25
|
import { connectedJobs } from "./decorators/connect";
|
|
21
26
|
import { reactiveFields, signalForObject } from "./decorators/reactive";
|
|
22
27
|
import { requiredProperties } from "./decorators/require";
|
|
@@ -206,6 +211,23 @@ export abstract class Component
|
|
|
206
211
|
|
|
207
212
|
this.#ignoreAttributeUpdates--;
|
|
208
213
|
this.#initialised = false;
|
|
214
|
+
|
|
215
|
+
// schedule a sync of all reflecting attributes
|
|
216
|
+
const reflectingAttributes = (this.constructor as typeof Component).attributeConfig
|
|
217
|
+
.values()
|
|
218
|
+
.filter((attr) => attr.reflect)
|
|
219
|
+
.toArray();
|
|
220
|
+
if (reflectingAttributes.length > 0) {
|
|
221
|
+
queueMicrotask(() => {
|
|
222
|
+
for (const attr of reflectingAttributes) {
|
|
223
|
+
const value = toAttribute((this as any)[attr.property], attr.type);
|
|
224
|
+
const current = this.getAttribute(attr.name);
|
|
225
|
+
if (value !== current) {
|
|
226
|
+
this.setAttributeQuietly(attr.name, value);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
209
231
|
}
|
|
210
232
|
|
|
211
233
|
protected connectedCallback() {
|
|
@@ -4,6 +4,7 @@ import { expect, test } from "vitest";
|
|
|
4
4
|
import { attribute, Component } from "../component";
|
|
5
5
|
import { Signal } from "@bodil/signal";
|
|
6
6
|
import { attributeGetter, attributeSetter } from "./attribute";
|
|
7
|
+
import { html, nothing } from "lit";
|
|
7
8
|
|
|
8
9
|
test("@attribute", async () => {
|
|
9
10
|
@customElement("attribute-test-class")
|
|
@@ -84,20 +85,19 @@ test("@attribute getter/setter", async () => {
|
|
|
84
85
|
@customElement("attribute-getter-test-class")
|
|
85
86
|
class AttributeGetterTestClass extends Component {
|
|
86
87
|
#value = "Mike";
|
|
87
|
-
@attributeGetter get value(): string {
|
|
88
|
+
@attributeGetter({ reflect: true }) get value(): string {
|
|
88
89
|
return this.#value;
|
|
89
90
|
}
|
|
90
|
-
@attributeSetter set value(value: string) {
|
|
91
|
+
@attributeSetter({ reflect: true }) set value(value: string) {
|
|
91
92
|
this.#value = value;
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
#synced = "Mike";
|
|
95
|
-
@attributeGetter
|
|
96
|
+
@attributeGetter get synced(): string {
|
|
96
97
|
return this.#synced;
|
|
97
98
|
}
|
|
98
|
-
@attributeSetter
|
|
99
|
+
@attributeSetter set synced(value: string) {
|
|
99
100
|
this.#synced = value;
|
|
100
|
-
this.setAttributeQuietly("synced", value);
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
@@ -106,7 +106,7 @@ test("@attribute getter/setter", async () => {
|
|
|
106
106
|
await t.updateComplete;
|
|
107
107
|
|
|
108
108
|
expect(t.value).toBe("Mike");
|
|
109
|
-
expect(t.getAttribute("value")).
|
|
109
|
+
expect(t.getAttribute("value")).toBe("Mike");
|
|
110
110
|
|
|
111
111
|
t.setAttribute("value", "Joe");
|
|
112
112
|
expect(t.value).toBe("Joe");
|
|
@@ -114,7 +114,7 @@ test("@attribute getter/setter", async () => {
|
|
|
114
114
|
|
|
115
115
|
t.value = "Robert";
|
|
116
116
|
expect(t.value).toBe("Robert");
|
|
117
|
-
expect(t.getAttribute("value")).toBe("
|
|
117
|
+
expect(t.getAttribute("value")).toBe("Robert");
|
|
118
118
|
|
|
119
119
|
expect(t.synced).toBe("Mike");
|
|
120
120
|
expect(t.getAttribute("synced")).toBeNull();
|
|
@@ -125,7 +125,7 @@ test("@attribute getter/setter", async () => {
|
|
|
125
125
|
|
|
126
126
|
t.synced = "Robert";
|
|
127
127
|
expect(t.synced).toBe("Robert");
|
|
128
|
-
expect(t.getAttribute("synced")).toBe("
|
|
128
|
+
expect(t.getAttribute("synced")).toBe("Joe");
|
|
129
129
|
});
|
|
130
130
|
|
|
131
131
|
test("@attribute init ordering", async () => {
|
|
@@ -160,3 +160,52 @@ test("@attribute init ordering", async () => {
|
|
|
160
160
|
expect(t.wibble).toBe("Robert");
|
|
161
161
|
expect(t.getAttribute("wibble")).toBe("Robert");
|
|
162
162
|
});
|
|
163
|
+
|
|
164
|
+
test("subcomponent attributes are initialised properly", async () => {
|
|
165
|
+
@customElement("subcomp-init-root")
|
|
166
|
+
class SubcompInitRoot extends Component {
|
|
167
|
+
render() {
|
|
168
|
+
return html`
|
|
169
|
+
<subcomp-init-sub></subcomp-init-sub>
|
|
170
|
+
<subcomp-init-sub
|
|
171
|
+
attr1="Mike"
|
|
172
|
+
attr2="Mike"
|
|
173
|
+
attr3="Mike"
|
|
174
|
+
attr4="Mike"
|
|
175
|
+
></subcomp-init-sub>
|
|
176
|
+
`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@customElement("subcomp-init-sub")
|
|
181
|
+
class SubcompInitSub extends Component {
|
|
182
|
+
@attribute({ reflect: true, reactive: true }) accessor attr1 = "Joe";
|
|
183
|
+
@attribute({ reflect: true, reactive: false }) accessor attr2 = "Joe";
|
|
184
|
+
@attribute({ reflect: false, reactive: true }) accessor attr3 = "Joe";
|
|
185
|
+
@attribute({ reflect: false, reactive: false }) accessor attr4 = "Joe";
|
|
186
|
+
|
|
187
|
+
render() {
|
|
188
|
+
return nothing;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const root = document.createElement("subcomp-init-root") as SubcompInitRoot;
|
|
193
|
+
document.body.append(root);
|
|
194
|
+
await root.updateComplete;
|
|
195
|
+
|
|
196
|
+
const subs = root.queryAll("subcomp-init-sub") as NodeListOf<SubcompInitSub>;
|
|
197
|
+
expect(subs.length).toBe(2);
|
|
198
|
+
|
|
199
|
+
expect(subs[0].attr1).toBe("Joe");
|
|
200
|
+
expect(subs[0].attr2).toBe("Joe");
|
|
201
|
+
expect(subs[0].attr3).toBe("Joe");
|
|
202
|
+
expect(subs[0].attr4).toBe("Joe");
|
|
203
|
+
expect(subs[0].outerHTML).toBe(`<subcomp-init-sub attr1="Joe" attr2="Joe"></subcomp-init-sub>`);
|
|
204
|
+
expect(subs[1].attr1).toBe("Mike");
|
|
205
|
+
expect(subs[1].attr2).toBe("Mike");
|
|
206
|
+
expect(subs[1].attr3).toBe("Mike");
|
|
207
|
+
expect(subs[1].attr4).toBe("Mike");
|
|
208
|
+
expect(subs[1].outerHTML).toBe(
|
|
209
|
+
`<subcomp-init-sub attr1="Mike" attr2="Mike" attr3="Mike" attr4="Mike"></subcomp-init-sub>`,
|
|
210
|
+
);
|
|
211
|
+
});
|
|
@@ -241,10 +241,8 @@ export function attribute<C extends Component, T extends string | number | boole
|
|
|
241
241
|
);
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
`Getter/setter attributes cannot be declared with reflect: true (on ${JSON.stringify(context.name)})`,
|
|
247
|
-
);
|
|
244
|
+
if (context.kind === "setter") {
|
|
245
|
+
return setter(options, value as ClassSetterDecoratorTarget<C, T>);
|
|
248
246
|
}
|
|
249
247
|
};
|
|
250
248
|
}
|
|
@@ -262,6 +260,18 @@ function syncAttribute<C extends Component, T>(
|
|
|
262
260
|
}
|
|
263
261
|
}
|
|
264
262
|
|
|
263
|
+
function setter<C extends Component, T>(
|
|
264
|
+
options: AttributeConfig,
|
|
265
|
+
setter: ClassSetterDecoratorTarget<C, T>,
|
|
266
|
+
): ClassSetterDecoratorResult<C, T> {
|
|
267
|
+
if (options.reflect) {
|
|
268
|
+
return function (this: C, value: T) {
|
|
269
|
+
setter.call(this, value);
|
|
270
|
+
syncAttribute(this, options, value);
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
265
275
|
function accessor<C extends Component, T>(
|
|
266
276
|
options: AttributeConfig,
|
|
267
277
|
context: ClassAccessorDecoratorContext<C, T>,
|
|
@@ -287,9 +297,6 @@ function accessor<C extends Component, T>(
|
|
|
287
297
|
},
|
|
288
298
|
init(this: C, value: T): T {
|
|
289
299
|
initValue = Some(value);
|
|
290
|
-
if (options.reflect) {
|
|
291
|
-
syncAttribute(this, options, value, true);
|
|
292
|
-
}
|
|
293
300
|
return value;
|
|
294
301
|
},
|
|
295
302
|
};
|
|
@@ -304,11 +311,5 @@ function accessor<C extends Component, T>(
|
|
|
304
311
|
syncAttribute(this, options, newValue);
|
|
305
312
|
}
|
|
306
313
|
},
|
|
307
|
-
init(this: C, value: T): T {
|
|
308
|
-
if (options.reflect) {
|
|
309
|
-
syncAttribute(this, options, value, true);
|
|
310
|
-
}
|
|
311
|
-
return value;
|
|
312
|
-
},
|
|
313
314
|
};
|
|
314
315
|
}
|
|
@@ -30,27 +30,27 @@ test("@require", async () => {
|
|
|
30
30
|
expect(inits).toBe(0);
|
|
31
31
|
expect(updates).toBe(0);
|
|
32
32
|
c.bar = "bar";
|
|
33
|
-
await sleep(
|
|
33
|
+
await sleep(50);
|
|
34
34
|
// foo and bar are both set, should cause an update
|
|
35
35
|
expect(inits).toBe(1);
|
|
36
36
|
expect(updates).toBe(1);
|
|
37
37
|
c.foo = undefined;
|
|
38
|
-
await sleep(
|
|
38
|
+
await sleep(50);
|
|
39
39
|
// foo has gone undefined, shouldn't update
|
|
40
40
|
expect(inits).toBe(1);
|
|
41
41
|
expect(updates).toBe(1);
|
|
42
42
|
c.foo = "wibble";
|
|
43
|
-
await sleep(
|
|
43
|
+
await sleep(50);
|
|
44
44
|
// foo has stopped being undefined, should update
|
|
45
45
|
expect(inits).toBe(2);
|
|
46
46
|
expect(updates).toBe(2);
|
|
47
47
|
c.bar = "bar";
|
|
48
|
-
await sleep(
|
|
48
|
+
await sleep(50);
|
|
49
49
|
// bar has been redeclared with the same value, shouldn't update
|
|
50
50
|
expect(inits).toBe(2);
|
|
51
51
|
expect(updates).toBe(2);
|
|
52
52
|
c.bar = "wibble";
|
|
53
|
-
await sleep(
|
|
53
|
+
await sleep(50);
|
|
54
54
|
// bar has a new value, should update
|
|
55
55
|
expect(inits).toBe(3);
|
|
56
56
|
expect(updates).toBe(3);
|