UNPKG

@zag-js/solid

Version:

The solid.js wrapper for zag

260 lines (259 loc) • 7.43 kB
// src/machine.ts import { createScope, findTransition, getExitEnterStates, hasTag, INIT_STATE, MachineStatus, matchesState, resolveStateValue } from "@zag-js/core"; import { callAll, compact, ensure, isFunction, isString, toArray, warn } from "@zag-js/utils"; import { createMemo, mergeProps, onCleanup, onMount, untrack } from "solid-js"; import { createBindable } from "./bindable.mjs"; import { createRefs } from "./refs.mjs"; import { createTrack } from "./track.mjs"; function useMachine(machine, userProps = {}) { const scope = createMemo(() => { const { id, ids, getRootNode } = access(userProps); return createScope({ id, ids, getRootNode }); }); const debug = (...args) => { if (machine.debug) console.log(...args); }; const props = createMemo( () => machine.props?.({ props: compact(access(userProps)), scope: scope() }) ?? access(userProps) ); const prop = createProp(props); const context = machine.context?.({ prop, bindable: createBindable, get scope() { return scope(); }, flush, getContext() { return ctx; }, getComputed() { return computed; }, getRefs() { return refs; }, getEvent() { return getEvent(); } }); const ctx = { get(key) { return context?.[key].get(); }, set(key, value) { context?.[key].set(value); }, initial(key) { return context?.[key].initial; }, hash(key) { const current = context?.[key].get(); return context?.[key].hash(current); } }; const effects = { current: /* @__PURE__ */ new Map() }; const transitionRef = { current: null }; const previousEventRef = { current: null }; const eventRef = { current: { type: "" } }; const getEvent = () => mergeProps(eventRef.current, { current() { return eventRef.current; }, previous() { return previousEventRef.current; } }); const getState = () => mergeProps(state, { matches(...values) { const current = state.get(); return values.some((value) => matchesState(current, value)); }, hasTag(tag) { const current = state.get(); return hasTag(machine, current, tag); } }); const refs = createRefs(machine.refs?.({ prop, context: ctx }) ?? {}); const getParams = () => ({ state: getState(), context: ctx, event: getEvent(), prop, send, action, guard, track: createTrack, refs, computed, flush, get scope() { return 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: eventRef.current, prop, refs, scope: scope(), computed }); }; const state = createBindable(() => ({ defaultValue: resolveStateValue(machine, machine.initialState({ prop })), onChange(nextState, prevState) { 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); }); } })); let status = MachineStatus.NotStarted; onMount(() => { const started = status === MachineStatus.Started; status = MachineStatus.Started; debug(started ? "rehydrating..." : "initializing..."); state.invoke(state.initial, INIT_STATE); }); onCleanup(() => { debug("unmounting..."); status = MachineStatus.Stopped; const fns = effects.current; fns.forEach((fn) => fn?.()); effects.current = /* @__PURE__ */ new Map(); transitionRef.current = null; action(machine.exit); }); const send = (event) => { queueMicrotask(() => { if (status !== MachineStatus.Started) return; previousEventRef.current = eventRef.current; eventRef.current = event; let currentState = untrack(() => state.get()); 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); debug("transition", event.type, transition.target || currentState, `(${transition.actions})`); const changed = target !== currentState; if (changed) { 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, get scope() { return scope(); }, refs, computed, event: getEvent(), getStatus: () => status }; } function flush(fn) { fn(); } function access(value) { return isFunction(value) ? value() : value; } function createProp(value) { return function get(key) { return value()[key]; }; } export { useMachine };