Skip to content

Commit

Permalink
feat(component-store): add patchState method (#2788)
Browse files Browse the repository at this point in the history
* feat(component-store): add patchState method

* use setState in patchState; remove isInitialized check; improve test case with callback
  • Loading branch information
markostanimirovic committed Nov 23, 2020
1 parent dcf338e commit ecedadb
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 0 deletions.
71 changes: 71 additions & 0 deletions modules/component-store/spec/component-store.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -511,6 +541,47 @@ 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<State>;

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((state) => ({
value2: { foo: `${state.value2.foo}2` },
}));

m.expect(componentStore.state$).toBeObservable(
m.hot('s', {
s: { ...INIT_STATE, value2: { foo: 'bar2' } },
})
);
})
);
});

describe('selector', () => {
interface State {
value: string;
Expand Down
23 changes: 23 additions & 0 deletions modules/component-store/src/component-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,29 @@ export class ComponentStore<T extends object> 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<T> | ((state: T) => Partial<T>)
): void {
this.setState((state) => {
const patchedState =
typeof partialStateOrUpdaterFn === 'function'
? partialStateOrUpdaterFn(state)
: partialStateOrUpdaterFn;

return {
...state,
...patchedState,
};
});
}

protected get(): T;
protected get<R>(projector: (s: T) => R): R;
protected get<R>(projector?: (s: T) => R): R | T {
Expand Down

0 comments on commit ecedadb

Please sign in to comment.