如何在@ngrx/data 中扩展自定义实体 Collection 减速器

how to EXTEND Custom Entity Collection Reducers in @ngrx/data

在阅读并尝试了迄今为止的所有方法以将我的额外Collection状态更新到我的实体Collection之后(基于文档中的一些信息,, , and an issue from the github repository)我发现自己被困住了在使用 ngrx 之前应该是一项相当常见的任务并且完全没有问题:使用 ngrx 对存储的 collection 实体进行排序。搜索解决方案将我带回到我已经看过 5 次的相同 10 个链接,大多数示例要么过于基础或过时,要么根本不使用 ngrx/data,或者类似的问题根本没有答案。

环境:Angular 9.1.7,ngrx 9.1.2,macOS 10.14.6

目标

将自己的 additionalCollectionState 添加并更新到我的实体 Collection,如 in the docs 所述,以进行排序和分页。我 从服务器获取 sortQuery 属性(sortField、sortDirection),而是在我的 GUI 中设置它并对 collection 进行排序基于它们。我知道 EntityMetadataMap 上存在 sortComparer 设置,但它不够灵活,我想在应用程序的不同 MatTables 中对 collection 进行动态排序。只要 MatTable 中的排序顺序/字段发生变化,就应该更新这个额外的 属性。

目前我取得的成就:

ngrx/data方式的additionalCollection状态如上所述有效,我的collection是initialized 与我在 EntityMetadataMap 中提供的数据。然而,更新自定义 collection 属性并不像我从文档中理解的那样起作用。由于我没有从服务器获取 sortQuery 属性,因此不会调用 ResultsHandler。

我用两种方法解决了这个问题:a) 使用 ngrx & ngrx/entity 方法和 b) ngrx/data 方法通过尝试扩展自定义 EntityCollectionReducer

a) 我成功创建了自定义 Action 和自定义 Reducer 并更新了 state 属性 但不是之前定义的 collection 属性。我的操作基本上是:

export const selectResourceSortQuery = createAction('[Resource] Get Last Sort Query', props<{ sortQuery: any }>());

它是从服务调用的:

    this.store.dispatch(selectResourceSortQuery({ sortQuery }));

并在reducer中处理:

import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
import { Action, createReducer, on } from '@ngrx/store';
import * as ResourceActions from './resource.actions';
import { Resource } from 'src/app/sdk';

export interface State extends EntityState<Resource> {
    // additional state property
    sortQuery: any | null;
}

export const adapter: EntityAdapter<Resource> = createEntityAdapter<Resource>();

export const initialState: State = adapter.getInitialState({
    // additional entity state properties
    sortQuery: {
        sortDirection: 'asc',
        sortField: 'product'
    },
});

export const resourceReducer = createReducer(
    initialState,
    on(ResourceActions.selectResourceSortQuery, (state, { sortQuery }) => {
        console.log(`REDUCER called`);
        return { ...state, sortQuery };
    }),
);

export function reducer(state: State | undefined, action: Action) {
    return resourceReducer(state, action);
}

减速器在 reducers/index.ts:

中导出
import {
  ActionReducer,
  ActionReducerMap,
  createFeatureSelector,
  createSelector,
  MetaReducer
} from '@ngrx/store';
import { environment } from 'src/environments/environment';
import * as fromResourceReducers from 'src/app/store/entities/resource/resource.reducer';


export interface State {

}

export const reducers: ActionReducerMap<State> = {
  resourceSort: fromResourceReducers.reducer
};


export const metaReducers: MetaReducer<State>[] = !environment.production ? [] : [];

在 app.module 中设置为:

StoreModule.forRoot(reducers, {
  metaReducers: [
    ngrxEntityRelationshipReducer //ignore this
  ],
  runtimeChecks: {
    strictStateImmutability: true,
    strictActionImmutability: true
  }
}),

这会创建一个 resourceSort Collection 和 sortQuery 属性 从这个屏幕截图可以看出: 并根据更改后的 sortQuery 属性 更新它。但是,我更喜欢在 Resource entityCache collection 中使用已经初始化的 sortQuery 属性 (是的,我也尝试将它添加到 entityCache 功能 StoreModule.forFeature('entityCache', reducers)忽略那个):

b) 因此,另一种方法是在 app.module:

