NgRx/Entity Example
January 06, 2018
This page will walk through NgRx/Entity example.
@ngrx/entity
library manages collections of entities. It provides APIs to manipulate and query entity collections. @ngrx/entity
library helps to reduce boilerplate coding of reducers that manage a collections of entities. @ngrx/entity
also sorts the collection for the given entity property. When we disable sorting, @ngrx/entity
provides better performance in CRUD operations for managing entity collections. @ngrx/entity
provides EntityState
, EntityAdapter
interfaces and createEntityAdapter
method. Entity state is created by extending EntityState
interface. EntityAdapter
is instantiated using createEntityAdapter
method. EntityAdapter
provides methods to add, update and remove entities from collections. It also provides getInitialState
and getSelectors
methods. getInitialState
provides initial state of our entity state. getSelectors
is used to create selectors to query entity collections. Here on this page we will provide complete example to add, update, remove and select the entities from the collections step by step.
Contents
- 1. Technologies Used
- 2. Install NgRx
- 3. Entity State
- 4. Entity Adapter
- 5. addOne, addMany and addAll
- 6. updateOne and updateMany using Update Type
- 7. removeOne, removeMany and removeAll
- 8. Entity Selectors
- 9. Select by Id
- 10. Angular In-Memory Web API
- 11. Complete Example
- 12. Test Application
- 13. References
- 14. Download Source Code
1. Technologies Used
Find the technologies being used in our example.1. Angular 5.0.0
2. Angular CLI 1.6.3
3. NgRx 4.1.1
4. TypeScript 2.4.2
5. Node.js 6.11.0
6. NPM 3.10.10
2. Install NgRx
We will install Entity, Store and Effects using NPM as following.1. To install
@ngrx/entity
, run following command.
npm install @ngrx/entity --save
@ngrx/store
, run following command.
npm install @ngrx/store --save
@ngrx/effects
, run following command.
npm install @ngrx/effects --save
npm i angular-in-memory-web-api@0.5.3 --save
Find the print screen of sample output of our application.

