UNPKG

@ngrx/signals

Version:

Reactive Store and Set of Utilities for Angular Signals

477 lines (459 loc) 16.3 kB
import * as i0 from '@angular/core'; import { untracked, isSignal, computed, assertInInjectionContext, inject, Injector, effect, DestroyRef, signal, Injectable, linkedSignal } from '@angular/core'; const DEEP_SIGNAL = Symbol('DEEP_SIGNAL'); function toDeepSignal(signal) { return new Proxy(signal, { has(target, prop) { return !!this.get(target, prop, undefined); }, get(target, prop) { const value = untracked(target); if (!isRecord(value) || !(prop in value)) { if (isSignal(target[prop]) && target[prop][DEEP_SIGNAL]) { delete target[prop]; } return target[prop]; } if (!isSignal(target[prop])) { Object.defineProperty(target, prop, { value: computed(() => target()[prop]), configurable: true, }); target[prop][DEEP_SIGNAL] = true; } return toDeepSignal(target[prop]); }, }); } const nonRecords = [ WeakSet, WeakMap, Promise, Date, Error, RegExp, ArrayBuffer, DataView, Function, ]; function isRecord(value) { if (value === null || typeof value !== 'object' || isIterable(value)) { return false; } let proto = Object.getPrototypeOf(value); if (proto === Object.prototype) { return true; } while (proto && proto !== Object.prototype) { if (nonRecords.includes(proto.constructor)) { return false; } proto = Object.getPrototypeOf(proto); } return proto === Object.prototype; } function isIterable(value) { return typeof value?.[Symbol.iterator] === 'function'; } function deepComputed(computation) { return toDeepSignal(computed(computation)); } function signalMethod(processingFn, config) { if (!config?.injector) { assertInInjectionContext(signalMethod); } const watchers = []; const sourceInjector = config?.injector ?? inject(Injector); const signalMethodFn = (input, config) => { if (isSignal(input)) { const callerInjector = getCallerInjector(); if (typeof ngDevMode !== 'undefined' && ngDevMode && config?.injector === undefined && callerInjector === undefined) { console.warn('@ngrx/signals: The function returned by signalMethod was called', 'outside the injection context with a signal. This may lead to', 'a memory leak. Make sure to call it within the injection context', '(e.g. in a constructor or field initializer) or pass an injector', 'explicitly via the config parameter.\n\nFor more information, see:', 'https://ngrx.io/guide/signals/signal-method#automatic-cleanup'); } const instanceInjector = config?.injector ?? callerInjector ?? sourceInjector; const watcher = effect(() => { const value = input(); untracked(() => processingFn(value)); }, { injector: instanceInjector }); watchers.push(watcher); instanceInjector.get(DestroyRef).onDestroy(() => { const ix = watchers.indexOf(watcher); if (ix !== -1) { watchers.splice(ix, 1); } }); return watcher; } else { processingFn(input); return { destroy: () => void true }; } }; signalMethodFn.destroy = () => watchers.forEach((watcher) => watcher.destroy()); return signalMethodFn; } function getCallerInjector() { try { return inject(Injector); } catch { return undefined; } } const STATE_WATCHERS = new WeakMap(); const STATE_SOURCE = Symbol('STATE_SOURCE'); function isWritableSignal(value) { return (isSignal(value) && 'set' in value && 'update' in value && typeof value.set === 'function' && typeof value.update === 'function'); } function isWritableStateSource(stateSource) { const signals = stateSource[STATE_SOURCE]; return Reflect.ownKeys(stateSource[STATE_SOURCE]).every((key) => { return isWritableSignal(signals[key]); }); } function patchState(stateSource, ...updaters) { const currentState = untracked(() => getState(stateSource)); const newState = updaters.reduce((nextState, updater) => ({ ...nextState, ...(typeof updater === 'function' ? updater(nextState) : updater), }), currentState); const signals = stateSource[STATE_SOURCE]; const stateKeys = Reflect.ownKeys(stateSource[STATE_SOURCE]); for (const key of Reflect.ownKeys(newState)) { if (stateKeys.includes(key)) { const signalKey = key; if (currentState[signalKey] !== newState[signalKey]) { signals[signalKey].set(newState[signalKey]); } } else if (typeof ngDevMode !== 'undefined' && ngDevMode) { console.warn(`@ngrx/signals: patchState was called with an unknown state slice '${String(key)}'.`, 'Ensure that all state properties are explicitly defined in the initial state.', 'Updates to properties not present in the initial state will be ignored.'); } } notifyWatchers(stateSource); } function getState(stateSource) { const signals = stateSource[STATE_SOURCE]; return Reflect.ownKeys(stateSource[STATE_SOURCE]).reduce((state, key) => { const value = signals[key](); return { ...state, [key]: value, }; }, {}); } function watchState(stateSource, watcher, config) { if (!config?.injector) { assertInInjectionContext(watchState); } const injector = config?.injector ?? inject(Injector); const destroyRef = injector.get(DestroyRef); addWatcher(stateSource, watcher); watcher(getState(stateSource)); const destroy = () => removeWatcher(stateSource, watcher); destroyRef.onDestroy(destroy); return { destroy }; } function getWatchers(stateSource) { return STATE_WATCHERS.get(stateSource[STATE_SOURCE]) || []; } function notifyWatchers(stateSource) { const watchers = getWatchers(stateSource); for (const watcher of watchers) { const state = untracked(() => getState(stateSource)); watcher(state); } } function addWatcher(stateSource, watcher) { const watchers = getWatchers(stateSource); STATE_WATCHERS.set(stateSource[STATE_SOURCE], [...watchers, watcher]); } function removeWatcher(stateSource, watcher) { const watchers = getWatchers(stateSource); STATE_WATCHERS.set(stateSource[STATE_SOURCE], watchers.filter((w) => w !== watcher)); } function signalState(initialState) { const stateKeys = Reflect.ownKeys(initialState); const stateSource = stateKeys.reduce((signalsDict, key) => ({ ...signalsDict, [key]: signal(initialState[key]), }), {}); const signalState = computed(() => stateKeys.reduce((state, key) => ({ ...state, [key]: stateSource[key]() }), {})); Object.defineProperty(signalState, STATE_SOURCE, { value: stateSource, }); for (const key of stateKeys) { Object.defineProperty(signalState, key, { value: toDeepSignal(stateSource[key]), }); } return signalState; } function signalStore(...args) { const signalStoreArgs = [...args]; const config = typeof signalStoreArgs[0] === 'function' ? {} : signalStoreArgs.shift(); const features = signalStoreArgs; class SignalStore { constructor() { const innerStore = features.reduce((store, feature) => feature(store), getInitialInnerStore()); const { stateSignals, props, methods, hooks } = innerStore; const storeMembers = { ...stateSignals, ...props, ...methods, }; this[STATE_SOURCE] = innerStore[STATE_SOURCE]; for (const key of Reflect.ownKeys(storeMembers)) { this[key] = storeMembers[key]; } const { onInit, onDestroy } = hooks; if (onInit) { onInit(); } if (onDestroy) { inject(DestroyRef).onDestroy(onDestroy); } } /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: SignalStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: SignalStore, providedIn: config.providedIn || null }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: SignalStore, decorators: [{ type: Injectable, args: [{ providedIn: config.providedIn || null }] }], ctorParameters: () => [] }); return SignalStore; } function getInitialInnerStore() { return { [STATE_SOURCE]: {}, stateSignals: {}, props: {}, methods: {}, hooks: {}, }; } function signalStoreFeature(...args) { const features = (typeof args[0] === 'function' ? args : args.slice(1)); return (inputStore) => features.reduce((store, feature) => feature(store), inputStore); } function type() { return undefined; } function assertUniqueStoreMembers(store, newMemberKeys) { if (typeof ngDevMode === 'undefined' || !ngDevMode) { return; } const storeMembers = { ...store.stateSignals, ...store.props, ...store.methods, }; const overriddenKeys = Reflect.ownKeys(storeMembers).filter((memberKey) => newMemberKeys.includes(memberKey)); if (overriddenKeys.length > 0) { console.warn('@ngrx/signals: SignalStore members cannot be overridden.', 'Trying to override:', overriddenKeys.map((key) => String(key)).join(', ')); } } function withProps(propsFactory) { return (store) => { const props = propsFactory({ [STATE_SOURCE]: store[STATE_SOURCE], ...store.stateSignals, ...store.props, ...store.methods, }); assertUniqueStoreMembers(store, Reflect.ownKeys(props)); return { ...store, props: { ...store.props, ...props }, }; }; } function withComputed(computedFactory) { return withProps((store) => { const computedResult = computedFactory(store); const computedResultKeys = Reflect.ownKeys(computedResult); return computedResultKeys.reduce((prev, key) => { const signalOrComputation = computedResult[key]; return { ...prev, [key]: isSignal(signalOrComputation) ? signalOrComputation : computed(signalOrComputation), }; }, {}); }); } /** * @description * Allows passing properties, methods, or signals from a SignalStore * to a feature. * * @usageNotes * ```typescript * signalStore( * withMethods((store) => ({ * load(id: number): Observable<Entity> { * return of({ id, name: 'John' }); * }, * })), * withFeature( * // 👇 has full access to the store * (store) => withEntityLoader((id) => firstValueFrom(store.load(id))) * ) * ); * ``` * * @param featureFactory function returning the actual feature */ function withFeature(featureFactory) { return (store) => { const storeForFactory = { [STATE_SOURCE]: store[STATE_SOURCE], ...store['stateSignals'], ...store['props'], ...store['methods'], }; return featureFactory(storeForFactory)(store); }; } function withHooks(hooksOrFactory) { return (store) => { const storeMembers = { [STATE_SOURCE]: store[STATE_SOURCE], ...store.stateSignals, ...store.props, ...store.methods, }; const hooks = typeof hooksOrFactory === 'function' ? hooksOrFactory(storeMembers) : hooksOrFactory; const createHook = (name) => { const hook = hooks[name]; const currentHook = store.hooks[name]; return hook ? () => { if (currentHook) { currentHook(); } hook(storeMembers); } : currentHook; }; return { ...store, hooks: { onInit: createHook('onInit'), onDestroy: createHook('onDestroy'), }, }; }; } /** * @description * * Adds linked state slices to a SignalStore. * * @usageNotes * * ```typescript * const OptionsStore = signalStore( * withState({ options: [1, 2, 3] }), * withLinkedState(({ options }) => ({ * selectedOption: () => options()[0], * })) * ); * ``` * * This returns a state of type `{ options: number[], selectedOption: number | undefined }`. * When the `options` signal changes, the `selectedOption` automatically updates. * * For advanced use cases, `linkedSignal` or any other `WritableSignal` instance can be used within `withLinkedState`: * * ```typescript * type Option = { id: number; label: string }; * * const OptionsStore = signalStore( * withState({ options: [] as Option[] }), * withLinkedState(({ options }) => ({ * selectedOption: linkedSignal<Option[], Option>({ * source: options, * computation: (newOptions, previous) => { * const option = newOptions.find((o) => o.id === previous?.value.id); * return option ?? newOptions[0]; * }, * }) * })) * ) * ``` * * @param linkedStateFactory A function that returns an object literal with properties containing an actual `linkedSignal` or the computation function. */ function withLinkedState(linkedStateFactory) { return (store) => { const linkedState = linkedStateFactory({ ...store.stateSignals, ...store.props, }); const stateKeys = Reflect.ownKeys(linkedState); assertUniqueStoreMembers(store, stateKeys); const stateSource = store[STATE_SOURCE]; const stateSignals = {}; for (const key of stateKeys) { const signalOrComputationFn = linkedState[key]; stateSource[key] = isWritableSignal(signalOrComputationFn) ? signalOrComputationFn : linkedSignal(signalOrComputationFn); stateSignals[key] = toDeepSignal(stateSource[key]); } return { ...store, stateSignals: { ...store.stateSignals, ...stateSignals }, }; }; } function withMethods(methodsFactory) { return (store) => { const methods = methodsFactory({ [STATE_SOURCE]: store[STATE_SOURCE], ...store.stateSignals, ...store.props, ...store.methods, }); assertUniqueStoreMembers(store, Reflect.ownKeys(methods)); return { ...store, methods: { ...store.methods, ...methods }, }; }; } function withState(stateOrFactory) { return (store) => { const state = (typeof stateOrFactory === 'function' ? stateOrFactory() : stateOrFactory); const stateKeys = Reflect.ownKeys(state); assertUniqueStoreMembers(store, stateKeys); const stateSource = store[STATE_SOURCE]; const stateSignals = {}; for (const key of stateKeys) { stateSource[key] = signal(state[key]); stateSignals[key] = toDeepSignal(stateSource[key]); } return { ...store, stateSignals: { ...store.stateSignals, ...stateSignals }, }; }; } /** * Generated bundle index. Do not edit. */ export { deepComputed, getState, isWritableStateSource, patchState, signalMethod, signalState, signalStore, signalStoreFeature, type, watchState, withComputed, withFeature, withHooks, withLinkedState, withMethods, withProps, withState }; //# sourceMappingURL=ngrx-signals.mjs.map