Skip to content

Commit

Permalink
feat(signals): add patchState function and remove $update method (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
markostanimirovic authored Sep 7, 2023
1 parent eeadd3f commit f2514ba
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 188 deletions.
77 changes: 77 additions & 0 deletions modules/signals/spec/patch-state.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { patchState, signalState } from '../src';

describe('patchState', () => {
const initialState = {
user: {
firstName: 'John',
lastName: 'Smith',
},
foo: 'bar',
numbers: [1, 2, 3],
ngrx: 'signals',
};

it('patches state via partial state object', () => {
const state = signalState(initialState);

patchState(state, {
user: { firstName: 'Johannes', lastName: 'Schmidt' },
foo: 'baz',
});

expect(state()).toEqual({
...initialState,
user: { firstName: 'Johannes', lastName: 'Schmidt' },
foo: 'baz',
});
});

it('patches state via updater function', () => {
const state = signalState(initialState);

patchState(state, (state) => ({
numbers: [...state.numbers, 4],
ngrx: 'rocks',
}));

expect(state()).toEqual({
...initialState,
numbers: [1, 2, 3, 4],
ngrx: 'rocks',
});
});

it('patches state via sequence of partial state objects and updater functions', () => {
const state = signalState(initialState);

patchState(
state,
{ user: { firstName: 'Johannes', lastName: 'Schmidt' } },
(state) => ({ numbers: [...state.numbers, 4], foo: 'baz' }),
(state) => ({ user: { ...state.user, firstName: 'Jovan' } }),
{ foo: 'foo' }
);

expect(state()).toEqual({
...initialState,
user: { firstName: 'Jovan', lastName: 'Schmidt' },
foo: 'foo',
numbers: [1, 2, 3, 4],
});
});

it('patches state immutably', () => {
const state = signalState(initialState);

patchState(state, {
foo: 'bar',
numbers: [3, 2, 1],
ngrx: 'rocks',
});

expect(state.user()).toBe(initialState.user);
expect(state.foo()).toBe(initialState.foo);
expect(state.numbers()).not.toBe(initialState.numbers);
expect(state.ngrx()).not.toBe(initialState.ngrx);
});
});
219 changes: 76 additions & 143 deletions modules/signals/spec/signal-state.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { effect, isSignal } from '@angular/core';
import { signalState } from '../src';
import { patchState, signalState } from '../src';
import { testEffects } from './helpers';

describe('signalState', () => {
Expand All @@ -13,167 +13,100 @@ describe('signalState', () => {
ngrx: 'signals',
};

describe('$update', () => {
it('updates state via partial state object', () => {
const state = signalState(initialState);
it('creates signals for nested state slices', () => {
const state = signalState(initialState);

state.$update({
user: { firstName: 'Johannes', lastName: 'Schmidt' },
foo: 'baz',
});
expect(state()).toBe(initialState);
expect(isSignal(state)).toBe(true);

expect(state()).toEqual({
...initialState,
user: { firstName: 'Johannes', lastName: 'Schmidt' },
foo: 'baz',
});
});
expect(state.user()).toBe(initialState.user);
expect(isSignal(state.user)).toBe(true);

it('updates state via updater function', () => {
const state = signalState(initialState);
expect(state.user.firstName()).toBe(initialState.user.firstName);
expect(isSignal(state.user.firstName)).toBe(true);

state.$update((state) => ({
numbers: [...state.numbers, 4],
ngrx: 'rocks',
}));
expect(state.foo()).toBe(initialState.foo);
expect(isSignal(state.foo)).toBe(true);

expect(state()).toEqual({
...initialState,
numbers: [1, 2, 3, 4],
ngrx: 'rocks',
});
});
expect(state.numbers()).toBe(initialState.numbers);
expect(isSignal(state.numbers)).toBe(true);

expect(state.ngrx()).toBe(initialState.ngrx);
expect(isSignal(state.ngrx)).toBe(true);
});

it('does not modify props that are not state slices', () => {
const state = signalState(initialState);
(state as any).x = 1;
(state.user as any).x = 2;
(state.user.firstName as any).x = 3;

expect((state as any).x).toBe(1);
expect((state.user as any).x).toBe(2);
expect((state.user.firstName as any).x).toBe(3);

it('updates state via sequence of partial state objects and updater functions', () => {
expect((state as any).y).toBe(undefined);
expect((state.user as any).y).toBe(undefined);
expect((state.user.firstName as any).y).toBe(undefined);
});

it(
'emits new values only for affected signals',
testEffects((tick) => {
const state = signalState(initialState);
let numbersEmitted = 0;
let userEmitted = 0;
let firstNameEmitted = 0;

state.$update(
{ user: { firstName: 'Johannes', lastName: 'Schmidt' } },
(state) => ({ numbers: [...state.numbers, 4], foo: 'baz' }),
(state) => ({ user: { ...state.user, firstName: 'Jovan' } }),
{ foo: 'foo' }
);

expect(state()).toEqual({
...initialState,
user: { firstName: 'Jovan', lastName: 'Schmidt' },
foo: 'foo',
numbers: [1, 2, 3, 4],
effect(() => {
state.numbers();
numbersEmitted++;
});
});

it('updates state immutably', () => {
const state = signalState(initialState);
effect(() => {
state.user();
userEmitted++;
});

state.$update({
foo: 'bar',
numbers: [3, 2, 1],
ngrx: 'rocks',
effect(() => {
state.user.firstName();
firstNameEmitted++;
});

expect(state.user()).toBe(initialState.user);
expect(state.foo()).toBe(initialState.foo);
expect(state.numbers()).not.toBe(initialState.numbers);
expect(state.ngrx()).not.toBe(initialState.ngrx);
});
});
expect(numbersEmitted).toBe(0);
expect(userEmitted).toBe(0);
expect(firstNameEmitted).toBe(0);

describe('nested signals', () => {
it('creates signals for nested state slices', () => {
const state = signalState(initialState);
tick();

expect(state()).toBe(initialState);
expect(isSignal(state)).toBe(true);
expect(numbersEmitted).toBe(1);
expect(userEmitted).toBe(1);
expect(firstNameEmitted).toBe(1);

expect(state.user()).toBe(initialState.user);
expect(isSignal(state.user)).toBe(true);
patchState(state, { numbers: [1, 2, 3] });
tick();

expect(state.user.firstName()).toBe(initialState.user.firstName);
expect(isSignal(state.user.firstName)).toBe(true);
expect(numbersEmitted).toBe(2);
expect(userEmitted).toBe(1);
expect(firstNameEmitted).toBe(1);

expect(state.foo()).toBe(initialState.foo);
expect(isSignal(state.foo)).toBe(true);
patchState(state, (state) => ({
user: { ...state.user, lastName: 'Schmidt' },
}));
tick();

expect(state.numbers()).toBe(initialState.numbers);
expect(isSignal(state.numbers)).toBe(true);
expect(numbersEmitted).toBe(2);
expect(userEmitted).toBe(2);
expect(firstNameEmitted).toBe(1);

expect(state.ngrx()).toBe(initialState.ngrx);
expect(isSignal(state.ngrx)).toBe(true);
});
patchState(state, (state) => ({
user: { ...state.user, firstName: 'Johannes' },
}));
tick();

it('does not modify props that are not state slices', () => {
const state = signalState(initialState);
(state as any).x = 1;
(state.user as any).x = 2;
(state.user.firstName as any).x = 3;

expect((state as any).x).toBe(1);
expect((state.user as any).x).toBe(2);
expect((state.user.firstName as any).x).toBe(3);

expect((state as any).y).toBe(undefined);
expect((state.user as any).y).toBe(undefined);
expect((state.user.firstName as any).y).toBe(undefined);
});

it(
'emits new values only for affected signals',
testEffects((tick) => {
const state = signalState(initialState);
let numbersEmitted = 0;
let userEmitted = 0;
let firstNameEmitted = 0;

effect(() => {
state.numbers();
numbersEmitted++;
});

effect(() => {
state.user();
userEmitted++;
});

effect(() => {
state.user.firstName();
firstNameEmitted++;
});

expect(numbersEmitted).toBe(0);
expect(userEmitted).toBe(0);
expect(firstNameEmitted).toBe(0);

tick();

expect(numbersEmitted).toBe(1);
expect(userEmitted).toBe(1);
expect(firstNameEmitted).toBe(1);

state.$update({ numbers: [1, 2, 3] });
tick();

expect(numbersEmitted).toBe(2);
expect(userEmitted).toBe(1);
expect(firstNameEmitted).toBe(1);

state.$update((state) => ({
user: { ...state.user, lastName: 'Schmidt' },
}));
tick();

expect(numbersEmitted).toBe(2);
expect(userEmitted).toBe(2);
expect(firstNameEmitted).toBe(1);

state.$update((state) => ({
user: { ...state.user, firstName: 'Johannes' },
}));
tick();

expect(numbersEmitted).toBe(2);
expect(userEmitted).toBe(3);
expect(firstNameEmitted).toBe(2);
})
);
});
expect(numbersEmitted).toBe(2);
expect(userEmitted).toBe(3);
expect(firstNameEmitted).toBe(2);
})
);
});
2 changes: 1 addition & 1 deletion modules/signals/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { PartialStateUpdater, patchState } from './patch-state';
export { selectSignal } from './select-signal';
export { signalState } from './signal-state';
export { SignalStateUpdater } from './signal-state-models';
20 changes: 20 additions & 0 deletions modules/signals/src/patch-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { SIGNAL_STATE_META_KEY, SignalStateMeta } from './signal-state';

export type PartialStateUpdater<State extends Record<string, unknown>> =
| Partial<State>
| ((state: State) => Partial<State>);

export function patchState<State extends Record<string, unknown>>(
signalState: SignalStateMeta<State>,
...updaters: PartialStateUpdater<State>[]
): void {
signalState[SIGNAL_STATE_META_KEY].update((currentState) =>
updaters.reduce(
(nextState: State, updater) => ({
...nextState,
...(typeof updater === 'function' ? updater(nextState) : updater),
}),
currentState
)
);
}
21 changes: 0 additions & 21 deletions modules/signals/src/signal-state-models.ts

This file was deleted.

Loading

0 comments on commit f2514ba

Please sign in to comment.