Store: 馃摎 [DOCS]: Share your custom State Operators here!

Created on 19 Mar 2019  路  25Comments  路  Source: ngxs/store

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!

RULES to keep this thread clean:

  • Please only submit one comment per operator (pointless comments will be deleted).
  • Vote for the operators using the standard emojis (please don't add needless comments)
  • In your comment please provide the following:

    • A description of the use case for the operator

    • A usage example of the operator as code

    • A link to a github gist with your operator _and preferably tests too_!

    • Any other useful information

_PS. What makes a good operator?_

  • It only returns a new state if changes are needed. For Example:

    • if there is a predicate and it is not matched, then just return the original state

    • if the new value equals the old value, then just return the original state

  • It is composable (you can pass other operators in, if applicable)

    • if a parameter requires a value then provide the option of passing an operator or a value (where it makes sense - ie. for updateItem but not for insertItem)

  • These are good examples:

PS. If you have no clue what this is about read this article.

discussion not an issue

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:

removeAll

removeAll

2-Complete only if text is 'ipsum':

removeIpsum

removeIpsum

3-Complete only idices 0 and 2:

removeindices

removeindices

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

All 25 comments

(As an example)

Use Case:

Sometimes we need to modify the state only if some condition is true. This operator will help you out here!

Example:

setState(
  iif((state) => state.message === 'Hello', 
    patch<MyState>({ waving: true }),
    patch<MyState>({ waving: false })
  )
);

The Code:

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:

removeAll

removeAll

2-Complete only if text is 'ipsum':

removeIpsum

removeIpsum

3-Complete only idices 0 and 2:

removeindices

removeindices

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:

removeManyItems

removeManyItems

2-Remove index 1 and 2:

removeByIndices

removeByIndices

Source: https://github.com/mailok/todo-ngxs/blob/master/src/app/store/operators/remove-many-items.ts

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 patchKey to 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

Use Case:

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.

Example:

Filter out duplicates :

setState(
  patch({ selectedId: uniqArray() }),
);

Or be composable :

setState(
  patch({
    selectedId: compose(
      append([3, 1]),
      uniqArray(),
    ),
  }),
);

The Code:

Source: uniqArray.ts && uniqArray.spec.ts

Use Case:

When you need to append items but skip those already in existing array. Allows to compare by any field if is object

Example:

setState(
  patch({ items: appendUniq(newItems, i => i.id) })
)

The Code:

Source: append-uniq

Use Case:

When you need to sort items before you store them. Allows to sort by any field of T

Example:

setState(
  patch({ items: sortBy(i => i.id) })
)

The Code:

sort-by

@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 patchKey to 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.

Entities State Operators

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(entities: T[], getId?: (t: T) => number | string): StateOperator> {
const idPatchMap = patch(map(entities, getId));
return patch>({ map: idPatchMap });
}

export function setMap(entities: T[], getId?: (t: T) => number | string): StateOperator> {
return patch>({
map: map(entities, getId)
});
}

export function patchEntity(id: number | string, deepPartial: DeepPartial): StateOperator> {
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(entity: T, id: number | string = getKey(entity)): StateOperator> {
const idPatchMap = patch({ [id as any]: entity });
return patch>({ map: idPatchMap });
}

### 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 };
  };
}

Usage

  • clear

    • deselects all ids

    • setState(clear());

  • deselect

    • deselects ids provided

    • setState(deselect(payload.ids));

  • toggle

    • toggles boolean values of provided ids

    • setState(toggle(payload.ids));

  • select

    • selects all ids provided.

    • setState(select(payload.ids));

Upsert Item

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;
    };
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

akisvolanis picture akisvolanis  路  18Comments

piernik picture piernik  路  19Comments

Carniatto picture Carniatto  路  22Comments

internalsystemerror picture internalsystemerror  路  33Comments

markwhitfeld picture markwhitfeld  路  41Comments