Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(signals): add withProps base feature #4607

Merged
merged 4 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions modules/signals/entities/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
6 changes: 3 additions & 3 deletions modules/signals/entities/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ export type NamedEntityState<Entity, Collection extends string> = {
[K in keyof EntityState<Entity> as `${Collection}${Capitalize<K>}`]: EntityState<Entity>[K];
};

export type EntityComputed<Entity> = {
export type EntityProps<Entity> = {
entities: Signal<Entity[]>;
};

export type NamedEntityComputed<Entity, Collection extends string> = {
[K in keyof EntityComputed<Entity> as `${Collection}${Capitalize<K>}`]: EntityComputed<Entity>[K];
export type NamedEntityProps<Entity, Collection extends string> = {
[K in keyof EntityProps<Entity> as `${Collection}${Capitalize<K>}`]: EntityProps<Entity>[K];
};

export type SelectEntityId<Entity> = (entity: Entity) => EntityId;
Expand Down
10 changes: 5 additions & 5 deletions modules/signals/entities/src/with-entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import {
withState,
} from '@ngrx/signals';
import {
EntityComputed,
EntityProps,
EntityId,
EntityMap,
EntityState,
NamedEntityComputed,
NamedEntityProps,
NamedEntityState,
} from './models';
import { getEntityStateKeys } from './helpers';
Expand All @@ -20,7 +20,7 @@ export function withEntities<Entity>(): SignalStoreFeature<
EmptyFeatureResult,
{
state: EntityState<Entity>;
computed: EntityComputed<Entity>;
props: EntityProps<Entity>;
methods: {};
}
>;
Expand All @@ -31,7 +31,7 @@ export function withEntities<Entity, Collection extends string>(config: {
EmptyFeatureResult,
{
state: NamedEntityState<Entity, Collection>;
computed: NamedEntityComputed<Entity, Collection>;
props: NamedEntityProps<Entity, Collection>;
methods: {};
}
>;
Expand All @@ -41,7 +41,7 @@ export function withEntities<Entity>(config: {
EmptyFeatureResult,
{
state: EntityState<Entity>;
computed: EntityComputed<Entity>;
props: EntityProps<Entity>;
methods: {};
}
>;
Expand Down
2 changes: 1 addition & 1 deletion modules/signals/spec/signal-store-feature.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('signalStoreFeature', () => {
return signalStoreFeature(
{
state: type<{ foo: string }>(),
computed: type<{ s: Signal<number> }>(),
props: type<{ s: Signal<number> }>(),
},
withState({ foo1: 1 }),
withState({ foo2: 2 })
Expand Down
53 changes: 51 additions & 2 deletions modules/signals/spec/signal-store.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
withComputed,
withHooks,
withMethods,
withProps,
withState,
} from '../src';
import { STATE_SOURCE } from '../src/state-source';
Expand Down Expand Up @@ -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() };
})
Expand All @@ -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');
});

Expand All @@ -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' };
})
Expand All @@ -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');
});

Expand Down Expand Up @@ -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';
},
})
Expand All @@ -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';
},
})
Expand Down
10 changes: 5 additions & 5 deletions modules/signals/spec/types/signal-store.types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,7 @@ describe('signalStore', () => {
return signalStoreFeature(
{
state: type<{ q1: string }>(),
computed: type<{ sig: Signal<boolean> }>(),
props: type<{ sig: Signal<boolean> }>(),
},
withState({ y: initialY }),
withComputed(() => ({ sigY: computed(() => 'sigY') })),
Expand Down Expand Up @@ -932,7 +932,7 @@ describe('signalStore', () => {
${baseSnippet}

const feature = signalStoreFeature(
{ computed: type<{ sig: Signal<boolean> }>() },
{ props: type<{ sig: Signal<boolean> }>() },
withX(),
withState({ q1: 'q1' }),
withY(),
Expand Down Expand Up @@ -976,7 +976,7 @@ describe('signalStore', () => {
${baseSnippet}

const feature = signalStoreFeature(
{ computed: type<{ sig: Signal<string> }>() },
{ props: type<{ sig: Signal<string> }>() },
withX(),
withState({ q1: 'q1' }),
withY(),
Expand Down Expand Up @@ -1007,7 +1007,7 @@ describe('signalStore', () => {

const feature = signalStoreFeature(
{
computed: type<{ sig: Signal<boolean> }>(),
props: type<{ sig: Signal<boolean> }>(),
methods: type<{ f(): void; g(arg: string): string; }>(),
},
withX(),
Expand Down Expand Up @@ -1046,7 +1046,7 @@ describe('signalStore', () => {
entities: Entity[];
selectedEntity: Entity | null;
};
computed: {
props: {
selectedEntity2: Signal<Entity | undefined>;
};
methods: {
Expand Down
8 changes: 4 additions & 4 deletions modules/signals/spec/with-computed.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
50 changes: 50 additions & 0 deletions modules/signals/spec/with-props.spec.ts
Original file line number Diff line number Diff line change
@@ -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'
);
});
});
1 change: 1 addition & 0 deletions modules/signals/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
2 changes: 1 addition & 1 deletion modules/signals/src/signal-store-assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function assertUniqueStoreMembers(

const storeMembers = {
...store.stateSignals,
...store.computedSignals,
...store.props,
...store.methods,
};
const overriddenKeys = Object.keys(storeMembers).filter((memberKey) =>
Expand Down
2 changes: 1 addition & 1 deletion modules/signals/src/signal-store-feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Prettify } from './ts-helpers';

type PrettifyFeatureResult<Result extends SignalStoreFeatureResult> = Prettify<{
state: Prettify<Result['state']>;
computed: Prettify<Result['computed']>;
props: Prettify<Result['props']>;
methods: Prettify<Result['methods']>;
}>;

Expand Down
12 changes: 6 additions & 6 deletions modules/signals/src/signal-store-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<State>;
computedSignals: ComputedSignals;
props: Props;
methods: Methods;
hooks: SignalStoreHooks;
} & WritableStateSource<State>;

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<Input['state'], Input['computed'], Input['methods']>
) => InnerSignalStore<Output['state'], Output['computed'], Output['methods']>;
store: InnerSignalStore<Input['state'], Input['props'], Input['methods']>
) => InnerSignalStore<Output['state'], Output['props'], Output['methods']>;
Loading