From 887e074f9cf1558854fb352870953489ebb6eff3 Mon Sep 17 00:00:00 2001 From: markostanimirovic Date: Tue, 17 Nov 2020 00:33:56 +0100 Subject: [PATCH 1/2] feat(component-store): add patchState method --- .../spec/component-store.spec.ts | 69 +++++++++++++++++++ .../component-store/src/component-store.ts | 23 +++++++ 2 files changed, 92 insertions(+) diff --git a/modules/component-store/spec/component-store.spec.ts b/modules/component-store/spec/component-store.spec.ts index 1f781534ba..e07e0c12ac 100644 --- a/modules/component-store/spec/component-store.spec.ts +++ b/modules/component-store/spec/component-store.spec.ts @@ -83,6 +83,36 @@ describe('Component Store', () => { }) ); + it('throws an Error when patchState with an object is called before initialization', () => { + const componentStore = new ComponentStore(); + + expect(() => { + componentStore.patchState({ foo: 'bar' }); + }).toThrow( + new Error( + 'ComponentStore has not been initialized yet. ' + + 'Please make sure it is initialized before updating/getting.' + ) + ); + }); + + it( + 'throws an Error when patchState with a function/callback is called' + + ' before initialization', + () => { + const componentStore = new ComponentStore(); + + expect(() => { + componentStore.patchState(() => ({ foo: 'bar' })); + }).toThrow( + new Error( + 'ComponentStore has not been initialized yet. ' + + 'Please make sure it is initialized before updating/getting.' + ) + ); + } + ); + it( 'throws an Error when updater is called before initialization', marbles((m) => { @@ -511,6 +541,45 @@ describe('Component Store', () => { ); }); + describe('patches the state', () => { + interface State { + value1: string; + value2: { foo: string }; + } + const INIT_STATE: State = { value1: 'value1', value2: { foo: 'bar' } }; + let componentStore: ComponentStore; + + beforeEach(() => { + componentStore = new ComponentStore(INIT_STATE); + }); + + it( + 'with a specific value', + marbles((m) => { + componentStore.patchState({ value1: 'val1' }); + + m.expect(componentStore.state$).toBeObservable( + m.hot('s', { + s: { ...INIT_STATE, value1: 'val1' }, + }) + ); + }) + ); + + it( + 'with a value based on the previous state', + marbles((m) => { + componentStore.patchState(() => ({ value2: { foo: 'fooBar' } })); + + m.expect(componentStore.state$).toBeObservable( + m.hot('s', { + s: { ...INIT_STATE, value2: { foo: 'fooBar' } }, + }) + ); + }) + ); + }); + describe('selector', () => { interface State { value: string; diff --git a/modules/component-store/src/component-store.ts b/modules/component-store/src/component-store.ts index f968b8f016..3aafb1757f 100644 --- a/modules/component-store/src/component-store.ts +++ b/modules/component-store/src/component-store.ts @@ -156,6 +156,29 @@ export class ComponentStore implements OnDestroy { } } + /** + * Patches the state with provided partial state. + * + * @param partialStateOrUpdaterFn a partial state or a partial updater + * function that accepts the state and returns the partial state. + * @throws Error if the state is not initialized. + */ + patchState( + partialStateOrUpdaterFn: Partial | ((state: T) => Partial) + ): void { + if (!this.isInitialized) { + throw new Error(this.notInitializedErrorMessage); + } + + const updaterFn = (state: T) => ({ + ...state, + ...(typeof partialStateOrUpdaterFn === 'function' + ? partialStateOrUpdaterFn(state) + : partialStateOrUpdaterFn), + }); + this.updater(updaterFn)(); + } + protected get(): T; protected get(projector: (s: T) => R): R; protected get(projector?: (s: T) => R): R | T { From 2c2fc556a6ff2f6fac9fc3769dfdc3a5f7242154 Mon Sep 17 00:00:00 2001 From: markostanimirovic Date: Tue, 17 Nov 2020 17:32:43 +0100 Subject: [PATCH 2/2] use setState in patchState; remove isInitialized check; improve test case with callback --- .../spec/component-store.spec.ts | 6 ++++-- modules/component-store/src/component-store.ts | 18 +++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/modules/component-store/spec/component-store.spec.ts b/modules/component-store/spec/component-store.spec.ts index e07e0c12ac..b3903fdc7d 100644 --- a/modules/component-store/spec/component-store.spec.ts +++ b/modules/component-store/spec/component-store.spec.ts @@ -569,11 +569,13 @@ describe('Component Store', () => { it( 'with a value based on the previous state', marbles((m) => { - componentStore.patchState(() => ({ value2: { foo: 'fooBar' } })); + componentStore.patchState((state) => ({ + value2: { foo: `${state.value2.foo}2` }, + })); m.expect(componentStore.state$).toBeObservable( m.hot('s', { - s: { ...INIT_STATE, value2: { foo: 'fooBar' } }, + s: { ...INIT_STATE, value2: { foo: 'bar2' } }, }) ); }) diff --git a/modules/component-store/src/component-store.ts b/modules/component-store/src/component-store.ts index 3aafb1757f..6865cc9fc3 100644 --- a/modules/component-store/src/component-store.ts +++ b/modules/component-store/src/component-store.ts @@ -166,17 +166,17 @@ export class ComponentStore implements OnDestroy { patchState( partialStateOrUpdaterFn: Partial | ((state: T) => Partial) ): void { - if (!this.isInitialized) { - throw new Error(this.notInitializedErrorMessage); - } + this.setState((state) => { + const patchedState = + typeof partialStateOrUpdaterFn === 'function' + ? partialStateOrUpdaterFn(state) + : partialStateOrUpdaterFn; - const updaterFn = (state: T) => ({ - ...state, - ...(typeof partialStateOrUpdaterFn === 'function' - ? partialStateOrUpdaterFn(state) - : partialStateOrUpdaterFn), + return { + ...state, + ...patchedState, + }; }); - this.updater(updaterFn)(); } protected get(): T;