中使用 EntityCollectionReducerRegistry
import { resourceReducer } from './store/entities/resource/resource.reducer';

// does not work: resourceReducer is of type ActionReducer<State, Action> and the same as posted above (with the added handling of the sortQuery property), and apparently what this function expects
entityCollectionReducerRegistry.registerReducer('Resources', resourceReducer);

// this does work, but just creates the basic default entity collection reducer without my custom sortQuery handling
entityCollectionReducerRegistry.registerReducer('Resources', entityCollectionReducerFactory.create('resources'))

来自文档:

The Entity Reducer is the master reducer for all entity collections in the stored entity cache. The library doesn't have a named entity reducer type. Rather it relies on the EntityCacheReducerFactory.create() method to produce that reducer, which is an NgRx ActionReducer<EntityCache, EntityAction>.

我有一种感觉,使用 b) 方法我可能走在正确的轨道上,但是我一直缺乏文档和示例来解决这个问题 - 有没有人能帮助我?

如果需要更多详细信息,我很乐意提供,如果有什么不清楚,因为英语不是我的母语,我很乐意澄清,如果有必要,请尝试提供一个简化的 stackblitz 示例。谢谢!

您采用的方法与我最初采用的方法相同:尝试注册自定义缩减器来处理 additionalCollectionState 属性。在花了一些时间解读文档之后,我终于能够更新一个集合 属性。以下是文档所说的以及我是如何做到的:

The NgRx Data library generates selectors for these properties, but has no way to update them. You'll have to create or extend the existing reducers to do that yourself. (additionalCollectionState)

在此之后继续后端示例,但我不需要那个;然而,我使用从第 2 步开始的信息来扩展(将被破解)EntityCollectionReducerMethods 来拦截我的 属性:

extended-entity-collection-reducer-methods.ts

export class ExtendedEntityCollectionReducerMethods<T> extends EntityCollectionReducerMethods<T> {
    constructor(
        public entityName: string,
        public definition: EntityDefinition<T>) {
        super(entityName, definition);
    }

    protected updateOne(collection: EntityCollection<T>, action: EntityAction<Update<T>>): EntityCollection<T> {
    const superMethod = {...super.updateOne(collection, action)};

    if (action.payload.entityName === 'Project' &&
        (action.payload.data.changes as any).id === 0) {
        (superMethod as any).lastProjectId = (action.payload.data.changes as any).lastProjectId;
    }

    return superMethod;
}

然后需要由工厂实例化:

extended-entity-collection-reducer-methods-factory.ts

@Injectable()
export class ExtendedEntityCollectionReducerMethodsFactory {
    constructor(
        private entityDefinitionService: EntityDefinitionService
    ) {
    }

    create<T>(entityName: string): EntityCollectionReducerMethodMap<T> {
        const definition = this.entityDefinitionService.getDefinition<T>(entityName);
        const methodsClass = new ExtendedEntityCollectionReducerMethods(entityName, definition);
        return methodsClass.methods;
    }
}

ExtendedEntityCollectionReducerMethodsFactory需要替换原来的

app-module.ts

providers: [
        {
            provide: EntityCollectionReducerMethodsFactory,
            useClass: ExtendedEntityCollectionReducerMethodsFactory
        }
    ]

现在您可以在组件中偷偷摸摸地执行此操作:

this.someService.updateOneInCache({id: 0, lastProjectId: projectId} as any);

someService 需要扩展出色的 EntityCollectionServiceBase class:

一些-service.ts

@Injectable({providedIn: 'root'})
export class SomeService extends EntityCollectionServiceBase<SomeEntity> {
    constructor(serviceElementsFactory: EntityCollectionServiceElementsFactory) {
        super('SomeEntity', serviceElementsFactory);
    }
}

ExtendedEntityCollectionReducerMethods 中的 updateOne 方法将通过操作的有效负载选择 属性,您可以在 return superMethod 的结果之前将其设置在那里。

我觉得这种做法很糟糕,但它确实有效。在徒劳地尝试使用 EntityCacheReducerFactory 及其奇怪的家伙后,我没有找到其他方法来更新集合的附加属性。

我已将更新方法的 ID 设置为 0,否则它会抱怨缺少 ID。由于我没有任何 ID 为 0 的实体,它应该可以正常工作而不会产生任何意外后果。