zag-ripple 0.0.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.
- package/package.json +38 -0
- package/src/bindable.ripple +68 -0
- package/src/index.ripple +3 -0
- package/src/machine.ripple +293 -0
- package/src/normalize-props.ripple +6 -0
- package/src/refs.ripple +13 -0
- package/src/track.ripple +31 -0
- package/tsconfig.json +6 -0
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "zag-ripple",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "RippleJS Adapter for Zag JS",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"js",
|
|
7
|
+
"machine",
|
|
8
|
+
"xstate",
|
|
9
|
+
"statechart",
|
|
10
|
+
"component",
|
|
11
|
+
"zag-js",
|
|
12
|
+
"RippleJS",
|
|
13
|
+
"ripple"
|
|
14
|
+
],
|
|
15
|
+
"author": "Abraham Aremu <anubra266@gmail.com>",
|
|
16
|
+
"homepage": "https://github.com/anubra266/zag-ripple#readme",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"main": "src/index.ripple",
|
|
19
|
+
"repository": "https://github.com/anubra266/zag-ripple/tree/main/packages/zag-ripple",
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/anubra266i/zag-ripple/issues"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "echo \"No test specified\""
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@zag-js/core": "^1.3.3",
|
|
32
|
+
"@zag-js/types": "latest",
|
|
33
|
+
"@zag-js/utils": "latest"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"typescript": "~5.6.2"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { identity, isFunction } from "@zag-js/utils"
|
|
2
|
+
import { track, effect, flushSync, untrack } from "ripple"
|
|
3
|
+
|
|
4
|
+
export function createBindable(props) {
|
|
5
|
+
const initial = props().defaultValue ?? props().value
|
|
6
|
+
const eq = props().isEqual ?? Object.is
|
|
7
|
+
|
|
8
|
+
let value = track(initial)
|
|
9
|
+
let controlled = track(() => props().value !== undefined)
|
|
10
|
+
|
|
11
|
+
let valueRef = { current: untrack(() => @value) }
|
|
12
|
+
let prevValue = { current: undefined }
|
|
13
|
+
|
|
14
|
+
effect(() => {
|
|
15
|
+
const v = @controlled ? props().value : @value
|
|
16
|
+
valueRef = { current: v }
|
|
17
|
+
prevValue = { current: v }
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const setValueFn = (v) => {
|
|
21
|
+
const next = isFunction(v) ? v(valueRef.current) : v
|
|
22
|
+
const prev = prevValue.current
|
|
23
|
+
if (props().debug) {
|
|
24
|
+
console.log(`[bindable > ${props().debug}] setValue`, { next, prev })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!@controlled) {
|
|
28
|
+
@value = next;
|
|
29
|
+
}
|
|
30
|
+
if (!eq(next, prev)) {
|
|
31
|
+
props().onChange?.(next, prev)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function get(): T {
|
|
36
|
+
return @controlled ? props().value : @value
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
initial,
|
|
41
|
+
ref: valueRef,
|
|
42
|
+
get,
|
|
43
|
+
set(val) {
|
|
44
|
+
const exec = props().sync ? flushSync : identity
|
|
45
|
+
exec(() => setValueFn(val))
|
|
46
|
+
},
|
|
47
|
+
invoke: (nextValue, prevValue) => {
|
|
48
|
+
props().onChange?.(nextValue, prevValue)
|
|
49
|
+
},
|
|
50
|
+
hash: (value) => {
|
|
51
|
+
return props().hash?.(value) ?? String(value)
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
createBindable.cleanup = (_fn: VoidFunction) => {
|
|
57
|
+
effect(() => fn, [])
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
createBindable.ref = (defaultValue: any) => {
|
|
61
|
+
let value = defaultValue
|
|
62
|
+
return {
|
|
63
|
+
get: () => value,
|
|
64
|
+
set: (next) => {
|
|
65
|
+
value = next
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/index.ripple
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActionsOrFn,
|
|
3
|
+
BindableContext,
|
|
4
|
+
ChooseFn,
|
|
5
|
+
ComputedFn,
|
|
6
|
+
EffectsOrFn,
|
|
7
|
+
GuardFn,
|
|
8
|
+
Machine,
|
|
9
|
+
MachineSchema,
|
|
10
|
+
Params,
|
|
11
|
+
Service,
|
|
12
|
+
} from "@zag-js/core"
|
|
13
|
+
import { createScope, INIT_STATE, MachineStatus } from "@zag-js/core"
|
|
14
|
+
import { compact, ensure, isFunction, isString, toArray, warn } from "@zag-js/utils"
|
|
15
|
+
import { track, effect, flushSync } from "ripple"
|
|
16
|
+
import { createBindable } from "./bindable.ripple"
|
|
17
|
+
import { createRefs } from "./refs.ripple"
|
|
18
|
+
import { createTrack } from "./track.ripple"
|
|
19
|
+
|
|
20
|
+
function access(userProps) {
|
|
21
|
+
if (isFunction(userProps)) return userProps()
|
|
22
|
+
return userProps
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useMachine(
|
|
26
|
+
machine,
|
|
27
|
+
userProps = {},
|
|
28
|
+
) {
|
|
29
|
+
let scope = track(() => {
|
|
30
|
+
const { id, ids, getRootNode } = access(userProps)
|
|
31
|
+
return createScope({ id, ids, getRootNode })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const debug = (...args: any[]) => {
|
|
35
|
+
if (machine.debug) console.log(...args)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let props = track(() =>
|
|
39
|
+
machine.props?.({ props: compact(access(userProps)), scope: @scope }) ?? access(userProps)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const prop = useProp(() => @props)
|
|
43
|
+
|
|
44
|
+
const context = machine.context?.({
|
|
45
|
+
prop,
|
|
46
|
+
bindable: createBindable,
|
|
47
|
+
get scope() {
|
|
48
|
+
return @scope
|
|
49
|
+
},
|
|
50
|
+
flush: flush,
|
|
51
|
+
getContext() {
|
|
52
|
+
return ctx
|
|
53
|
+
},
|
|
54
|
+
getComputed() {
|
|
55
|
+
return computed
|
|
56
|
+
},
|
|
57
|
+
getRefs() {
|
|
58
|
+
return refs
|
|
59
|
+
},
|
|
60
|
+
getEvent() {
|
|
61
|
+
return getEvent()
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const ctx = {
|
|
66
|
+
get(key) {
|
|
67
|
+
return context?.[key]?.get()
|
|
68
|
+
},
|
|
69
|
+
set(key, value) {
|
|
70
|
+
context?.[key]?.set(value)
|
|
71
|
+
},
|
|
72
|
+
initial(key) {
|
|
73
|
+
return context?.[key]?.initial
|
|
74
|
+
},
|
|
75
|
+
hash(key) {
|
|
76
|
+
const current = context?.[key]?.get()
|
|
77
|
+
return context?.[key]?.hash(current)
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let effects = new Map()
|
|
82
|
+
let transitionRef = { current: null }
|
|
83
|
+
|
|
84
|
+
let previousEventRef = { current: null }
|
|
85
|
+
let eventRef = { current: { type: "" } }
|
|
86
|
+
|
|
87
|
+
const getEvent = () => ({
|
|
88
|
+
...eventRef.current,
|
|
89
|
+
current() {
|
|
90
|
+
return eventRef.current
|
|
91
|
+
},
|
|
92
|
+
previous() {
|
|
93
|
+
return previousEventRef.current
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const getState = () => ({
|
|
98
|
+
...state,
|
|
99
|
+
matches(...values) {
|
|
100
|
+
const current = state.get()
|
|
101
|
+
return values.includes(current)
|
|
102
|
+
},
|
|
103
|
+
hasTag(tag) {
|
|
104
|
+
const current = state.get()
|
|
105
|
+
return !!machine.states[current]?.tags?.includes(tag)
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const refs = createRefs(machine.refs?.({ prop, context: ctx }) ?? {})
|
|
110
|
+
|
|
111
|
+
const getParams = () => ({
|
|
112
|
+
state: getState(),
|
|
113
|
+
context: ctx,
|
|
114
|
+
event: getEvent(),
|
|
115
|
+
prop,
|
|
116
|
+
send,
|
|
117
|
+
action,
|
|
118
|
+
guard,
|
|
119
|
+
track: createTrack,
|
|
120
|
+
refs,
|
|
121
|
+
computed,
|
|
122
|
+
flush,
|
|
123
|
+
scope: @scope,
|
|
124
|
+
choose,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const action = (keys) => {
|
|
128
|
+
const strs = isFunction(keys) ? keys(getParams()) : keys
|
|
129
|
+
if (!strs) return
|
|
130
|
+
const fns = strs.map((s) => {
|
|
131
|
+
const fn = machine.implementations?.actions?.[s]
|
|
132
|
+
if (!fn) warn(`[zag-js] No implementation found for action "${JSON.stringify(s)}"`)
|
|
133
|
+
return fn
|
|
134
|
+
})
|
|
135
|
+
for (const fn of fns) {
|
|
136
|
+
fn?.(getParams())
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const guard = (str) => {
|
|
141
|
+
if (isFunction(str)) return str(getParams())
|
|
142
|
+
return machine.implementations?.guards?.[str](getParams())
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const effectFn = (keys) => {
|
|
146
|
+
const strs = isFunction(keys) ? keys(getParams()) : keys
|
|
147
|
+
if (!strs) return
|
|
148
|
+
const fns = strs.map((s) => {
|
|
149
|
+
const fn = machine.implementations?.effects?.[s]
|
|
150
|
+
if (!fn) warn(`[zag-js] No implementation found for effect "${JSON.stringify(s)}"`)
|
|
151
|
+
return fn
|
|
152
|
+
})
|
|
153
|
+
const cleanups = []
|
|
154
|
+
for (const fn of fns) {
|
|
155
|
+
const cleanup = fn?.(getParams())
|
|
156
|
+
if (cleanup) cleanups.push(cleanup)
|
|
157
|
+
}
|
|
158
|
+
return () => cleanups.forEach((fn) => fn?.())
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const choose = (transitions) => {
|
|
162
|
+
return toArray(transitions).find((t) => {
|
|
163
|
+
let result = !t.guard
|
|
164
|
+
if (isString(t.guard)) result = !!guard(t.guard)
|
|
165
|
+
else if (isFunction(t.guard)) result = t.guard(getParams())
|
|
166
|
+
return result
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const computed = (key) => {
|
|
171
|
+
ensure(machine.computed, () => `[zag-js] No computed object found on machine`)
|
|
172
|
+
const fn = machine.computed[key]
|
|
173
|
+
return fn({
|
|
174
|
+
context: ctx,
|
|
175
|
+
event: getEvent(),
|
|
176
|
+
prop,
|
|
177
|
+
refs,
|
|
178
|
+
scope: @scope,
|
|
179
|
+
computed: computed,
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const state = createBindable(() => ({
|
|
184
|
+
defaultValue: machine.initialState({ prop }),
|
|
185
|
+
onChange(nextState: string, prevState: string | undefined) {
|
|
186
|
+
// compute effects: exit -> transition -> enter
|
|
187
|
+
|
|
188
|
+
// exit effects
|
|
189
|
+
if (prevState) {
|
|
190
|
+
const exitEffects = effects.get(prevState)
|
|
191
|
+
exitEffects?.()
|
|
192
|
+
effects.delete(prevState)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// exit actions
|
|
196
|
+
if (prevState && prevState !== INIT_STATE) {
|
|
197
|
+
action(machine.states[prevState]?.exit)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// transition actions
|
|
201
|
+
if (transitionRef.current?.actions) {
|
|
202
|
+
action(transitionRef.current.actions)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// enter effects
|
|
206
|
+
const cleanup = effectFn( machine.states[nextState]?.effects)
|
|
207
|
+
if (cleanup) effects.set(nextState, cleanup)
|
|
208
|
+
|
|
209
|
+
// root entry actions
|
|
210
|
+
if (prevState === INIT_STATE) {
|
|
211
|
+
action(machine.entry)
|
|
212
|
+
const cleanup = effectFn(machine.effects)
|
|
213
|
+
if (cleanup) effects.set(INIT_STATE, cleanup)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// enter actions
|
|
217
|
+
action(machine.states[nextState]?.entry)
|
|
218
|
+
},
|
|
219
|
+
}))
|
|
220
|
+
|
|
221
|
+
let status = MachineStatus.NotStarted
|
|
222
|
+
|
|
223
|
+
// Initialize machine immediately - RippleJS components run imperatively
|
|
224
|
+
status = MachineStatus.Started
|
|
225
|
+
debug("initializing...")
|
|
226
|
+
const initialState = machine.initialState({ prop })
|
|
227
|
+
state.set(initialState)
|
|
228
|
+
|
|
229
|
+
const send = (event) => {
|
|
230
|
+
if (status !== MachineStatus.Started) return
|
|
231
|
+
|
|
232
|
+
previousEventRef.current = eventRef.current
|
|
233
|
+
eventRef.current = event
|
|
234
|
+
|
|
235
|
+
let currentState = state.get()
|
|
236
|
+
|
|
237
|
+
const transitions =
|
|
238
|
+
machine.states[currentState]?.on?.[event.type] ??
|
|
239
|
+
machine.on?.[event.type]
|
|
240
|
+
|
|
241
|
+
const transition = choose(transitions)
|
|
242
|
+
if (!transition) return
|
|
243
|
+
|
|
244
|
+
// save current transition
|
|
245
|
+
transitionRef.current = transition
|
|
246
|
+
const target = transition.target ?? currentState
|
|
247
|
+
|
|
248
|
+
debug("transition", event.type, transition.target || currentState, `(${transition.actions})`)
|
|
249
|
+
|
|
250
|
+
const changed = target !== currentState
|
|
251
|
+
if (changed) {
|
|
252
|
+
state.set(target)
|
|
253
|
+
} else if (transition.reenter && !changed) {
|
|
254
|
+
// reenter will re-invoke the current state
|
|
255
|
+
state.invoke?.(currentState, currentState)
|
|
256
|
+
} else {
|
|
257
|
+
// call transition actions
|
|
258
|
+
action(transition.actions ?? [])
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
machine.watch?.(getParams())
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
get state() {
|
|
266
|
+
return getState()
|
|
267
|
+
},
|
|
268
|
+
send,
|
|
269
|
+
context: ctx,
|
|
270
|
+
prop,
|
|
271
|
+
get scope() {
|
|
272
|
+
return @scope
|
|
273
|
+
},
|
|
274
|
+
refs,
|
|
275
|
+
computed,
|
|
276
|
+
get event() {
|
|
277
|
+
return getEvent()
|
|
278
|
+
},
|
|
279
|
+
getStatus: () => status,
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function useProp(value) {
|
|
284
|
+
return function get(key) {
|
|
285
|
+
return value()[key]
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function flush(fn: VoidFunction) {
|
|
290
|
+
flushSync(() => {
|
|
291
|
+
queueMicrotask(() => fn())
|
|
292
|
+
})
|
|
293
|
+
}
|
package/src/refs.ripple
ADDED
package/src/track.ripple
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { isEqual, isFunction } from "@zag-js/utils";
|
|
2
|
+
import { effect, track } from "ripple";
|
|
3
|
+
|
|
4
|
+
const access = (v) => {
|
|
5
|
+
if (isFunction(v)) return v()
|
|
6
|
+
return v;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const createTrack = (deps: any[], effectCallback: VoidFunction ) => {
|
|
10
|
+
let prevDeps: any[] = [];
|
|
11
|
+
let isFirstRun = true;
|
|
12
|
+
|
|
13
|
+
effect(() => {
|
|
14
|
+
if (isFirstRun) {
|
|
15
|
+
prevDeps = deps.map(d => access(d));
|
|
16
|
+
isFirstRun = false;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
let changed = false;
|
|
20
|
+
for (let i = 0; i < deps.length; i++) {
|
|
21
|
+
if (!isEqual(prevDeps[i], access(deps[i]))) {
|
|
22
|
+
changed = true;
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (changed) {
|
|
27
|
+
prevDeps = deps.map(d => access(d));
|
|
28
|
+
effectCallback();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
};
|