UNPKG

@zag-js/solid

Version:

The solid.js wrapper for zag

423 lines (418 loc) • 11.7 kB
export { Key } from '@solid-primitives/keyed'; import { createScope, INIT_STATE, MachineStatus, mergeProps as mergeProps$1 } from '@zag-js/core'; import { isObject, isString, isNumber, compact, ensure, isFunction, warn, toArray, isEqual } from '@zag-js/utils'; import { createMemo, mergeProps, onMount, onCleanup, untrack, createSignal, createEffect } from 'solid-js'; import { createNormalizer } from '@zag-js/types'; // src/index.ts function createBindable(props) { const initial = props().value ?? props().defaultValue; const eq = props().isEqual ?? Object.is; const [value, setValue] = createSignal(initial); const controlled = createMemo(() => props().value != void 0); const valueRef = { current: value() }; const prevValue = { current: void 0 }; createEffect(() => { const v = controlled() ? props().value : value(); prevValue.current = v; valueRef.current = v; }); const set = (v) => { const prev = prevValue.current; const next = isFunction(v) ? v(valueRef.current) : v; if (props().debug) { console.log(`[bindable > ${props().debug}] setValue`, { next, prev }); } if (!controlled()) setValue(next); if (!eq(next, prev)) { props().onChange?.(next, prev); } }; function get() { const v = controlled() ? props().value : value; return isFunction(v) ? v() : v; } return { initial, ref: valueRef, get, set, invoke(nextValue, prevValue2) { props().onChange?.(nextValue, prevValue2); }, hash(value2) { return props().hash?.(value2) ?? String(value2); } }; } createBindable.cleanup = (fn) => { onCleanup(() => fn()); }; createBindable.ref = (defaultValue) => { let value = defaultValue; return { get: () => value, set: (next) => { value = next; } }; }; // src/refs.ts function createRefs(refs) { const ref = { current: refs }; return { get(key) { return ref.current[key]; }, set(key, value) { ref.current[key] = value; } }; } function access(v) { if (isFunction(v)) return v(); return v; } var createTrack = (deps, effect) => { let prevDeps = []; let isFirstRun = true; createEffect(() => { if (isFirstRun) { prevDeps = deps.map((d) => access(d)); isFirstRun = false; return; } let changed = false; for (let i = 0; i < deps.length; i++) { if (!isEqual(prevDeps[i], access(deps[i]))) { changed = true; break; } } if (changed) { prevDeps = deps.map((d) => access(d)); effect(); } }); }; // src/machine.ts function useMachine(machine, userProps = {}) { const scope = createMemo(() => { const { id, ids, getRootNode } = access2(userProps); return createScope({ id, ids, getRootNode }); }); const debug = (...args) => { if (machine.debug) console.log(...args); }; const props = createMemo( () => machine.props?.({ props: compact(access2(userProps)), scope: scope() }) ?? access2(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.includes(current); }, hasTag(tag) { const current = state.get(); return !!machine.states[current]?.tags?.includes(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()); 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: eventRef.current, prop, refs, scope: scope(), computed }); }; const state = createBindable(() => ({ defaultValue: machine.initialState({ prop }), onChange(nextState, prevState) { if (prevState) { const exitEffects = effects.current.get(prevState); exitEffects?.(); effects.current.delete(prevState); } if (prevState) { action(machine.states[prevState]?.exit); } action(transitionRef.current?.actions); const cleanup = effect(machine.states[nextState]?.effects); if (cleanup) effects.current.set(nextState, cleanup); if (prevState === INIT_STATE) { action(machine.entry); const cleanup2 = effect(machine.effects); if (cleanup2) effects.current.set(INIT_STATE, cleanup2); } 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); }); 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 = ( // @ts-ignore machine.states[currentState].on?.[event.type] ?? // @ts-ignore machine.on?.[event.type] ); const transition = choose(transitions); if (!transition) return; transitionRef.current = transition; const target = transition.target ?? currentState; debug("transition", event.type, transition.target || currentState, `(${transition.actions})`); 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(); }, refs, computed, event: getEvent(), getStatus: () => status }; } function flush(fn) { fn(); } function access2(value) { return isFunction(value) ? value() : value; } function createProp(value) { return function get(key) { return value()[key]; }; } function mergeProps2(...sources) { const target = {}; for (let i = 0; i < sources.length; i++) { let source = sources[i]; if (typeof source === "function") source = source(); if (source) { const descriptors = Object.getOwnPropertyDescriptors(source); for (const key in descriptors) { if (key in target) continue; Object.defineProperty(target, key, { enumerable: true, get() { let e = {}; if (key === "style" || key === "class" || key === "className" || key.startsWith("on")) { for (let i2 = 0; i2 < sources.length; i2++) { let s = sources[i2]; if (typeof s === "function") s = s(); e = mergeProps$1(e, { [key]: (s || {})[key] }); } return e[key]; } for (let i2 = sources.length - 1; i2 >= 0; i2--) { let v, s = sources[i2]; if (typeof s === "function") s = s(); v = (s || {})[key]; if (v !== void 0) return v; } } }); } } } return target; } var eventMap = { onFocus: "onFocusIn", onBlur: "onFocusOut", onDoubleClick: "onDblClick", onChange: "onInput", defaultChecked: "checked", defaultValue: "value", htmlFor: "for", className: "class" }; var format = (v) => v.startsWith("--") ? v : hyphenateStyleName(v); function toSolidProp(prop) { return prop in eventMap ? eventMap[prop] : prop; } var normalizeProps = createNormalizer((props) => { const normalized = {}; for (const key in props) { const value = props[key]; if (key === "readOnly" && value === false) { continue; } if (key === "style" && isObject(value)) { normalized["style"] = cssify(value); continue; } if (key === "children") { if (isString(value)) { normalized["textContent"] = value; } continue; } normalized[toSolidProp(key)] = value; } return normalized; }); function cssify(style) { let css = {}; for (const property in style) { const value = style[property]; if (!isString(value) && !isNumber(value)) continue; css[format(property)] = value; } return css; } var uppercasePattern = /[A-Z]/g; var msPattern = /^ms-/; function toHyphenLower(match) { return "-" + match.toLowerCase(); } var cache = {}; function hyphenateStyleName(name) { if (cache.hasOwnProperty(name)) return cache[name]; var hName = name.replace(uppercasePattern, toHyphenLower); return cache[name] = msPattern.test(hName) ? "-" + hName : hName; } export { mergeProps2 as mergeProps, normalizeProps, useMachine };