Skip to content

Commit 66f7828

Browse files
committed
chore: allow composing bindable
1 parent 2509596 commit 66f7828

File tree

8 files changed

+199
-5
lines changed

8 files changed

+199
-5
lines changed

.changeset/kind-cows-divide.md

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@zag-js/preact": patch
3+
"@zag-js/svelte": patch
4+
"@zag-js/react": patch
5+
"@zag-js/solid": patch
6+
"@zag-js/vue": patch
7+
"@zag-js/core": patch
8+
---
9+
10+
[Internal] Support `ref` and `cleanup` on bindable function to help create using state compositions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { Bindable, BindableFn, BindableParams } from "@zag-js/core"
2+
import { useMachine } from "@zag-js/react"
3+
import { isFunction } from "@zag-js/utils"
4+
5+
interface ValidateParams<T> {
6+
validate?: (b: T) => boolean
7+
defaultValid?: boolean
8+
onValidityChange?: (props: { valid: boolean; lastValid: T }) => void
9+
}
10+
11+
function withLastValid<T>(bindable: BindableFn, props: () => BindableParams<T> & ValidateParams<T>): Bindable<T> {
12+
const value = bindable(props)
13+
14+
const lastValidValue = bindable(() => ({
15+
defaultValue: props().validate?.(value.get()) ? value.get() : undefined,
16+
}))
17+
18+
const valid = bindable<boolean>(() => ({
19+
defaultValue: props().defaultValid,
20+
onChange(valid) {
21+
props().onValidityChange?.({ valid, lastValid: lastValidValue.get() })
22+
},
23+
}))
24+
25+
return {
26+
...value,
27+
set(valueOrFn) {
28+
const nextValue = isFunction(valueOrFn) ? valueOrFn(value.get()) : valueOrFn
29+
if (props().validate?.(nextValue)) {
30+
lastValidValue.set(nextValue)
31+
valid.set(true)
32+
} else {
33+
valid.set(false)
34+
}
35+
value.set(nextValue)
36+
},
37+
}
38+
}
39+
40+
function withAutoreset<T>(bindable: BindableFn, props: () => BindableParams<T> & { resetAfter: number }): Bindable<T> {
41+
const value = bindable(props)
42+
43+
const timer = bindable.ref<NodeJS.Timeout | undefined>(undefined)
44+
45+
const resetValue = () =>
46+
setTimeout(() => {
47+
value.set(value.initial)
48+
}, props().resetAfter)
49+
50+
bindable.cleanup(() => {
51+
if (timer.get()) clearTimeout(timer.get()!)
52+
timer.set(undefined)
53+
})
54+
55+
return {
56+
...value,
57+
set(valueOrFn) {
58+
value.set(valueOrFn)
59+
const currentTimer = timer.get()
60+
if (currentTimer) clearTimeout(currentTimer)
61+
timer.set(resetValue())
62+
},
63+
}
64+
}
65+
66+
export default function Page() {
67+
const service = useMachine({
68+
initialState() {
69+
return "idle"
70+
},
71+
72+
context({ bindable }) {
73+
return {
74+
count: withLastValid<string>(bindable, () => ({
75+
defaultValue: "hello",
76+
validate(value) {
77+
return value.length > 3
78+
},
79+
onValidityChange(valid) {
80+
console.log("valid", valid)
81+
},
82+
})),
83+
index: withAutoreset<number>(bindable, () => ({
84+
defaultValue: 0,
85+
resetAfter: 1000,
86+
})),
87+
}
88+
},
89+
90+
states: {
91+
idle: {
92+
on: {
93+
CLICK: { target: "active" },
94+
},
95+
},
96+
},
97+
})
98+
return (
99+
<main>
100+
<div>{service.context.get("count")}</div>
101+
<button onClick={() => service.context.set("count", "df")}>Click</button>
102+
<hr />
103+
<div>{service.context.get("index")}</div>
104+
<button onClick={() => service.context.set("index", (prev) => prev + 1)}>Click</button>
105+
</main>
106+
)
107+
}

packages/core/src/types.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,15 @@ export interface BindableContext<T extends Dict> {
6363
hash<K extends keyof T["context"]>(key: K): string
6464
}
6565

