The ngrx-data library maintains a cache of entity collection data in the ngrx store.
You tell the ngrx-data library about those collections and the entities they contain with entity metadata.
The entities within a collection belong to the same entity type.
Each entity type appears as named instance of the ngrx-data EntityMetadata<T>
interface.
You can specify metadata for several entities at the same time in an EntityMetadataMap
.
Here is an example EntityMetadataMap
similar to the one in the demo app
that defines metadata for two entities, Hero
and Villain
.
export const appEntityMetadata: EntityMetadataMap = {
Hero: {
/* optional settings */
filterFn: nameFilter,
sortComparer: sortByName
},
Villain: {
villainSelectId, // necessary if key is not `id`
/* optional settings */
entityName: 'Villain', // optional because same as map key
filterFn: nameAndSayingFilter,
entityDispatcherOptions: { optimisticAdd: true, optimisticUpdate: true }
}
};
You must register the metadata with the ngrx-data EntityDefinitionService
.
The easiest way to register metadata is to define a single EntityMetadataMap
for the entire application and specify it in the one place where you initialize the ngrx-data library:
NgrxDataModule.forRoot({
...
entityMetadata: appEntityMetadata,
...
})
If you define entities in several, different eagerly-loaded Angular modules, you can add the metadata for each module with the multi-provider.
{ provide: ENTITY_METADATA_TOKEN, multi: true, useValue: someEntityMetadata }
This technique won't work for a lazy-loaded module.
The ENTITY_METADATA_TOKEN
provider was already set and consumed by the time the lazy-loaded module arrives.
The module should inject the EntityDefinitionService
instead and register metadata directly with one of the registration methods.
@NgModule({...})
class LazyModule {
constructor(eds: EntityDefinitionService) {
eds.registerMetadataMap(this.lazyMetadataMap);
}
...
}
The EntityMetadata<T>
interface describes aspects of an entity type that tell the ngrx-data library how to manage collections of entity data of type T
.
Type T
is your application's TypeScript representation of that entity; it can be an interface or a class.
The entityName
of the type is the only required metadata property.
It's the unique key of the entity type's metadata in cache.
It must be specified for individual EntityMetadata
instances.
If you omit it in an EntityMetadataMap
, the map key becomes the entityName
as in this example.
const map = {
Hero: {} // "Hero" becomes the entityName
};
The spelling and case (typically PascalCase) of the entityName
is important for ngrx-data conventions. It appears in the generated entity actions, in error messages, and in the persistence operations.
Importantly, the default entity dataservice creates HTTP resource URLs from the lowercase version of this name. For example, if the entityName
is "Hero", the default data service will POST to a URL such as 'api/hero'
.
By default it generates the plural of the entity name when preparing a collection resource URL.
It isn't good at pluralization. It would produce
'api/heros'
for the URL to fetch all heroes because it blindly adds an's'
to the end of the lowercase entity name.Of course the proper plural of "hero" is "heroes", not "heros". You'll see how to correct this problem below.
Many applications allow the user to filter a cached entity collection.
In the accompanying demonstration app, the user can filter heroes by name and can filter villains by name or the villain's saying.
We felt this common scenario is worth building into the ngrx-data library. So every entity can have an optional filter function.
Each collection's filteredEntities
selector applies the filter function to the collection, based on the user's filtering criteria, which are held in the the stored entity collection's filter
property.
If there is no filter function, the filteredEntities
selector is the same as the selectAll
selector, which returns all entities in the collection.
A filter function (see EntityFilterFn<T>
) takes an entity collection and the user's filtering criteria (the filter pattern) and returns an array of the selected entities.
Here's an example that filters for entities with a name
property whose value contains the search string.
export function nameFilter(entities: { name: string }[], search: string) {
return entities.filter(e => -1 < e.name.indexOf(search));
}
The ngrx-data library includes a helper function, PropsFilterFnFactory<T>
, that creates an entity filter function which will treat the user's input
as a case-insensitive, regular expression and apply it to one or more properties of the entity.
The demo uses this helper to create hero and villain filters. Here's how the app creates the nameAndSayingFilter
function for villains.
/**
* Filter for entities whose name or saying
* matches the case-insensitive pattern.
*/
export function nameAndSayingFilter(entities: Villain[], pattern: string) {
return PropsFilterFnFactory < Villain > ['name', 'saying'](entities, pattern);
}
Every entity type must have a primary key whose value is an integer or a string.
The ngrx-data library assumes that the entity has an id
property whose value is the primary key.
Not every entity will have a primary key property named id
. For some entities, the primary key could be the combined value of two or more properties.
In these cases, you specify a selectId
function that, given an entity instance, returns an integer or string primary key value.
In the entity reducer tests, the Villain
type has a string primary key property named key
.
The selectorId
function is this:
selectId: (villain: Villain) => villain.key;
The ngrx-data library keeps the collection entities in a specific order.
This is actually a feature of the underlying
@ngrx/entity
library.
The default order is the order in which the entities arrive from the server. The entities you add are pushed to the end of the collection.
You may prefer to maintain the collection in some other order.
When you provide a sortComparer
function, the ngrx-lib keeps the collection in the order prescribed by your comparer.
In the demo app, the villains metadata has no comparer so its entities are in default order.
The hero metadata have a sortByName
comparer that keeps the collection in alphabetical order by name
.
export function sortByName(a: { name: string }, b: { name: string }): number {
return a.name.localeCompare(b.name);
}
Run the demo app and try changing existing hero names or adding new heroes.
Your app can call the selectKey
selector to see the collection's ids
property, which returns an array of the collection's primary key values in sorted order.
These options determine the default behavior of the collection's dispatcher which sends actions to the reducers and effects.
A dispatcher save command will add, delete, or update
the collection before sending a corresponding HTTP request (optimistic) or after (pessimistic).
The caller can specify in the optional isOptimistic
parameter.
If the caller doesn't specify, the dispatcher chooses based on default options.
The default defaults are the safe ones: optimistic for delete and pessimistic for add and update. You can override those choices here.
Each ngrx-data entity collection in the the store has predefined properties.
You can add your own collection properties by setting the additionalCollectionState
property to an object with those custom collection properties.
The entity selectors tests illustrate by adding foo
and bar
collection properties to test hero metadata.
additionalCollectionState: {
foo: 'Foo',
bar: 3.14
}
The property values become the initial collection values for those properties when ngrx-data first creates the collection in the store.
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.
The ngrx-data DefaultDataService
relies on the HttpUrlGenerator
to create conventional HTTP resource names (URLs) for each entity type.
By convention, an HTTP request targeting a single entity item contains the lowercase, singular version of the entity type name. For example, if the entity type entityName
is "Hero", the default data service will POST to a URL such as 'api/hero'
.
By convention, an HTTP request targeting multiple entities contains the lowercase, plural version of the entity type name. The URL of a GET request that retrieved all heroes should be something like 'api/heroes'
.
The HttpUrlGenerator
can't pluralize the entity type name on its own. It delegates to an injected pluralizing class, called Pluralizer
.
The Pluralizer
class has a pluralize() method that takes the singular string and returns the plural string.
The default Pluralizer
handles many of the common English pluralization rules such as appending an 's'
.
That's fine for the Villain
type (which becomes "Villains") and even for Company
(which becomes "Companies").
It's far from perfect. For example, it incorrectly turns Hero
into "Heros" instead of "Heroes".
Fortunately, the default Pluralizer
also injects a map of singular to plural strings (with the PLURAL_NAMES_TOKEN
).
Its pluralize()
method looks for the singular entity name in that map and uses the corresponding plural value if found.
Otherwise, it returns the default pluralization of the entity name.
If this scheme works for you, create a map of singular-to-plural entity names for the exceptional cases:
export const pluralNames = {
// Case matters. Match the case of the entity name.
Hero: 'Heroes'
};
Then specify this map while configuring the ngrx-data library.
NgrxDataModule.forRoot({
...
pluralNames: pluralNames
})
If you define your entity model in separate Angular modules, you can incrementally add a plural names map with the multi-provider.
{ provide: PLURAL_NAMES_TOKEN, multi: true, useValue: morePluralNames }
If this scheme isn't working for you, replace the Pluralizer
class with your own invention.
{ provide: Pluralizer, useClass: MyPluralizer }