UNPKG

@zag-js/preact

Version:

The preact wrapper for zag

267 lines (266 loc) 7.87 kB
// src/machine.ts import { createScope, findTransition, getExitEnterStates, hasTag, INIT_STATE, MachineStatus, matchesState, resolveStateValue } from "@zag-js/core"; import { callAll, ensure, isFunction, isString, toArray, warn } from "@zag-js/utils"; import { flushSync } from "preact/compat"; import { useLayoutEffect, useMemo, useRef } from "preact/hooks"; import { useBindable } from "./bindable.mjs"; import { useRefs } from "./refs.mjs"; import { useTrack } from "./track.mjs"; function useMachine(machine, userProps = {}) { const scope = useMemo(() => { const { id, ids, getRootNode } = userProps; return createScope({ id, ids, getRootNode }); }, [userProps]); const debug = (...args) => { if (machine.debug) console.log(...args); }; const props = machine.props?.({ props: userProps, scope }) ?? userProps; const prop = useProp(props); const context = machine.context?.({ prop, bindable: useBindable, scope, flush, getContext() { return ctx; }, getComputed() { return computed; }, getRefs() { return refs; }, getEvent() { return getEvent(); } }); const contextRef = useLiveRef(context); const ctx = { get(key) { return contextRef.current?.[key].get(); }, set(key, value) { contextRef.current?.[key].set(value); }, initial(key) { return contextRef.current?.[key].initial; }, hash(key) { const current = contextRef.current?.[key].get(); return contextRef.current?.[key].hash(current); } }; const effects = useRef(/* @__PURE__ */ new Map()); const transitionRef = useRef(null); const previousEventRef = useRef(null); const eventRef = useRef({ type: "" }); const refs = useRefs(machine.refs?.({ prop, context: ctx }) ?? {}); const getEvent = () => ({ ...eventRef.current, current() { return eventRef.current; }, previous() { return previousEventRef.current; } }); const getState = () => ({ ...state, hasTag(tag) { const currentState = state.get(); return hasTag(machine, currentState, tag); }, matches(...values) { const currentState = state.get(); return values.some((value) => matchesState(currentState, value)); } }); const getParams = () => ({ state: getState(), context: ctx, event: getEvent(), prop, send, action, guard, track: useTrack, refs, computed, flush, scope, choose }); const action = (keys) => { const strs = isFunction(keys) ? keys(getParams()) : keys; if (!strs) return; const fns = strs.map((s) => { const fn = machine.implementations?.actions?.[s]; if (!fn) warn(`[zag-js] No implementation found for action "${JSON.stringify(s)}"`); return fn; }); for (const fn of fns) { fn?.(getParams()); } }; const guard = (str) => { if (isFunction(str)) return str(getParams()); const fn = machine.implementations?.guards?.[str]; if (!fn) warn(`[zag-js] No implementation found for guard "${JSON.stringify(str)}"`); return fn?.(getParams()); }; const effect = (keys) => { const strs = isFunction(keys) ? keys(getParams()) : keys; if (!strs) return; const fns = strs.map((s) => { const fn = machine.implementations?.effects?.[s]; if (!fn) warn(`[zag-js] No implementation found for effect "${JSON.stringify(s)}"`); return fn; }); const cleanups = []; for (const fn of fns) { const cleanup = fn?.(getParams()); if (cleanup) cleanups.push(cleanup); } return () => cleanups.forEach((fn) => fn?.()); }; const choose = (transitions) => { return toArray(transitions).find((t) => { let result = !t.guard; if (isString(t.guard)) result = !!guard(t.guard); else if (isFunction(t.guard)) result = t.guard(getParams()); return result; }); }; const computed = (key) => { ensure(machine.computed, () => `[zag-js] No computed object found on machine`); const fn = machine.computed[key]; return fn({ context: ctx, event: getEvent(), prop, refs, scope, computed }); }; const state = useBindable(() => ({ defaultValue: resolveStateValue(machine, machine.initialState({ prop })), onChange(nextState, prevState) { currentStateRef.current = nextState; const { exiting, entering } = getExitEnterStates(machine, prevState, nextState, transitionRef.current?.reenter); exiting.forEach((item) => { const exitEffects = effects.current.get(item.path); exitEffects?.(); effects.current.delete(item.path); }); exiting.forEach((item) => { action(item.state?.exit); }); action(transitionRef.current?.actions); entering.forEach((item) => { const cleanup = effect(item.state?.effects); if (cleanup) { const existing = effects.current.get(item.path); effects.current.set(item.path, existing ? callAll(existing, cleanup) : cleanup); } }); if (prevState === INIT_STATE) { action(machine.entry); const cleanup = effect(machine.effects); if (cleanup) { const existing = effects.current.get(INIT_STATE); effects.current.set(INIT_STATE, existing ? callAll(existing, cleanup) : cleanup); } } entering.forEach((item) => { action(item.state?.entry); }); } })); const currentStateRef = useRef(state.initial); const hydratedStateRef = useRef(void 0); const statusRef = useRef(MachineStatus.NotStarted); useLayoutEffect(() => { const started = statusRef.current === MachineStatus.Started; statusRef.current = MachineStatus.Started; debug(started ? "rehydrating..." : "initializing..."); const initialState = hydratedStateRef.current ?? state.initial; state.invoke(initialState, started ? state.get() : INIT_STATE); const fns = effects.current; return () => { const currentState = getCurrentState(); debug("unmounting..."); hydratedStateRef.current = currentState; statusRef.current = MachineStatus.Stopped; fns.forEach((fn) => fn?.()); effects.current = /* @__PURE__ */ new Map(); transitionRef.current = null; action(machine.exit); }; }, []); const getCurrentState = () => { return currentStateRef.current; }; const send = (event) => { queueMicrotask(() => { if (statusRef.current !== MachineStatus.Started) return; previousEventRef.current = eventRef.current; eventRef.current = event; let currentState = getCurrentState(); const { transitions, source } = findTransition(machine, currentState, event.type); const transition = choose(transitions); if (!transition) return; transitionRef.current = transition; const target = resolveStateValue(machine, transition.target ?? currentState, source); const changed = target !== currentState; if (changed) { currentStateRef.current = target; flushSync(() => state.set(target)); } else if (transition.reenter) { state.invoke(currentState, currentState); } else { action(transition.actions ?? []); } }); }; machine.watch?.(getParams()); return { state: getState(), send, context: ctx, prop, scope, refs, computed, event: getEvent(), getStatus: () => statusRef.current }; } function useLiveRef(value) { const ref = useRef(value); ref.current = value; return ref; } function useProp(value) { const ref = useLiveRef(value); return function get(key) { return ref.current[key]; }; } function flush(fn) { queueMicrotask(() => { flushSync(() => fn()); }); } export { useMachine };