UNPKG

@solid-primitives/transition-group

Version:

Reactive primitives for implementing transition effects in SolidJS

197 lines (196 loc) 7.81 kB
import { batch, createSignal, untrack, $TRACK, createComputed, createMemo, useTransition, } from "solid-js"; import { isServer } from "solid-js/web"; const noop = () => { /* noop */ }; const noopTransition = (el, done) => done(); /** * Create an element transition interface for switching between single elements. * It can be used to implement own transition effect, or a custom `<Transition>`-like component. * * It will observe {@link source} and return a signal with array of elements to be rendered (current one and exiting ones). * * @param source a signal with the current element. Any nullish value will mean there is no element. * Any object can used as the source, but most likely you will want to use a `HTMLElement` or `SVGElement`. * @param options transition options {@link SwitchTransitionOptions} * @returns a signal with an array of the current element and exiting previous elements. * * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/transition-group#createSwitchTransition * * @example * const [el, setEl] = createSignal<HTMLDivElement>(); * * const rendered = createSwitchTransition(el, { * onEnter(el, done) { * // the enter callback is called before the element is inserted into the DOM * // so run the animation in the next animation frame / microtask * queueMicrotask(() => { ... }) * }, * onExit(el, done) { * // the exitting element is kept in the DOM until the done() callback is called * }, * }) * * // change the source to trigger the transition * setEl(refToHtmlElement); */ export function createSwitchTransition(source, options) { const initSource = untrack(source); const initReturned = initSource ? [initSource] : []; if (isServer) { return () => initReturned; } const { onEnter = noopTransition, onExit = noopTransition } = options; const [returned, setReturned] = createSignal(options.appear ? [] : initReturned); const [isTransitionPending] = useTransition(); let next; let isExiting = false; function exitTransition(el, after) { if (!el) return after && after(); isExiting = true; onExit(el, () => { batch(() => { isExiting = false; setReturned(p => p.filter(e => e !== el)); after && after(); }); }); } function enterTransition(after) { const el = next; if (!el) return after && after(); next = undefined; setReturned(p => [el, ...p]); onEnter(el, after ?? noop); } const triggerTransitions = options.mode === "out-in" ? // exit -> enter // exit -> enter prev => isExiting || exitTransition(prev, enterTransition) : options.mode === "in-out" ? // enter -> exit // enter -> exit prev => enterTransition(() => exitTransition(prev)) : // exit & enter // exit & enter prev => { exitTransition(prev); enterTransition(); }; createComputed((prev) => { const el = source(); if (untrack(isTransitionPending)) { // wait for pending transition to end before animating isTransitionPending(); return prev; } if (el !== prev) { next = el; batch(() => untrack(() => triggerTransitions(prev))); } return el; }, options.appear ? undefined : initSource); return returned; } /** * Create an element list transition interface for changes to the list of elements. * It can be used to implement own transition effect, or a custom `<TransitionGroup>`-like component. * * It will observe {@link source} and return a signal with array of elements to be rendered (current ones and exiting ones). * * @param source a signal with the current list of elements. * Any object can used as the element, but most likely you will want to use a `HTMLElement` or `SVGElement`. * @param options transition options {@link ListTransitionOptions} * * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/transition-group#createListTransition * * @example * const [els, setEls] = createSignal<HTMLElement[]>([]); * * const rendered = createListTransition(els, { * onChange({ list, added, removed, unchanged, finishRemoved }) { * // the callback is called before the added elements are inserted into the DOM * // so run the animation in the next animation frame / microtask * queueMicrotask(() => { ... }) * * // the removed elements are kept in the DOM until the finishRemoved() callback is called * finishRemoved(removed); * } * }) * * // change the source to trigger the transition * setEls([...refsToHTMLElements]); */ export function createListTransition(source, options) { const initSource = untrack(source); if (isServer) { const copy = initSource.slice(); return () => copy; } const { onChange } = options; // if appear is enabled, the initial transition won't have any previous elements. // otherwise the elements will match and transition skipped, or transitioned if the source is different from the initial value let prevSet = new Set(options.appear ? undefined : initSource); const exiting = new WeakSet(); const [toRemove, setToRemove] = createSignal([], { equals: false }); const [isTransitionPending] = useTransition(); const finishRemoved = options.exitMethod === "remove" ? noop : els => { setToRemove(p => (p.push.apply(p, els), p)); for (const el of els) exiting.delete(el); }; const handleRemoved = options.exitMethod === "remove" ? noop : options.exitMethod === "keep-index" ? (els, el, i) => els.splice(i, 0, el) : (els, el) => els.push(el); return createMemo(prev => { const elsToRemove = toRemove(); const sourceList = source(); sourceList[$TRACK]; // top level store tracking if (untrack(isTransitionPending)) { // wait for pending transition to end before animating isTransitionPending(); return prev; } if (elsToRemove.length) { const next = prev.filter(e => !elsToRemove.includes(e)); elsToRemove.length = 0; onChange({ list: next, added: [], removed: [], unchanged: next, finishRemoved }); return next; } return untrack(() => { const nextSet = new Set(sourceList); const next = sourceList.slice(); const added = []; const removed = []; const unchanged = []; for (const el of sourceList) { (prevSet.has(el) ? unchanged : added).push(el); } let nothingChanged = !added.length; for (let i = 0; i < prev.length; i++) { const el = prev[i]; if (!nextSet.has(el)) { if (!exiting.has(el)) { removed.push(el); exiting.add(el); } handleRemoved(next, el, i); } if (nothingChanged && el !== next[i]) nothingChanged = false; } // skip if nothing changed if (!removed.length && nothingChanged) return prev; onChange({ list: next, added, removed, unchanged, finishRemoved }); prevSet = nextSet; return next; }); }, options.appear ? [] : initSource.slice()); }