Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Create application state interface.
As with any NgRX application, we need to create a new app.state.ts file. We recommend creating is file in a root state directory, located at src/app/state. Import the IEntityState interface from @briebug/ngrx-auto-entity as well, as we will be using it later.
import { IEntityState } from '@briebug/ngrx-auto-entity';
export interface AppState {
// todo: add each entity state interface to our application state interface
}Define the AppState interface as normal. We will eventually define each entity state property here. For now, we've left it blank, and will fill it in later on in this quick start.
Remember to export the AppState type so that it may be used throughout the rest of your app.
Adding NgRx & Auto-Entity to an App
If you have not used NgRx before, and need to start from scratch, this guide should get you going. Let's start by creating a state module. We recommend creating this module in a root state directory, located at src/app/state. In this directory, create a new state.module.ts file:
import { NgModule } from '@angular/core';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store';
import { EntityOperators } from '@briebug/ngrx-auto-entity';
@NgModule({
imports: [
StoreModule.forRoot()
EffectsModule.forRoot([]),
NgrxAutoEntityModule.forRoot()
]
})
export class StateModule {}Import the NgRx StoreModule and EffectsModule as well as the Auto-Entity NgrxAutoEntityModule. Make sure you call the .forRoot() initializer on each of them to ensure they are properly imported. It is important that the NgrxAutoEntityModule be brought in after the EffectsModule, as this ensures all automatic effects will be properly registered.
The NgRx Auto Entity module is now imported in your application, giving you access to ready-made generic actions, automatic effects, pre-defined reducers and prefabricated facade classes to handle the bulk of your CRUD meeds.
Next, we need to set up the state of our application.
Create application state module.
Now that we have implemented our root state interface & reducer map, we need to update the state module we created in the first step:
import { NgModule } from '@angular/core';
import { NgrxAutoEntityModule } from '@briebug/ngrx-auto-entity';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store';
import { appMetaReducers, appReducer } from './app.state';
@NgModule({
imports: [
StoreModule.forRoot(appReducer),
EffectsModule.forRoot([]),
NgrxAutoEntityModule.forRoot()
]
})
export class StateModule {}Update the import of the StoreModule. In the forRoot() static method, specify the appReducer and (if necessary) appMetaReducers from your previously created app.reducer as with any normal NgRX app.
If you have any custom effects you may have implemented, include those classes in the EffectsModule import's forRoot() call. Effects are optional with NgRx Auto-Entity, so if you have no effects just pass an empty array.
Create your entity models.
In a departure from classic @ngrx/entity models, each model in your application should be defined as a class (see note below). Here is an example of a Customer model:
Next we need to import the Key and Entity decorators. The Key decorator is used to specify the property in your model that is the unique identifier. Decorate the id property, which is the unique identifier for Customer model. Read more about entity keys in the advanced documentation.
The Entity decorator is used to attach metadata to your entity model that the NgRx Auto-Entity library can use to perform many of its automated tasks. In version 0.5 of the library, only the
modelNameNote that the model must be a class and not an interface. This is because interfaces are a compile-time only feature of TypeScript, while classes are a native runtime construct in JavaScript. Further, the modelName must be defined on the entity, as this is the name that the library will use at runtime for minified/uglified code (critical, read more in advanced documentation.)
import { Entity, Key } from '@briebug/ngrx-auto-entity';
@Entity({
modelName: 'Customer',
uriName: 'customers'
})
export class Customer {
@Key id: number;
name: string;
catchPhrase: string;
}Define the initial entity state
In our example we are building the state for the Customer entity. As such, we've created a new customer.state.ts file located at src/app/state/customer.state.ts.
Import the buildState function from the ngrx-auto-entity module. This function builds the initial state and selectors for each entity. Call the function by passing in the Customer entity class (note, the class must be passed in!) We use object destructuring on the return type access the initialState , selectors and facade base class from the result of buildState.
We can now further destructure the selectors object to map each type of standard selector to model-specific names for import into other modules:
selectAll selects the Array of entities
selectEntities selects the Dictionary of entities
selectIds
Note that retrieving and exporting the selectors are optional if you extract the facade. The facade base class generated by buildState fully encapsulates all of the functionality you will need to interact with your entity state, and we generally recommend only using the facade. Demonstration of how to access selectors directly, such as in the event that you may need to create your own custom selectors, is simply for completeness here.
Finally, we define the customerReducer function. The reducer function accepts two arguments: state, which defaults to the initialCustomerState, and the action.
import { Action } from '@ngrx/store';
import { buildState, IEntityState } from '@briebug/ngrx-auto-entity';
import { Customer } from '../models/customer.model';
export const {
initialState: initialCustomerState,
selectors: {
selectAll: allCustomers
},
facade: CustomerFacadeBase
} = buildState(Customer);
export function customerReducer(state = initialCustomerState): IEntityState<Customer> {
return state;
}ArrayselectTotal selects the number of entities
Whats mine is yours, whats yours is mine!
When the need arises to create a custom action & custom effects, Auto-Entity can still help you reduce your workload. It may be that you can integrate your actions, effects and data with Auto-Entity managed state, so long as the data is compatible.
As a simple example, you may wish to create a custom action for handling type-ahead lookups that search a given entity.
export const SearchCustomers = createAction(
'[Customer] Typeahead Search',
props<{criteria: string }>()
);You could then implement your own effect for handling this action to ensure the proper implementation and behavior:
In this case, we need switchMap semantics...that is, we wish to cancel any previous requests that may have stale information in order to utilize the most up to date search criteria the user may have entered.
Take note, however, of the new actions returned by this effect:
We are returning generic actions! Since these are customers, as long as we have built auto-entity state for the Customer entity, you may combine custom actions you implement yourself with generic actions from the Auto-Entity library.
These actions will ultimately cause the returned customer search results to be reduced into state managed by Auto-Entity for you. You only have to create just one action. Just make sure the shape of the data returned by your custom service is compatible, or is otherwise transformed into a compatible shape.
this.customerService.search(criteria).pipe(
map(result => new LoadAllSuccess(Customer, result)),
catchError(err => of(new LoadAllFailure(Customer, err)))
))import {Actions, createEffect} from '@ngrx/effects';
import {LoadAllSuccess, LoadAllFailure} from '@briebug/ngrx-auto-entity';
import {SearchCustomers} from 'state/customer.actions';
export class CustomerEffects {
searchCustomers$ = createEffect(
() => this.actions$.pipe(
ofType(SearchCustomers),
map(action => action.criteria),
switchMap(criteria =>
this.customerService.search(criteria).pipe(
map(result => new LoadAllSuccess(Customer, result)),
catchError(err => of(new LoadAllFailure(Customer, err)))
)
)
),
{ resubscribeOnError: false }
);
constructor(private actions$: Actions, private customerService: CustomerService) {}
}Only if necessary...
New in version 0.5 is the ability to "optionally" load data. This functionality was added with the IfNecessary set of loading actions. These actions will perform some basic checks against the existing state in your application, which necessitates that you actually expose your state to the library (more on this in a moment.) In essence, if the entities are already in state, the actual load action will not be dispatched. Optionally, a maxAge may be specified in the IfNecessary actions, or a defaultMaxAge may be defined on the model with the @Entity decorator.
In order for optional loading actions to function, they must be able to access your state. This means you must expose your store to the auto-entity library. This is achieved by providing the NGRX_AUTO_ENTITY_APP_STORE injection token with a factory function that will return your Store instance.
There is not much to it, other than to create a factory function that depends on the NgRx Store, and to specify Store as a dependency of your provider. You should also provide this token in your root application module, to ensure it is within the root injection scope (otherwise Auto-Entity will be unable to find it, as it is unable to use child injection scopes defined by your own applications.)
Once you have provided your store to Auto-Entity, you may use the optional loading actions. These actions are available for each of the different loading actions that already exist:
LoadIfNecessary
LoadAllIfNecessary
LoadManyIfNecessary
These actions will attempt to verify that the requested data already exists within state. For loading a single entity, it will look for that entity (its key) in state. For loading all or many entities, it will check if any entities are in state. (Note: Currently, we will not try to verify if custom criteria matches, as auto-entity currently does not store the previous criteria. This may change in a future version of auto-entity.) For loading a page or range, the current page/range criteria will be compared against existing state.
Optional loading is an additional "layer" on top of normal loading. When dispatching a LoadIfNecessary action, this action simply invokes an effect that performs the appropriate verification against state to determine if a Load action should be dispatched or not. If the entity or entities are found in state, then a Load action will NOT be dispatched. Otherwise, the Load action will be dispatched normally.
The *IfNecessary actions therefor only introduce new functionality, and otherwise do not change the existing load functionality. Any custom code that may rely on the existing loading mechanism in Auto-Entity to continue working the way it does will continue to work when using optional loading. You may also add effects that expect *Success or *Failure load result actions, as they will still be dispatched appropriately when using *IfNecessary actions.
When performing an optional load, a maximum age, in seconds, may be specified. If the last loadedAt timestamp for the entity in question is older than the specified age, then entities will be loaded. A maximum age may be specified within the *IfNecessary action as a parameter, or a default max age may be specified on the entity model using the @Entity decorator:
If both a default maximum age is specified on the model, and a maximum age is passed in the action, the action age takes precedence. Auto-Entity provides an enumeration of pre-defined ages, for convenience:
Minute
Hour
QuarterDay
The enum is numeric, and as such may be combined with basic math to produce N number of minutes, days, etc.:
LoadPageIfNecessary
LoadRangeIfNecessary
HalfDay
Day
Week
export function provideAppStore(store: Store<any>) {
return store;
}
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, HttpClientModule, StateModule],
providers: [
{ provide: Customer, useClass: EntityService },
{
provide: NGRX_AUTO_ENTITY_APP_STORE,
useFactory: provideAppStore,
deps: [Store]
}
],
bootstrap: [AppComponent]
})
export class AppModule {}@Entity({
modelName: 'Customer',
defaultMaxAge: 180
})
export class Customer {}@Entity({
modelName: 'Customer',
defaultMaxAge: EntityAge.Minute * 3
})
export class Customer {}NgRx Auto-Entity ships with numerous utility functions to help you work with your auto entities. This includes utilities to retrieve the various entity names (model, plural, and uri), custom comparers, transforms, and more.
Some utility functions require you to pass in the model type. This is the class of the model that defines the entity, rather than instances of that class. This is due to the fact that entity metadata is attached to the model class via the @Entity and @Key decorators. Unless you explicitly convert your object instances to your model types using another utility function, generic plain old javascript objects will not directly include the necessary metadata required to determine much of the information these utility functions provide.
Maker of Entities!!
If you have read through the previous documentation, or used NgRx Auto-Entity for some time, you may have noticed the prevalence of the model type in method and function signatures throughout the library. This requirement on the model type (the class you define your entities with, the class you decorate with `@Entity`) is to ensure that the necessary metadata required for auto-entity to do its automatic magic is available.
Due to the lack of automatic typing, NgRx Auto-Entity requires, in most of its functions, methods and constructors, the model type itself (the class, not objects created from the class) to be provided. This ensures that Auto-Entity has access to the necessary metadata. Any time you need to retrieve the metadata for an entity, you can always find a function that accepts the model type, and possibly an entity instance, to retrieve that metadata, or some aspect of it (i.e. such as the entity key).
When creating an entity instance you will rarely be creating them from the model type itself. Instead, you will most likely create simple, plain old javascript objects, or POJOs for short. Something like this:
Such an object is prototyped only by Object itself, not your custom entity model type. If you were to try and use this model with any function or method in Auto-Entity that requires entity metadata (provided by the @Entity and @Key decorators), you would sadly discover that no such metadata exists. Not on your POJO object instance, anyway.
Not to fret, we have a solution to this small little problem. If you wish to make sure your entities are properly prototyped by your model types, you can call the makeEntity function. This function simply converts a POJO into a properly prototyped entity.
The `makeEntity` function is a curried function. This means, generally speaking, that it is a function who's arguments have been broken into subsequent function calls. It is also a higher order function, in that the outer function returns another function.
The outer function requires the model type as a parameter. It then returns another function that can make entities of that type. The makeEntity function is intended to be used in a particular manner, like so:
This approach allows you to create a maker function for specific entity types, and reuse those functions over and over within your application.
NgRx Auto-Entity actually makes it easier for you. Whenever you call buildState(), an entity maker function is created for you. You simply need to destructure it from the standard result:
const customer = {
name: 'John Doe',
email: 'john.doe@doesn-exist.com',
address: {
street1: '123 A Street',
city: 'A City',
state: 'OO',
zip: 12345
}
};const makeCustomer = makeEntity(Customer);
const customer1 = { ... };
const customer2 = { ... };
const customer1Entity = makeCustomer(customer1);
const customer2Entity = makeCustomer(customer2);const {
// ...
makeEntity: makeCustomer
} = buildState(Customer);Stuff about Things
import { Entity, Key } from '@briebug/ngrx-auto-entity';
@Entity({
modelName: 'Customer',
uriName: 'customers'
})
export class Customer {
import { Entity, Key } from '@briebug/ngrx-auto-entity';
@Entity({
modelName: 'Address',
uriName: 'addresses'
})
export class Address {
import { Key } from '@briebug/ngrx-auto-entity';
export type ISODate = string; // YYYY-MM-DDTHH:mm:ss-ZZ:zz
export type Never = 'never';
export enum OrderStatus {
PENDING = 'pending',
ONHOLD = 'on-hold',
PARTIAL = 'partial-fill',
FILLED = 'filled',
PARTSHIP = 'partial-shipped',
SHIPPED = 'shipped',
CLOSED = 'closed'
}
@Entity({
modelName: 'Order',
uriName: 'orders'
})
export class Order {
@Key id?: number;
purchaseOrderNo: string;
status: OrderStatus;
dateCreated: ISODate;
dateClosed: ISODate | Never;
history: OrderHistory[];
}
export class OrderHistory {
dateOfAction: ISODate;
action: string;
newStatus: OrderStatus;
}import { Key } from '@briebug/ngrx-auto-entity';
@Entity({
modelName: 'LineItem',
uriName: 'line-items'
})
export class LineItem {
@Key orderId: number;
@Key productId: number;
quantity: number;
isRush: boolean;
}Rogue UI
import { Component } from '@angular/core';
import { CustomerFacade } from '../facades';
@Component({
selector: 'app-customer-edit',
templateUrl: './customer-edit.component.html',
styleUrls:
<div class="customer-exit">
<div>
<h2>Edit Customer</h2>
<app-customer-edit-form #form
[customer]="customers.current$ | async"
(submitted)="modal.dismiss($event)"
(validated)="canSave = $event"
>
</app-customer-edit-form>
</div>
<div>
<button [disabled]="canSave" click="form.submit()">Save</span>
<button (click)="modal.dismiss()">Cancel</span>
</div>
</div>That junk don't belong here!
Once you have enhanced your facade with functionality that belongs in the facade and not the component, it's time to clean up your component. Using the new functionality we have implemented in our customer facade, our component can become reduced to a simpler form:
Encapsulate!
In our CustomerComponent, there are a few methods that exhibit "class envy" which is a kind of anti-pattern. For those not familiar with the concept, class envy occurs when methods of one class are more dependent on functionality within another class. It then becomes prudent to move the method into the class of envy, and if necessary parameterize any input required from the method's previous home.
Our CustomerComponent has two potential candidates for encapsulation within our CustomerFacade class: hasCustomer and onSave. We can easily move this functionality into our facade class and make these behaviors reusable in any component that may require interaction with customer entity state:
Getting started with NgRx Auto-Entity is easy!
If you are already familiar with NgRx, then adding Auto-Entity to your Angular application is very easy. There are four major steps required to add the module, create new entity state, and provide your entity services.
First things first, you must bring in the NgrxAutoEntityModule into your app module and call the forRoot() method to configure the library.
Update the application state interface.
Now that we have the standard initial implementation for NgRX and Auto-Entity in place, we need to wire our models into our state.
Note that we have added a new customer property to the IAppState interface of type IEntityState<Customer>, which we imported from @briebug/ngrx-auto-entity at the top of the file.
For most basic CRUD states, you will not need to implement any custom state interfaces, effects or reducers. This is the simplicity that NgRX Auto-Entity brings to the table!
Leverage the power of facades
Now that you have set up state for an entity, it is time to start using it. With NgRx Auto-Entity, if you leverage our pre-fabricated facades, we have made using state about as easy as it can get. Start by creating a facade class that derives from the facade base class generated by your call to buildState:
With your facade in hand, inject it into your component and use the facade to interact with your entities:
Note the changes here. We imported only the activated route and a facade into our component. Our component does not import any state-related types at all. No actions, no store, no app state interface, none of the usual suspects. All state interactions occur through the facade.
Simplifying Reactive State!
Entity facades include a considerable amount of ready-to-go functionality. Check out the advanced facade documentation here to learn more about everything our facades provide and how to extend them.
export class CustomerFacade extends CustomerFacadeBase {
constructor(store: Store<AppState>) {
super(Customer, store);
}
hasCustomer(id: number): Observable<boolean> {
return this.ids$.pipe(
map((ids: number[]) => ids.indexOf(id) > -1)
);
}
loadIfMissing(id: number): void {
this.hasCustomer(id)
.pipe(first())
.subscribe(exists =>
exists ? this.load(id) : false
);
}
save(customer: Customer): void {
if (updatedCustomer.id == null) {
this.customerFacade.create(customer); // Facades FTW!
} else {
this.customerFacade.update(customer); // Facades FTW!
}
}
}Learn about the required paradigm changes for Auto-Entity
For NgRX Auto-Entity to function properly, some minor changes will be required in the way you implement a couple standard bits of code. This includes service implementations and model implementations. These changes are not particularly significant, and for services we provide a lot of meta information to assist you in achieving whatever is necessary for your applications.
As models and services are standard elements of any Angular application, there should be no additional boilerplate code to implement here. These would be implemented regardless. However, be aware of the nuanced changes in how the model and service are used, and the explicit interface requirement for the service.
More significant changes occur when utilizing facades to interact with state. Facades encapsulate the complexities of @ngrx, presenting you with a simplified, logical and more standard API into working with your stateful data.
import { Component, Input, Output } from '@angular/core';
import { Customer } from '../models';
@Component({
selector: 'app-customers-edit-form',
templateUrl: './customers-edit-form.component.html',
styleUrls: ['./customers-edit-form.component.scss']
})
export class CustomersEditFormComponent implements OnChanges, OnInit {
@Input() customer: Customer;
@Output() submitted = new EventEmitter<Customer>();
@Output() validated = new EventEmitter<boolean>();
form: FormGroup;
constructor(private builder: FormBuilder) {}
ngOnInit(): void {
this.form = this.buildForm(this.builder);
this.form.statusChanges.pipe(
map(() => this.form.valid)
).subscribe(this.validated.emit);
}
ngOnChanges(): void {
if (this.customer) {
form.patchValue(customer);
}
}
buildForm(builder: FormBuilder): FormGroup {
return builder.group({
name: [null, [Validators.required, Validators.maxLength(30)]],
title: [null, [Validators.required, Validators.maxLength(60)]],
email: [null, [Validators.required, Validators.maxLength(35)]],
handles: builder.group({
twitter: [null, Validators.maxLength(50)],
facebook: [null, Validators.maxLenth(50)]
}),
address: builder.group({
city: [null, Validators.maxLength(50)],
state: [null, Validators.maxLength(2)],
zip: [null, [Validators.minLength(5), Validators.maxLength(10)]]
})
});
}
submit() {
if (this.form.invalid) {
return;
}
const updatedCustomer = {
...this.customer,
...this.form.value
};
this.submitted.emit(updatedCustomer);
}
}@Component({
selector: 'app-customer',
templateUrl: './customer.component.html',
styleUrls: ['./customer.component.scss']
})
export class CustomerComponent implements OnInit {
constructor(
private activatedRoute: ActivatedRoute,
public customerFacade: CustomerFacade // No store, no selectors!
) {}
ngOnInit() {
this.activatedRoute.paramMap.pipe(
filter(params => params.has('id')),
map(params => +params.get('id'))
).subscribe(id => {
this.customerFacade.selectByKey(id); // Facades FTW!
this.customerFacade.loadIfMissing(id); // Facades FTW!
});
}
}
Note the use of strictActionSerializability being set to false here. This is an important setting with NgRx Auto-Entity. This library uses special actions that reference classes, which are types. Not instances of those classes, which would be objects (data), but the classes themselves. Classes, being types, are not serializable, which prevents auto-entity actions from being compatible with strict action serializability in NgRx.
Providing the type allows Auto-Entity to gain rich knowledge about each entity, including any metadata/config you may attach to your entities with the @Entity and @Key decorators.
Before you can actually create your state, you will need to create your entity models, as you would normally do. Auto-Entity requires two small changes to how you create models for NgRx.
First, your models must be classes rather than interfaces (see advanced documentation for more info.) Second, your entity identity must be decorated with the @Key directive. For entities with composite keys, simply decorate each property that is part of the key.
You will also need to create an entity service to handle CRUD behavior for each entity. Entity services may be shared if your API uses a common pattern. Otherwise you may need to implement a service for each entity as you usually do with NgRx.
In the example above we have a simple shared entity service that supports a basic REST API where each entity conforms to a simple pattern:
/<rootUrl>/<entityName>[/<key>]
Auto-Entity provides basic entity metadata, such as the model name, in the entityInfo parameter of each entity service method.
Once you have created an entity service or services, you will need to provide them in your app module. With Auto-Entity, services must be provided in a slightly different manner than normal, to ensure that Auto-Entity is able to find entity services dynamically.
In our example here, we are sharing a single entity service, EntityService, for all entities. We must provide each model and useClass to specify the service class to use.
Finally, now that you have your models and have provided your entity services, you need to build your state for each model. Add a new state file for each model following the pattern depicted here:
Finally, include your entity states in the AppState interface, and your stub reducers in the action reducer map:
With that, you are ready to start using your automatic entity state! Continue on to the next section to learn how.
appReducer constant to include the customer property (with the customer reducer function as the value). It's important that both properties have the same name.// ... imports ...
import { Customer } from '../models';
import { customerReducer } from './customer.state.ts'
export interface AppState {
customer: IEntityState<Customer>;
}
export const appReducer: ActionReducerMap<AppState> = {
customer: customerReducer
};import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from 'state/app.state';
import { CustomerFacadeBase } from '../state/customer.state';
import { Customer } from '../models';
@Injectable({ providedIn: 'root' })
export class CustomerFacade extends CustomerFacadeBase {
constructor(store: Store<AppState>) {
super(Customer, store);
}
// TODO: Extend your facade's functionality here!
}import { ActivatedRoute } from '@angular/router';
// ... other imports ...
import { CustomerFacade } from '../facades';
import { Customer } from '../models';
@Component({
selector: 'app-customer',
templateUrl: './customer.component.html',
styleUrls: ['./customer.component.scss']
})
export class CustomerComponent implements OnInit {
customer$: Observable<Customer>;
constructor(
private activatedRoute: ActivatedRoute,
private customerFacade: CustomerFacade // No store, no selectors!
) {}
ngOnInit() {
this.customer$ = this.activatedRoute.paramMap.pipe(
filter(params => params.has('id')),
map(params => +params.get('id')),
tap(id => {
this.customerFacade.selectByKey(id); // Facades FTW!
this.hasCustomer(id)
.pipe(first())
.subscribe(exists => {
if (!exists) {
this.customerFacade.load(id); // Facades FTW!
}
});
}),
switchMap(() => this.customerFacade.current$) // Facades FTW!
);
}
hasCustomer(id: number): Observable<boolean> {
return this.customerFacade.ids$.pipe( // Facades FTW!
map((ids: number[]) => ids.indexOf(id) > -1)
);
}
onSave(customer: Customer): void {
if (customer.id == null) {
this.customerFacade.create(customer); // Facades FTW!
} else {
this.customerFacade.update(customer); // Facades FTW!
}
}
}Create application reducer and meta reducer.
Also like normal NgRX apps, add a reducer map to your app.state.ts file. We recommend creating this file in a root state directory, located at src/app/state.
import { ActionReducerMap, MetaReducer } from '@ngrx/store';
import { IEntityState } from '@briebug/ngrx-auto-entity';
import { environment } from '../../environments/environment';
export interface AppState {
// todo: add each entity state interface to our application state interface
}
export const appReducer: ActionReducerMap<AppState> = {
// todo: add each entity reducer
};In versions of NgRx Auto-Entity prior to v0.2, the developer was also responsible for including the autoEntityMetaReducer in the app meta reducers collection. As of version 0.2 of the library, import of the NgrxAutoEntityModule with the .forRoot() call is all that is necessary to include the meta reducer.
If you are upgrading from a version prior to v0.2, you should remove the autoEntityMetaReducer from your app meta reducers!
Finally, in order for NgRx Auto-Entity to find the entity service you just created, you must provide it in your application state. Providing entity services is slightly different than a normal provider, which simply provides itself as the service class.
import { NgModule } from '@angular/core';
import { StateModule } from './state';
import { Customer } from './models';
import { EntityService } from './services';
@NgModule({
imports: [BrowserModule, StateModule],
providers: [
{ provide: Customer, useClass: EntityService }
]
})
export class AppModule {}Here, we have provided the model type as the provider, and specified the EntityService class as the actual service class via useClass. This is the simplest model for using Auto-Entity, and for simple backend APIs that follow a common pattern, a single service like this may be reused for any number of entities.
provide: Model, useClass: EntityService
In the event that you have a more complex backend API, or even must integrate with many backend APIs, you may create custom entity services for each entity if necessary, and provide each model with its own unique service class following the same pattern as above.
Finally, import the StateModule we created earlier into your root AppModule to bring in all of your root state, including NgRx Auto-Entity.
And, with that, you are done! You can now start using your entity in your app.
Create service for handling data interactions with server
In our example we are creating a service for persisting entities via a simple REST API. As such, we've created a new entity.service.ts file and defined an injectable EntityService class.
It's important that each entity service implement the IAutoEntity interface. This interface supports the following methods:
load()
loadAll()
loadMany()
loadPage()
loadRange()
create()
createMany()
update()
updateMany()
replace()
replaceMany()
delete()
deleteMany()
These methods perform the CRUD operators for entity retrieval and persistence.
To create an entity service, we must import the IAutoEntityService and IEntityInfo interfaces. The entity service must implement the IAutoEntityService interface. The IEntityInfo object provides metadata about the entities, which can be used to help build urls if necessary.
Finally, we implement each of the necessary methods for retrieving and persisting an entities.
<form [formGroup]="form">
<div>
<label>Customer Name</label>
<input id="name" formControlName="name">
<i class="text-red"
*ngIf="name.invalid && (name.dirty || name.touched)">
Please fill out this field.
</i>
</div>
<div>
<label>title</label>
<input id="title" formControlName="title">
<i class="text-red"
*ngIf="title.invalid && (title.dirty || title.touched)">
Please fill out this field.
</i>
</div>
<div>
<label>Email Address</label>
<input id="email" formControlName="email" type="email">
<i class="text-red"
*ngIf="email.invalid && (email.dirty || email.touched)">
Please fill out this field.
</i>
</div>
</div>
<div>
<div>
<label>City</label>
<input id="city" formControlName="city">
</div>
<div>
<label>State</label>
<select id="state" formControlName="state">
<option value="AZ">Arizona</option>
<option value="CO">Colorado</option>
<option value="NM">New Mexico</option>
<option value="UT">Utah</option>
</select>
</div>
<div>
<label>Zip</label>
<input id="zip" formControlName="zip">
</div>
</div>
</form><div class="customers">
<div>
<h2>Customer</h2>
<app-customer-form
#customerForm
[customer]="customerFacade.current$ | async"
(saved)="customerFacade.save($event)">
</app-customer-form>
<div>
<button routerLink="/customers">Cancel</button>
<button (click)="customerForm.save()" [disabled]="!customerForm.valid">
Save
</button>
</div>
</div>
</div>import { Key } from '@briebug/ngrx-auto-entity';
export class Customer {
@Key id: number;
name: string;
address: Address;
}import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { IAutoEntityService, IEntityInfo } from '@briebug/ngrx-auto-entity';
import { environment } from '../../environments/environment';
@Injectable()
export class EntityService implements IAutoEntityService<any> {
constructor(private http: HttpClient) {
}
load(entityInfo: IEntityInfo, id: any): Observable<any> {
return this.http.get<any>(
`${environment.rootUrl}/${entityInfo.modelName}/${id}`
);
}
loadAll(entityInfo: IEntityInfo): Observable<any[]> {
return this.http.get<any[]>(
`${environment.rootUrl}/${entityInfo.modelName}`
);
}
create(entityInfo: IEntityInfo, entity: any): Observable<any> {
return this.http.post<any>(
`${environment.rootUrl}/${entityInfo.modelName}`,
entity
);
}
update(entityInfo: IEntityInfo, entity: any): Observable<any> {
return this.http.patch<any>(
`${environment.rootUrl}/${entityInfo.modelName}/${entity.id}`,
entity
);
}
delete(entityInfo: IEntityInfo, entity: any): Observable<any> {
return this.http.delete<any>(
`${environment.rootUrl}/${entityInfo.modelName}/${entity.id}`
).pipe(map(() => entity));
}
}import { NgrxAutoEntityModule } from '@briebug/ngrx-auto-entity';
import { Customer, Order } from './models';
import { EntityService } from './services';
// ... other imports ...
@NgModule({
imports: [
BrowserModule,
StoreModule.forRoot(...),
EffectsModule.forRoot(...),
NgrxAutoEntityModule.forRoot()
],
// Add a provider for each model, mapping to the relevant entity service:
providers: [
{ provide: Customer, useClass: EntityService },
{ provide: Order, useClass: EntityService }
]
})
export class AppModule {}import { buildState, IEntityState } from '@briebug/ngrx-auto-entity';
import { Customer } from '../models'
export const { initialState, facade: CustomerFacadeBase } = buildState(Customer);
// A "stub" reducer is required to support AOT
export function customerReducer(state = initialState): IEntityState<Customer> {
return state;
}import { IEntityState } from '@briebug/ngrx-auto-entity';
import { Customer, Order } from 'models';
import { customerReducer } from './customer.state.ts'
import { orderReducer } from './order.state.ts'
export interface IAppState {
// ... other states ...
customer: IEntityState<Customer>;
order: IEntityState<Order>;
}
export type AppState = IAppState;
export const appReducer: ActionReducerMap<AppState> = {
// ... other reducers ...
customer: customerReducer
order: orderReducer
};import { NgrxAutoEntityModule } from '@briebug/ngrx-auto-entity';
// ... other imports ...
@NgModule({
imports: [
BrowserModule,
StoreModule.forRoot(appReducer, {
metaReducers: appMetaReducers,
runtimeChecks: {
// Auto-Entity includes classes in its actions:
strictActionSerializability: false
}
}),
EffectsModule.forRoot([]),
NgrxAutoEntityModule.forRoot() // Add this!
]
})
export class AppModule {} <div class="customers">
<div>
<h2>Customer</h2>
<app-customer-form
#customerForm
[customer]="customer$ | async"
(saved)="onSave($event)">
</app-customer-form>
<div>
<button routerLink="/customers">Cancel</button>
<button (click)="customerForm.save()" [disabled]="!customerForm.valid">
Save
</button>
</div>
</div>
</div>import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { IAutoEntityService, IEntityInfo } from '@briebug/ngrx-auto-entity';
import { environment } from '../../environments/environment';
@Injectable()
export class EntityService implements IAutoEntityService<any> {
constructor(private http: HttpClient) {
}
load(entityInfo: IEntityInfo, id: any): Observable<any> {
return this.http.get<any>(
`${environment.rootUrl}/${entityInfo.modelName}/${id}`
);
}
loadAll(entityInfo: IEntityInfo): Observable<any[]> {
return this.http.get<any[]>(
`${environment.rootUrl}/${entityInfo.modelName}`
);
}
create(entityInfo: IEntityInfo, entity: any): Observable<any> {
return this.http.post<any>(
`${environment.rootUrl}/${entityInfo.modelName}`,
entity
);
}
update(entityInfo: IEntityInfo, entity: any): Observable<any> {
return this.http.patch<any>(
`${environment.rootUrl}/${entityInfo.modelName}/${entity.id}`,
entity
);
}
delete(entityInfo: IEntityInfo, entity: any): Observable<any> {
return this.http.delete<any>(
`${environment.rootUrl}/${entityInfo.modelName}/${entity.id}`
).pipe(map(() => entity));
}
}Supported Versions
@angular/common
8.0.0
8.x, 9.x, 10.x*
@angular/core
8.0.0
8.x, 9.x, 10.x*
@ngrx/effects
8.0.0
8.x, 9.x, 10.x
@ngrx/store
8.0.0
8.x, 9.x, 10.x
rxjs
6.0.0
6.x
A quick note about Angular 10. While NgRx Auto-Entity has been tested with Angular 10, not every use case exhibited flawless support. There have been use cases where some uses of Auto-Entity with Angular 10 have worked just fine, and others that seemed to have issues. So we tentatively support Angular 10, but official guaranteed support is still pending. As such, we do not recommend you use Auto-Entity with Angular 10 in production environments at this time.
Further testing of Auto-Entity with Angular 10 is ongoing, and outstanding issues will be resolved as they are discovered. One of our key goals with Auto-Entity is to remain backwards compatible with prior versions of Angular as best we can. Due to certain TypeScript requirements, our minimum supported versions are Angular 8 and NgRx 8. We hope this will expand Auto-Entity viability to the broadest range of enterprise customers with established Angular projects, as well as cutting edge projects on the bleeding edge. Our compatibility goals may require more complex build, packaging and deployment in the future, and there may come a time when our minimum versions must change to keep abreast of the Angular platform.
For now, Angular 10 support is tentatively there. If you run into issues, let us know by opening a ticket in our GitHub repo:
Module
Minimum Required Version
@ngrx@ngrxAuto-Entity wires in ready-made effects to handle standard behaviors, as well as provides a core meta reducer that reduces all entity state managed by this library. For most use cases, the implementation burden for you the developer will be limited to basic initial @ngrx setup, creation of models for each entity & creation of services for each entity. This is functionality that would need to be implemented by you the developer regardless.
Where we save you time is by eliminating the need to implement unique sets of CRUD actions for each and every entity, as well as the side effects that handle standard CRUD behavior for each and every entity. This can be a significant amount of development effort for each @ngrx application, and we hope the savings we offer will allow you to focus on solving the critical business needs of your customers.
Further, Auto-Entity generates pre-fabricated Facades around your entity state, allowing you to extract all @ngrx and other state related code from your components, encapsulate them in a facade layer that presents a much simpler, more logical and easier to comprehend and use API for your entities.
Learn how to make NgRX Auto-Entity for you
NgRX Auto-Entity aims to provide a seamless means of utilizing a standard set entity actions and effects with minimal repetitive code requirements, while preserving the fundamental nature of NgRX itself. This library is not a replacement for or alternative to NgRX. It works within the standard paradigm that NgRX has set forth, making use of actions, reducers & effects like any other NgRX application.
Understand changes to service implementation
With a normal @ngrx application, you are free to implement services however you see fit. There are no explicit requirements by @ngrx, since you are also on the hook to implement the effects as well, and it is your own effects that call your services.
NgRX Auto-Entities dynamic nature requires that data services implement the IAutoEntityService<TModel> interface. This interface defines the contract by which Auto-Entity interacts with your services. This interface is described as follows:
This interface defines a different method that corresponds to each different kind of initiating generic action provided as a part of the NgRX Auto-Entity library. Every method provides entityInfo
npm i @briebug/ngrx-auto-entity@~0.5.0yarn add @briebug/ngrx-auto-entity@~0.5.0npm i @angular/{common,core}@^8.0 @ngrx/{effects,store}@^8.0 rxjs@^6yarn add @angular/{common,core}@^8.0 @ngrx/{effects,store}@^8.0 rxjs@^6What Auto-Entity does do is provide a set of ready-made, generic actions for handling all of the standard CRUD operations for entities, so you neither have to write nor generate any of that code yourself. Auto-Entity presents a flexible framework that you may use in its entirety for all of your entity needs, or use piecemeal as necessary in order to achieve your specific goals.
High Performance and Efficiency
Auto-Entity has been implemented with high performance in mind, and is capable of reducing very large data sets. We have tested NgRx Auto-Entity with up to millions of records in state, while adding millions more, with reduction times in the fractions of a second. We do not generally recommend loading such significant volumes of data into your Angular applications, and for more common usage at much lower volumes, you should encounter no performance limitations with Auto-Entity.
Should your application need to work with millions of entities in state in the browser, NgRx Auto-Entity has been tested under high entity volumes. With tens to hundreds of thousands of entities, reduction times should be sub-second, although subject to memory limitations. For millions of entities, performance may vary with memory, and under limited memory situations reductions of very large volumes of entities may require more time. Tests with MacBook Pros with 16 gigabytes of ram, reduction of 2 million entities takes 0.5 to 2 seconds under normal usage.