UNPKG

@tanstack/table-core

Version:

Headless UI for building powerful tables & datagrids for TS/JS.

270 lines (269 loc) 8.87 kB
//#region src/utils.ts /** * Applies a TanStack updater to a value. * * If the updater is a function it is called with the previous value; otherwise the updater value is returned directly. */ function functionalUpdate(updater, input) { return typeof updater === "function" ? updater(input) : updater; } /** * Clones table state values while preserving non-plain objects. * * Plain objects and arrays are copied recursively so state updates can avoid mutating existing references. */ function cloneState(value) { if (Array.isArray(value)) return value.map(cloneState); if (value && typeof value === "object") { const proto = Object.getPrototypeOf(value); if (proto !== Object.prototype && proto !== null) return value; const copy = proto === null ? makeObjectMap() : {}; const keys = Object.keys(value); for (let i = 0; i < keys.length; i++) { const key = keys[i]; Object.defineProperty(copy, key, { configurable: true, enumerable: true, value: cloneState(value[key]), writable: true }); } return copy; } return value; } /** * Copies prototype-instance own properties without carrying over lazy memo * closures or the per-row cell cache, both of which are bound to the source * instance (cached cells reference the source row). */ function copyInstancePropertiesWithoutMemos(target, source) { const keys = Object.keys(source); const targetRecord = target; for (let i = 0; i < keys.length; i++) { const key = keys[i]; if (!key.startsWith("_memo_") && key !== "_cellsCache") targetRecord[key] = source[key]; } return target; } /** * Creates an object intended only for string-keyed dictionary lookups. * * The null prototype keeps user-controlled ids such as `__proto__` and * `hasOwnProperty` as plain data keys. */ function makeObjectMap() { return Object.create(null); } /** * Checks whether an object owns a key, including null-prototype dictionaries. */ function hasOwn(obj, key) { return Object.prototype.hasOwnProperty.call(obj, key); } /** * Creates a table state updater for a single state slice. * * The updater writes through the table base atom for the slice and supports both value and functional updater forms. */ function makeStateUpdater(key, instance) { return (updater) => { (instance.options.atoms?.[key] ?? instance.baseAtoms[key]).set((old) => functionalUpdate(updater, old)); }; } /** * Returns whether a value is a function. */ function isFunction(d) { return d instanceof Function; } /** * Flattens a tree of nodes by recursively reading child nodes. * * The original nodes are preserved in depth-first order. */ function flattenBy(arr, getChildren) { const flat = []; const recurse = (subArr) => { subArr.forEach((item) => { flat.push(item); const children = getChildren(item); if (children.length) recurse(children); }); }; recurse(arr); return flat; } /** * Creates a dependency-tracked memoized function for table internals. * * The memo recomputes only when its dependency tuple changes and can emit debug timing information. */ const memo = ({ fn, memoDeps, onAfterCompare, onAfterUpdate, onBeforeCompare, onBeforeUpdate }) => { let deps = []; let result; const memoizedFn = (depArgs) => { onBeforeCompare?.(); const newDeps = memoDeps?.(depArgs); let depsChanged = !newDeps || newDeps.length !== deps?.length; if (!depsChanged && newDeps) { for (let i = 0; i < newDeps.length; i++) if (newDeps[i] !== deps[i]) { depsChanged = true; break; } } onAfterCompare?.(depsChanged); if (!depsChanged) return result; deps = newDeps; onBeforeUpdate?.(); result = fn(...newDeps ?? []); onAfterUpdate?.(result); return result; }; return memoizedFn; }; /** * Wraps a callback so that its first invocation is skipped. * * Row-model `onAfterUpdate` hooks schedule auto-resets when their inputs * change. The initial computation of a row model is not a change, so state * resets must not fire for it — otherwise merely reading a row model on mount * would wipe initial or controlled state. */ function skipFirstRun(fn) { let hasRun = false; return () => { if (!hasRun) { hasRun = true; return; } fn(); }; } const pad = (str, num) => { str = String(str); while (str.length < num) str = " " + str; return str; }; /** * Creates a table-aware memoized function. * * This wraps `memo` with table debug options and feature metadata so row models and derived APIs can share consistent diagnostics. */ function tableMemo({ feature, fnName, objectId, onAfterUpdate, table, ...memoOptions }) { let startCalcTime; let endCalcTime; let runCount = 0; let debug; if (process.env.NODE_ENV === "development") { const { debugAll } = table.options; const { parentName } = getFunctionNameInfo(fnName, "."); const debugByParent = table.options[`debug${(parentName != "table" ? parentName + "s" : parentName).replace(parentName, parentName.charAt(0).toUpperCase() + parentName.slice(1))}`]; const debugByFeature = feature ? table.options[`debug${feature.charAt(0).toUpperCase() + feature.slice(1)}`] : false; debug = debugAll || debugByParent || debugByFeature; } function logTime(time, depsChanged) { const runType = runCount === 0 ? "(1st run)" : depsChanged ? "(rerun #" + runCount + ")" : "(cache)"; runCount++; console.groupCollapsed(`%c⏱ ${pad(`${time.toFixed(1)} ms`, 12)} %c${runType}%c ${fnName}%c ${objectId ? `(${fnName.split(".")[0]}Id: ${objectId})` : ""}`, `font-size: .6rem; font-weight: bold; ${depsChanged ? `color: hsl( ${Math.max(0, Math.min(120 - Math.log10(time) * 60, 120))}deg 100% 31%);` : ""} `, `color: ${runCount < 2 ? "#FF00FF" : "#FF1493"}`, "color: #666", "color: #87CEEB"); console.info({ feature, state: table.store.state, deps: memoOptions.memoDeps?.toString() }); console.trace(); console.groupEnd(); } const onAfterUpdateHandler = () => { if (!onAfterUpdate) return; const { schedule, untrack } = table._reactivity; schedule(() => untrack(() => onAfterUpdate())); }; const debugOptions = process.env.NODE_ENV === "development" ? { onBeforeCompare: () => {}, onAfterCompare: (depsChanged) => {}, onBeforeUpdate: () => { if (debug) startCalcTime = performance.now(); }, onAfterUpdate: () => { if (debug) { endCalcTime = performance.now(); logTime(Math.round((endCalcTime - startCalcTime) * 100) / 100, true); } onAfterUpdateHandler(); } } : { onAfterUpdate: () => { onAfterUpdateHandler(); } }; return memo({ ...memoOptions, ...debugOptions }); } /** * Assumes that a function name is in the format of `parentName_fnKey` and returns the `fnKey` and `fnName` in the format of `parentName.fnKey`. */ function getFunctionNameInfo(staticFnName, splitBy = "_") { const [parentName, fnKey] = staticFnName.split(splitBy); return { fnKey, fnName: `${parentName}.${fnKey}`, parentName }; } /** * Assigns Table API methods directly to the table instance. * Unlike row/cell/column/header, the table is a singleton so methods are assigned directly. */ function assignTableAPIs(feature, table, apis) { for (const [staticFnName, { fn, memoDeps }] of Object.entries(apis)) { const { fnKey, fnName } = getFunctionNameInfo(staticFnName); table[fnKey] = memoDeps ? tableMemo({ memoDeps, fn, fnName, table, feature }) : fn; } } /** * Assigns API methods to a prototype object for memory-efficient method sharing. * All instances created with this prototype will share the same method references. * * For memoized methods, the memo state is lazily created and stored on each instance. * This provides the best of both worlds: shared method code + per-instance caching. */ function assignPrototypeAPIs(feature, prototype, table, apis) { for (const [staticFnName, { fn, memoDeps }] of Object.entries(apis)) { const { fnKey, fnName } = getFunctionNameInfo(staticFnName); if (memoDeps) { const memoKey = `_memo_${fnKey}`; prototype[fnKey] = function(...args) { if (!this[memoKey]) { const self = this; this[memoKey] = tableMemo({ memoDeps: (depArgs) => memoDeps(self, depArgs), fn: (...deps) => fn(self, ...deps), fnName, objectId: self.id, table, feature }); } return this[memoKey](...args); }; } else prototype[fnKey] = function(...args) { return fn(this, ...args); }; } } /** * Looks to run the memoized function with the builder pattern on the object if it exists, otherwise fall back to the static method passed in. */ function callMemoOrStaticFn(obj, fnKey, staticFn, ...args) { return obj[fnKey]?.(...args) ?? staticFn(obj, ...args); } //#endregion export { assignPrototypeAPIs, assignTableAPIs, callMemoOrStaticFn, cloneState, copyInstancePropertiesWithoutMemos, flattenBy, functionalUpdate, getFunctionNameInfo, hasOwn, isFunction, makeObjectMap, makeStateUpdater, memo, skipFirstRun, tableMemo };