@zag-js/solid
Version:
The solid.js wrapper for zag
260 lines (259 loc) • 7.43 kB
JavaScript
// 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
};