UNPKG

@zag-js/svelte

Version:

The svelte wrapper for zag

261 lines (260 loc) • 8.05 kB
import { createScope, INIT_STATE, MachineStatus } from "@zag-js/core"; import { compact, ensure, isFunction, isString, toArray, warn } from "@zag-js/utils"; import { flushSync, onDestroy, onMount } from "svelte"; import { bindable } from "./bindable.svelte"; import { useRefs } from "./refs.svelte"; import { track } from "./track.svelte"; function access(userProps) { if (isFunction(userProps)) return userProps(); return userProps; } export function useMachine(machine, userProps) { const scope = $derived.by(() => { const { id, ids, getRootNode } = access(userProps); return createScope({ id, ids, getRootNode }); }); const debug = (...args) => { if (machine.debug) console.log(...args); }; const props = $derived(machine.props?.({ props: compact(access(userProps)), scope }) ?? access(userProps)); const prop = useProp(() => props); const context = machine.context?.({ prop, bindable: bindable, get scope() { return scope; }, flush: 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); }, }; let effects = new Map(); let transitionRef = { current: null }; let previousEventRef = { current: null }; let eventRef = { current: { type: "" } }; const getEvent = () => ({ ...eventRef.current, current() { return eventRef.current; }, previous() { return previousEventRef.current; }, }); const getState = () => ({ ...state, hasTag(tag) { const currentState = state.get(); return !!machine.states[currentState]?.tags?.includes(tag); }, matches(...values) { const currentState = state.get(); return values.includes(currentState); }, }); const refs = useRefs(machine.refs?.({ prop, context: ctx }) ?? {}); const getParams = () => ({ state: getState(), context: ctx, event: getEvent(), prop, send, action, guard, track, 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()); return machine.implementations?.guards?.[str](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: computed, }); }; const state = bindable(() => ({ defaultValue: machine.initialState({ prop }), onChange(nextState, prevState) { // compute effects: exit -> transition -> enter // exit effects if (prevState) { const exitEffects = effects.get(prevState); exitEffects?.(); effects.delete(prevState); } // exit actions if (prevState) { action(machine.states[prevState]?.exit); } // transition actions action(transitionRef.current?.actions); // enter effect const cleanup = effect(machine.states[nextState]?.effects); if (cleanup) effects.set(nextState, cleanup); // root entry actions if (prevState === INIT_STATE) { action(machine.entry); const cleanup = effect(machine.effects); if (cleanup) effects.set(INIT_STATE, cleanup); } // enter actions action(machine.states[nextState]?.entry); }, })); let status = MachineStatus.NotStarted; onMount(() => { const started = status === MachineStatus.Started; status = MachineStatus.Started; debug(started ? "rehydrating..." : "initializing..."); state.invoke(state.initial, INIT_STATE); }); onDestroy(() => { debug("unmounting..."); status = MachineStatus.Stopped; effects.forEach((fn) => fn?.()); effects = new Map(); transitionRef.current = null; action(machine.exit); }); const send = (event) => { if (status !== MachineStatus.Started) return; previousEventRef.current = eventRef.current; eventRef.current = event; let currentState = state.get(); // @ts-ignore const transitions = machine.states[currentState].on?.[event.type] ?? machine.on?.[event.type]; const transition = choose(transitions); if (!transition) return; // save current transition transitionRef.current = transition; const target = transition.target ?? currentState; debug("transition", event.type, transition.target || currentState, `(${transition.actions})`); const changed = target !== currentState; if (changed) { // state change is high priority state.set(target); } else if (transition.reenter && !changed) { // reenter will re-invoke the current state state.invoke(currentState, currentState); } else { // call transition actions action(transition.actions); } }; machine.watch?.(getParams()); return { get state() { return getState(); }, send, context: ctx, prop, get scope() { return scope; }, refs, computed, get event() { return getEvent(); }, getStatus: () => status, }; } function useProp(value) { return function get(key) { return value()[key]; }; } function flush(fn) { flushSync(() => { queueMicrotask(() => fn()); }); }