Skip to content

Commit

Permalink
feat(signals): add patch function and remove $update method
Browse files Browse the repository at this point in the history
  • Loading branch information
markostanimirovic committed Sep 4, 2023
1 parent eeadd3f commit 11c517d
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 53 deletions.
19 changes: 10 additions & 9 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 { patch, signalState } from '../src';
import { testEffects } from './helpers';

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

describe('$update', () => {
describe('patch', () => {
it('updates state via partial state object', () => {
const state = signalState(initialState);

state.$update({
patch(state, {
user: { firstName: 'Johannes', lastName: 'Schmidt' },
foo: 'baz',
});
Expand All @@ -32,7 +32,7 @@ describe('signalState', () => {
it('updates state via updater function', () => {
const state = signalState(initialState);

state.$update((state) => ({
patch(state, (state) => ({
numbers: [...state.numbers, 4],
ngrx: 'rocks',
}));
Expand All @@ -47,7 +47,8 @@ describe('signalState', () => {
it('updates state via sequence of partial state objects and updater functions', () => {
const state = signalState(initialState);

state.$update(
patch(
state,
{ user: { firstName: 'Johannes', lastName: 'Schmidt' } },
(state) => ({ numbers: [...state.numbers, 4], foo: 'baz' }),
(state) => ({ user: { ...state.user, firstName: 'Jovan' } }),
Expand All @@ -65,7 +66,7 @@ describe('signalState', () => {
it('updates state immutably', () => {
const state = signalState(initialState);

state.$update({
patch(state, {
foo: 'bar',
numbers: [3, 2, 1],
ngrx: 'rocks',
Expand Down Expand Up @@ -149,14 +150,14 @@ describe('signalState', () => {
expect(userEmitted).toBe(1);
expect(firstNameEmitted).toBe(1);

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

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

state.$update((state) => ({
patch(state, (state) => ({
user: { ...state.user, lastName: 'Schmidt' },
}));
tick();
Expand All @@ -165,7 +166,7 @@ describe('signalState', () => {
expect(userEmitted).toBe(2);
expect(firstNameEmitted).toBe(1);

state.$update((state) => ({
patch(state, (state) => ({
user: { ...state.user, firstName: 'Johannes' },
}));
tick();
Expand Down
3 changes: 1 addition & 2 deletions modules/signals/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export { selectSignal } from './select-signal';
export { signalState } from './signal-state';
export { SignalStateUpdater } from './signal-state-models';
export { patch, signalState, SignalStateUpdater } from './signal-state';
21 changes: 0 additions & 21 deletions modules/signals/src/signal-state-models.ts

This file was deleted.

60 changes: 39 additions & 21 deletions modules/signals/src/signal-state.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,52 @@
import { signal, WritableSignal } from '@angular/core';
import { toDeepSignal } from './deep-signal';
import { DeepSignal, toDeepSignal } from './deep-signal';
import { defaultEqual } from './select-signal';
import {
NotAllowedStateCheck,
SignalState,
SignalStateUpdate,
} from './signal-state-models';

type SignalState<State extends Record<string, unknown>> = DeepSignal<State> &
SignalStateMeta<State>;

const SIGNAL_STATE_META_KEY = Symbol('SIGNAL_STATE_META_KEY');

type SignalStateMeta<State extends Record<string, unknown>> = {
[SIGNAL_STATE_META_KEY]: WritableSignal<State>;
};

/**
* Signal state cannot contain optional properties.
*/
type NotAllowedStateCheck<State> = State extends Required<State>
? State extends Record<string, unknown>
? { [K in keyof State]: State[K] & NotAllowedStateCheck<State[K]> }
: unknown
: never;

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

export function signalState<State extends Record<string, unknown>>(
initialState: State & NotAllowedStateCheck<State>
): SignalState<State> {
const stateSignal = signal(initialState as State, { equal: defaultEqual });
const deepSignal = toDeepSignal(stateSignal.asReadonly());
(deepSignal as SignalState<State>).$update =
signalStateUpdateFactory(stateSignal);
Object.defineProperty(deepSignal, SIGNAL_STATE_META_KEY, {
value: stateSignal,
});

return deepSignal as SignalState<State>;
}

export function signalStateUpdateFactory<State extends Record<string, unknown>>(
stateSignal: WritableSignal<State>
): SignalStateUpdate<State>['$update'] {
return (...updaters) =>
stateSignal.update((state) =>
updaters.reduce(
(currentState: State, updater) => ({
...currentState,
...(typeof updater === 'function' ? updater(currentState) : updater),
}),
state
)
);
export function patch<State extends Record<string, unknown>>(
signalState: SignalStateMeta<State>,
...updaters: SignalStateUpdater<State>[]
): void {
signalState[SIGNAL_STATE_META_KEY].update((currentState) =>
updaters.reduce(
(nextState: State, updater) => ({
...nextState,
...(typeof updater === 'function' ? updater(nextState) : updater),
}),
currentState
)
);
}

0 comments on commit 11c517d

Please sign in to comment.