UNPKG

@humanspeak/svelte-keyed

Version:

A powerful writable derived store for Svelte that enables deep object and array manipulation with TypeScript support

127 lines (126 loc) 4.1 kB
import { derived } from 'svelte/store'; /** * Converts a string path with array notation into an array of tokens. * Optimized version that avoids unnecessary string operations and uses a single pass. * * @param key - The path string to tokenize (e.g., "users[0].name" or "deeply.nested.property") * @returns An array of string tokens representing each path segment * * @example * ```ts * getTokens('users[0].name') // returns ['users', '0', 'name'] * getTokens('deeply.nested.property') // returns ['deeply', 'nested', 'property'] * ``` */ export const getTokens = (key) => { const tokens = []; let currentToken = ''; let i = 0; const len = key.length; while (i < len) { const char = key[i]; if (char === '[') { // If we have accumulated characters, push them as a token if (currentToken) { tokens.push(currentToken); currentToken = ''; } // Extract the array index i++; let index = ''; while (i < len && key[i] !== ']') { index += key[i]; i++; } tokens.push(index); i++; // Skip the closing bracket } else if (char === '.') { // Push accumulated token if exists if (currentToken) { tokens.push(currentToken); currentToken = ''; } i++; } else { currentToken += char; i++; } } // Push any remaining token if (currentToken) { tokens.push(currentToken); } return tokens; }; /** * Safely retrieves a nested value from an object using an array of key tokens. * Returns undefined if any intermediate value in the path is null or undefined. * * @param root - The root object to traverse * @param keyTokens - Array of string tokens representing the path to the desired value * @returns The value at the specified path, or undefined if the path is invalid * * @internal */ /* trunk-ignore(eslint/@typescript-eslint/no-explicit-any) */ const getNested = (root, keyTokens) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any let current = root; for (const key of keyTokens) { if (current == null) { return undefined; } current = current[key]; } return current; }; /** * Creates a shallow clone of an object while preserving its prototype chain. * * @param source - The source object to clone * @returns A new object with the same properties and prototype as the source * * @internal */ const clonedWithPrototype = (source) => { const clone = Object.create(source); Object.assign(clone, source); return clone; }; export function keyed(parent, path) { const keyTokens = getTokens(path); if (keyTokens.some((token) => token === '__proto__')) { throw new Error('key cannot include "__proto__"'); } const branchTokens = keyTokens.slice(0, keyTokens.length - 1); const leafToken = keyTokens[keyTokens.length - 1]; const keyedValue = derived(parent, ($parent) => getNested($parent, keyTokens)); const set = (value) => { parent.update(($parent) => { if ($parent == null) { return $parent; } const newParent = Array.isArray($parent) ? [...$parent] : clonedWithPrototype($parent); getNested(newParent, branchTokens)[leafToken] = value; return newParent; }); }; const update = (fn) => { parent.update(($parent) => { if ($parent == null) { return $parent; } const newValue = fn(getNested($parent, keyTokens)); const newParent = Array.isArray($parent) ? [...$parent] : clonedWithPrototype($parent); getNested(newParent, branchTokens)[leafToken] = newValue; return newParent; }); }; return { subscribe: keyedValue.subscribe, set, update }; }