@bodil/dom 0.1.8 → 0.1.9

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/index.d.ts CHANGED
@@ -7,4 +7,5 @@ import * as css from "./css";
7
7
  import * as dom from "./dom";
8
8
  import * as event from "./event";
9
9
  import * as geometry from "./geometry";
10
- export { component, css, dom, event, geometry };
10
+ import * as signal from "./signal";
11
+ export { component, css, dom, event, geometry, signal };
package/dist/index.js CHANGED
@@ -7,5 +7,6 @@ import * as css from "./css";
7
7
  import * as dom from "./dom";
8
8
  import * as event from "./event";
9
9
  import * as geometry from "./geometry";
10
- export { component, css, dom, event, geometry };
10
+ import * as signal from "./signal";
11
+ export { component, css, dom, event, geometry, signal };
11
12
  //# 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;AAEvC,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,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;AAEnC,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC"}
@@ -0,0 +1,20 @@
1
+ import { Signal } from "@bodil/signal";
2
+ import { AsyncDirective } from "lit/async-directive.js";
3
+ import { type DirectiveResult, type Part } from "lit/directive.js";
4
+ declare class WatchDirective<T> extends AsyncDirective {
5
+ #private;
6
+ render(signal: Signal.State<T> | Signal.Computed<T>, mapFn?: (value: T) => unknown): unknown;
7
+ update(part: Part, [signal, mapFn]: [
8
+ signal: Signal.State<T> | Signal.Computed<T>,
9
+ mapFn?: (value: T) => unknown
10
+ ]): unknown;
11
+ protected disconnected(): void;
12
+ protected reconnected(): void;
13
+ }
14
+ export type WatchDirectiveFunction = <T>(signal: Signal.State<T> | Signal.Computed<T>, mapFn?: (value: T) => unknown) => DirectiveResult<typeof WatchDirective<T>>;
15
+ /**
16
+ * Render a signal and subscribe to it, updating the part when the signal
17
+ * changes independently of the host component.
18
+ */
19
+ export declare const watch: WatchDirectiveFunction;
20
+ export {};
package/dist/signal.js ADDED
@@ -0,0 +1,72 @@
1
+ import { defer } from "@bodil/core/async";
2
+ import { id } from "@bodil/core/fun";
3
+ import { Signal } from "@bodil/signal";
4
+ import { AsyncDirective } from "lit/async-directive.js";
5
+ import { directive } from "lit/directive.js";
6
+ let effectsPending = false;
7
+ const hostlessWatcher = new Signal.subtle.Watcher(() => {
8
+ if (!effectsPending) {
9
+ effectsPending = true;
10
+ defer(() => {
11
+ effectsPending = false;
12
+ for (const signal of hostlessWatcher.getPending()) {
13
+ signal.get();
14
+ }
15
+ hostlessWatcher.watch();
16
+ });
17
+ }
18
+ });
19
+ class WatchDirective extends AsyncDirective {
20
+ #host;
21
+ #signal;
22
+ #mapper = id;
23
+ #watcher;
24
+ #computed;
25
+ #watch() {
26
+ if (this.#watcher !== undefined) {
27
+ return;
28
+ }
29
+ this.#computed = new Signal.Computed(() => {
30
+ const value = this.#signal === undefined ? undefined : this.#mapper(this.#signal.get());
31
+ if (this.#host?.isUpdatePending === true) {
32
+ return;
33
+ }
34
+ this.setValue(value);
35
+ return value;
36
+ });
37
+ this.#watcher = hostlessWatcher;
38
+ this.#watcher.watch(this.#computed);
39
+ Signal.subtle.untrack(() => this.#computed?.get());
40
+ }
41
+ #unwatch() {
42
+ if (this.#watcher !== undefined) {
43
+ this.#watcher.unwatch(this.#computed);
44
+ this.#watcher = undefined;
45
+ }
46
+ }
47
+ render(signal, mapFn) {
48
+ return Signal.subtle.untrack(() => mapFn === undefined ? signal.get() : mapFn(signal.get()));
49
+ }
50
+ update(part, [signal, mapFn]) {
51
+ this.#host ??= part.options?.host;
52
+ if (signal !== this.#signal && this.#signal !== undefined) {
53
+ this.#unwatch();
54
+ }
55
+ this.#signal = signal;
56
+ this.#mapper = mapFn ?? id;
57
+ this.#watch();
58
+ return Signal.subtle.untrack(() => this.#mapper(this.#signal.get()));
59
+ }
60
+ disconnected() {
61
+ this.#unwatch();
62
+ }
63
+ reconnected() {
64
+ this.#watch();
65
+ }
66
+ }
67
+ /**
68
+ * Render a signal and subscribe to it, updating the part when the signal
69
+ * changes independently of the host component.
70
+ */
71
+ export const watch = directive(WatchDirective);
72
+ //# sourceMappingURL=signal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signal.js","sourceRoot":"","sources":["../src/signal.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,135 @@
1
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
2
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
3
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
4
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
5
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
6
+ var _, done = false;
7
+ for (var i = decorators.length - 1; i >= 0; i--) {
8
+ var context = {};
9
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
10
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
11
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
12
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
13
+ if (kind === "accessor") {
14
+ if (result === void 0) continue;
15
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
16
+ if (_ = accept(result.get)) descriptor.get = _;
17
+ if (_ = accept(result.set)) descriptor.set = _;
18
+ if (_ = accept(result.init)) initializers.unshift(_);
19
+ }
20
+ else if (_ = accept(result)) {
21
+ if (kind === "field") initializers.unshift(_);
22
+ else descriptor[key] = _;
23
+ }
24
+ }
25
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
26
+ done = true;
27
+ };
28
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
29
+ var useValue = arguments.length > 2;
30
+ for (var i = 0; i < initializers.length; i++) {
31
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
32
+ }
33
+ return useValue ? value : void 0;
34
+ };
35
+ import { Signal } from "@bodil/signal";
36
+ import { html } from "lit";
37
+ import { customElement } from "lit/decorators.js";
38
+ import { expect, test } from "vitest";
39
+ import { Component } from "./component";
40
+ import { watch } from "./signal";
41
+ test("watch directive", async () => {
42
+ const counter = Signal(1);
43
+ let renders = 0;
44
+ let WatchDirectiveTest = (() => {
45
+ let _classDecorators = [customElement("watch-directive-test")];
46
+ let _classDescriptor;
47
+ let _classExtraInitializers = [];
48
+ let _classThis;
49
+ let _classSuper = Component;
50
+ var WatchDirectiveTest = class extends _classSuper {
51
+ static { _classThis = this; }
52
+ static {
53
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
54
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
55
+ WatchDirectiveTest = _classThis = _classDescriptor.value;
56
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
57
+ __runInitializers(_classThis, _classExtraInitializers);
58
+ }
59
+ render() {
60
+ renders++;
61
+ return html `<p>${watch(counter)}</p>`;
62
+ }
63
+ };
64
+ return WatchDirectiveTest = _classThis;
65
+ })();
66
+ const t = document.createElement("watch-directive-test");
67
+ document.body.append(t);
68
+ await t.updateComplete;
69
+ expect(t.query("p")?.innerText).toBe("1");
70
+ expect(renders).toBe(1);
71
+ // update the counter and yield, it should update immediately.
72
+ counter.set(2);
73
+ await Promise.resolve();
74
+ expect(t.query("p")?.innerText).toBe("2");
75
+ expect(renders).toBe(1);
76
+ // request an update, it should not update immediately on yield
77
+ // because of the scheduled update
78
+ t.requestUpdate();
79
+ counter.set(3);
80
+ await Promise.resolve();
81
+ expect(t.query("p")?.innerText).toBe("2");
82
+ expect(renders).toBe(1);
83
+ // wait for the update to complete, it should now be in sync
84
+ await t.updateComplete;
85
+ expect(t.query("p")?.innerText).toBe("3");
86
+ expect(renders).toBe(2);
87
+ });
88
+ test("watch directive with mapper function", async () => {
89
+ const counter = Signal(1);
90
+ let renders = 0;
91
+ let WatchDirectiveMapperTest = (() => {
92
+ let _classDecorators = [customElement("watch-directive-mapper-test")];
93
+ let _classDescriptor;
94
+ let _classExtraInitializers = [];
95
+ let _classThis;
96
+ let _classSuper = Component;
97
+ var WatchDirectiveMapperTest = class extends _classSuper {
98
+ static { _classThis = this; }
99
+ static {
100
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
101
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
102
+ WatchDirectiveMapperTest = _classThis = _classDescriptor.value;
103
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
104
+ __runInitializers(_classThis, _classExtraInitializers);
105
+ }
106
+ render() {
107
+ renders++;
108
+ return html `<p>${watch(counter, (i) => i + 1000)}</p>`;
109
+ }
110
+ };
111
+ return WatchDirectiveMapperTest = _classThis;
112
+ })();
113
+ const t = document.createElement("watch-directive-mapper-test");
114
+ document.body.append(t);
115
+ await t.updateComplete;
116
+ expect(t.query("p")?.innerText).toBe("1001");
117
+ expect(renders).toBe(1);
118
+ // update the counter and yield, it should update immediately.
119
+ counter.set(2);
120
+ await Promise.resolve();
121
+ expect(t.query("p")?.innerText).toBe("1002");
122
+ expect(renders).toBe(1);
123
+ // request an update, it should not update immediately on yield
124
+ // because of the scheduled update
125
+ t.requestUpdate();
126
+ counter.set(3);
127
+ await Promise.resolve();
128
+ expect(t.query("p")?.innerText).toBe("1002");
129
+ expect(renders).toBe(1);
130
+ // wait for the update to complete, it should now be in sync
131
+ await t.updateComplete;
132
+ expect(t.query("p")?.innerText).toBe("1003");
133
+ expect(renders).toBe(2);
134
+ });
135
+ //# sourceMappingURL=signal.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signal.test.js","sourceRoot":"","sources":["../src/signal.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bodil/dom",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "DOM and web component tools",
5
5
  "homepage": "https://codeberg.org/bodil/dom",
