Skip to content

Commit ecedadb

Browse files
feat(component-store): add patchState method (#2788)
* feat(component-store): add patchState method * use setState in patchState; remove isInitialized check; improve test case with callback
1 parent dcf338e commit ecedadb

File tree

2 files changed

+94
-0
lines changed

2 files changed

+94
-0
lines changed

modules/component-store/spec/component-store.spec.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,36 @@ describe('Component Store', () => {
8383
})
8484
);
8585

86+
it('throws an Error when patchState with an object is called before initialization', () => {
87+
const componentStore = new ComponentStore();
88+
89+
expect(() => {
90+
componentStore.patchState({ foo: 'bar' });
91+
}).toThrow(
92+
new Error(
93+
'ComponentStore has not been initialized yet. ' +
94+
'Please make sure it is initialized before updating/getting.'
95+
)
96+
);
97+
});
98+
99+
it(
100+
'throws an Error when patchState with a function/callback is called' +
101+
' before initialization',
102+
() => {
103+
const componentStore = new ComponentStore();
104+
105+
expect(() => {
106+
componentStore.patchState(() => ({ foo: 'bar' }));
107+
}).toThrow(
108+
new Error(
109+
'ComponentStore has not been initialized yet. ' +
110+
'Please make sure it is initialized before updating/getting.'
111+
)
112+
);
113+
}
114+
);
115+
86116
it(
87117
'throws an Error when updater is called before initialization',
88118
marbles((m) => {
@@ -511,6 +541,47 @@ describe('Component Store', () => {
511541
);
512542
});
513543

544+
describe('patches the state', () => {
545+
interface State {
546+
value1: string;
547+
value2: { foo: string };
548+
}
549+
const INIT_STATE: State = { value1: 'value1', value2: { foo: 'bar' } };
550+
let componentStore: ComponentStore<State>;
551+
552+
beforeEach(() => {
553+
componentStore = new ComponentStore(INIT_STATE);
554+
});
555+
556+
it(
557+
'with a specific value',
558+
marbles((m) => {
559+
componentStore.patchState({ value1: 'val1' });
560+
561+
m.expect(componentStore.state$).toBeObservable(
562+
m.hot('s', {
563+
s: { ...INIT_STATE, value1: 'val1' },
564+
})
565+
);
566+
})
567+
);
568+
569+
it(
570+
'with a value based on the previous state',
571+
marbles((m) => {
572+
componentStore.patchState((state) => ({
573+
value2: { foo: `${state.value2.foo}2` },
574+
}));
575+
576+
m.expect(componentStore.state$).toBeObservable(
577+
m.hot('s', {
578+
s: { ...INIT_STATE, value2: { foo: 'bar2' } },
579+
})
580+
);
581+
})
582+
);
583+
});
584+
514585
describe('selector', () => {
515586
interface State {
516587
value: string;

modules/component-store/src/component-store.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,29 @@ export class ComponentStore<T extends object> implements OnDestroy {
156156
}
157157
}
158158

159+
/**
160+
* Patches the state with provided partial state.
161+
*
162+
* @param partialStateOrUpdaterFn a partial state or a partial updater
163+
* function that accepts the state and returns the partial state.
164+
* @throws Error if the state is not initialized.
165+
*/
166+
patchState(
167+
partialStateOrUpdaterFn: Partial<T> | ((state: T) => Partial<T>)
168+
): void {
169+
this.setState((state) => {
170+
const patchedState =
171+
typeof partialStateOrUpdaterFn === 'function'
172+
? partialStateOrUpdaterFn(state)
173+
: partialStateOrUpdaterFn;
174+
175+
return {
176+
...state,
177+
...patchedState,
178+
};
179+
});
180+
}
181+
159182
protected get(): T;
160183
protected get<R>(projector: (s: T) => R): R;
161184
protected get<R>(projector?: (s: T) => R): R | T {

0 commit comments

Comments
 (0)