diff --git a/modules/signals/entities/src/index.ts b/modules/signals/entities/src/index.ts index 8ddc0bacdd..f6bdb94a50 100644 --- a/modules/signals/entities/src/index.ts +++ b/modules/signals/entities/src/index.ts @@ -12,11 +12,11 @@ export { updateAllEntities } from './updaters/update-all-entities'; export { entityConfig } from './entity-config'; export { - EntityComputed, EntityId, EntityMap, + EntityProps, EntityState, - NamedEntityComputed, + NamedEntityProps, NamedEntityState, SelectEntityId, } from './models'; diff --git a/modules/signals/entities/src/models.ts b/modules/signals/entities/src/models.ts index ae23bb830c..956311b50b 100644 --- a/modules/signals/entities/src/models.ts +++ b/modules/signals/entities/src/models.ts @@ -13,12 +13,12 @@ export type NamedEntityState = { [K in keyof EntityState as `${Collection}${Capitalize}`]: EntityState[K]; }; -export type EntityComputed = { +export type EntityProps = { entities: Signal; }; -export type NamedEntityComputed = { - [K in keyof EntityComputed as `${Collection}${Capitalize}`]: EntityComputed[K]; +export type NamedEntityProps = { + [K in keyof EntityProps as `${Collection}${Capitalize}`]: EntityProps[K]; }; export type SelectEntityId = (entity: Entity) => EntityId; diff --git a/modules/signals/entities/src/with-entities.ts b/modules/signals/entities/src/with-entities.ts index d1a6a8a118..2177c6afe1 100644 --- a/modules/signals/entities/src/with-entities.ts +++ b/modules/signals/entities/src/with-entities.ts @@ -7,11 +7,11 @@ import { withState, } from '@ngrx/signals'; import { - EntityComputed, + EntityProps, EntityId, EntityMap, EntityState, - NamedEntityComputed, + NamedEntityProps, NamedEntityState, } from './models'; import { getEntityStateKeys } from './helpers'; @@ -20,7 +20,7 @@ export function withEntities(): SignalStoreFeature< EmptyFeatureResult, { state: EntityState; - computed: EntityComputed; + props: EntityProps; methods: {}; } >; @@ -31,7 +31,7 @@ export function withEntities(config: { EmptyFeatureResult, { state: NamedEntityState; - computed: NamedEntityComputed; + props: NamedEntityProps; methods: {}; } >; @@ -41,7 +41,7 @@ export function withEntities(config: { EmptyFeatureResult, { state: EntityState; - computed: EntityComputed; + props: EntityProps; methods: {}; } >; diff --git a/modules/signals/spec/signal-store-feature.spec.ts b/modules/signals/spec/signal-store-feature.spec.ts index 2ad0cbf23f..b855d84733 100644 --- a/modules/signals/spec/signal-store-feature.spec.ts +++ b/modules/signals/spec/signal-store-feature.spec.ts @@ -33,7 +33,7 @@ describe('signalStoreFeature', () => { return signalStoreFeature( { state: type<{ foo: string }>(), - computed: type<{ s: Signal }>(), + props: type<{ s: Signal }>(), }, withState({ foo1: 1 }), withState({ foo2: 2 }) diff --git a/modules/signals/spec/signal-store.spec.ts b/modules/signals/spec/signal-store.spec.ts index 5db462aafb..02c8bab06b 100644 --- a/modules/signals/spec/signal-store.spec.ts +++ b/modules/signals/spec/signal-store.spec.ts @@ -6,6 +6,7 @@ import { withComputed, withHooks, withMethods, + withProps, withState, } from '../src'; import { STATE_SOURCE } from '../src/state-source'; @@ -146,14 +147,54 @@ describe('signalStore', () => { }); }); + describe('withProps', () => { + it('provides previously defined state slices and properties as input argument', () => { + const Store = signalStore( + withState(() => ({ foo: 'foo' })), + withComputed(() => ({ bar: signal('bar').asReadonly() })), + withProps(() => ({ num: 10 })), + withProps(({ foo, bar, num }) => { + expect(foo()).toBe('foo'); + expect(bar()).toBe('bar'); + expect(num).toBe(10); + + return { baz: num + 1 }; + }) + ); + + const store = new Store(); + + expect(store[STATE_SOURCE]()).toEqual({ foo: 'foo' }); + expect(store.foo()).toBe('foo'); + expect(store.bar()).toBe('bar'); + expect(store.num).toBe(10); + expect(store.baz).toBe(11); + }); + + it('executes withProps factory in injection context', () => { + const TOKEN = new InjectionToken('TOKEN', { + providedIn: 'root', + factory: () => ({ foo: 'bar' }), + }); + const Store = signalStore(withProps(() => inject(TOKEN))); + + TestBed.configureTestingModule({ providers: [Store] }); + const store = TestBed.inject(Store); + + expect(store.foo).toBe('bar'); + }); + }); + describe('withComputed', () => { - it('provides previously defined state slices and computed signals as input argument', () => { + it('provides previously defined state slices and properties as input argument', () => { const Store = signalStore( withState(() => ({ foo: 'foo' })), withComputed(() => ({ bar: signal('bar').asReadonly() })), - withComputed(({ foo, bar }) => { + withProps(() => ({ num: 10 })), + withComputed(({ foo, bar, num }) => { expect(foo()).toBe('foo'); expect(bar()).toBe('bar'); + expect(num).toBe(10); return { baz: signal('baz').asReadonly() }; }) @@ -164,6 +205,7 @@ describe('signalStore', () => { expect(store[STATE_SOURCE]()).toEqual({ foo: 'foo' }); expect(store.foo()).toBe('foo'); expect(store.bar()).toBe('bar'); + expect(store.num).toBe(10); expect(store.baz()).toBe('baz'); }); @@ -187,11 +229,13 @@ describe('signalStore', () => { withState(() => ({ foo: 'foo' })), withComputed(() => ({ bar: signal('bar').asReadonly() })), withMethods(() => ({ baz: () => 'baz' })), + withProps(() => ({ num: 100 })), withMethods((store) => { expect(store[STATE_SOURCE]()).toEqual({ foo: 'foo' }); expect(store.foo()).toBe('foo'); expect(store.bar()).toBe('bar'); expect(store.baz()).toBe('baz'); + expect(store.num).toBe(100); return { m: () => 'm' }; }) @@ -203,6 +247,7 @@ describe('signalStore', () => { expect(store.foo()).toBe('foo'); expect(store.bar()).toBe('bar'); expect(store.baz()).toBe('baz'); + expect(store.num).toBe(100); expect(store.m()).toBe('m'); }); @@ -263,12 +308,14 @@ describe('signalStore', () => { withState(() => ({ foo: 'foo' })), withComputed(() => ({ bar: signal('bar').asReadonly() })), withMethods(() => ({ baz: () => 'baz' })), + withProps(() => ({ num: 10 })), withHooks({ onInit(store) { expect(store[STATE_SOURCE]()).toEqual({ foo: 'foo' }); expect(store.foo()).toBe('foo'); expect(store.bar()).toBe('bar'); expect(store.baz()).toBe('baz'); + expect(store.num).toBe(10); message = 'onInit'; }, }) @@ -285,11 +332,13 @@ describe('signalStore', () => { withState(() => ({ foo: 'foo' })), withComputed(() => ({ bar: signal('bar').asReadonly() })), withMethods(() => ({ baz: () => 'baz' })), + withProps(() => ({ num: 100 })), withHooks({ onDestroy(store) { expect(store.foo()).toBe('foo'); expect(store.bar()).toBe('bar'); expect(store.baz()).toBe('baz'); + expect(store.num).toBe(100); message = 'onDestroy'; }, }) diff --git a/modules/signals/spec/types/signal-store.types.spec.ts b/modules/signals/spec/types/signal-store.types.spec.ts index 415086dc9e..aeb0ab73f4 100644 --- a/modules/signals/spec/types/signal-store.types.spec.ts +++ b/modules/signals/spec/types/signal-store.types.spec.ts @@ -846,7 +846,7 @@ describe('signalStore', () => { return signalStoreFeature( { state: type<{ q1: string }>(), - computed: type<{ sig: Signal }>(), + props: type<{ sig: Signal }>(), }, withState({ y: initialY }), withComputed(() => ({ sigY: computed(() => 'sigY') })), @@ -932,7 +932,7 @@ describe('signalStore', () => { ${baseSnippet} const feature = signalStoreFeature( - { computed: type<{ sig: Signal }>() }, + { props: type<{ sig: Signal }>() }, withX(), withState({ q1: 'q1' }), withY(), @@ -976,7 +976,7 @@ describe('signalStore', () => { ${baseSnippet} const feature = signalStoreFeature( - { computed: type<{ sig: Signal }>() }, + { props: type<{ sig: Signal }>() }, withX(), withState({ q1: 'q1' }), withY(), @@ -1007,7 +1007,7 @@ describe('signalStore', () => { const feature = signalStoreFeature( { - computed: type<{ sig: Signal }>(), + props: type<{ sig: Signal }>(), methods: type<{ f(): void; g(arg: string): string; }>(), }, withX(), @@ -1046,7 +1046,7 @@ describe('signalStore', () => { entities: Entity[]; selectedEntity: Entity | null; }; - computed: { + props: { selectedEntity2: Signal; }; methods: { diff --git a/modules/signals/spec/with-computed.spec.ts b/modules/signals/spec/with-computed.spec.ts index cf6be19b1b..d4bb9808f7 100644 --- a/modules/signals/spec/with-computed.spec.ts +++ b/modules/signals/spec/with-computed.spec.ts @@ -11,11 +11,11 @@ describe('withComputed', () => { const store = withComputed(() => ({ s1, s2 }))(initialStore); - expect(Object.keys(store.computedSignals)).toEqual(['s1', 's2']); - expect(Object.keys(initialStore.computedSignals)).toEqual([]); + expect(Object.keys(store.props)).toEqual(['s1', 's2']); + expect(Object.keys(initialStore.props)).toEqual([]); - expect(store.computedSignals.s1).toBe(s1); - expect(store.computedSignals.s2).toBe(s2); + expect(store.props.s1).toBe(s1); + expect(store.props.s2).toBe(s2); }); it('logs warning if previously defined signal store members have the same name', () => { diff --git a/modules/signals/spec/with-props.spec.ts b/modules/signals/spec/with-props.spec.ts new file mode 100644 index 0000000000..b52897004c --- /dev/null +++ b/modules/signals/spec/with-props.spec.ts @@ -0,0 +1,50 @@ +import { signal } from '@angular/core'; +import { of } from 'rxjs'; +import { withMethods, withProps, withState } from '../src'; +import { getInitialInnerStore } from '../src/signal-store'; + +describe('withProps', () => { + it('adds properties to the store immutably', () => { + const initialStore = getInitialInnerStore(); + + const store = withProps(() => ({ p1: 1, p2: 2 }))(initialStore); + + expect(Object.keys(store.props)).toEqual(['p1', 'p2']); + expect(Object.keys(initialStore.props)).toEqual([]); + + expect(store.props.p1).toBe(1); + expect(store.props.p2).toBe(2); + }); + + it('logs warning if previously defined signal store members have the same name', () => { + const initialStore = [ + withState({ + s1: 10, + s2: 's2', + }), + withProps(() => ({ + p1: of(100), + p2: 10, + })), + withMethods(() => ({ + m1() {}, + m2() {}, + })), + ].reduce((acc, feature) => feature(acc), getInitialInnerStore()); + jest.spyOn(console, 'warn').mockImplementation(); + + withProps(() => ({ + s1: { foo: 'bar' }, + p: 10, + p2: signal(100), + m1: { ngrx: 'rocks' }, + m3: of('m3'), + }))(initialStore); + + expect(console.warn).toHaveBeenCalledWith( + '@ngrx/signals: SignalStore members cannot be overridden.', + 'Trying to override:', + 's1, p2, m1' + ); + }); +}); diff --git a/modules/signals/src/index.ts b/modules/signals/src/index.ts index e7332786e5..b7506ba5a8 100644 --- a/modules/signals/src/index.ts +++ b/modules/signals/src/index.ts @@ -23,4 +23,5 @@ export { Prettify } from './ts-helpers'; export { withComputed } from './with-computed'; export { withHooks } from './with-hooks'; export { withMethods } from './with-methods'; +export { withProps } from './with-props'; export { withState } from './with-state'; diff --git a/modules/signals/src/signal-store-assertions.ts b/modules/signals/src/signal-store-assertions.ts index be0a1947c5..865cddbef2 100644 --- a/modules/signals/src/signal-store-assertions.ts +++ b/modules/signals/src/signal-store-assertions.ts @@ -12,7 +12,7 @@ export function assertUniqueStoreMembers( const storeMembers = { ...store.stateSignals, - ...store.computedSignals, + ...store.props, ...store.methods, }; const overriddenKeys = Object.keys(storeMembers).filter((memberKey) => diff --git a/modules/signals/src/signal-store-feature.ts b/modules/signals/src/signal-store-feature.ts index b8a2b27a9e..cf3da5d0f5 100644 --- a/modules/signals/src/signal-store-feature.ts +++ b/modules/signals/src/signal-store-feature.ts @@ -7,7 +7,7 @@ import { Prettify } from './ts-helpers'; type PrettifyFeatureResult = Prettify<{ state: Prettify; - computed: Prettify; + props: Prettify; methods: Prettify; }>; diff --git a/modules/signals/src/signal-store-models.ts b/modules/signals/src/signal-store-models.ts index 0102f2d3e1..451a03df47 100644 --- a/modules/signals/src/signal-store-models.ts +++ b/modules/signals/src/signal-store-models.ts @@ -22,26 +22,26 @@ export type SignalStoreHooks = { export type InnerSignalStore< State extends object = object, - ComputedSignals extends SignalsDictionary = SignalsDictionary, + Props extends object = object, Methods extends MethodsDictionary = MethodsDictionary > = { stateSignals: StateSignals; - computedSignals: ComputedSignals; + props: Props; methods: Methods; hooks: SignalStoreHooks; } & WritableStateSource; export type SignalStoreFeatureResult = { state: object; - computed: SignalsDictionary; + props: object; methods: MethodsDictionary; }; -export type EmptyFeatureResult = { state: {}; computed: {}; methods: {} }; +export type EmptyFeatureResult = { state: {}; props: {}; methods: {} }; export type SignalStoreFeature< Input extends SignalStoreFeatureResult = SignalStoreFeatureResult, Output extends SignalStoreFeatureResult = SignalStoreFeatureResult > = ( - store: InnerSignalStore -) => InnerSignalStore; + store: InnerSignalStore +) => InnerSignalStore; diff --git a/modules/signals/src/signal-store.ts b/modules/signals/src/signal-store.ts index 5d5da1505f..60116a79da 100644 --- a/modules/signals/src/signal-store.ts +++ b/modules/signals/src/signal-store.ts @@ -15,7 +15,7 @@ type SignalStoreMembers = Prettify< OmitPrivate< StateSignals & - FeatureResult['computed'] & + FeatureResult['props'] & FeatureResult['methods'] > >; @@ -1354,8 +1354,8 @@ export function signalStore( (store, feature) => feature(store), getInitialInnerStore() ); - const { stateSignals, computedSignals, methods, hooks } = innerStore; - const storeMembers = { ...stateSignals, ...computedSignals, ...methods }; + const { stateSignals, props, methods, hooks } = innerStore; + const storeMembers = { ...stateSignals, ...props, ...methods }; (this as any)[STATE_SOURCE] = innerStore[STATE_SOURCE]; @@ -1382,7 +1382,7 @@ export function getInitialInnerStore(): InnerSignalStore { return { [STATE_SOURCE]: signal({}), stateSignals: {}, - computedSignals: {}, + props: {}, methods: {}, hooks: {}, }; diff --git a/modules/signals/src/with-computed.ts b/modules/signals/src/with-computed.ts index a2abedca3c..4ce8559532 100644 --- a/modules/signals/src/with-computed.ts +++ b/modules/signals/src/with-computed.ts @@ -1,34 +1,22 @@ -import { assertUniqueStoreMembers } from './signal-store-assertions'; import { - InnerSignalStore, SignalsDictionary, SignalStoreFeature, SignalStoreFeatureResult, StateSignals, } from './signal-store-models'; import { Prettify } from './ts-helpers'; +import { withProps } from './with-props'; export function withComputed< Input extends SignalStoreFeatureResult, ComputedSignals extends SignalsDictionary >( signalsFactory: ( - store: Prettify & Input['computed']> + store: Prettify & Input['props']> ) => ComputedSignals ): SignalStoreFeature< Input, - { state: {}; computed: ComputedSignals; methods: {} } + { state: {}; props: ComputedSignals; methods: {} } > { - return (store) => { - const computedSignals = signalsFactory({ - ...store.stateSignals, - ...store.computedSignals, - }); - assertUniqueStoreMembers(store, Object.keys(computedSignals)); - - return { - ...store, - computedSignals: { ...store.computedSignals, ...computedSignals }, - } as InnerSignalStore, ComputedSignals>; - }; + return withProps(signalsFactory); } diff --git a/modules/signals/src/with-hooks.ts b/modules/signals/src/with-hooks.ts index 7b34a47673..6067a6bc07 100644 --- a/modules/signals/src/with-hooks.ts +++ b/modules/signals/src/with-hooks.ts @@ -10,7 +10,7 @@ import { Prettify } from './ts-helpers'; type HookFn = ( store: Prettify< StateSignals & - Input['computed'] & + Input['props'] & Input['methods'] & WritableStateSource> > @@ -19,7 +19,7 @@ type HookFn = ( type HooksFactory = ( store: Prettify< StateSignals & - Input['computed'] & + Input['props'] & Input['methods'] & WritableStateSource> > @@ -48,7 +48,7 @@ export function withHooks( const storeMembers = { [STATE_SOURCE]: store[STATE_SOURCE], ...store.stateSignals, - ...store.computedSignals, + ...store.props, ...store.methods, }; const hooks = diff --git a/modules/signals/src/with-methods.ts b/modules/signals/src/with-methods.ts index 9fef0270c0..cd5252ab0d 100644 --- a/modules/signals/src/with-methods.ts +++ b/modules/signals/src/with-methods.ts @@ -17,17 +17,17 @@ export function withMethods< methodsFactory: ( store: Prettify< StateSignals & - Input['computed'] & + Input['props'] & Input['methods'] & WritableStateSource> > ) => Methods -): SignalStoreFeature { +): SignalStoreFeature { return (store) => { const methods = methodsFactory({ [STATE_SOURCE]: store[STATE_SOURCE], ...store.stateSignals, - ...store.computedSignals, + ...store.props, ...store.methods, }); assertUniqueStoreMembers(store, Object.keys(methods)); diff --git a/modules/signals/src/with-props.ts b/modules/signals/src/with-props.ts new file mode 100644 index 0000000000..6ed348c9b4 --- /dev/null +++ b/modules/signals/src/with-props.ts @@ -0,0 +1,38 @@ +import { STATE_SOURCE, WritableStateSource } from './state-source'; +import { assertUniqueStoreMembers } from './signal-store-assertions'; +import { + InnerSignalStore, + SignalStoreFeature, + SignalStoreFeatureResult, + StateSignals, +} from './signal-store-models'; +import { Prettify } from './ts-helpers'; + +export function withProps< + Input extends SignalStoreFeatureResult, + Props extends object +>( + propsFactory: ( + store: Prettify< + StateSignals & + Input['props'] & + Input['methods'] & + WritableStateSource> + > + ) => Props +): SignalStoreFeature { + return (store) => { + const props = propsFactory({ + [STATE_SOURCE]: store[STATE_SOURCE], + ...store.stateSignals, + ...store.props, + ...store.methods, + }); + assertUniqueStoreMembers(store, Object.keys(props)); + + return { + ...store, + props: { ...store.props, ...props }, + } as InnerSignalStore; + }; +} diff --git a/modules/signals/src/with-state.ts b/modules/signals/src/with-state.ts index 5b3cf94d68..a6fe37c274 100644 --- a/modules/signals/src/with-state.ts +++ b/modules/signals/src/with-state.ts @@ -15,19 +15,19 @@ export function withState( stateFactory: () => State ): SignalStoreFeature< EmptyFeatureResult, - { state: State; computed: {}; methods: {} } + { state: State; props: {}; methods: {} } >; export function withState( state: State ): SignalStoreFeature< EmptyFeatureResult, - { state: State; computed: {}; methods: {} } + { state: State; props: {}; methods: {} } >; export function withState( stateOrFactory: State | (() => State) ): SignalStoreFeature< SignalStoreFeatureResult, - { state: State; computed: {}; methods: {} } + { state: State; props: {}; methods: {} } > { return (store) => { const state = diff --git a/projects/ngrx.io/content/guide/signals/signal-store/custom-store-features.md b/projects/ngrx.io/content/guide/signals/signal-store/custom-store-features.md index 949ec2def3..6f95529943 100644 --- a/projects/ngrx.io/content/guide/signals/signal-store/custom-store-features.md +++ b/projects/ngrx.io/content/guide/signals/signal-store/custom-store-features.md @@ -153,7 +153,7 @@ State changes will be logged to the console whenever the `BooksStore` state is u ## Creating a Custom Feature with Input -The `signalStoreFeature` function provides the ability to create a custom feature that requires specific state slices, computed signals, and/or methods to be defined in the store where it is used. +The `signalStoreFeature` function provides the ability to create a custom feature that requires specific state slices, properties, and/or methods to be defined in the store where it is used. This enables the utilization of input properties within the custom feature, even if they are not explicitly defined within the feature itself. The expected input type should be defined as the first argument of the `signalStoreFeature` function, using the `type` helper function from the `@ngrx/signals` package. @@ -238,9 +238,9 @@ export const BooksStore = signalStore( -### Example 4: Defining Computed Props and Methods as Input +### Example 4: Defining Properties and Methods as Input -In addition to state, it's also possible to define expected computed signals and methods in the following way: +In addition to state, it's also possible to define expected properties and methods in the following way: @@ -250,7 +250,7 @@ import { signalStoreFeature, type, withMethods } from '@ngrx/signals'; export function withBaz<Foo extends string | number>() { return signalStoreFeature( { - computed: type<{ foo: Signal<Foo> }>(), + props: type<{ foo: Signal<Foo> }>(), methods: type<{ bar(foo: number): void }>(), }, withMethods((store) => ({ @@ -264,7 +264,7 @@ export function withBaz<Foo extends string | number>() { -The `withBaz` feature can only be used in a store where the computed signal `foo` and the method `bar` are defined. +The `withBaz` feature can only be used in a store where the property `foo` and the method `bar` are defined. ## Known TypeScript Issues diff --git a/projects/ngrx.io/content/guide/signals/signal-store/custom-store-properties.md b/projects/ngrx.io/content/guide/signals/signal-store/custom-store-properties.md new file mode 100644 index 0000000000..5acdb11362 --- /dev/null +++ b/projects/ngrx.io/content/guide/signals/signal-store/custom-store-properties.md @@ -0,0 +1,72 @@ +# Custom Store Properties + +The `withProps` feature can be used to add static properties, observables, dependencies, or other custom properties to a SignalStore. +It accepts a factory function that returns an object containing additional properties for the store. +The factory function receives an object containing state signals, previously defined properties, and methods as its input argument. + +## Exposing Observables + +`withProps` can be useful for exposing observables from a SignalStore, which can serve as integration points with RxJS-based APIs: + + + +import { toObservable } from '@angular/core/rxjs-interop'; +import { signalStore, withProps, withState } from '@ngrx/signals'; +import { Book } from './book.model'; + +type BooksState = { + books: Book[]; + isLoading: boolean; +}; + +export const BooksStore = signalStore( + withState<BooksState>({ books: [], isLoading: false }), + withProps(({ isLoading }) => ({ + isLoading$: toObservable(isLoading), + })), +); + + + +## Grouping Dependencies + +Dependencies required across multiple store features can be grouped using `withProps`: + + + +import { inject } from '@angular/core'; +import { signalStore, withProps, withState } from '@ngrx/signals'; +import { Logger } from './logger'; +import { Book } from './book.model'; +import { BooksService } from './books.service'; + +type BooksState = { + books: Book[]; + isLoading: boolean; +}; + +export const BooksStore = signalStore( + withState<BooksState>({ books: [], isLoading: false }), + withProps(() => ({ + booksService: inject(BooksService), + logger: inject(Logger), + })), + withMethods(({ booksService, logger, ...store }) => ({ + async loadBooks(): Promise<void> { + logger.debug('Loading books...'); + patchState(store, { isLoading: true }); + + const books = await booksService.getAll(); + logger.debug('Books loaded successfully', books); + + patchState(store, { books, isLoading: false }); + }, + })), + withHooks({ + onInit({ logger }) { + logger.debug('BooksStore initialized'); + }, + }), +); + + diff --git a/projects/ngrx.io/content/guide/signals/signal-store/index.md b/projects/ngrx.io/content/guide/signals/signal-store/index.md index fcabd558fc..becada673a 100644 --- a/projects/ngrx.io/content/guide/signals/signal-store/index.md +++ b/projects/ngrx.io/content/guide/signals/signal-store/index.md @@ -7,7 +7,7 @@ The simplicity and flexibility of SignalStore, coupled with its opinionated and ## Creating a Store A SignalStore is created using the `signalStore` function. This function accepts a sequence of store features. -Through the combination of store features, the SignalStore gains state, computed signals, and methods, allowing for a flexible and extensible store implementation. +Through the combination of store features, the SignalStore gains state, properties, and methods, allowing for a flexible and extensible store implementation. Based on the utilized features, the `signalStore` function returns an injectable service that can be provided and injected where needed. The `withState` feature is used to add state slices to the SignalStore. @@ -141,11 +141,11 @@ export class BooksComponent { -## Defining Computed Signals +## Defining Store Properties Computed signals can be added to the store using the `withComputed` feature. This feature accepts a factory function as an input argument, which is executed within the injection context. -The factory should return a dictionary of computed signals, utilizing previously defined state and computed signals that are accessible through its input argument. +The factory should return a dictionary of computed signals, utilizing previously defined state signals and properties that are accessible through its input argument. @@ -159,7 +159,7 @@ const initialState: BooksState = { /* ... */ }; export const BooksStore = signalStore( withState(initialState), - // 👇 Accessing previously defined state and computed signals. + // 👇 Accessing previously defined state signals and properties. withComputed(({ books, filter }) => ({ booksCount: computed(() => books().length), sortedBooks: computed(() => { @@ -174,12 +174,19 @@ export const BooksStore = signalStore( +
+ +The `withProps` feature can be used to add static properties, observables, dependencies, and any other custom properties to a SignalStore. +For more details, see the [Custom Store Properties](/guide/signals/signal-store/custom-store-properties) guide. + +
+ ## Defining Store Methods Methods can be added to the store using the `withMethods` feature. This feature takes a factory function as an input argument and returns a dictionary of methods. Similar to `withComputed`, the `withMethods` factory is also executed within the injection context. -The store instance, including previously defined state, computed signals, and methods, is accessible through the factory input. +The store instance, including previously defined state signals, properties, and methods, is accessible through the factory input. @@ -200,8 +207,8 @@ const initialState: BooksState = { /* ... */ }; export const BooksStore = signalStore( withState(initialState), withComputed(/* ... */), - // 👇 Accessing a store instance with previously defined state, - // computed signals, and methods. + // 👇 Accessing a store instance with previously defined state signals, + // properties, and methods. withMethods((store) => ({ updateQuery(query: string): void { // 👇 Updating state using the `patchState` function. diff --git a/projects/ngrx.io/content/guide/signals/signal-store/private-store-members.md b/projects/ngrx.io/content/guide/signals/signal-store/private-store-members.md index 6b65747f63..71a19ff9e5 100644 --- a/projects/ngrx.io/content/guide/signals/signal-store/private-store-members.md +++ b/projects/ngrx.io/content/guide/signals/signal-store/private-store-members.md @@ -1,17 +1,19 @@ # Private Store Members SignalStore allows defining private members that cannot be accessed from outside the store by using the `_` prefix. -This includes root-level state slices, computed signals, and methods. +This includes root-level state slices, properties, and methods. import { computed } from '@angular/core'; +import { toObservable } from '@angular/core/rxjs-interop'; import { patchState, signalStore, withComputed, withMethods, + withProps, withState, } from '@ngrx/signals'; @@ -26,6 +28,11 @@ export const CounterStore = signalStore( _doubleCount1: computed(() => count1() * 2), doubleCount2: computed(() => _count2() * 2), })), + withProps(({ count2, _doubleCount1 }) => ({ + // 👇 private property + _count2$: toObservable(count2), + doubleCount1$: toObservable(_doubleCount1), + })), withMethods((store) => ({ increment1(): void { patchState(store, { count1: store.count1() + 1 }); @@ -58,6 +65,9 @@ export class CounterComponent implements OnInit { console.log(this.store._doubleCount1()); // ❌ console.log(this.store.doubleCount2()); // ✅ + this.store._count2$.subscribe(console.log); // ❌ + this.store.doubleCount1$.subscribe(console.log); // ✅ + this.store.increment1(); // ✅ this.store._increment2(); // ❌ } diff --git a/projects/ngrx.io/content/navigation.json b/projects/ngrx.io/content/navigation.json index b90c92046e..46ab962f18 100644 --- a/projects/ngrx.io/content/navigation.json +++ b/projects/ngrx.io/content/navigation.json @@ -293,6 +293,10 @@ "title": "Lifecycle Hooks", "url": "guide/signals/signal-store/lifecycle-hooks" }, + { + "title": "Custom Store Properties", + "url": "guide/signals/signal-store/custom-store-properties" + }, { "title": "State Tracking", "url": "guide/signals/signal-store/state-tracking"