NGXS Provides some default state operators but the real power is in the pattern it offers.
Please share your custom state operators here, someone else may find them useful.
If they are general purpose, logical and popular then they might even get added to the default operators in the lib!
updateItem but not for insertItem)PS. If you have no clue what this is about read this article.
(As an example)
Sometimes we need to modify the state only if some condition is true. This operator will help you out here!
setState(
iif((state) => state.message === 'Hello',
patch<MyState>({ waving: true }),
patch<MyState>({ waving: false })
)
);
Source: iif.ts
Tests: iif.spec.ts
(... or just a gist link here if you have a gist with the source and tests 馃殌 )
Hi! @markwhitfeld This is my operator proposal.
I took your advice of what you answered me: https://twitter.com/MarkWhitfeld/status/1106659719068950530
https://twitter.com/MarkWhitfeld/status/1106656004320755713
Use Case:
Sometimes we need to modify multiple elements of an array only if some condition is true or a array of indices. This operator will help you out here!
Examples:
1-Complete all Todos:

2-Complete only if text is 'ipsum':

3-Complete only idices 0 and 2:

Source: https://github.com/mailok/todo-ngxs/blob/master/src/app/store/operators/update-many-items.ts
Test: https://github.com/mailok/todo-ngxs/blob/master/src/app/store/operators/update-many-items.spec.ts
Utils: https://github.com/mailok/todo-ngxs/blob/master/src/app/store/operators/utils.ts
and another proposal
Use Case:
Sometimes we need to remove multiple elements of an array only if some condition is true or a array of indices. This operator will help you out here!
Examples:
1-Remove all Todos complete:

2-Remove index 1 and 2:

