'Format data in store with @ngrx/data and @ngrx/entity, not in component (subscriber)

While working with ngrx/data I find myself parsing and formatting data from the store in multiple places: converting strings to date objects or formatting strings. I recently discovered filterFn which lets me fetch only the entities that are relevant for my use case from the store, but I can't seem to find anything which lets me apply formatting to all of the entities of a certain kind.

Here is an example where I have to format and parse my data in my component before subscribing to it. Ideally this should not be done here, but in a more central place...any ideas of how to deal with this issue in ngrx?

entity.data.config.ts

// I would like to parse and format data here. Is that a good idea?
const entityMetadata: EntityMetadataMap = {
    letter: {
        // https://ngrx.io/guide/data/entity-metadata#filterfn
        filterFn: (entities, clientId) => {
            return entities.filter(entity => entity.clientId === Number(clientId));
        },
    },
};

my.component.ts

@Component({
    selector: 'my-component',
    templateUrl: './my.component.html',
})
export class MyComponent implements OnInit {
    public letters$: Observable<LetterParsed[]>;
    public clientId: string;

    public constructor(public letterDataService: LetterDataService) {}

    public ngOnInit(): void {
        this.clientId = '1';

        // call API, GET letter voor current clientId and add to store
        this.letterDataService.getWithQuery({
            clientId: this.clientId,
        });

        // set filter and fetch only filtered letter entities from store
        this.letterDataService.setFilter(this.clientId);
        this.letters$ = this.letterDataService.filteredEntities$.pipe(
            // this is where I do the parsing, which ideally I want to avoid...
            map((letters: Letter[]) => letters.map(letter => this.parseLetter(letter))),
        );
    }

    private parseLetter(letter: Letter): LetterParsed {
        return {
            ...letter,
            status: this.formatStatus(letter.status),
            date: new Date(letter.date),
        };
    }

    /**
     * @param status e.g 'I_LOVE_ANGULAR'
     * @returns 'I love angular'
     */
    private formatStatus(status: string): string {
        const splitLowercase = status.split('_').join(' ').toLowerCase();
        return splitLowercase[0].toUpperCase() + splitLowercase.slice(1);
    }
}


Solution 1:[1]

You can extend the default DataService to write a custom data service and format your entities there. That way, you'll only need to format them once per entity type.

Refer to the documentation here: https://ngrx.io/guide/data/entity-dataservice#custom-entitydataservice

Here's how you might do this for your use case:

@Injectable()
export class LetterDataService extends DefaultDataService<Letter> {
  constructor(
    http: HttpClient,
    httpUrlGenerator: HttpUrlGenerator,
    logger: Logger
  ) {
    super("Letter", http, httpUrlGenerator);
  }

  getWithQuery(params: string | QueryParams): Observable<Letter[]> {
    return super
      .getWithQuery(params)
      .pipe(
        map((entities) => entities.map((entity) => this.mapEntity(entity)))
      );
  }

  private mapEntity(entity: Letter): Letter {
    return {
      ...entity,
      status: this.formatStatus(letter.status),
      date: new Date(letter.date),
    };
  }

  /**
   * @param status e.g 'I_LOVE_ANGULAR'
   * @returns 'I love angular'
   */
  private formatStatus(status: string): string {
    const splitLowercase = status.split("_").join(" ").toLowerCase();
    return splitLowercase[0].toUpperCase() + splitLowercase.slice(1);
  }
}

You'll also need to register this custom data service, like so:

import { EntityDataService } from '@ngrx/data'; // <-- import the NgRx Data data service registry
import { LetterDataService } from './letter-data-service';

@NgModule({
  imports: [ ... ],
  providers: [ LetterDataService ] // <-- provide the data service
})
export class EntityStoreModule {
  constructor(
    entityDataService: EntityDataService,
    letterDataService: LetterDataService,
  ) {
    entityDataService.registerService('Letter', letterDataService); // <-- register it
  }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Sourav