@ngrx/signals
Version:
Reactive Store and Set of Utilities for Angular Signals
477 lines (459 loc) • 16.3 kB
JavaScript
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