Search code examples
angularngxs

How to apply updateItem to multiple/all items in Ngxs?


I need an alternative to change the same attribute, with the same value, for all items in an array in Ngxs.

I tried to use updateItem(() => true, patch({ ... })), but it doesn't work because the first function, () => true, just returns the first match and ends up updating only a single item.

The following is a simplified example of what I tried to do:

import { State, Action, StateContext } from '@ngxs/store';
import { patch, append, removeItem, insertItem, updateItem } from '@ngxs/store/operators';

export interface AnimalsStateModel {
  others: { /*...*/ }[];
  rabbits: Rabbit[];
}

export interface Rabbit {
  name: string,
  alive: boolean,
}

export class ChangeRabbitAlive {
  static readonly type = '[Animals] Change rabbit alive';
  constructor(public payload: { name: string; alive: boolean }) {}
}

export class ChangeAllRabbitsDead {
  static readonly type = '[Animals] Change all rabbits dead';
  constructor(public payload: { }) {}
}

@State<AnimalsStateModel>({
  name: 'animals',
  defaults: {
    others: [ /*...*/ ],
    rabbits: [
      { name: 'Michael', alive: true },
      { name: 'John', alive: true },
      { name: 'Jimmy', alive: true },
      { name: 'Jake', alive: true },
      { name: 'Alan', alive: true },
    ]
  }
})
export class AnimalsState {
  
  @Action(ChangeRabbitAlive)
  changeRabbitAlive(ctx: StateContext<AnimalsStateModel>, { payload }: ChangeRabbitAlive) {
    ctx.setState(
      patch({
        rabbits: updateItem<Rabbit>(rabbit => rabbit.name === payload.name, patch({ alive: payload.alive }))
      })
    );
  }
  
  @Action(ChangeAllRabbitsDead)
  changeAllRabbitsDead(ctx: StateContext<AnimalsStateModel>, { payload }: ChangeAllRabbitsDead) {
    const isAlive = { alive: false };
    ctx.setState(
      patch({
        //rabbits: updateItem<Rabbit>(name => true, patch({ alive: false }))// Type 'boolean' is not assignable to type 'false'
        rabbits: updateItem<Rabbit>(() => true, patch(isAlive))
      })
    );
  }
}

Is there any way to kill all the rabbits at once?

Extra: If you could explain why the error "Type 'boolean' is not assignable to type 'false'" occurs in that situation, that would also be appreciated.


Solution

  • I found this thread [DOCS]: Share your custom State Operators here! that mentions a custom operator updateManyItems, apparently what I was looking for. However, it is not available in the general library, nor could I find its implementation. So I tried some alternatives mixing basic operators and came up with a simple solution using compose:

    To update them all, just add an updateItem for each index in the list:

    const isAlive = { alive: false };
    const updateItems = ctx.getState().rabbits.map((r, i) => updateItem<Rabbit>(i, patch(isAlive)));
    ctx.setState(
        patch({
            rabbits: compose(...updateItems),
        })
    );
    

    To update a portion of the items, just add an updateItem filtering by some unique identifier of the item:

    const isAlive = { alive: false };
    const updateItems = payload.rabbitNames.map((rn) => updateItem<Rabbit>(rabbit => rabbit.name === rn, patch(isAlive)));
    ctx.setState(
        patch({
            rabbits: compose(...updateItems),
        })
    );
    

    If the item identifier may be duplicated in the list, it is possible to update by index as well:

    const isAlive = { alive: false };
    const indexList = ctx.getState().rabbits.reduce((result, r, i) => 
        (payload.rabbitNames.includes(r.name) ? result.push(i) : null, result) , []);
    const updateItems = indexList.map((i) => updateItem<Rabbit>(i, patch(isAlive)));
    ctx.setState(
        patch({
            rabbits: compose(...updateItems),
        })
    );
    

    A native operator could be better optimized, but these alternatives still work fine.