开发者问题收集

如何在@ngrx/data 中扩展自定义实体集合 Reducer

2020-07-10
3325

在阅读并尝试了到目前为止将我的 additionalCollectionStates 更新为我的 EntityCollection 的所有方法后(基于文档中的一些信息, 这个 SO 问题 这个其他 SO 问题 另一个 SO 问题 来自 github 存储库的问题 )我发现自己被困在一个应该是一个相当常见的任务上,并且在使用 ngrx 之前根本没有问题:使用 ngrx 对存储的实体集合进行排序。搜索解决方案让我回到了已经看过 5 次的相同 10 个链接,并且大多数示例要么太基础要么太过时,要么根本不使用 ngrx/data,或者类似的问题根本没有答案。

环境 :Angular 9.1.7、ngrx 9.1.2、macOS 10.14.6

目标

按照 在文档中 所述,将自己的 additionalCollectionState 添加并更新到我的 EntityCollection,以进行排序和分页。我 没有 从服务器获取 sortQuery 属性(sortField、sortDirection),而是在我的 GUI 中设置它并根据它们对集合进行排序。我知道 EntityMetadataMap 上存在 sortComparer 设置,但它对我来说不够灵活,我想动态地对应用程序中不同 MatTable 中的集合进行排序。每当 MatTable 中的排序顺序/字段发生变化时,都应该更新此附加属性。

我目前所取得的成就:

如上所述的 additionalCollectionState 的 ngrx/data 方式 有效 ,我的集合使用我在 EntityMetadataMap 中提供的数据 初始化 。但是,更新自定义集合属性并不像我从文档中理解的那样有效。由于我没有从服务器获取 sortQuery 属性,因此不会调用 ResultsHandler。

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

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

Reducer 在 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 集合,其具有 sortQuery 属性,如下图所示: enter image description here ,并根据更改后的 sortQuery 属性对其进行更新。但是,我更喜欢使用 Resource entityCache 集合中已初始化的 sortQuery 属性(是的,我也尝试过使用 StoreModule.forFeature('entityCache', Reducers) 将其添加到 entityCache 功能中,忽略它):

在此处输入图像描述

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 示例。谢谢!

1个回答

您处理这个问题的方式与我最初处理的方式相同:尝试注册自定义 Reducer 来处理 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 步中的信息来扩展(读作 hack)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;
}

然后需要由 Factory 实例化:

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 类:

some-service.ts

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

ExtendedEntityCollectionReducerMethods 中的 updateOne 方法将通过操作的有效负载拾取属性,您可以在返回 superMethod 的结果之前在那里设置它。

我觉得这有点令人不寒而栗,但它确实有效。在尝试使用 EntityCacheReducerFactory 及其奇怪的伙伴后,我找不到其他方法来更新集合的附加属性。

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

RepentAndBelieveTheGospel
2020-07-29