Search code examples
angularngrx

Overwriting handleSuccess of NGRX DefaultPersistenceResultHandler


I am having a bit of difficulty with @ngrx/data and am hoping one of you Geniuses out there can help me out.

I have an entity collection and would like to store some additional information to reduce round trips to the server and also reduce redundant loading. I have a table of data and only want to load one page at at time into the entity collection, in order to be able to do this I would like to add additional meta data to my collection so that I know when to load more data. E.g. when I reach the end of the loaded data load more (pagination would need to know how many records exist and how many have been loaded).

According to the documentation I can add additionalCollectionState but need some way of updating the new state properties.

I figured I would copy/paste the example code that they have as a base and modify it to reflect my own properties.. the problem is that I immediately get a typescript error on the =>Action

Generic type 'Action' requires 1 type argument(s)

export class AdditionalPersistenceResultHandler  extends DefaultPersistenceResultHandler {

handleSuccess(originalAction: EntityAction): (data: any) => Action {
    const actionHandler = super.handleSuccess(originalAction);
    // return a factory to get a data handler to
    // parse data from DataService and save to action.payload
    return function(data: any) {
      const action = actionHandler.call(this, data);
      if (action && data && data.foo) {
        // save the data.foo to action.payload.foo
        (action as any).payload.foo = data.foo;
      }
      return action;
    };
  }
}

I'm also not sure if this is the right way to go about this or am I going about this too complicated, could I "simply" update the additional collection state manually somehow (in my dataservice call getWithQuery() ) and if so what would be the best/recommended approach.

Cheers and Thanks

Gary

UPDATE

After Andrew pointed out my obvious import mistake I have now implemented the result handler but get the following error

ERROR in Error during template compile of 'AdditionalPropertyPersistenceResultHandler'
  Class AdditionalPropertyPersistenceResultHandler in D:/dev/angular/ng-vet/src/app/treatments/services/treatments-entity-result-handler.ts extends from a Injectable in another compilation unit without duplicating the decorator
    Please add a Injectable or Pipe or Directive or Component or NgModule decorator to the class.

which doesn't make any sense considering that the stackblitz doesn't have it and it works wonderfully.

my entityMetadataMap

const entityMetadata: EntityMetadataMap = {
  TreatmentTemplate: {
    entityDispatcherOptions: {
      optimisticUpdate: true
    }
  },
  Treatment: {
    additionalCollectionState: {
      totalRecords: 0
    },
    entityDispatcherOptions: {
      optimisticUpdate: true
    }
  }
};

and providers:

providers: [
    TreatmentsDataService,
    TreatmentEntityService,
    TreatmentTemplateResolver,
    TreatmentTemplatesDataService,
    TreatmentTemplateEntityService,
    {
      provide: PersistenceResultHandler,
      useClass: AdditionalPropertyPersistenceResultHandler
    },
    {
      provide: EntityCollectionReducerMethodsFactory,
      useClass: AdditionalEntityCollectionReducerMethodsFactory
    }
  ]

I basically copy pasted the methods from the stackblitz..

at ^8.0.2 of angular and ^8.6.0 of ngrx could that be the problem?


Solution

  • There's one caveat to using additionalCollectionState.

    The default QUERY_MANY_SUCCESS reducer is expecting action.payload.data to be an array of entities but the QUERY_MANY action action.payload.data will be whatever the api returns.

    Say it returns

    interface QueryManyAPIResponse<T> {
        total: number,
        entities: T[]
    }
    

    You can add a property total to the action payload but action.payload.data needs to be the entities.

    @Injectable()
    export class AdditionalPersistenceResultHandler  extends DefaultPersistenceResultHandler {
    
        handleSuccess(originalAction: EntityAction): (data: any) => Action {
            const actionHandler = super.handleSuccess(originalAction);
    
            return function(data: any) {
                /** Default success action */
                const successAction = actionHandler.call(this, data);
    
                /** Change payload for query many */
                if (successAction && data && data.total) {
                    (successAction as any).payload.total = data.total;
                }
                if (successAction && data && data.entities) {
                    (successAction as any).payload.data = data.entities;
                }
    
                return action;
            };
        }
    }
    

    This is making changes to the action only.

    Whilst the default reducer will take care of entities, steps 2 & 3 of the NgRx Docs are needed to add the total as a property to the entity collection.


    pagination would need to know how many records exist and how many have been loaded

    how many have been loaded is derivable (id.length, hence you can use a selector for this) and therefore doesn't need to be saved in the store.

    I have an entity collection and would like to store some additional information to reduce round trips to the server and also reduce redundant loading.

    It's a hard problem. The cleanest most direct way to reduce api calls I've found is to use a server side pagination table (see reference below) and provide a page prior to the table that offers a dashboard with statistics (would need to implement this API) with various filters and links to the table page with query params built in.

    Few spend time paging through a paginated mat table if the data is already on page 1.
    And no one wants to.

    dashboard.component.html

            <button
              mat-raised-button
              [routerLink]="['/procurement/orders']"
              [queryParams]="{ awaitingPrices: 'yes', employeeID: userID }"
            >
              {{ statistics.awaitingPricesUser }}
            </button>
    

    orders.component.html (container)

    <app-order-find (filter)="onFilter($event)"></app-order-find>
    <app-order-filters
      [suppliers]="suppliers$ | async"
      [employees]="employees$ | async"
      [filters]="filters"
      (filtersUpdate)="updateFilters($event)"
    ></app-order-filters>
    <app-orders-table
      [totalNumberOfOrders]="totalNumberOfOrders$ | async"
      [filters]="filters"
      (review)="onReview($event)"
      (edit)="onEdit($event)"
      (delete)="onDelete($event)"
    >
      ></app-orders-table
    >
    

    Alternatives

    Are you expecting to load all the data into the store i.e. in the background load paginated data with page 1, 2, ..., last page. Once all the data is loaded you could do client-side pagination fully through selectors.

    If you expect to go back and forth between table and details view or back and forth on same pages and same filters you could keep a cache map of { urlWithQueryParams: entityIds } and use selectors to get from the store. You'd need to throw these away if you delete a relevant entity.

    References

    Server side pagination table - https://blog.angular-university.io/angular-material-data-table/