66-
interface BindableFn {
66+
interface BindableRef<T> {
67+
get: () => T
68+
set: (next: T) => void
69+
}
70+
71+
export interface BindableFn {
6772
<K>(params: () => BindableParams<K>): Bindable<K>
73+
cleanup: (fn: VoidFunction) => void
74+
ref: <T>(defaultValue: T) => BindableRef<T>
6875
}
6976

7077
export interface Scope {

packages/frameworks/preact/src/bindable.ts

+14
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,17 @@ export function useBindable<T>(props: () => BindableParams<T>): Bindable<T> {
5757
},
5858
}
5959
}
60+
61+
useBindable.cleanup = (fn: VoidFunction) => {
62+
useLayoutEffect(() => fn, [])
63+
}
64+
65+
useBindable.ref = <T>(defaultValue: T) => {
66+
const value = useRef(defaultValue)
67+
return {
68+
get: () => value.current,
69+
set: (next: T) => {
70+
value.current = next
71+
},
72+
}
73+
}

packages/frameworks/react/src/bindable.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Bindable, BindableParams } from "@zag-js/core"
22
import { identity, isFunction } from "@zag-js/utils"
3-
import { useRef, useState } from "react"
3+
import { useEffect, useRef, useState } from "react"
44
import { flushSync } from "react-dom"
55
import { useSafeLayoutEffect } from "./use-layout-effect"
66

@@ -56,3 +56,17 @@ export function useBindable<T>(props: () => BindableParams<T>): Bindable<T> {
5656
},
5757
}
5858
}
59+
60+
useBindable.cleanup = (fn: VoidFunction) => {
61+
useEffect(() => fn, [])
62+
}
63+
64+
useBindable.ref = <T>(defaultValue: T) => {
65+
const value = useRef(defaultValue)
66+
return {
67+
get: () => value.current,
68+
set: (next: T) => {
69+
value.current = next
70+
},
71+
}
72+
}

packages/frameworks/solid/src/bindable.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Bindable, BindableParams } from "@zag-js/core"
22
import { isFunction } from "@zag-js/utils"
3-
import { createEffect, createMemo, createSignal, type Accessor } from "solid-js"
3+
import { createEffect, createMemo, createSignal, type Accessor, onCleanup } from "solid-js"
44

55
export function createBindable<T>(props: Accessor<BindableParams<T>>): Bindable<T> {
66
const initial = props().value ?? props().defaultValue
@@ -51,3 +51,17 @@ export function createBindable<T>(props: Accessor<BindableParams<T>>): Bindable<
5151
},
5252
}
5353
}
54+
55+
createBindable.cleanup = (fn: VoidFunction) => {
56+
onCleanup(() => fn())
57+
}
58+
59+
createBindable.ref = <T>(defaultValue: T) => {
60+
let value = defaultValue
61+
return {
62+
get: () => value,
63+
set: (next: T) => {
64+
value = next
65+
},
66+
}
67+
}

packages/frameworks/svelte/src/bindable.svelte.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type Bindable, type BindableParams } from "@zag-js/core"
22
import { identity, isFunction } from "@zag-js/utils"
3-
import { flushSync } from "svelte"
3+
import { flushSync, onDestroy } from "svelte"
44

55
export function bindable<T>(props: () => BindableParams<T>): Bindable<T> {
66
const initial = props().defaultValue ?? props().value
@@ -51,3 +51,17 @@ export function bindable<T>(props: () => BindableParams<T>): Bindable<T> {
5151
},
5252
}
5353
}
54+
55+
bindable.cleanup = (fn: VoidFunction) => {
56+
onDestroy(() => fn())
57+
}
58+
59+
bindable.ref = <T>(defaultValue: T) => {
60+
let value = defaultValue
61+
return {
62+
get: () => value,
63+
set: (next: T) => {
64+
value = next
65+
},
66+
}
67+
}

packages/frameworks/vue/src/bindable.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Bindable, BindableParams } from "@zag-js/core"
22
import { isFunction } from "@zag-js/utils"
3-
import { computed as __computed, shallowRef } from "vue"
3+
import { computed as __computed, onUnmounted, shallowRef } from "vue"
44

55
export function bindable<T>(props: () => BindableParams<T>): Bindable<T> {
66
const initial = props().defaultValue ?? props().value
@@ -36,3 +36,17 @@ export function bindable<T>(props: () => BindableParams<T>): Bindable<T> {
3636
},
3737
}
3838
}
39+
40+
bindable.cleanup = (fn: VoidFunction) => {
41+
onUnmounted(() => fn())
42+
}
43+
44+
bindable.ref = <T>(defaultValue: T) => {
45+
let value = defaultValue
46+
return {
47+
get: () => value,
48+
set: (next: T) => {
49+
value = next
50+
},
51+
}
52+
}

0 commit comments

Comments
 (0)