@zag-js/vue
Version:
The vue wrapper for zag
351 lines (347 loc) • 9.55 kB
JavaScript
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 };