import { AnimatedPatch, DeepPartial, PatchableState, RenderPatch } from "./vode"; /** * State context for type-safe access and manipulation of nested state paths * while still be able to access the parent state. */ export interface StateContext extends SubContext { /** * parent state * @see PatchableState */ get state(): S; } /** * State context for type-safe access and manipulation of nested sub-state values without knowledge of the parent state. */ export interface SubContext { /** * Reads the current value of the substate if it exists. * * @returns The current value, or undefined if the path doesn't exist */ get(): SubState | undefined; /** * Updates the nested sub-state value WITHOUT triggering a render. * This performs a silent mutation of the parent state object. * * @param {DeepPartial} value - The new value or partial update to apply */ put(value: SubState | Partial | DeepPartial | undefined | null): void; /** * Updates the nested sub-state value AND triggers a render. * This is the recommended way to update nested state in most cases. * * @param value - The new value or partial update to apply */ patch(value: SubState | Partial | DeepPartial | Array> | undefined | null): void; } export type ProxyStateContext = StateContext & { [K in keyof SubState]-?: SubState[K] extends object ? ProxyStateContext : StateContext }; export type ProxySubContext = SubContext & { [K in keyof SubState]-?: SubState[K] extends object ? ProxySubContext : SubContext }; /** * create a ProxyStateContext for type-safe dynamic access to nested state * * @example * ```typescript * const state = createState({ * user: { * profile: { * settings: { theme: 'dark', lang: 'en' } * } * }); * app(element, state, (s) => [DIV]); * * // Create a proxy context for the state * const ctx = context(state).user.profile.settings; * * // Access nested state dynamically * const settings = ctx.get(); // { theme: 'dark', lang: 'en' } * * // Update and trigger render * ctx.patch({ theme: 'light' }); * * // Update without render (silent mutation) * ctx.put({ lang: 'de' }); * state.patch({}); // trigger render manually later * ``` * * @param state * @returns */ export function context(state: S): ProxyStateContext { return new ProxyStateContextImpl(state, []) as unknown as ProxyStateContext; } class ProxyStateContextImpl implements StateContext { constructor( public readonly state: S, private readonly keys: string[] ) { function putDeep(value: SubState | DeepPartial | undefined | null, target: S | DeepPartial) { if (keys.length > 1) { let i = 0; let raw = (target)[keys[i]]; if (typeof raw !== "object" || raw === null) { (target)[keys[i]] = raw = {}; } for (i = 1; i < keys.length - 1; i++) { const p = raw; raw = raw[keys[i]]; if (typeof raw !== "object" || raw === null) { p[keys[i]] = raw = {}; } } raw[keys[i]] = value; } else if (keys.length === 1) { if (typeof (target)[keys[0]] === "object" && typeof value === "object") Object.assign((target)[keys[0]], value); else (target)[keys[0]] = value; } else { Object.assign(target, value as DeepPartial); } } function createPatch(value: SubState | DeepPartial | undefined | null): RenderPatch { const renderPatch: DeepPartial = {}; putDeep(value, renderPatch); return renderPatch; } function get(): SubState | undefined { if (keys.length === 0) return state as unknown as SubState; let raw = state ? (state)[keys[0]] : undefined; for (let i = 1; i < keys.length && !!raw; i++) { raw = raw[keys[i]]; } return raw; } function put(value: SubState | DeepPartial | undefined | null) { putDeep(value, state); } function patch(value: SubState | DeepPartial | Array> | undefined | null) { if (Array.isArray(value)) { const animation: AnimatedPatch = []; for (const v of value) { animation.push(createPatch(v)); } state.patch(animation); } else { state.patch(createPatch(value as DeepPartial)); } } return new Proxy(this, { get: (target, prop, receiver) => { if (prop === 'state') return state; if (prop === 'get') return get; if (prop === 'put') return put; if (prop === 'patch') return patch; // otherwise return a new ProxyStateContext for nested access const newKeys = [...target.keys, String(prop)]; return new ProxyStateContextImpl(target.state, newKeys); } }); } get(): SubState | undefined { throw 'implemented in ctor' } put(value: SubState | DeepPartial | null | undefined): void { throw 'implemented in ctor' } patch(value: SubState | DeepPartial | DeepPartial[] | null | undefined): void { throw 'implemented in ctor' } }