Source: https://github.com/mailok/todo-ngxs/blob/master/src/app/store/operators/remove-many-items.ts
In our app, we have a lot of state that looks like this :
{
folder1: [{name: 'file1'}, {name: 'file2'}, {name: 'file3'}],
folder2: [{name: 'file5'}, {name: 'file4'}],
}
And the key are dynamic and can be added at runtime.
So we created an operator patchKey to work on a key, and create it if not created.
It can be used to set the value :
setState(
patchKey('folder1', [{"name": "file5"}]),
);
Or to apply a state operator to the value :
setState(
patchKey('folder1', updateItem(item => item.name === 'file2', patch({ isOpen: true }))),
);
Source: patchKey.ts && patchKey.spec.ts
When you have in your store an array where you could have duplication, like a state of selected items :
{
selectedId: [1, 2, 3, 2],
}
With this operator, you can filter out dupliacates. It can be use in a standalone way or with composition.
Filter out duplicates :
setState(
patch({ selectedId: uniqArray() }),
);
Or be composable :
setState(
patch({
selectedId: compose(
append([3, 1]),
uniqArray(),
),
}),
);
When you need to append items but skip those already in existing array. Allows to compare by any field if is object
setState(
patch({ items: appendUniq(newItems, i => i.id) })
)
Source: append-uniq
When you need to sort items before you store them. Allows to sort by any field of T
setState(
patch({ items: sortBy(i => i.id) })
)
@markwhitfeld can we expect some of this state-operators to become part of the library? any timeline for this? thanks!
@joaqcid PR's please)
@joaqcid For sure we're open to PRs. But also we expect those operators to be beneficial for the community. If I create an operator that traverses binary tree in my application that doesn't mean that it will be used by anyone else except me. Do you agree with me? (or maybe I'm wrong at some points)
@arturovt ok, yes agreed, though you never know when a binary-tree-traverse state-operator might become handy ;)
@markwhitfeld This issue is it relevant now?
Use Case:
In our app, we have a lot of state that looks like this :
{ folder1: [{name: 'file1'}, {name: 'file2'}, {name: 'file3'}], folder2: [{name: 'file5'}, {name: 'file4'}], }And the key are dynamic and can be added at runtime.
So we created an operator
patchKeyto work on a key, and create it if not created.Example:
It can be used to set the value :
setState( patchKey('folder1', [{"name": "file5"}]), );Or to apply a state operator to the value :
setState( patchKey('folder1', updateItem(item => item.name === 'file2', patch({ isOpen: true }))), );The Code:
Source: patchKey.ts && patchKey.spec.ts
@beaussan Your patchKey operator seems really useful but your gist link doesn't seem to be working.
@beaussan Your patchKey operator seems really useful but your gist link doesn't seems to be working.
Thank you! Just fixed the url :)
@beaussan Your patchKey operator seems really useful but your gist link doesn't seems to be working.
Thank you! Just fixed the url :)
@beaussan I'm sorry, I think you fixed a the uniqarray Gist but the patchkey one is still broken. Thanks!
@beyondsanity you are right, updated both in original comments
My team _just_ started getting into state operators. It's a super cool concept, and I am certainly looking for more operators my team can create/adopt. We've identified two commonly used state model patterns in our application. I refer to these as entities state and selected state. I know there is an @ngxs-labs/entity-state package, so please do not get that confused with this post. These operators are not intended to be used with that package.
Our entities style of state is simply a map of ids to things.
// helpers
export const getKey = <T>(entity: T, fn?: (e: T) => number | string) => (fn ? fn(entity) : (entity as any).id);
export function map<T>(entities: T[], getId?: (t: T) => number | string) {
return entities.reduce((acc, entity) => {
acc[getKey(entity, getId)] = entity;
return acc;
}, {});
}
```ts
// operators
export interface EntitiesStateModel
map: { [id: string]: T };
}
export function patchMap
const idPatchMap = patch(map(entities, getId));
return patch
}
export function setMap
return patch
map: map(entities, getId)
});
}
export function patchEntity
return (state: Readonly
...state,
map: {
...state.map,
// we have a custom implementation to deepMerge, but it does what it sounds like: https://www.npmjs.com/package/deepmerge
[id]: deepMerge(state.map[id], deepPartial)
}
});
}
export function setEntity
const idPatchMap = patch({ [id as any]: entity });
return patch
}
### Usage:
* `patchMap`
* Adds things to `map` keyed by an `id`
* ```setState(patchMap(brands));```
* ```setState(patchMap(brands, b => b.brandId));```
* `setMap`
* same as `patchMap` except it throws away the things in previous `map`
* `patchEntity`
* modifies a single thing in a map
* ```setState(patchEntity<Portfolio>(portfolioId, { mappedBrandIds }));```
* `setEntity`
* adds a single thing to map.
* ```setState(setEntity(mappedBrandIds, accountId));```
## Selected State Operators
Our `selected` style of state maintains a map of ids that are "selected". An `id` is selected iff it's `id` maps to true.
```ts
// operators
export interface SelectedStateModel {
ids: { [id: number]: boolean };
}
export function clear(): StateOperator<SelectedStateModel> {
return patch<SelectedStateModel>({
ids: {}
});
}
export function deselect(idsToDeselect: number[]): StateOperator<SelectedStateModel> {
return (state: Readonly<SelectedStateModel>) => {
const previous = state.ids;
const blacklist = idsToDeselect.reduce((acc, id) => {
acc[id] = id;
return acc;
}, {});
const ids = Object.keys(state.ids).reduce((acc, id) => {
if (id in blacklist) {
return acc;
}
acc[id] = previous[id];
return acc;
}, {});
return { ...state, ids };
};
}
export function toggle(idsToToggle: number[]): StateOperator<SelectedStateModel> {
return (state: Readonly<SelectedStateModel>) => {
const ids = idsToToggle.reduce(
(acc, id) => {
acc[id] = !acc[id];
return acc;
},
{ ...state.ids }
);
return { ...state, ids };
};
}
export function select(idsToSelect: number[]): StateOperator<SelectedStateModel> {
return (state: Readonly<SelectedStateModel>) => {
const ids = idsToSelect.reduce(
(acc, id) => {
acc[id] = true;
return acc;
},
{ ...state.ids }
);
return { ...state, ids };
};
}
clearidssetState(clear());deselectids providedsetState(deselect(payload.ids));toggleidssetState(toggle(payload.ids));selectids provided.setState(select(payload.ids));I want to insert or update an item of an array in the state, I can use iif combined with updateItem and insertItem
ctx.setState(
patch<FoodModel>({
foods: iif<Food[]>(
(foods) => foods.some((f) => f.id === foodId),
updateItem<Food>((f) => f.id === foodId, food),
insertItem(food)
),
})
)
To simplify this I can use upsertItem
ctx.setState(
patch<FoodModel>({
foods: upsertItem<Food>((f) => f.id === foodId, food),
})
)
Here is the
upsertItem operator:
import { StateOperator } from '@ngxs/store';
import { Predicate } from '@ngxs/store/operators/internals';
export function upsertItem<T>(
selector: number | Predicate<T>,
upsertValue: T
): StateOperator<RepairType<T>[]> {
return function insertItemOperator(
existing: Readonly<RepairType<T>[]>
): RepairType<T>[] {
let index = -1;
if (isPredicate(selector)) {
index = existing.findIndex(selector);
} else if (isNumber(selector)) {
index = selector;
}
if (invalidIndex(index)) {
// Insert Value
// Have to check explicitly for `null` and `undefined`
// because `value` can be `0`, thus `!value` will return `true`
if (isNil(upsertValue) && existing) {
return existing as RepairType<T>[];
}
// Property may be dynamic and might not existed before
if (!Array.isArray(existing)) {
return [upsertValue as RepairType<T>];
}
const clone = existing.slice();
clone.splice(0, 0, upsertValue as RepairType<T>);
return clone;
} else {
// Update Value
// If the value hasn't been mutated
// then we just return `existing` array
if (upsertValue === existing[index]) {
return existing as RepairType<T>[];
}
const clone = existing.slice();
clone[index] = upsertValue as RepairType<T>;
return clone;
}
};
}
export function isUndefined(value: any): value is undefined {
return typeof value === 'undefined';
}
export function isPredicate<T>(
value: Predicate<T> | boolean | number
): value is Predicate<T> {
return typeof value === 'function';
}
export function isNumber(value: any): value is number {
return typeof value === 'number';
}
export function invalidIndex(index: number): boolean {
return Number.isNaN(index) || index === -1;
}
export function isNil<T>(
value: T | null | undefined
): value is null | undefined {
return value === null || isUndefined(value);
}
export type RepairType<T> = T extends true
? boolean
: T extends false
? boolean
: T;
@marcjulian here are some basic tests for upsertItem
import { patch } from '@ngxs/store/operators';
import { upsertItem } from './upsert-item';
type Item = { id: string; name?: string };
type ItemsModel = { items: Item[] };
describe('upsertItem', () => {
it('inserts if not exists', () => {
// Arrange
const before: ItemsModel = { items: [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }] };
// Act
const newItem = { id: '0', name: 'joaq' };
const after = patch<ItemsModel>({
items: upsertItem<Item>((x) => x.id === newItem.id, newItem),
})(before);
// Assert
expect(after.items).toEqual([newItem, { id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }]);
});
it('updates if exists', () => {
// Arrange
const before: ItemsModel = { items: [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }] };
// Act
const updatedItem = { id: '3', name: 'joaq' };
const after = patch<ItemsModel>({
items: upsertItem<Item>((x) => x.id === updatedItem.id, updatedItem),
})(before);
// Assert
expect(after.items).toEqual([{ id: '1' }, { id: '2' }, updatedItem, { id: '4' }]);
});
});
@joaqcid wow that is awesome, thanks for writing tests for upsertItem.
@marcjulian An alternative to your implementation would be to combine the existing operators to create a new operator.
This version also handles a number of other cases including a null array as well as inserting into the specified index (if the selector was a number).
See the following code:
import { StateOperator } from '@ngxs/store';
import { Predicate } from '@ngxs/store/operators/internals';
import { compose, iif, insertItem, updateItem } from '@ngxs/store/operators';
export function upsertItem<T>(
selector: number | Predicate<T>,
upsertValue: T
): StateOperator<T[]> {
return compose<T[]>(
(foods) => <T[]>(foods || []),
iif<T[]>(
(foods) => Number(selector)=== selector,
iif<T[]>(
(foods) => selector < foods.length,
<StateOperator<T[]>>updateItem(selector, upsertValue),
<StateOperator<T[]>>insertItem(upsertValue, <number>selector)
),
iif<T[]>(
(foods) => foods.some(<any>selector),
<StateOperator<T[]>>updateItem(selector, upsertValue),
<StateOperator<T[]>>insertItem(upsertValue)
)
)
);
}
What do you think?
@markwhitfeld Awesome! That is what I was looking for, I think its much better to take advantage of already existing operators such as insertItem and updateItem. I tried something like this before with just one iif but couldn't get it to work. I didn't use compose, I will use it next time!
Thanks improving my upsertItem operator. Could this operator be added to the existing operators? 馃槃
@marcjulian All of the operators here are candidates but to be eligible we would need a full suite of tests, including all edge cases. Could you add these to your original comment?
Hi,
I met a problem with your amazing operator @mailok .
My feature should set the options of an object array.
something like:
[{ id: 1, checked: false }, { id: 2, checked: false }, { id: 3, checked: false }]
And what i want to do is checked few of them by filtring by them id
if you try to do
`
const target = [1, 3]
updateManyItems(
item => target.includes(item.id),
patch({ checked: true })
)
`
you should failed.
The first error spotted is about the invalidIndexs function.
you couldn't do
if (!existing[indices[i]] || !isNumber(indices[i]) || invalidIndex(indices[i])) {
return true;
}
// indices = [0, 2]
// existing = [{ id: 1, checked: false }, { id: 2, checked: false }, { id: 3, checked: false }]
and indices[2] is undefined, and of course existing[undefined] doesn't exist
the same probleme is present in update-many-items.ts to
const clone = [...existing];
const keys = Object.keys(values);
for (const i in keys) {
clone[keys[i]] = values[keys[i]];
}
Thanks!
I'm trying to write a custom operator and have the following. I was following the logic from the existing operators and ended up using the same internal utils.
I know I could write these in a different way that doesn't use any of the provided helpers..but I'd rather just use what's already available. As you might guess, I'm getting Module not found: Error: Can't resolve '@ngxs/store/operators/utils' in ....
It seems like these helpers aren't supposed to be available for custom operators which seems counter intuitive (i.e. we have these examples that use these helpers, but we're not allowed to use them ourselves). I wasn't able to find a module for angular to use these. Am I just missing something or am I really not supposed to use these functions/type?
import { StateOperator } from '@ngxs/store';
import { Predicate } from '@ngxs/store/operators/internals';
import { invalidIndex, isNumber, isPredicate, RepairType } from '@ngxs/store/operators/utils';
/**
* @param fromPositionSelector - Index (or selector to find item) to move from
* @param [beforePositionSelector] - Index (or selector to find item) to move to
*/
export function moveItem<T>(fromPositionSelector: number | Predicate<T>, beforePositionSelector?: number | Predicate<T>)
: StateOperator<RepairType<T>[]> {
return function moveItemOperator(existing: Readonly<RepairType<T>[]>): RepairType<T>[] {
let fromIndex = -1;
if (isPredicate(fromPositionSelector)) {
fromIndex = existing.findIndex(fromPositionSelector);
} else if (isNumber(fromPositionSelector)) {
fromIndex = fromPositionSelector;
}
if (invalidIndex(fromIndex)) {
return existing as RepairType<T>[];
}
let toIndex;
if (isPredicate(beforePositionSelector)) {
toIndex = existing.findIndex(beforePositionSelector);
} else if (isNumber(beforePositionSelector)) {
toIndex = beforePositionSelector;
} else {
toIndex = 0;
}
if (fromIndex < toIndex) {
toIndex--;
}
const item = existing[fromIndex];
const clone = existing.slice();
clone.splice(fromIndex, 1);
clone.splice(toIndex, 0, item as RepairType<T>);
return clone;
};
}
Most helpful comment
Hi! @markwhitfeld This is my operator proposal.
I took your advice of what you answered me: https://twitter.com/MarkWhitfeld/status/1106659719068950530
https://twitter.com/MarkWhitfeld/status/1106656004320755713
Use Case:
Sometimes we need to modify multiple elements of an array only if some condition is true or a array of indices. This operator will help you out here!
Examples:
1-Complete all Todos:
2-Complete only if text is 'ipsum':
3-Complete only idices 0 and 2:
Source: https://github.com/mailok/todo-ngxs/blob/master/src/app/store/operators/update-many-items.ts
Test: https://github.com/mailok/todo-ngxs/blob/master/src/app/store/operators/update-many-items.spec.ts
Utils: https://github.com/mailok/todo-ngxs/blob/master/src/app/store/operators/utils.ts