@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.
@@ -9,14 +9,12 @@ import { html, nothing } from "lit";
9
9
  test("@attribute", async () => {
10
10
  @customElement("attribute-test-class")
11
11
  class AttributeTestClass extends Component {
12
- @attribute accessor wibble: string | undefined = "Joe";
13
- @attribute({ type: Number, reflect: true }) accessor wobble: number | undefined = 1;
14
- @attribute({ type: Boolean, reflect: true }) accessor noMeansNo: boolean | undefined =
15
- false;
16
- @attribute({ name: "wolp", reactive: false, reflect: true }) accessor welp:
17
- | string
18
- | undefined = "Joe";
19
- @attribute({ reflect: false }) accessor hide: string | undefined = "no";
12
+ @attribute accessor wibble: string | null = "Joe";
13
+ @attribute({ type: Number, reflect: true }) accessor wobble: number | null = 1;
14
+ @attribute({ type: Boolean, reflect: true }) accessor noMeansNo: boolean | null = false;
15
+ @attribute({ name: "wolp", reactive: false, reflect: true }) accessor welp: string | null =
16
+ "Joe";
17
+ @attribute({ reflect: false }) accessor hide: string | null = "no";
20
18
  }
21
19
 
22
20
  const t = document.createElement("attribute-test-class") as AttributeTestClass;
@@ -32,17 +30,17 @@ test("@attribute", async () => {
32
30
  expect(t.getAttribute("wibble")).toBe("Mike");
33
31
  expect(wibble.get()).toBe("Mike");
34
32
  t.removeAttribute("wibble");
35
- expect(t.wibble).toBeUndefined();
33
+ expect(t.wibble).toBeNull();
36
34
  expect(t.getAttribute("wibble")).toBeNull();
37
- expect(wibble.get()).toBe(undefined);
35
+ expect(wibble.get()).toBe(null);
38
36
  t.setAttribute("wibble", "Robert");
39
37
  expect(t.wibble).toBe("Robert");
40
38
  expect(t.getAttribute("wibble")).toBe("Robert");
41
39
  expect(wibble.get()).toBe("Robert");
42
- t.wibble = undefined;
43
- expect(t.wibble).toBeUndefined();
40
+ t.wibble = null;
41
+ expect(t.wibble).toBeNull();
44
42
  expect(t.getAttribute("wibble")).toBeNull();
45
- expect(wibble.get()).toBe(undefined);
43
+ expect(wibble.get()).toBe(null);
46
44
 
47
45
  expect(t.wobble).toBe(1);
48
46
  expect(t.getAttribute("wobble")).toBe("1");
@@ -133,7 +131,7 @@ test("@attribute init ordering", async () => {
133
131
  class AttributeInitTestClass extends Component {
134
132
  @attribute accessor movieStar = "Joe";
135
133
 
136
- @attribute accessor foo: string | undefined;
134
+ @attribute accessor foo: string | null = null;
137
135
 
138
136
  #wibble = "Joe";
139
137
  @attributeGetter get wibble(): string {
@@ -209,3 +207,32 @@ test("subcomponent attributes are initialised properly", async () => {
209
207
  `<subcomp-init-sub attr1="Mike" attr2="Mike" attr3="Mike" attr4="Mike"></subcomp-init-sub>`,
210
208
  );
211
209
  });
210
+
211
+ test("@attributeGetter is reactive", async () => {
212
+ @customElement("reactive-getter")
213
+ class ReactiveGetter extends Component {
214
+ @attribute({ type: Boolean }) accessor disabled = false;
215
+ @attributeGetter({ type: Number, reactive: true }) get tabindex(): number {
216
+ return this.disabled ? -1 : 0;
217
+ }
218
+ }
219
+
220
+ const t = document.createElement("reactive-getter") as ReactiveGetter;
221
+ // document.body.append(t);
222
+ // await t.updateComplete;
223
+
224
+ expect(t.getAttribute("disabled")).toBeNull();
225
+ expect(t.tabindex).toBe(0);
226
+ // Attributes won't update until we yield, so yield.
227
+ await Promise.resolve();
228
+ expect(t.getAttribute("tabindex")).toBe("0");
229
+
230
+ t.disabled = true;
231
+
232
+ expect(t.getAttribute("disabled")).not.toBeNull();
233
+ // change should be immediately reflected in the computed property
234
+ expect(t.tabindex).toBe(-1);
235
+ // but attributes will only update after a yield
236
+ await Promise.resolve();
237
+ expect(t.getAttribute("tabindex")).toBe("-1");
238
+ });
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/unified-signatures */
2
2
 
3
- import { isDeepEqual, isNullish, unreachable } from "@bodil/core/assert";
3
+ import { assertNever, isDeepEqual, isNullish, unreachable } from "@bodil/core/assert";
4
4
  import { None, Some, type Option } from "@bodil/opt";
5
5
  import { Signal } from "@bodil/signal";
6
6
 
@@ -54,21 +54,21 @@ export function toAttribute(value: unknown, type: AttributeType): string | null
54
54
  }
55
55
  }
56
56
 
57
- export function fromAttribute(value: string | null, type: typeof Number): number | undefined;
57
+ export function fromAttribute(value: string | null, type: typeof Number): number | null;
58
58
  export function fromAttribute(value: string | null, type: typeof Boolean): boolean;
59
- export function fromAttribute(value: string | null, type: typeof String): string | undefined;
59
+ export function fromAttribute(value: string | null, type: typeof String): string | null;
60
60
  export function fromAttribute(
61
61
  value: string | null,
62
62
  type: AttributeType,
63
- ): string | number | boolean | undefined;
63
+ ): string | number | boolean | null;
64
64
  export function fromAttribute(
65
65
  value: string | null,
66
66
  type: AttributeType,
67
- ): string | number | boolean | undefined {
67
+ ): string | number | boolean | null {
68
68
  switch (type) {
69
69
  case Number: {
70
- const num = value === null ? undefined : Number(value);
71
- if (num !== undefined && Number.isNaN(num)) {
70
+ const num = value === null ? null : Number(value);
71
+ if (num !== null && Number.isNaN(num)) {
72
72
  throw new TypeError(
73
73
  `numeric attribute value ${JSON.stringify(value)} parsed as NaN`,
74
74
  );
@@ -78,7 +78,7 @@ export function fromAttribute(
78
78
  case Boolean:
79
79
  return value !== null;
80
80
  case String:
81
- return value ?? undefined;
81
+ return value ?? null;
82
82
  default:
83
83
  unreachable();
84
84
  }
@@ -109,22 +109,22 @@ type AttributeDecoratorFunction<C, T> =
109
109
  | ClassSetterDecoratorFunction<C, T>;
110
110
 
111
111
  // Getter with number type
112
- export function attributeGetter<C extends Component, T extends number | undefined>(
112
+ export function attributeGetter<C extends Component, T extends number | null>(
113
113
  options: AttributeOptions & { type: typeof Number },
114
114
  ): ClassGetterDecoratorFunction<C, T>;
115
115
 
116
116
  // Getter with boolean type
117
- export function attributeGetter<C extends Component, T extends boolean | undefined>(
117
+ export function attributeGetter<C extends Component, T extends boolean | null>(
118
118
  options: AttributeOptions & { type: typeof Boolean },
119
119
  ): ClassGetterDecoratorFunction<C, T>;
120
120
 
121
121
  // Getter with string type
122
- export function attributeGetter<C extends Component, T extends string | undefined>(
122
+ export function attributeGetter<C extends Component, T extends string | null>(
123
123
  options: AttributeOptions,
124
124
  ): ClassGetterDecoratorFunction<C, T>;
125
125
 
126
126
  // Getter with no options
127
- export function attributeGetter<C extends Component, T extends string | undefined>(
127
+ export function attributeGetter<C extends Component, T extends string | null>(
128
128
  value: ClassGetterDecoratorTarget<C, T>,
129
129
  context: ClassGetterDecoratorContext<C, T>,
130
130
  ): ClassGetterDecoratorResult<C, T>;
@@ -138,22 +138,22 @@ export function attributeGetter<C extends Component, T>(
138
138
  }
139
139
 
140
140
  // Setter with number type
141
- export function attributeSetter<C extends Component, T extends number | undefined>(
141
+ export function attributeSetter<C extends Component, T extends number | null>(
142
142
  options: AttributeOptions & { type: typeof Number },
143
143
  ): ClassSetterDecoratorFunction<C, T>;
144
144
 
145
145
  // Setter with boolean type
146
- export function attributeSetter<C extends Component, T extends boolean | undefined>(
146
+ export function attributeSetter<C extends Component, T extends boolean | null>(
147
147
  options: AttributeOptions & { type: typeof Boolean },
148
148
  ): ClassSetterDecoratorFunction<C, T>;
149
149
 
150
150
  // Setter with string type
151
- export function attributeSetter<C extends Component, T extends string | undefined>(
151
+ export function attributeSetter<C extends Component, T extends string | null>(
152
152
  options: AttributeOptions,
153
153
  ): ClassSetterDecoratorFunction<C, T>;
154
154
 
155
155
  // Setter with no options
156
- export function attributeSetter<C extends Component, T extends string | undefined>(
156
+ export function attributeSetter<C extends Component, T extends string | null>(
157
157
  value: ClassSetterDecoratorTarget<C, T>,
158
158
  context: ClassSetterDecoratorContext<C, T>,
159
159
  ): ClassSetterDecoratorResult<C, T>;
@@ -167,27 +167,27 @@ export function attributeSetter<C extends Component, T>(
167
167
  }
168
168
 
169
169
  // Accessor with number type
170
- export function attribute<C extends Component, T extends number | undefined>(
170
+ export function attribute<C extends Component, T extends number | null>(
171
171
  options: AttributeOptions & { type: typeof Number },
172
172
  ): ClassAccessorDecoratorFunction<C, T>;
173
173
 
174
174
  // Accessor with boolean type
175
- export function attribute<C extends Component, T extends boolean | undefined>(
175
+ export function attribute<C extends Component, T extends boolean | null>(
176
176
  options: AttributeOptions & { type: typeof Boolean },
177
177
  ): ClassAccessorDecoratorFunction<C, T>;
178
178
 
179
179
  // Accessor with string type
180
- export function attribute<C extends Component, T extends string | undefined>(
180
+ export function attribute<C extends Component, T extends string | null>(
181
181
  options: AttributeOptions,
182
182
  ): ClassAccessorDecoratorFunction<C, T>;
183
183
 
184
184
  // Accessor with no options
185
- export function attribute<C extends Component, T extends string | undefined>(
185
+ export function attribute<C extends Component, T extends string | null>(
186
186
  value: ClassAccessorDecoratorTarget<C, T>,
187
187
  context: ClassAccessorDecoratorContext<C, T>,
188
188
  ): ClassAccessorDecoratorResult<C, T>;
189
189
 
190
- export function attribute<C extends Component, T extends string | number | boolean | undefined>(
190
+ export function attribute<C extends Component, T extends string | number | boolean | null>(
191
191
  valueOrOptions?: AttributeDecoratorTarget<C, T> | AttributeOptions,
192
192
  context?: AttributeDecoratorContext<C, T>,
193
193
  ): AttributeDecoratorResult<C, T> | AttributeDecoratorFunction<C, T> {
@@ -234,16 +234,21 @@ export function attribute<C extends Component, T extends string | number | boole
234
234
  return accessor(options, context, value as ClassAccessorDecoratorTarget<C, T>);
235
235
  }
236
236
 
237
- // ensure getters/setters aren't configured with reactive or reflect
238
- if (options.reactive) {
239
- throw new TypeError(
240
- `Getter/setter attributes cannot be declared with reactive: true (on ${JSON.stringify(context.name)})`,
241
- );
242
- }
243
-
244
237
  if (context.kind === "setter") {
238
+ // ensure setters aren't configured with reactive
239
+ if (options.reactive) {
240
+ throw new TypeError(
241
+ `Setter attributes cannot be declared with reactive: true (on ${JSON.stringify(context.name)})`,
242
+ );
243
+ }
245
244
  return setter(options, value as ClassSetterDecoratorTarget<C, T>);
246
245
  }
246
+
247
+ if (context.kind === "getter") {
248
+ return getter(options, context, value as ClassGetterDecoratorTarget<C, T>);
249
+ }
250
+
251
+ assertNever(context);
247
252
  };
248
253
  }
249
254
 
@@ -260,6 +265,29 @@ function syncAttribute<C extends Component, T>(
260
265
  }
261
266
  }
262
267
 
268
+ function getter<C extends Component, T>(
269
+ options: AttributeConfig,
270
+ context: ClassGetterDecoratorContext<C, T>,
271
+ getter: ClassGetterDecoratorTarget<C, T>,
272
+ ): ClassGetterDecoratorResult<C, T> {
273
+ if (options.reactive) {
274
+ const getSig = (obj: Component) =>
275
+ signalForObject(obj, context.name, () => {
276
+ const sig = Signal.computed(getter.bind(obj));
277
+ // FIXME this leaks when obj isn't explicitly disposed
278
+ obj.use(
279
+ Signal.effect(() => {
280
+ syncAttribute(obj, options, sig.get());
281
+ }),
282
+ );
283
+ return sig;
284
+ }) as Signal.Computed<T>;
285
+ return function (this: C): T {
286
+ return getSig(this).get();
287
+ };
288
+ }
289
+ }
290
+
263
291
  function setter<C extends Component, T>(
264
292
  options: AttributeConfig,
265
293
  setter: ClassSetterDecoratorTarget<C, T>,
@@ -11,7 +11,7 @@ test("@require", async () => {
11
11
  @customElement("require-test-class")
12
12
  class RequireTestClass extends Component {
13
13
  @require @reactive foo?: string;
14
- @require @attribute accessor bar: string | undefined;
14
+ @require @attribute accessor bar: string | null = null;
15
15
 
16
16
  protected override initialised() {
17
17
  inits++;
@@ -0,0 +1,23 @@
1
+ import { html } from "lit";
2
+ import { expect, test } from "vitest";
3
+
4
+ import { findDescendants } from "./dom";
5
+ import { testHTML } from "./test";
6
+
7
+ test("findDescendants", async () => {
8
+ const root = await testHTML(html`
9
+ <p>foo</p>
10
+ <div><p>bar</p></div>
11
+ <article>
12
+ <div>
13
+ <p>baz</p>
14
+ <p>gazonk</p>
15
+ </div>
16
+ </article>
17
+ `);
18
+ expect(
19
+ findDescendants(root, (el) => el instanceof HTMLParagraphElement)
20
+ .map((el) => el.innerText)
21
+ .toArray(),
22
+ ).toEqual(["foo", "bar", "baz", "gazonk"]);
23
+ });
package/src/dom.ts CHANGED
@@ -3,12 +3,13 @@
3
3
  * @module
4
4
  */
5
5
 
6
- import { assertNever, isNullish, unreachable } from "@bodil/core/assert";
7
- import { toDisposable } from "@bodil/core/disposable";
6
+ import { assertNever, isEmpty, isNullish, unreachable } from "@bodil/core/assert";
7
+ import { DisposableContext, toDisposable } from "@bodil/core/disposable";
8
8
  import type { ReactiveController, ReactiveControllerHost } from "lit";
9
9
  import type { OptionalKeysOf } from "type-fest";
10
10
 
11
11
  import type { Pixels } from "./css";
12
+ import { eventListener } from "./event";
12
13
  import { contains } from "./geometry";
13
14
 
14
15
  /**
@@ -129,20 +130,26 @@ export class DOMIterator extends Iterator<Node> {
129
130
  }
130
131
 
131
132
  /**
132
- * Get an iterator over the child elements of an {@link Element} or {@link DocumentFragment}.
133
+ * Return all descendants of `root` which match the `predicate` function.
133
134
  */
134
- export function childElements(parent: Element | DocumentFragment | null): IteratorObject<Element> {
135
- if (parent === null) {
135
+ export function findDescendants<T extends Element>(
136
+ root: Element | DocumentFragment | null,
137
+ predicate: (child: Element) => child is T,
138
+ ): IteratorObject<T>;
139
+ export function findDescendants(
140
+ root: Element | DocumentFragment | null,
141
+ predicate: (child: Element) => boolean,
142
+ ): IteratorObject<Element>;
143
+ export function findDescendants(
144
+ root: Element | DocumentFragment | null,
145
+ predicate: (child: Element) => boolean,
146
+ ): IteratorObject<Element> {
147
+ if (root === null) {
136
148
  return Iterator.from([]);
137
149
  }
138
- const iter = function* () {
139
- let el = parent.firstElementChild;
140
- while (el !== null) {
141
- yield el;
142
- el = el.nextElementSibling;
143
- }
144
- };
145
- return Iterator.from(iter());
150
+ return Iterator.from(root.children).flatMap((el) =>
151
+ predicate(el) ? [el, ...findDescendants(el, predicate)] : findDescendants(el, predicate),
152
+ );
146
153
  }
147
154
 
148
155
  /** Test whether the user has asked for reduced motion. */
@@ -199,9 +206,17 @@ export async function animationEnd(el: HTMLElement): Promise<void> {
199
206
  await Promise.all(
200
207
  el.getAnimations().map((animation) => {
201
208
  return new Promise((resolve) => {
202
- const handleAnimationEvent = () => requestAnimationFrame(resolve);
203
- animation.addEventListener("cancel", () => handleAnimationEvent(), { once: true });
204
- animation.addEventListener("finish", () => handleAnimationEvent(), { once: true });
209
+ const context = new DisposableContext();
210
+ const handleAnimationEvent = () => {
211
+ context.dispose();
212
+ requestAnimationFrame(resolve);
213
+ };
214
+ context.use(
215
+ eventListener(animation, "cancel", handleAnimationEvent, { once: true }),
216
+ );
217
+ context.use(
218
+ eventListener(animation, "finish", handleAnimationEvent, { once: true }),
219
+ );
205
220
  });
206
221
  }),
207
222
  );
@@ -421,6 +436,30 @@ export function scrollToItem(el: HTMLElement, scrollToItemOptions: ScrollToItemO
421
436
  }
422
437
  }
423
438
 
439
+ const ids: Record<string, number> = {};
440
+
441
+ function freshID(tagName: string): number {
442
+ const next = ids[tagName] ?? 1;
443
+ ids[tagName] = next + 1;
444
+ return next;
445
+ }
446
+
447
+ /**
448
+ * Assign a unique ID to the target element.
449
+ *
450
+ * Returns the ID that was assigned.
451
+ *
452
+ * If the target element already has an `id` attribute, this will not be
453
+ * overwritten.
454
+ */
455
+ export function applyUniqueID(target: HTMLElement): string {
456
+ if (isEmpty(target.id)) {
457
+ const name = target.tagName.toLowerCase();
458
+ target.id = `${name}-${freshID(name)}`;
459
+ }
460
+ return target.id;
461
+ }
462
+
424
463
  // HasSlotController nicked largely verbatim from Shoelace
425
464
  // https://github.com/shoelace-style/shoelace/blob/next/src/internal/slot.ts
426
465
  /**
package/src/emitter.ts CHANGED
@@ -69,6 +69,8 @@ export class EmitterElement extends HTMLElement {
69
69
  * class MyElement extends EmitterElement {
70
70
  * emits!: Emits<"my-event" | "my-other-event";
71
71
  * }
72
+ *
73
+ * @category Events
72
74
  */
73
75
  emits!: { [K in keyof HTMLElementEventMap]?: never };
74
76
 
@@ -76,6 +78,8 @@ export class EmitterElement extends HTMLElement {
76
78
  * Emit a custom event with the given name and detail.
77
79
  *
78
80
  * Event init options default to `{ bubbles: true, composed: true }`.
81
+ *
82
+ * @category Events
79
83
  */
80
84
  emit<
81
85
  M extends CustomEventTypes<keyof this["emits"] & keyof HTMLElementEventMap>,
@@ -101,6 +105,8 @@ export class EmitterElement extends HTMLElement {
101
105
  * @example
102
106
  * this.emitEvent("click", MouseEvent, { button: 2 });
103
107
  * // emits: new MouseEvent("click", { button: 2 });
108
+ *
109
+ * @category Events
104
110
  */
105
111
  emitEvent<
106
112
  K extends keyof this["emits"] & keyof HTMLElementEventMap,
package/src/index.ts CHANGED
@@ -9,5 +9,6 @@ import * as dom from "./dom";
9
9
  import * as event from "./event";
10
10
  import * as geometry from "./geometry";
11
11
  import * as signal from "./signal";
12
+ import * as test from "./test";
12
13
 
13
- export { component, css, dom, event, geometry, signal };
14
+ export { component, css, dom, event, geometry, signal, test };
package/src/test.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Tools for testing web components.
3
+ * @module
4
+ */
5
+
6
+ import { render } from "lit";
7
+
8
+ import { Component } from "./component";
9
+
10
+ /**
11
+ * Render a Lit template and wait for any {@link Component}s inside it to
12
+ * stabilise before returning the {@link HTMLElement} containing the rendered
13
+ * template.
14
+ *
15
+ * The root element is also a {@link Disposable} which will remove itself from
16
+ * the DOM and dispose its {@link Component}s when disposed.
17
+ *
18
+ * @example
19
+ * using root = await testHTML(html`<my-component></my-component>`);
20
+ * expect(root.querySelector("my-component")).toBeInstanceOf(MyComponent);
21
+ */
22
+ export async function testHTML(template: unknown): Promise<HTMLElement & Disposable> {
23
+ const root = document.createElement("section") as HTMLElement & Disposable;
24
+ document.body.append(root);
25
+ render(template, root, { host: root });
26
+ root[Symbol.dispose] = () => {
27
+ Iterator.from(root.children)
28
+ .filter((el) => el instanceof Component)
29
+ .forEach((comp) => comp[Symbol.dispose]());
30
+ root.remove();
31
+ };
32
+ await Promise.all(
33
+ Iterator.from(root.children)
34
+ .filter((el) => el instanceof Component)
35
+ .map((el) => el.hasStabilised),
36
+ );
37
+ return root;
38
+ }
@@ -1,119 +0,0 @@
1
- import { sleep } from "@bodil/core/async";
2
- import { Signal } from "@bodil/signal";
3
- import { customElement } from "lit/decorators.js";
4
- import { expect, test } from "vitest";
5
-
6
- import { Component, connect, connectEffect } from "../component";
7
- import type { ConnectFunction } from "./connect";
8
-
9
- test("@connect", () => {
10
- let methodConnected = 0,
11
- methodDisconnected = 0;
12
- let fieldConnected = 0,
13
- fieldDisconnected = 0;
14
-
15
- @customElement("connected-test-class")
16
- class ConnectedTestClass extends Component {
17
- @connect onConnected() {
18
- methodConnected++;
19
- return () => {
20
- methodDisconnected++;
21
- };
22
- }
23
-
24
- @connect field: ConnectFunction = () => {
25
- fieldConnected++;
26
- return () => {
27
- fieldDisconnected++;
28
- };
29
- };
30
- }
31
-
32
- const c = document.createElement("connected-test-class") as ConnectedTestClass;
33
- expect(methodConnected).toBe(0);
34
- expect(methodDisconnected).toBe(0);
35
- expect(fieldConnected).toEqual(methodConnected);
36
- expect(fieldDisconnected).toEqual(methodDisconnected);
37
- document.body.append(c);
38
- expect(methodConnected).toBe(1);
39
- expect(methodDisconnected).toBe(0);
40
- expect(fieldConnected).toEqual(methodConnected);
41
- expect(fieldDisconnected).toEqual(methodDisconnected);
42
- c.remove();
43
- expect(methodConnected).toBe(1);
44
- expect(methodDisconnected).toBe(1);
45
- expect(fieldConnected).toEqual(methodConnected);
46
- expect(fieldDisconnected).toEqual(methodDisconnected);
47
- document.body.append(c);
48
- expect(methodConnected).toBe(2);
49
- expect(methodDisconnected).toBe(1);
50
- expect(fieldConnected).toEqual(methodConnected);
51
- expect(fieldDisconnected).toEqual(methodDisconnected);
52
- c.remove();
53
- expect(methodConnected).toBe(2);
54
- expect(methodDisconnected).toBe(2);
55
- expect(fieldConnected).toEqual(methodConnected);
56
- expect(fieldDisconnected).toEqual(methodDisconnected);
57
- });
58
-
59
- test("@connectEffect", async () => {
60
- let methodRun = 0,
61
- methodDisposed = 0;
62
- let fieldRun = 0,
63
- fieldDisposed = 0;
64
- const signal = Signal.from(1);
65
-
66
- @customElement("connect-effect-test-class")
67
- class ConnectEffectTestClass extends Component {
68
- @connectEffect onConnected() {
69
- methodRun += signal.get();
70
- return () => {
71
- methodDisposed++;
72
- };
73
- }
74
-
75
- @connectEffect field: ConnectFunction = () => {
76
- fieldRun += signal.get();
77
- return () => {
78
- fieldDisposed++;
79
- };
80
- };
81
- }
82
-
83
- const c = document.createElement("connect-effect-test-class") as ConnectEffectTestClass;
84
- expect(methodRun).toBe(0);
85
- expect(methodDisposed).toBe(0);
86
- expect(fieldRun).toEqual(methodRun);
87
- expect(fieldDisposed).toEqual(methodDisposed);
88
- document.body.append(c);
89
- await c.updateComplete;
90
- expect(methodRun).toBe(1);
91
- expect(methodDisposed).toBe(0);
92
- expect(fieldRun).toEqual(methodRun);
93
- expect(fieldDisposed).toEqual(methodDisposed);
94
- c.remove();
95
- expect(methodRun).toBe(1);
96
- expect(methodDisposed).toBe(1);
97
- expect(fieldRun).toEqual(methodRun);
98
- expect(fieldDisposed).toEqual(methodDisposed);
99
- document.body.append(c);
100
- expect(methodRun).toBe(2);
101
- expect(methodDisposed).toBe(1);
102
- expect(fieldRun).toEqual(methodRun);
103
- expect(fieldDisposed).toEqual(methodDisposed);
104
- signal.set(2);
105
- // wait for the effect to run
106
- await sleep(1);
107
- // run should be bumped by 2, the new value of the signal, and
108
- // disposed should be bumped by 1 because the previous effect is
109
- // disposed.
110
- expect(methodRun).toBe(4);
111
- expect(methodDisposed).toBe(2);
112
- expect(fieldRun).toEqual(methodRun);
113
- expect(fieldDisposed).toEqual(methodDisposed);
114
- c.remove();
115
- expect(methodRun).toBe(4);
116
- expect(methodDisposed).toBe(3);
117
- expect(fieldRun).toEqual(methodRun);
118
- expect(fieldDisposed).toEqual(methodDisposed);
119
- });