Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

📚 [DOCS]: Share your custom State Operators here! #926

Closed
markwhitfeld opened this issue Mar 19, 2019 · 27 comments
Closed

📚 [DOCS]: Share your custom State Operators here! #926

markwhitfeld opened this issue Mar 19, 2019 · 27 comments
Labels
discussion An issue that discusses design decisions. not an issue An issue logged that doesn't require fixing (maybe just a question)

Comments

@markwhitfeld
Copy link
Member

markwhitfeld commented Mar 19, 2019

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.

@markwhitfeld markwhitfeld pinned this issue Mar 19, 2019
@markwhitfeld
Copy link
Member Author

markwhitfeld commented Mar 19, 2019

(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 🚀 )

@mailok
Copy link

mailok commented Mar 19, 2019

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

@mailok
Copy link

mailok commented Mar 19, 2019

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

@beaussan
Copy link

beaussan commented Mar 21, 2019

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
Copy link

beaussan commented Mar 21, 2019

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

@joaqcid
Copy link
Contributor

joaqcid commented Jul 18, 2019

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

@joaqcid
Copy link
Contributor

joaqcid commented Jul 26, 2019

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

@joaqcid
Copy link
Contributor

joaqcid commented Sep 10, 2019

@markwhitfeld can we expect some of this state-operators to become part of the library? any timeline for this? thanks!

@splincode
Copy link
Member

@joaqcid PR's please)

@arturovt
Copy link
Member

@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)

@joaqcid
Copy link
Contributor

joaqcid commented Sep 10, 2019

@arturovt ok, yes agreed, though you never know when a binary-tree-traverse state-operator might become handy ;)

@arturovt arturovt added discussion An issue that discusses design decisions. not an issue An issue logged that doesn't require fixing (maybe just a question) labels Oct 13, 2019
@splincode

This comment has been minimized.

@splincode splincode changed the title CONTRIB WIKI: Share your custom State Operators here! 📚[DOCS]: CONTRIB WIKI Share your custom State Operators here! Nov 4, 2019
@splincode splincode changed the title 📚[DOCS]: CONTRIB WIKI Share your custom State Operators here! 📚[DOCS]:Share your custom State Operators here! Nov 4, 2019
@beyondsanity
Copy link

beyondsanity commented Mar 4, 2020

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

This comment has been minimized.

@beyondsanity

This comment has been minimized.

@beaussan

This comment has been minimized.

@troydietz
Copy link
Contributor

troydietz commented Jun 17, 2020

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;
  }, {});
}
// operators 
export interface EntitiesStateModel<T> {
  map: { [id: string]: T };
}

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

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

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

// 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));

@marcjulian
Copy link

marcjulian commented Jul 1, 2020

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;

@splincode splincode changed the title 📚[DOCS]:Share your custom State Operators here! 📚 [DOCS]: Share your custom State Operators here! Jul 1, 2020
@joaqcid
Copy link
Contributor

joaqcid commented Jul 10, 2020

@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' }]);
  });
});

@marcjulian
Copy link

@joaqcid wow that is awesome, thanks for writing tests for upsertItem.

@markwhitfeld
Copy link
Member Author

@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?

@marcjulian
Copy link

marcjulian commented Jul 13, 2020

@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? 😄

@markwhitfeld
Copy link
Member Author

@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?

@AdrienMoretti
Copy link

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!

@dmrickey
Copy link

dmrickey commented Oct 1, 2020

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

@markwhitfeld
Copy link
Member Author

Please participate in the RFC for the upsertItem operator.
The discussion is found here: #1796

@splincode
Copy link
Member

@markwhitfeld maybe move it's issue to discussions region?

@ngxs ngxs locked and limited conversation to collaborators Aug 5, 2022
@markwhitfeld markwhitfeld converted this issue into discussion #1900 Aug 5, 2022

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
discussion An issue that discusses design decisions. not an issue An issue logged that doesn't require fixing (maybe just a question)
Projects
None yet
Development

No branches or pull requests