UNPKG

@zag-js/vue

Version:
351 lines (347 loc) • 9.55 kB
import { createScope, INIT_STATE, MachineStatus } from '@zag-js/core'; export { mergeProps } from '@zag-js/core'; import { createNormalizer } from '@zag-js/types'; import { compact, ensure, isFunction, warn, toArray, isString, isEqual } from '@zag-js/utils'; import { computed, toValue, onMounted, onBeforeUnmount, nextTick, shallowRef, ref, watch, onUnmounted } from 'vue'; // src/index.ts function toCase(txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); } var propMap = { htmlFor: "for", className: "class", onDoubleClick: "onDblclick", onChange: "onInput", onFocus: "onFocusin", onBlur: "onFocusout", defaultValue: "value", defaultChecked: "checked" }; var preserveKeys = "viewBox,className,preserveAspectRatio,fillRule,clipPath,clipRule,strokeWidth,strokeLinecap,strokeLinejoin,strokeDasharray,strokeDashoffset,strokeMiterlimit".split( "," ); function toVueProp(prop) { if (prop in propMap) return propMap[prop]; if (prop.startsWith("on")) return `on${toCase(prop.substr(2))}`; if (preserveKeys.includes(prop)) return prop; return prop.toLowerCase(); } var normalizeProps = createNormalizer((props) => { const normalized = {}; for (const key in props) { const value = props[key]; if (key === "children") { if (typeof value === "string") { normalized["innerHTML"] = value; } else if (process.env.NODE_ENV !== "production" && value != null) { console.warn("[Vue Normalize Prop] : avoid passing non-primitive value as `children`"); } } else { normalized[toVueProp(key)] = props[key]; } } return normalized; }); function bindable(props) { const initial = props().defaultValue ?? props().value; const eq = props().isEqual ?? Object.is; const v = shallowRef(initial); const controlled = computed(() => props().value !== void 0); const valueRef = shallowRef(controlled.value ? props().value : v.value); return { initial, ref: valueRef, get() { return controlled.value ? props().value : v.value; }, set(val) { const prev = controlled.value ? props().value : v.value; const next = isFunction(val) ? val(prev) : val; if (props().debug) { console.log(`[bindable > ${props().debug}] setValue`, { next, prev }); } if (!controlled.value) v.value = next; if (!eq(next, prev)) { props().onChange?.(next, prev); } }, invoke(nextValue, prevValue) { props().onChange?.(nextValue, prevValue); }, hash(value) { return props().hash?.(value) ?? String(value); } }; } bindable.cleanup = (fn) => { onUnmounted(() => fn()); }; bindable.ref = (defaultValue) => { let value = defaultValue; return { get: () => value, set: (next) => { value = next; } }; }; function useRefs(refs) { const __refs = ref(refs); return { get(key) { return __refs.value[key]; }, set(key, value) { __refs.value[key] = value; } }; } var useTrack = (deps, effect) => { watch( () => [...deps.map((d) => d())], (current, previous) => { let changed = false; for (let i = 0; i < current.length; i++) { if (!isEqual(previous[i], toValue(current[i]))) { changed = true; break; } } if (changed) { effect(); } } ); }; // src/machine.ts function useMachine(machine, userProps = {}) { const scope = computed(() => { const { id, ids, getRootNode } = toValue(userProps); return createScope({ id, ids, getRootNode }); }); const debug = (...args) => { if (machine.debug) console.log(...args); }; const props = computed( () => machine.props?.({ props: compact(toValue(userProps)), get scope() { return scope.value; } }) ?? toValue(userProps) ); const prop = useProp(props); const context = machine.context?.({ prop, bindable, get scope() { return scope.value; }, flush, getContext() { return ctx; }, getComputed() { return computed$1; }, getRefs() { return refs; } }); 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 = /* @__PURE__ */ new Map(); let transitionRef = null; let previousEventRef = { current: null }; let eventRef = { current: { type: "" } }; const getEvent = () => ({ ...eventRef.current, current() { return eventRef.current; }, previous() { return previousEventRef.current; } }); const getState = () => ({ ...state, matches(...values) { const currentState = state.get(); return values.includes(currentState); }, hasTag(tag) { const currentState = state.get(); return !!machine.states[currentState]?.tags?.includes(tag); } }); const refs = useRefs(machine.refs?.({ prop, context: ctx }) ?? {}); const getParams = () => ({ state: getState(), context: ctx, event: getEvent(), prop, send, action, guard, track: useTrack, refs, computed: computed$1, flush, get scope() { return scope.value; }, 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$1 = (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, get scope() { return scope.value; }, computed: computed$1 }); }; const state = bindable(() => ({ defaultValue: machine.initialState({ prop }), onChange(nextState, prevState) { if (prevState) { const exitEffects = effects.get(prevState); exitEffects?.(); effects.delete(prevState); } if (prevState) { action(machine.states[prevState]?.exit); } action(transitionRef?.actions); const cleanup = effect(machine.states[nextState]?.effects); if (cleanup) effects.set(nextState, cleanup); if (prevState === INIT_STATE) { action(machine.entry); const cleanup2 = effect(machine.effects); if (cleanup2) effects.set(INIT_STATE, cleanup2); } action(machine.states[nextState]?.entry); } })); let status = MachineStatus.NotStarted; onMounted(() => { const started = status === MachineStatus.Started; status = MachineStatus.Started; debug(started ? "rehydrating..." : "initializing..."); state.invoke(state.initial, INIT_STATE); }); onBeforeUnmount(() => { status = MachineStatus.Stopped; debug("unmounting..."); const fns = effects.values(); for (const fn of fns) fn?.(); effects = /* @__PURE__ */ new Map(); action(machine.exit); }); const send = (event) => { if (status !== MachineStatus.Started) return; previousEventRef.current = eventRef.current; eventRef.current = event; debug("send", event); let currentState = state.get(); const transitions = ( //@ts-expect-error machine.states[currentState].on?.[event.type] ?? machine.on?.[event.type] ); const transition = choose(transitions); if (!transition) return; transitionRef = transition; const target = transition.target ?? currentState; debug("transition", transition); const changed = target !== currentState; if (changed) { state.set(target); } else if (transition.reenter && !changed) { state.invoke(currentState, currentState); } else { action(transition.actions); } }; machine.watch?.(getParams()); return { state: getState(), send, context: ctx, prop, get scope() { return scope.value; }, refs, computed: computed$1, event: getEvent(), getStatus: () => status }; } function useProp(valueRef) { return function get(key) { return valueRef.value[key]; }; } var flush = (fn) => { nextTick().then(() => { fn(); }); }; export { normalizeProps, useMachine };