6
6
  "repository": {
@@ -50,6 +50,12 @@
50
50
  "types": "./dist/geometry.d.ts",
51
51
  "import": "./dist/geometry.js"
52
52
  }
53
+ },
54
+ "./signal": {
55
+ "import": {
56
+ "types": "./dist/signal.d.ts",
57
+ "import": "./dist/signal.js"
58
+ }
53
59
  }
54
60
  },
55
61
  "publishConfig": {
package/src/index.ts CHANGED
@@ -8,5 +8,6 @@ import * as css from "./css";
8
8
  import * as dom from "./dom";
9
9
  import * as event from "./event";
10
10
  import * as geometry from "./geometry";
11
+ import * as signal from "./signal";
11
12
 
12
- export { component, css, dom, event, geometry };
13
+ export { component, css, dom, event, geometry, signal };
@@ -0,0 +1,89 @@
1
+ import { Signal } from "@bodil/signal";
2
+ import { html } from "lit";
3
+ import { customElement } from "lit/decorators.js";
4
+ import { expect, test } from "vitest";
5
+
6
+ import { Component } from "./component";
7
+ import { watch } from "./signal";
8
+
9
+ test("watch directive", async () => {
10
+ const counter = Signal(1);
11
+ let renders = 0;
12
+
13
+ @customElement("watch-directive-test")
14
+ class WatchDirectiveTest extends Component {
15
+ render() {
16
+ renders++;
17
+ return html`<p>${watch(counter)}</p>`;
18
+ }
19
+ }
20
+
21
+ const t = document.createElement("watch-directive-test") as WatchDirectiveTest;
22
+ document.body.append(t);
23
+ await t.updateComplete;
24
+
25
+ expect(t.query("p")?.innerText).toBe("1");
26
+ expect(renders).toBe(1);
27
+
28
+ // update the counter and yield, it should update immediately.
29
+ counter.set(2);
30
+ await Promise.resolve();
31
+
32
+ expect(t.query("p")?.innerText).toBe("2");
33
+ expect(renders).toBe(1);
34
+
35
+ // request an update, it should not update immediately on yield
36
+ // because of the scheduled update
37
+ t.requestUpdate();
38
+ counter.set(3);
39
+ await Promise.resolve();
40
+
41
+ expect(t.query("p")?.innerText).toBe("2");
42
+ expect(renders).toBe(1);
43
+
44
+ // wait for the update to complete, it should now be in sync
45
+ await t.updateComplete;
46
+ expect(t.query("p")?.innerText).toBe("3");
47
+ expect(renders).toBe(2);
48
+ });
49
+
50
+ test("watch directive with mapper function", async () => {
51
+ const counter = Signal(1);
52
+ let renders = 0;
53
+
54
+ @customElement("watch-directive-mapper-test")
55
+ class WatchDirectiveMapperTest extends Component {
56
+ render() {
57
+ renders++;
58
+ return html`<p>${watch(counter, (i) => i + 1000)}</p>`;
59
+ }
60
+ }
61
+
62
+ const t = document.createElement("watch-directive-mapper-test") as WatchDirectiveMapperTest;
63
+ document.body.append(t);
64
+ await t.updateComplete;
65
+
66
+ expect(t.query("p")?.innerText).toBe("1001");
67
+ expect(renders).toBe(1);
68
+
69
+ // update the counter and yield, it should update immediately.
70
+ counter.set(2);
71
+ await Promise.resolve();
72
+
73
+ expect(t.query("p")?.innerText).toBe("1002");
74
+ expect(renders).toBe(1);
75
+
76
+ // request an update, it should not update immediately on yield
77
+ // because of the scheduled update
78
+ t.requestUpdate();
79
+ counter.set(3);
80
+ await Promise.resolve();
81
+
82
+ expect(t.query("p")?.innerText).toBe("1002");
83
+ expect(renders).toBe(1);
84
+
85
+ // wait for the update to complete, it should now be in sync
86
+ await t.updateComplete;
87
+ expect(t.query("p")?.innerText).toBe("1003");
88
+ expect(renders).toBe(2);
89
+ });
package/src/signal.ts ADDED
@@ -0,0 +1,98 @@
1
+ import { defer } from "@bodil/core/async";
2
+ import { id } from "@bodil/core/fun";
3
+ import { Signal } from "@bodil/signal";
4
+ import { AsyncDirective } from "lit/async-directive.js";
5
+ import { directive, type DirectiveResult, type Part } from "lit/directive.js";
6
+
7
+ // Most of this is from
8
+ // https://github.com/lit/lit/blob/main/packages/labs/signals/src/lib/watch.ts
9
+
10
+ import type { Component } from "./component";
11
+
12
+ let effectsPending = false;
13
+ const hostlessWatcher = new Signal.subtle.Watcher(() => {
14
+ if (!effectsPending) {
15
+ effectsPending = true;
16
+ defer(() => {
17
+ effectsPending = false;
18
+ for (const signal of hostlessWatcher.getPending()) {
19
+ signal.get();
20
+ }
21
+ hostlessWatcher.watch();
22
+ });
23
+ }
24
+ });
25
+
26
+ class WatchDirective<T> extends AsyncDirective {
27
+ #host?: Component;
28
+ #signal?: Signal.State<T> | Signal.Computed<T>;
29
+ #mapper: (value: T) => unknown = id;
30
+ #watcher?: typeof hostlessWatcher;
31
+ #computed: Signal.Computed<unknown> | undefined;
32
+
33
+ #watch() {
34
+ if (this.#watcher !== undefined) {
35
+ return;
36
+ }
37
+ this.#computed = new Signal.Computed(() => {
38
+ const value = this.#signal === undefined ? undefined : this.#mapper(this.#signal.get());
39
+ if (this.#host?.isUpdatePending === true) {
40
+ return;
41
+ }
42
+ this.setValue(value);
43
+ return value;
44
+ });
45
+ this.#watcher = hostlessWatcher;
46
+ this.#watcher.watch(this.#computed);
47
+ Signal.subtle.untrack(() => this.#computed?.get());
48
+ }
49
+
50
+ #unwatch() {
51
+ if (this.#watcher !== undefined) {
52
+ this.#watcher.unwatch(this.#computed!);
53
+ this.#watcher = undefined;
54
+ }
55
+ }
56
+
57
+ render(signal: Signal.State<T> | Signal.Computed<T>, mapFn?: (value: T) => unknown): unknown {
58
+ return Signal.subtle.untrack(() =>
59
+ mapFn === undefined ? signal.get() : mapFn(signal.get()),
60
+ );
61
+ }
62
+
63
+ override update(
64
+ part: Part,
65
+ [signal, mapFn]: [
66
+ signal: Signal.State<T> | Signal.Computed<T>,
67
+ mapFn?: (value: T) => unknown,
68
+ ],
69
+ ): unknown {
70
+ this.#host ??= part.options?.host as Component;
71
+ if (signal !== this.#signal && this.#signal !== undefined) {
72
+ this.#unwatch();
73
+ }
74
+ this.#signal = signal;
75
+ this.#mapper = mapFn ?? id;
76
+ this.#watch();
77
+ return Signal.subtle.untrack(() => this.#mapper(this.#signal!.get()));
78
+ }
79
+
80
+ protected override disconnected(): void {
81
+ this.#unwatch();
82
+ }
83
+
84
+ protected override reconnected(): void {
85
+ this.#watch();
86
+ }
87
+ }
88
+
89
+ export type WatchDirectiveFunction = <T>(
90
+ signal: Signal.State<T> | Signal.Computed<T>,
91
+ mapFn?: (value: T) => unknown,
92
+ ) => DirectiveResult<typeof WatchDirective<T>>;
93
+
94
+ /**
95
+ * Render a signal and subscribe to it, updating the part when the signal
96
+ * changes independently of the host component.
97
+ */
98
+ export const watch = directive(WatchDirective) as WatchDirectiveFunction;