3. Entity State
NgRx providesEntityState
interface that is predefined generic interface for a given entity collection. EntityState
has following attributes.
ids : Array of all primary ids in collection.
entities : A dictionary of collection items indexed by primary id.
Our State needs to extend
EntityState
to use it. Suppose we have an interface as following.
article.ts
export interface Article { id: string; title: string; category: string; }
EntityState
as following.
app.states.ts
export interface ArticleState extends EntityState<Article> { //Other entity state properties }
4. Entity Adapter
NgRx hasEntityAdapter
interface that provides many collection methods for managing the entity state. It is instantiated using NgRx createEntityAdapter
method as following.
article.adapter.ts
import { EntityAdapter, createEntityAdapter } from '@ngrx/entity'; export const adapter: EntityAdapter<Article> = createEntityAdapter<Article>();
4.1 createEntityAdapter
createEntityAdapter
method instantiates generic EntityAdapter
for a single entity state collection as given below.
export const adapter: EntityAdapter<Article> = createEntityAdapter<Article>();
createEntityAdapter
method can accept optional argument i.e. an object with properties selectId
and sortComparer
.
selectId : This is a method using which NgRx entity selects primary id for the collection.
sortComparer : This is a comparer function used to sort the collection. We should use this property if we want to sort the collection before displaying. If we only want high performance and not sorting such as in CRUD operation, we should set
sortComparer
value as false.
4.2 Sort Comparer
Here we will provide code snippet how to usesortComparer
with createEntityAdapter
. First we will define a comparer method. In our example we have article entity and suppose we want to sort collection on the basis of category of artcle. We will create a method to sort entity on the basis of article category. Then we will assign this comparer method to sortComparer
property while instantiating EntityAdapter
using createEntityAdapter
. Find the code snippet.
article.adapter.ts
export function sortByCategory(ob1: Article, ob2: Article): number { return ob1.category.localeCompare(ob2.category); } export const adapter: EntityAdapter<Article> = createEntityAdapter<Article>({ sortComparer: sortByCategory });
sortComparer
property.
export const adapter: EntityAdapter<Article> = createEntityAdapter<Article>({ sortComparer: false });
sortComparer
property.
4.3 Entity Adapter Methods
Here we will discussEntityAdapter
methods. It has getInitialState
method to get initial state of the entity, getSelectors
method to get entity selectors. EntityAdapter
extends EntityStateAdapter
and inherits its methods to add, update and remove entities. Find the methods of EntityAdapter
.
4.3.1 getInitialState
getInitialState
method returns initial state of our entity state.
article.reducer.ts
export const initialState: ArticleState = adapter.getInitialState({ //Initialize other entity state properties });
4.3.2 getSelectors
getSelectors
method returns NgRx EntitySelectors
that provides functions for selecting information from the collection of entities. The functions of EntitySelectors
are as follows.
selectIds: Selects array of ids.
selectEntities: Selects the dictionary of entities. We can use it to fetch entity by id.
selectAll: Selects array of all entities.
selectTotal: Selects the total count of entities.
In our example we will work with articles. Find the sample entity selectors using
getSelectors
for article.
article.adapter.ts
export const { selectIds: selectArticleIds, selectEntities: selectArticleEntities, selectAll: selectAllArticles, selectTotal: articlesCount } = adapter.getSelectors();
4.3.3 Add, Update and Remove
EntityAdapter
extends EntityStateAdapter
and inherits following methods.
addOne : It adds one entity to the collection.
addMany : It adds multiple entities to the collection.
addAll : It replaces the collection with given collection.
updateOne : It updates one entity in the collection against an id using NgRx
Update
type.
updateMany : It updates multiple entities in the collection against given ids using array of
Update
type.
removeOne : It removes one entity from the collection for the given id.
removeMany : It removes multiple entities from the collection for the given array of ids.
removeAll : It removes all entities from collection.
5. addOne, addMany and addAll
Here we will discussEntityAdapter
methods to add entities. These methods are addOne
, addMany
and addAll
. addOne
adds one entity to the collection, addMany
adds many entities to the collection. addAll
first clears the collection and then adds the given entities. It means addAll
completely replaces the existing collection entities with given entities.
We need to pass arguments to these methods as following.
addOne : Pass instance of entity and state.
addMany : Pass array of entities and state.
addAll : Pass array of entities and state.
Now find the steps to use
addOne
, addMany
and addAll
.
Step-1: Creating actions
article.actions.ts
//Action for addOne method. export class AddArticle implements Action { readonly type = ArticleActionTypes.ADD_ARTICLE; constructor(public payload: { article: Article }) {} } //Action for addMany method. export class AddArticles implements Action { readonly type = ArticleActionTypes.ADD_ARTICLES; constructor(public payload: { articles: Article[] }) {} } //Action for addAll method. export class LoadArticlesSuccess implements Action { readonly type = ArticleActionTypes.LOAD_ALL_ARTICLES_SUCCESS; constructor(public payload: { articles: Article[] }) {} }
article.reducer.ts
import * as fromAdapter from './article.adapter'; import * as fromActions from '../actions/article.actions'; export function reducer(state = initialState, action: fromActions.ARTICLE_ACTIONS): ArticleState { switch(action.type) { case fromActions.ArticleActionTypes.ADD_ARTICLE: { return fromAdapter.adapter.addOne(action.payload.article, state); } case fromActions.ArticleActionTypes.ADD_ARTICLES: { return fromAdapter.adapter.addMany(action.payload.articles, state); } case fromActions.ArticleActionTypes.LOAD_ALL_ARTICLES_SUCCESS: { return fromAdapter.adapter.addAll(action.payload.articles, state); } ------ } }
article.component.ts
addArticle(data: Article) { this.store.dispatch(new fromActions.AddArticle({ article: data })); } addArticles(data: Article[]) { this.store.dispatch(new fromActions.AddArticles({ articles: data })); }
addAll
method, we are fetching data from HTTP. We will create NgRx Effect to dispatch LoadArticlesSuccess
action as given below.
article.effects.ts
@Effect() loadAllArticles$: Observable<Action> = this.actions$ .ofType(fromActions.ArticleActionTypes.LOAD_ALL_ARTICLES) .switchMap(() => this.articleService.getAllArticles() .map(data => new fromActions.LoadArticlesSuccess({ articles: data })) );
6. updateOne and updateMany using Update Type
To update entity in collection,EntityAdapter
provides updateOne
and updateMany
methods. updateOne
updates one entity and updateMany
updates many entities. updateOne
and updateMany
methods accept argument of NgRx Update
type and state.
Update
type has two properties.
id: Id of entity which needs to be updated.
changes: Modified entity.
updateOne
and updateMany
methods will accept arguments as follows.
updateOne: Pass instance of
Update
and state.
updateMany: Pass array of
Update
and state.
Now find the steps to use
updateOne
and updateMany
.
Step-1: Creating actions
article.actions.ts
//Action for updateOne method. export class UpdateArticle implements Action { readonly type = ArticleActionTypes.UPDATE_ARTICLE; constructor(public payload: { article: Update<Article> }) {} } //Action for updateMany method. export class UpdateArticles implements Action { readonly type = ArticleActionTypes.UPDATE_ARTICLES; constructor(public payload: { articles: Update<Article>[] }) {} }
article.reducer.ts
import * as fromAdapter from './article.adapter'; import * as fromActions from '../actions/article.actions'; export function reducer(state = initialState, action: fromActions.ARTICLE_ACTIONS): ArticleState { switch(action.type) { case fromActions.ArticleActionTypes.UPDATE_ARTICLE: { return fromAdapter.adapter.updateOne(action.payload.article, state); } case fromActions.ArticleActionTypes.UPDATE_ARTICLES: { return fromAdapter.adapter.updateMany(action.payload.articles, state); } ------ } }
article.component.ts
updateArticle(data: Article) { this.store.dispatch(new fromActions.UpdateArticle({ article: {id: data.id, changes: data}})); } updateArticles(data: Article[]) { let allUpdates = data.map(article => Object.assign({}, {id: article.id, changes: article})); this.store.dispatch(new fromActions.UpdateArticles({ articles: allUpdates })); }
7. removeOne, removeMany and removeAll
To remove entities from collection,EntityAdapter
provides removeOne
, removeMany
and removeAll
methods. removeOne
removes one entity, removeMany
removes many entities and removeAll
clears the collection by removing all entities.
These methods accepts arguments as following.
removeOne: Pass entity id and state.
removeMany: Pass array of entity ids and state.
removeAll: Pass state.
Now find the steps to use
removeOne
, removeMany
and removeAll
methods.
Step-1: Creating actions
article.actions.ts
//Action for removeOne method. export class RemoveArticle implements Action { readonly type = ArticleActionTypes.REMOVE_ARTICLE; constructor(public payload: { id: string }) {} } //Action for removeMany method. export class RemoveArticles implements Action { readonly type = ArticleActionTypes.REMOVE_ARTICLES; constructor(public payload: { ids: string[] }) {} } //Action for removeAll method. export class ClearArticles implements Action { readonly type = ArticleActionTypes.CLEAR_ARTICLES; }
article.reducer.ts
import * as fromAdapter from './article.adapter'; import * as fromActions from '../actions/article.actions'; export function reducer(state = initialState, action: fromActions.ARTICLE_ACTIONS): ArticleState { switch(action.type) { case fromActions.ArticleActionTypes.REMOVE_ARTICLE: { return fromAdapter.adapter.removeOne(action.payload.id, state); } case fromActions.ArticleActionTypes.REMOVE_ARTICLES: { return fromAdapter.adapter.removeMany(action.payload.ids, state); } case fromActions.ArticleActionTypes.CLEAR_ARTICLES: { return fromAdapter.adapter.removeAll(state); } ------ } }
article.component.ts
removeArticle(articleId: string) { this.store.dispatch(new fromActions.RemoveArticle({ id: articleId })); } removeArticles(articleIds: string[]) { this.store.dispatch(new fromActions.RemoveArticles({ ids: articleIds })); } clearAllArticles() { this.store.dispatch(new fromActions.ClearArticles()); }
8. Entity Selectors
Here we will create our selectors to fetch entities from collection. We have already discussed to initializeEntitySelectors
above using getSelectors
method of EntityAdapter
. We initialize EntitySelectors
as following.
article.adapter.ts
export const { selectIds: selectArticleIds, selectEntities: selectArticleEntities, selectAll: selectAllArticles, selectTotal: articlesCount } = adapter.getSelectors();
article.reducer.ts
import * as fromAdapter from './article.adapter'; export const getArticleState = createFeatureSelector<ArticleState>('articleState'); export const selectArticleIds = createSelector(getArticleState, fromAdapter.selectArticleIds); export const selectArticleEntities = createSelector(getArticleState, fromAdapter.selectArticleEntities); export const selectAllArticles = createSelector(getArticleState, fromAdapter.selectAllArticles); export const articlesCount = createSelector(getArticleState, fromAdapter.articlesCount);
ngOnInit() { this.count$ = this.store.select(fromReducer.articlesCount); this.allArticles$ = this.store.select(fromReducer.selectAllArticles); this.articleIds$ = this.store.select(fromReducer.selectArticleIds); ------ }
9. Select by Id
We will discuss here how to select entity by id from collection. We need to follow below steps.Step-1: Create a property for id in our entity state. In our example we have state for article.
app.states.ts
export interface ArticleState extends EntityState<Article> { selectedArticleId: string | number | null; }
selectedArticleId
for which we will fetch entity.
Step-2: Initialize
selectedArticleId
while creating initial state.
article.reducer.ts
import * as fromAdapter from './article.adapter'; export const initialState: ArticleState = fromAdapter.adapter.getInitialState({ selectedArticleId: null });
article.actions.ts
export class SelectArticle implements Action { readonly type = ArticleActionTypes.SELECT_ARTICLE; constructor(public payload: { articleId: string }) {} }
article.reducer.ts
export function reducer(state = initialState, action: fromActions.ARTICLE_ACTIONS): ArticleState { switch(action.type) { case fromActions.ArticleActionTypes.SELECT_ARTICLE: { return Object.assign({ ...state, selectedArticleId: action.payload.articleId }); } ------ } }
SelectArticle
action we have set value to selectedArticleId
property of our entity state. We should reset its value while calling removeAll
method of EntityAdapter
.
article.reducer.ts
export function reducer(state = initialState, action: fromActions.ARTICLE_ACTIONS): ArticleState { switch(action.type) { case fromActions.ArticleActionTypes.SELECT_ARTICLE: { return Object.assign({ ...state, selectedArticleId: action.payload.articleId }); } case fromActions.ArticleActionTypes.CLEAR_ARTICLES: { return fromAdapter.adapter.removeAll({ ...state, selectedArticleId: null }); } ------ } }
article.reducer.ts
export const getArticleState = createFeatureSelector<ArticleState>('articleState'); export const getSelectedArticleId = (state: ArticleState) => state.selectedArticleId; export const selectCurrentArticleId = createSelector(getArticleState, getSelectedArticleId); export const selectCurrentArticle = createSelector( selectArticleEntities, selectCurrentArticleId, (articleEntities, articleId) => articleEntities[articleId] );
createSelector
method, the arguments (articleEntities, articleId)
will be filled in following way.
selectArticleEntities
will assign value to articleEntities
.
selectCurrentArticleId
will assign value to articleId
And finally
articleEntities[articleId]
will return entity for the given id.
Step-6: Use selector in component to get entity by id.
ngOnInit() { this.articleById$ = this.store.select(fromReducer.selectCurrentArticle); ------ }
10. Angular In-Memory Web API
We are using In-Memory Web API to load data into entity collection usingaddAll
method of EntityAdapter
. To work with Angular In-Memory Web API, find the steps.
Step-1: Create a class that will implement
InMemoryDbService
. Define createDb()
method with some dummy data.
test-data.ts
import { InMemoryDbService } from 'angular-in-memory-web-api'; export class TestData implements InMemoryDbService { createDb() { let articleDetails = [ {id: 'j1', title: 'Core Java Tutorial', category: 'Java'}, {id: 'a1', title: 'Angular Tutorial', category: 'Angular'}, ]; return { articles: articleDetails }; } }
/api/articles
InMemoryWebApiModule
in application module and configure TestData
class as following.
import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; import { TestData } from './test-data'; @NgModule({ imports: [ ------ InMemoryWebApiModule.forRoot(TestData) ], ------ }) export class AppModule { }
11. Complete Example
Find the project structure of our application.my-app | |--src | | | |--app | | | | | |--models | | | | | | | |--article.ts | | | | | |--actions | | | | | | | |--article.actions.ts | | | | | |--states | | | | | | | |--app.states.ts | | | | | |--reducers | | | | | | | |--article.adapter.ts | | | |--article.reducer.ts | | | |--index.ts | | | | | |--effects | | | | | | | |--article.effects.ts | | | | | |--services | | | | | | | |--article.service.ts | | | | | |--components | | | | | | | |--article.component.html | | | |--article.component.ts | | | | | |--app.component.ts | | |--app.module.ts | | |--test-data.ts | | | |--main.ts | |--index.html | |--styles.css | |--node_modules |--package.json
article.ts
export class Article { id = ''; title = ''; category = ''; }
import { Action } from '@ngrx/store'; import { Update } from '@ngrx/entity/src/models'; import { Article } from '../models/article'; export enum ArticleActionTypes { ADD_ARTICLE = '[ARTICLE] Add Article', ADD_ARTICLES = '[ARTICLE] Add Articles', UPDATE_ARTICLE = '[ARTICLE] Update Article', UPDATE_ARTICLES = '[ARTICLE] Update Articles', REMOVE_ARTICLE = '[ARTICLE] Remove Article', REMOVE_ARTICLES = '[ARTICLE] Remove Articles', CLEAR_ARTICLES = '[ARTICLE] Clear Articles', LOAD_ALL_ARTICLES = '[ARTICLE] Load All Articles', LOAD_ALL_ARTICLES_SUCCESS = '[ARTICLE] Load All Articles Success', SELECT_ARTICLE = '[ARTICLE] Article By Id' } export class AddArticle implements Action { readonly type = ArticleActionTypes.ADD_ARTICLE; constructor(public payload: { article: Article }) {} } export class AddArticles implements Action { readonly type = ArticleActionTypes.ADD_ARTICLES; constructor(public payload: { articles: Article[] }) {} } export class UpdateArticle implements Action { readonly type = ArticleActionTypes.UPDATE_ARTICLE; constructor(public payload: { article: Update<Article> }) {} } export class UpdateArticles implements Action { readonly type = ArticleActionTypes.UPDATE_ARTICLES; constructor(public payload: { articles: Update<Article>[] }) {} } export class RemoveArticle implements Action { readonly type = ArticleActionTypes.REMOVE_ARTICLE; constructor(public payload: { id: string }) {} } export class RemoveArticles implements Action { readonly type = ArticleActionTypes.REMOVE_ARTICLES; constructor(public payload: { ids: string[] }) {} } export class ClearArticles implements Action { readonly type = ArticleActionTypes.CLEAR_ARTICLES; } export class LoadArticles implements Action { readonly type = ArticleActionTypes.LOAD_ALL_ARTICLES; } export class LoadArticlesSuccess implements Action { readonly type = ArticleActionTypes.LOAD_ALL_ARTICLES_SUCCESS; constructor(public payload: { articles: Article[] }) {} } export class SelectArticle implements Action { readonly type = ArticleActionTypes.SELECT_ARTICLE; constructor(public payload: { articleId: string }) {} } export type ARTICLE_ACTIONS = AddArticle | AddArticles | UpdateArticle | UpdateArticles | RemoveArticle | RemoveArticles | ClearArticles | LoadArticlesSuccess | SelectArticle;
import { Article } from '../models/article'; import { EntityState } from '@ngrx/entity'; export interface AppState { articleState: ArticleState; } export interface ArticleState extends EntityState<Article> { selectedArticleId: string | number | null; }
import { EntityAdapter, createEntityAdapter } from '@ngrx/entity'; import { Article } from '../models/article'; export function sortByCategory(ob1: Article, ob2: Article): number { return ob1.category.localeCompare(ob2.category); } export const adapter: EntityAdapter<Article> = createEntityAdapter<Article>({ sortComparer: sortByCategory }); export const { selectIds: selectArticleIds, selectEntities: selectArticleEntities, selectAll: selectAllArticles, selectTotal: articlesCount } = adapter.getSelectors();
import { createFeatureSelector, createSelector } from '@ngrx/store'; import * as fromActions from '../actions/article.actions'; import { ArticleState } from '../states/app.states'; import * as fromAdapter from './article.adapter'; export const initialState: ArticleState = fromAdapter.adapter.getInitialState({ selectedArticleId: null }); export function reducer(state = initialState, action: fromActions.ARTICLE_ACTIONS): ArticleState { switch(action.type) { case fromActions.ArticleActionTypes.ADD_ARTICLE: { return fromAdapter.adapter.addOne(action.payload.article, state); } case fromActions.ArticleActionTypes.ADD_ARTICLES: { return fromAdapter.adapter.addMany(action.payload.articles, state); } case fromActions.ArticleActionTypes.UPDATE_ARTICLE: { return fromAdapter.adapter.updateOne(action.payload.article, state); } case fromActions.ArticleActionTypes.UPDATE_ARTICLES: { return fromAdapter.adapter.updateMany(action.payload.articles, state); } case fromActions.ArticleActionTypes.REMOVE_ARTICLE: { return fromAdapter.adapter.removeOne(action.payload.id, state); } case fromActions.ArticleActionTypes.REMOVE_ARTICLES: { return fromAdapter.adapter.removeMany(action.payload.ids, state); } case fromActions.ArticleActionTypes.CLEAR_ARTICLES: { return fromAdapter.adapter.removeAll({ ...state, selectedArticleId: null }); } case fromActions.ArticleActionTypes.LOAD_ALL_ARTICLES_SUCCESS: { return fromAdapter.adapter.addAll(action.payload.articles, state); } case fromActions.ArticleActionTypes.SELECT_ARTICLE: { return Object.assign({ ...state, selectedArticleId: action.payload.articleId }); } default: { return state; } } } export const getSelectedArticleId = (state: ArticleState) => state.selectedArticleId; export const getArticleState = createFeatureSelector<ArticleState>('articleState'); export const selectArticleIds = createSelector(getArticleState, fromAdapter.selectArticleIds); export const selectArticleEntities = createSelector(getArticleState, fromAdapter.selectArticleEntities); export const selectAllArticles = createSelector(getArticleState, fromAdapter.selectAllArticles); export const articlesCount = createSelector(getArticleState, fromAdapter.articlesCount); export const selectCurrentArticleId = createSelector(getArticleState, getSelectedArticleId); export const selectCurrentArticle = createSelector( selectArticleEntities, selectCurrentArticleId, (articleEntities, articleId) => articleEntities[articleId] );
import { ActionReducerMap, ActionReducer, MetaReducer } from '@ngrx/store'; import { AppState } from '../states/app.states'; import * as articleReducer from './article.reducer'; import { environment } from '../../environments/environment'; export const reducers: ActionReducerMap<AppState> = { articleState: articleReducer.reducer }; export function logger(reducer: ActionReducer<AppState>): ActionReducer<AppState> { return function(state: AppState, action: any): AppState { console.log('state', state); console.log('action', action); return reducer(state, action); }; } export const metaReducers: MetaReducer<AppState>[] = !environment.production ? [logger] : [];
import { Injectable } from '@angular/core'; import { Action } from '@ngrx/store'; import { Actions, Effect } from '@ngrx/effects'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/switchMap'; import * as fromActions from '../actions/article.actions'; import { ArticleService } from '../services/article.service'; @Injectable() export class ArticleEffects { constructor( private actions$: Actions, private articleService: ArticleService ) {} @Effect() loadAllArticles$: Observable<Action> = this.actions$ .ofType(fromActions.ArticleActionTypes.LOAD_ALL_ARTICLES) .switchMap(() => this.articleService.getAllArticles() .map(data => new fromActions.LoadArticlesSuccess({ articles: data })) ); }
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import { Article } from '../models/article'; @Injectable() export class ArticleService { url = "/api/articles"; constructor(private http: HttpClient) { } getAllArticles(): Observable<Article[]> { return this.http.get<Article[]>(this.url); } }
<button (click)="loadAllArticles()">Show All Articles</button> <button (click)="addArticleView()">Add Article</button> <button (click)="updateArticleView()">Update Article</button> <button (click)="removeArticleView()">Remove Article</button> <button (click)="articleByIdView()">Select Article By Id</button> <hr/> <div [ngSwitch]="task"> <ng-template ngSwitchCase = "all"> <b>Total Count: {{count$ | async}}</b> <br/><br/><b>Ids:</b> {{articleIds$ | async}} <br/><br/><b>Details:</b> <ul> <li *ngFor="let article of allArticles$ | async"> {{article.id}} - {{article.title}} - {{article.category}} </li> </ul> </ng-template> <ng-template ngSwitchCase = "add"> <form [formGroup]="articleForm" (ngSubmit)="onFormSubmitForAdd()"> <div formArrayName="articlesArray"> <div *ngFor = "let acl of articlesFormArray.controls; let idx = index" [formGroupName]="idx"> <p> <b>New Article: {{idx + 1}}</b> </p> <p> New Id: <input formControlName="id"></p> <p> Title: <input formControlName="title"> </p> <p> Category: <input formControlName="category"> </p> <p> <button type="button" (click)="deleteFormArrayControl(idx)">Delete</button></p> </div> </div> <button type="button" (click)="addMoreControlForAdd()">Add More Article</button> <hr/> <p *ngIf="articlesFormArray.length > 0"> <button> Submit </button> </p> </form> </ng-template> <ng-template ngSwitchCase = "update"> <form [formGroup]="articleForm" (ngSubmit)="onFormSubmitForUpdate()"> <div formArrayName="articlesArray"> <div *ngFor = "let acl of articlesFormArray.controls; let idx = index" [formGroupName]="idx"> <p> Id: <input formControlName="id" readonly></p> <p> Update Title: <input formControlName="title"> </p> <p> Update Category: <input formControlName="category"> </p> <p> <button type="button" (click)="deleteFormArrayControl(idx)">Delete</button> </p> </div> </div> <button type="button" (click)="addMoreControlForUpdate()">Update More Article</button> <hr/> <p *ngIf="articlesFormArray.length > 0"> <button> Update </button> </p> </form> </ng-template> <ng-template ngSwitchCase = "remove"> <form [formGroup]="articleForm" (ngSubmit)="onFormSubmitForRemove()"> <div formArrayName="articlesArray"> <ul><li *ngFor = "let acl of articlesFormArray.controls; let idx = index" [formGroupName]="idx"> <input type="checkbox" formControlName="chk"/> {{acl.get('articleData').value.id}} | {{acl.get('articleData').value.title}} | {{acl.get('articleData').value.category}} <input type="hidden" formControlName="articleData"> </li></ul> </div> <hr/> <p *ngIf="articlesFormArray.length > 0"> <button>Remove Selected</button> <button type="button" (click)="clearAllArticles()">Clear All</button> </p> </form> </ng-template> <ng-template ngSwitchCase = "select"> <p> Enter Id: <input [(ngModel)]="articleId"> <button type="button" (click)="selectArticleById()">Show Article</button> </p> <ul> <li *ngIf="articleById$ | async as article"> {{article.id}} - {{article.title}} - {{article.category}} </li> </ul> </ng-template> </div>
import { Component, OnInit } from '@angular/core'; import { FormGroup, FormBuilder, FormArray } from '@angular/forms' import { Store } from '@ngrx/store'; import { Observable } from 'rxjs/Observable'; import * as fromReducer from '../reducers/article.reducer'; import * as fromActions from '../actions/article.actions'; import { ArticleState } from '../states/app.states'; import { Article } from '../models/article'; @Component({ selector: 'app-article', templateUrl: 'article.component.html' }) export class ArticleComponent implements OnInit { allArticles$: Observable<Article[]> articleById$: Observable<Article> count$: Observable<number> articleIds$: Observable<string[] | number[]> task= ''; articleId: string; articleForm: FormGroup; constructor( private formBuilder:FormBuilder, private store: Store<ArticleState>) { } ngOnInit() { this.count$ = this.store.select(fromReducer.articlesCount); this.allArticles$ = this.store.select(fromReducer.selectAllArticles); this.articleIds$ = this.store.select(fromReducer.selectArticleIds); this.articleById$ = this.store.select(fromReducer.selectCurrentArticle); this.store.dispatch(new fromActions.LoadArticles()); } createBlankArticleForm() { this.articleForm = this.formBuilder.group({ articlesArray: this.formBuilder.array([]) }); } createArticleFormForAdd() { this.createBlankArticleForm(); this.addMoreControlForAdd(); } get articlesFormArray(): FormArray{ return this.articleForm.get('articlesArray') as FormArray; } addMoreControlForAdd() { let ag = this.formBuilder.group(new Article()); this.articlesFormArray.push(ag); } updateArticleForm() { this.createBlankArticleForm(); this.allArticles$.subscribe(articles => { if(articles && articles.length > 0) { let article = articles[0]; let ag = this.formBuilder.group(article); this.articlesFormArray.push(ag); } }); } addMoreControlForUpdate() { this.allArticles$.subscribe(articles => { if(articles && articles.length > 0 && this.articlesFormArray.length < articles.length) { let len = this.articlesFormArray.length; let article = articles[len]; let ag = this.formBuilder.group(article); this.articlesFormArray.push(ag); } }); } deleteFormArrayControl(idx: number) { this.articlesFormArray.removeAt(idx); } addArticleView() { this.task = 'add'; this.createArticleFormForAdd(); } updateArticleView() { this.task = 'update'; this.updateArticleForm(); } removeArticleView() { this.task = 'remove'; this.createBlankArticleForm(); this.allArticles$.subscribe(articles => { this.createBlankArticleForm(); articles.forEach(article => { let ag = this.formBuilder.group({ articleData: article, chk: false }); this.articlesFormArray.push(ag); }); }); } articleByIdView() { this.task = 'select'; } onFormSubmitForAdd() { if (this.articlesFormArray.length === 1) { this.addArticle(this.articlesFormArray.at(0).value); } else if (this.articlesFormArray.length > 1) { this.addArticles(this.articlesFormArray.value); } this.createBlankArticleForm(); this.loadAllArticles(); } onFormSubmitForUpdate() { if (this.articlesFormArray.length === 1) { this.updateArticle(this.articlesFormArray.at(0).value); } else if (this.articlesFormArray.length > 1) { this.updateArticles(this.articlesFormArray.value); } this.createBlankArticleForm(); this.loadAllArticles(); } onFormSubmitForRemove() { let articleIdsToDelete: string[] = []; this.articlesFormArray.controls.forEach(result => { if (result.get('chk').value) { articleIdsToDelete.push(result.get('articleData').value.id); } }); if (articleIdsToDelete.length == 1) { this.removeArticle(articleIdsToDelete[0]); } else if (articleIdsToDelete.length > 1 ) { this.removeArticles(articleIdsToDelete); } } addArticle(data: Article) { this.store.dispatch(new fromActions.AddArticle({ article: data })); } addArticles(data: Article[]) { this.store.dispatch(new fromActions.AddArticles({ articles: data })); } updateArticle(data: Article) { this.store.dispatch(new fromActions.UpdateArticle({ article: {id: data.id, changes: data}})); } updateArticles(data: Article[]) { let allUpdates = data.map(article => Object.assign({}, {id: article.id, changes: article})); this.store.dispatch(new fromActions.UpdateArticles({ articles: allUpdates })); } removeArticle(articleId: string) { this.store.dispatch(new fromActions.RemoveArticle({ id: articleId })); } removeArticles(articleIds: string[]) { this.store.dispatch(new fromActions.RemoveArticles({ ids: articleIds })); } clearAllArticles() { this.store.dispatch(new fromActions.ClearArticles()); } loadAllArticles() { this.task = 'all'; } selectArticleById() { this.store.dispatch(new fromActions.SelectArticle({ articleId: this.articleId })); } }
import { Component } from '@angular/core'; @Component({ selector: 'app-root', template: ` <app-article></app-article> ` }) export class AppComponent { }
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { ReactiveFormsModule, FormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; import { StoreModule } from '@ngrx/store'; import { EffectsModule } from '@ngrx/effects'; import { AppComponent } from './app.component'; import { ArticleComponent } from './components/article.component'; import { reducers, metaReducers } from './reducers'; import { ArticleEffects } from './effects/article.effects'; import { ArticleService } from './services/article.service'; //For InMemory testing import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; import { TestData } from './test-data'; @NgModule({ imports: [ BrowserModule, ReactiveFormsModule, FormsModule, HttpClientModule, StoreModule.forRoot(reducers, {metaReducers}), EffectsModule.forRoot([ArticleEffects]), InMemoryWebApiModule.forRoot(TestData) ], declarations: [ AppComponent, ArticleComponent ], providers: [ ArticleService ], bootstrap: [ AppComponent ] }) export class AppModule { }
input { width: 230px; background-color: #dfdfdf; font-size:16px; } input[type="checkbox"] { width: 20px; } button { background-color: #008CBA; color: white; } ul li { background: #f2f4f7; margin: 5px; }
12. Test Application
To run the application, find the following steps.1. Download source code using download link given below on this page.
2. Use downloaded src in your Angular CLI application. To install Angular CLI, find the link.
3. Install NgRx/Store, NgRx/Entity, NgRx/Effects and angular-in-memory-web-api@0.5.3 using NPM command.
4. Run ng serve using command prompt.
5. Now access the URL http://localhost:4200
When we click on Update Article button and then click on Update More Article, we will get following print screen.

13. References
Entity AdapterEntity Interfaces
@ngrx/entity
NgRx/Store 4 + Angular 5 Tutorial
NgRx/Effects 4 Example