Skip to content

Commit 1e1e82b

Browse files
committed
- new signals
- rename $effect to $bind - type fixes - back to signal render wrappers
1 parent f0409d6 commit 1e1e82b

File tree

11 files changed

+346
-425
lines changed

11 files changed

+346
-425
lines changed

README.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
- 🌐 Builder doesn't only work with `HTMLElement`(s) but works with any `Node` instance including `ShadowRoot`, `DocumentFragment`,
2424
`Document`... any `Node` type, including future ones.
2525
- 🎩 Builder converts existing methods on the `Node` instance to builder pattern with `Proxy`.
26-
-**Uses signals for reactivity.**
26+
-**Signal implementation that makes sense and useable.**
2727
- 🧙 **Signals are extendable,** allowing chaining with utilities like .pipe() and .derive() to build custom workflows..
2828
- ✂️ Allows direct DOM manipulation.
2929
- 📁 No special file extensions.
@@ -60,7 +60,7 @@ To install **purify.js**, follow the [jsr.io/@purifyjs/core](https://jsr.io/@pur
6060
### Counter
6161

6262
```ts
63-
import { computed, Lifecycle, ref, Signal, tags } from "@purifyjs/core";
63+
import { computed, Lifecycle, Signal, state, tags } from "@purifyjs/core";
6464

6565
const { div, section, button, ul, li, input } = tags;
6666

@@ -69,7 +69,7 @@ function App() {
6969
}
7070

7171
function Counter() {
72-
const count = ref(0);
72+
const count = state(0);
7373
const double = count.derive((count) => count * 2);
7474
const half = computed(() => count.val * 0.5);
7575

@@ -81,7 +81,7 @@ function Counter() {
8181
.title("Decrement by 1")
8282
.onclick(() => count.val--)
8383
.textContent("-"),
84-
input().type("number").$effect(useBindNumber(count)).step("1"),
84+
input().type("number").$bind(useBindNumber(count)).step("1"),
8585
button()
8686
.title("Increment by 1")
8787
.onclick(() => count.val++)
@@ -122,7 +122,7 @@ document.body.append(App().$node);
122122
### ShadowRoot
123123

124124
```ts
125-
import { Builder, ref, tags } from "@purifyjs/core";
125+
import { Builder, state, tags } from "@purifyjs/core";
126126

127127
const { div, button } = tags;
128128

@@ -134,7 +134,7 @@ function Counter() {
134134
const host = div();
135135
const shadow = new Builder(host.$node.attachShadow({ mode: "open" }));
136136

137-
const count = ref(0);
137+
const count = state(0);
138138

139139
shadow.replaceChildren$(
140140
button()
@@ -151,7 +151,7 @@ document.body.append(App().$node);
151151
### Web Components
152152

153153
```ts
154-
import { Builder, ref, tags, WithLifecycle } from "@purifyjs/core";
154+
import { Builder, state, tags, WithLifecycle } from "@purifyjs/core";
155155

156156
const { div, button } = tags;
157157

@@ -170,7 +170,7 @@ class CounterElement extends WithLifecycle(HTMLElement) {
170170
customElements.define("x-counter", CounterElement);
171171
}
172172

173-
#count = ref(0);
173+
#count = state(0);
174174

175175
constructor() {
176176
super();
@@ -267,7 +267,7 @@ JSX is not part of this library natively, but a wrapper can be made quite easily
267267
WithLifecycle(HTMLElement); // or
268268
WithLifecycle(HTMLDivElement);
269269
```
270-
It adds a lifecycle function called `$effect()` to any `HTMLElement` type. Which can later be extended by a custom element like
270+
It adds a lifecycle function called `$bind()` to any `HTMLElement` type. Which can later be extended by a custom element like
271271
```ts
272272
class MyElement extends WithLifecycle(HTMLElement)
273273
```

apps/size/src/app.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
import { computed, tags } from "@purifyjs/core";
2-
[computed, tags];
1+
import { Builder, computed, signal, state, tags, WithLifecycle } from "@purifyjs/core";
2+
[computed, state, signal, Builder, tags, WithLifecycle];

apps/vite/src/app.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { Builder, ref, tags } from "@purifyjs/core";
1+
import { Builder, state, tags } from "@purifyjs/core";
22

3-
const time = ref(new Date().toLocaleString(), (set) => {
3+
const time = state(new Date().toLocaleString(), (set) => {
44
const interval = setInterval(() => set(new Date().toLocaleString()), 1000);
55
return () => {
66
clearInterval(interval);

apps/vite/src/examples/counter-example.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { computed, Lifecycle, ref, Signal, tags } from "@purifyjs/core";
1+
import { computed, Lifecycle, Signal, state, tags } from "@purifyjs/core";
22

33
const { div, section, button, ul, li, input } = tags;
44

@@ -7,7 +7,7 @@ function App() {
77
}
88

99
function Counter() {
10-
const count = ref(0);
10+
const count = state(0);
1111
const double = count.derive((count) => count * 2);
1212
const half = computed(() => count.val * 0.5);
1313

@@ -19,7 +19,9 @@ function Counter() {
1919
.title("Decrement by 1")
2020
.onclick(() => count.val--)
2121
.textContent("-"),
22-
input().type("number").$effect(useBindNumber(count)).step("1"),
22+
input().type("number").$bind(useBindNumber(count)).step(
23+
"1",
24+
),
2325
button()
2426
.title("Increment by 1")
2527
.onclick(() => count.val++)

apps/vite/src/examples/shadowroot-example.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Builder, ref, tags } from "@purifyjs/core";
1+
import { Builder, state, tags } from "@purifyjs/core";
22

33
const { div, button } = tags;
44

@@ -10,7 +10,7 @@ function Counter() {
1010
const host = div();
1111
const shadow = new Builder(host.$node.attachShadow({ mode: "open" }));
1212

13-
const count = ref(0);
13+
const count = state(0);
1414

1515
shadow.replaceChildren$(
1616
button()

apps/vite/src/examples/web-component-example.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Builder, ref, tags, WithLifecycle } from "@purifyjs/core";
1+
import { Builder, state, tags, WithLifecycle } from "@purifyjs/core";
22

33
const { div, button } = tags;
44

@@ -17,7 +17,7 @@ class CounterElement extends WithLifecycle(HTMLElement) {
1717
customElements.define("x-counter", CounterElement);
1818
}
1919

20-
#count = ref(0);
20+
#count = state(0);
2121

2222
constructor() {
2323
super();

deno.lock

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/dom.test.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable @typescript-eslint/no-unused-vars */
22

3-
import { ref } from "./signals.ts";
3+
import { state } from "./signals.ts";
44
import { Builder, type tags as tags_type, type WithLifecycle } from "./dom.ts";
55
declare const tags: typeof tags_type;
66

@@ -31,29 +31,29 @@ _(() => {
3131

3232
svgBuilder.replaceChildren("123");
3333
/// @ts-expect-error Don't allow signals on elements without lifecycle
34-
svgBuilder.replaceChildren(ref("123"));
34+
svgBuilder.replaceChildren(state("123"));
3535
/// @ts-expect-error Don't allow signals on elements without lifecycle
36-
svgBuilder.ariaLabel(ref("foo"));
36+
svgBuilder.ariaLabel(state("foo"));
3737
/// @ts-expect-error Don't allow signals on elements without lifecycle
38-
svgBuilder.$effect(() => {});
38+
svgBuilder.$bind(() => {});
3939

4040
divBuilder.replaceChildren("123");
4141
divBuilder.replaceChildren$(divWithLifecycleBuilder);
4242
/// @ts-expect-error Don't allow signals on elements without lifecycle
43-
divBuilder.replaceChildren(ref("123"));
43+
divBuilder.replaceChildren(state("123"));
4444
/// @ts-expect-error Don't allow signals on elements without lifecycle
45-
divBuilder.ariaLabel(ref("foo"));
45+
divBuilder.ariaLabel(state("foo"));
4646
/// @ts-expect-error Don't allow signals on elements without lifecycle
47-
divBuilder.$effect(() => {});
47+
divBuilder.$bind(() => {});
4848

4949
divWithLifecycleBuilder.replaceChildren("123");
50-
divWithLifecycleBuilder.replaceChildren(ref("123"));
50+
divWithLifecycleBuilder.replaceChildren(state("123"));
5151
divWithLifecycleBuilder.replaceChildren$(divWithLifecycleBuilder);
52-
divWithLifecycleBuilder.ariaLabel(ref("foo"));
53-
divWithLifecycleBuilder.$effect(() => {});
52+
divWithLifecycleBuilder.ariaLabel(state("foo"));
53+
divWithLifecycleBuilder.$bind(() => {});
5454

5555
// Form element sometimes might cause issues since it has [key: string] and [index: number] in it, so be careful, keep this in mind
5656
formWithLifecycleBuilder.replaceChildren$("");
57-
formWithLifecycleBuilder.$effect(() => {});
57+
formWithLifecycleBuilder.$bind(() => {});
5858
formWithLifecycleBuilder.ariaAtomic("true");
5959
});

lib/dom.ts

Lines changed: 31 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Embrace some optimal ugly code, if it makes the minified code smaller.
22

33
import type { StrictARIA } from "./aria.ts";
4-
import { computed, Signal } from "./signals.ts";
4+
import { Signal } from "./signals.ts";
55
import type { _Event, Equal, Extends, Fn, If, IsReadonly, Not } from "./utils.ts";
66
import { instancesOf } from "./utils.ts";
77

@@ -136,7 +136,9 @@ type ProxyFunctionArgs<T extends Node, K extends keyof T, Args extends unknown[]
136136
: T extends WithLifecycle ? RecursiveSignalArgs<Args>
137137
: Args;
138138

139-
type MaybeNodeLikeArg<T> = T extends Node ? T | Builder<T> | null : T extends string ? string | { toString(): string } : T;
139+
type MaybeNodeLikeArg<T> = T extends Node ? T | Builder<T> | null | undefined
140+
: T extends string ? string | { toString(): string } | null | undefined
141+
: T;
140142
type MaybeNodeLikeArgs<Args extends unknown[], R extends unknown[] = []> = Args extends [infer Head, ...infer Tail]
141143
? MaybeNodeLikeArgs<Tail, [...R, MaybeNodeLikeArg<Head>]>
142144
: Args extends (infer U)[] ? MaybeNodeLikeArg<U>[]
@@ -205,7 +207,7 @@ export let Builder: BuilderConstructor = function <T extends Node & Partial<With
205207
};
206208

207209
if (instancesOf(value, Signal)) {
208-
node.$effect!(() => value.follow(setOrRemoveAttribute, true));
210+
node.$bind!(() => value.follow(setOrRemoveAttribute, true));
209211
} else {
210212
setOrRemoveAttribute(value);
211213
}
@@ -222,49 +224,14 @@ export let Builder: BuilderConstructor = function <T extends Node & Partial<With
222224
return target[targetName];
223225
}
224226

225-
nodeName = (targetName.at(-1) == "$" ? targetName.slice(0, -1) : targetName) as keyof T & string;
227+
nodeName = (targetName.at(-1) == "$" ? (targetName.slice(0, -1)) : targetName) as never;
226228
fn = (instancesOf(node[nodeName], Function) && !Object.hasOwn(node, nodeName))
227-
? (nodeName == targetName) ? (args: unknown[]) => (node[nodeName] as Fn)(...args) : ((
228-
args: unknown[],
229-
computedArgs: Signal<unknown[]>,
230-
hasSignal: boolean | undefined,
231-
unwrap = (value: unknown): string | Node | { toString(): string } => {
232-
if (value == null) {
233-
return unwrap([]);
234-
}
235-
if (instancesOf(value, Builder)) {
236-
return value.$node;
237-
}
238-
if (instancesOf(value, Signal)) {
239-
hasSignal = true;
240-
return unwrap(value.val);
241-
}
242-
if (instancesOf(value, Array)) {
243-
return unwrap(new Builder(document.createDocumentFragment()).append(...value.map(unwrap) as never[]));
244-
}
245-
return value;
246-
},
247-
unwrappedArgs: unknown[],
248-
) => {
249-
// This is the best i can come up with
250-
// Normally if we had a persistent document fragment with lifecyle,
251-
// we could have just wrapped signals with it in the DOM. Not doing these at all.
252-
//
253-
// I can wrap them with an element with lifecycle and give it `display:contents`,
254-
// but that causes other issues in the dx
255-
unwrappedArgs = args.map(unwrap);
256-
if (hasSignal) {
257-
computedArgs = computed(() => args.map(unwrap));
258-
cleanups[targetName] = node.$effect!(() =>
259-
computedArgs.follow((newArgs) => (node[nodeName] as Fn)(...newArgs), true)
260-
);
261-
} else {
262-
(node[nodeName] as Fn)(...unwrappedArgs);
263-
}
264-
})
229+
? (nodeName == targetName)
230+
? (args: unknown[]) => (node[nodeName] as Fn)(...args)
231+
: ((args: Member[]) => (node[nodeName] as Fn)(...args.map(toChild)))
265232
: (([value]: [unknown]) => {
266233
if (instancesOf(value, Signal)) {
267-
cleanups[targetName] = node.$effect!(() => value.follow((value) => node[nodeName] = value as never, true));
234+
cleanups[targetName] = node.$bind!(() => value.follow((value) => node[nodeName] = value as never, true));
268235
} else {
269236
node[nodeName] = value as never;
270237
}
@@ -290,12 +257,12 @@ export namespace Lifecycle {
290257
export type OffConnected = () => void;
291258
}
292259
export type Lifecycle<T extends HTMLElement = HTMLElement> = {
293-
$effect(callback: Lifecycle.OnConnected<T>): Lifecycle.OffConnected;
260+
$bind(callback: Lifecycle.OnConnected<T>): Lifecycle.OffConnected;
294261
};
295262

296263
export type WithLifecycle<T extends HTMLElement = HTMLElement> = T & Lifecycle<T>;
297264

298-
let withLifecycleCache = new Map<{ new (): HTMLElement }, { new (): WithLifecycle<HTMLElement> }>();
265+
let withLifecycleCache = new WeakMap<{ new (): HTMLElement }, { new (): WithLifecycle<HTMLElement> }>();
299266
export let WithLifecycle = <BaseConstructor extends { new (...params: any[]): HTMLElement }>(
300267
Base: BaseConstructor,
301268
): {
@@ -311,14 +278,14 @@ export let WithLifecycle = <BaseConstructor extends { new (...params: any[]): HT
311278
#callbacks = new Map<Lifecycle.OnConnected<Base>, ReturnType<Lifecycle.OnConnected<Base>> | null>();
312279

313280
connectedCallback(): void {
314-
this.#callbacks.keys().forEach((callback) => this.#callbacks.set(callback, callback(this)));
281+
this.#callbacks.forEach((_, callback) => this.#callbacks.set(callback, callback(this)));
315282
}
316283

317284
disconnectedCallback(): void {
318285
this.#callbacks.forEach((disconnectedCallbackOrNullish) => disconnectedCallbackOrNullish?.());
319286
}
320287

321-
$effect(callback: Lifecycle.OnConnected<Base>): Lifecycle.OffConnected {
288+
$bind(callback: Lifecycle.OnConnected<Base>): Lifecycle.OffConnected {
322289
this.#callbacks.set(callback, null);
323290
return () => {
324291
let off = this.#callbacks.get(callback);
@@ -332,3 +299,20 @@ export let WithLifecycle = <BaseConstructor extends { new (...params: any[]): HT
332299

333300
return constructor as never;
334301
};
302+
303+
export type Member = RecursiveSignalAndArrayOf<MaybeNodeLikeArg<Node | string>>;
304+
let toChild = (member: Member): string | Node => {
305+
if (instancesOf(member, Builder)) {
306+
return member.$node;
307+
}
308+
if (instancesOf(member, Signal)) {
309+
return toChild(
310+
tags.div({ style: "display:contents" })
311+
.$bind((element) => member.follow((value) => element.replaceChildren(toChild(value)), true)),
312+
);
313+
}
314+
if (instancesOf(member, Array)) {
315+
return toChild(new Builder(document.createDocumentFragment()).append(...member.map(toChild) as never[]));
316+
}
317+
return (member satisfies { toString(): string } | null | undefined) as string;
318+
};

0 commit comments

Comments
 (0)