@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bodil/dom",
3
- "version": "0.1.4",
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.1",
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.49.0",
70
- "@typescript-eslint/parser": "^8.49.0",
71
- "@vitest/coverage-v8": "^4.0.15",
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.15"
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": "tsc"
116
+ "prepublish": "run-p check test:browser"
115
117
  }
116
118
  }
@@ -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()).deep.equal([joe, mike]);
67
- expect(t.querySlot({ slot: "fixes-the-bug" })).deep.equal([robert]);
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()).deep.equal([robert]);
72
+ expect(slot.get()).toEqual([robert]);
72
73
  joe.slot = "fixes-the-bug"; // for argument's sake
73
- expect(slot.get()).deep.equal([joe, robert]);
74
- expect(t.querySlot()).deep.equal([mike]);
74
+ await sleep(1);
75
+ expect(slot.get()).toEqual([joe, robert]);
76
+ expect(t.querySlot()).toEqual([mike]);
75
77
  robert.removeAttribute("slot");
76
- expect(slot.get()).deep.equal([joe]);
77
- expect(t.querySlot()).deep.equal([mike, robert]);
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).deep.equal([mike, robert]);
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).deep.equal([mike, robert]);
88
+ expect(divsBySelector).toEqual([mike, robert]);
86
89
 
87
- expect(t.querySlot({ selector: "div.robert" })).deep.equal([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 { attributeConfig, fromAttribute, type AttributeConfig } from "./decorators/attribute";
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({ reflect: false }) get synced(): string {
96
+ @attributeGetter get synced(): string {
96
97
  return this.#synced;
97
98
  }
98
- @attributeSetter({ reflect: false }) set synced(value: string) {
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")).toBeNull();
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("Joe");
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("Robert");
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 (options.reflect) {
245
- throw new TypeError(
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(1);
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(1);
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(1);
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(1);
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(1);
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);