如何将 updateItem 应用于 Ngxs 中的 multiple/all 个项目?

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

我需要一个替代方法来为 Ngxs 中数组中的所有项目更改具有相同值的相同属性。

我尝试使用 updateItem(() => true, patch({ ... })),但它不起作用,因为第一个函数 () => true 只是 returns 第一个匹配项,最终只更新了一个项目。

以下是我尝试做的简化示例:

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

有没有什么办法可以一次杀光所有的兔子?

额外:如果您能解释为什么在这种情况下会出现错误“Type 'boolean' is not assignable to type 'false'”,我们将不胜感激。

不确定这是否是最漂亮的方式,但从概念上讲:

let state2 = {
  ...state,
  rabbits: state.rabbits.map(x => {
    return {
      name: x.name,
      alive: false
    }  
  })
}

您可以按照 NGXS 文档中的说明创建一个 custom state operator 来实现。

您可以尝试以下操作:

import { StateOperator } from '@ngxs/store';
import { isStateOperator } from '@ngxs/store/operators';
import { Predicate } from '@ngxs/store/operators/internals';

/**
 * @param selector - Array of indexes or a predicate function
 * that can be provided in `Array.prototype.findIndex`
 * @param operatorOrValue - New value under the `selector` index or a
 * function that can be applied to an existing value
 */
export function updateItems<T>(
  selector: number[] | Predicate<T>,
  operatorOrValue: Partial<T> | StateOperator<T>
): StateOperator<T[]> {
  return function updateItemsOperator(existing: T[]) {
    let indexes = [];
    if (!selector || !operatorOrValue || !existing) {
      return existing;
    }
    if (isPredicate(selector)) {
      indexes = findIndexes(selector, existing);
    } else if (isArrayNumber(selector)) {
      indexes = selector;
    }

    if (invalidIndexes(indexes, existing)) {
      return existing;
    }
    let values: Record<number, T> = {};

    if (isStateOperator(operatorOrValue)) {
      values = indexes.reduce(
        (acc, it) => ({ ...acc, [it]: operatorOrValue(existing[it]) }),
        {}
      );
    } else {
      values = indexes.reduce(
        (acc, it) =>
          isObject(existing[it])
            ? { ...acc, [it]: { ...existing[it], ...operatorOrValue } }
            : { ...acc, [it]: operatorOrValue },
        {}
      );
    }

    const clone = [...existing];
    const keys = Object.keys(values);
    for (const i in keys) {
      clone[keys[i]] = values[keys[i]];
    }
    return clone;
  };
}

function isObject(value: any): boolean {
  return typeof value === 'object';
}

function isPredicate<T>(
  value: Predicate<T> | boolean | number | number[]
): value is Predicate<T> {
  return typeof value === 'function';
}

function isArrayNumber(value: any[]): boolean {
  for (const i in value) {
    if (isNaN(value[i])) {
      return false;
    }
  }
  return true;
}

function invalidIndex(index: number): boolean {
  return isNaN(index) || index < 0;
}

function invalidIndexes<T>(
  indexes: number[],
  existing: Readonly<T[]>
): boolean {
  for (const i in indexes) {
    if (
      !existing ||
      existing?.length - 1 < indexes[i] ||
      isNaN(indexes[i]) ||
      invalidIndex(indexes[i])
    ) {
      return true;
    }
  }
  return false;
}

function findIndexes<T>(
  predicate: Predicate<T>,
  existing: Readonly<T[]>
): number[] {
  return existing.reduce((acc, it, i) => {
    const index = predicate(it as any) ? i : -1;
    return invalidIndex(index) ? acc : [...acc, index];
  }, []);
}

我发现这个线程 [DOCS]: Share your custom State Operators here! 提到了自定义运算符 updateManyItems,这显然是我要找的。但是,它在通用库中不可用,我也找不到它的实现。所以我尝试了一些混合基本运算符的替代方案,并使用 compose:

提出了一个简单的解决方案

要全部更新它们,只需为列表中的每个索引添加一个 updateItem

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

要更新部分项目,只需添加 updateItem 按项目的某些唯一标识符进行过滤:

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

如果项目标识符在列表中可能重复,也可以按索引更新:

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

可以更好地优化本机运算符,但这些替代方案仍然可以正常工作。