Home  >  Angular  >  NgRx

NgRx/Entity Example

By Arvind Rai, 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.

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 
2. To install @ngrx/store, run following command.
npm install @ngrx/store --save 
3. To install @ngrx/effects, run following command.
npm install @ngrx/effects --save 
4. In our application we will use angular-in-memory-web-api to load data. To install it, run following command.
npm i angular-in-memory-web-api@0.5.3 --save 

Find the print screen of sample output of our application.
NgRx/Entity Example

3. Entity State

NgRx provides EntityState 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;
} 
Now we will create our State extending EntityState as following.
app.states.ts
export interface ArticleState extends EntityState<Article> {
   //Other entity state properties
} 

4. Entity Adapter

NgRx has EntityAdapter 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 use sortComparer 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
}); 
To leave collection unsorted, we need to assign false value to sortComparer property.
export const adapter: EntityAdapter<Article> = createEntityAdapter<Article>({
   sortComparer: false
}); 
To get high performance in CRUD operations it is good to assign false value to sortComparer property.

4.3 Entity Adapter Methods

Here we will discuss EntityAdapter 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 discuss EntityAdapter 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[] }) {}
} 
Step-2: Creating reducer
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);
    }
    ------
  }	
} 
Step-3: Dispatch action
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 }));
} 
To test 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>[] }) {}
} 
Step-2: Creating reducer
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);
    }
    ------
  }	
} 
Step-3: Dispatch action
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;
} 
Step-2: Creating reducer
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);
    }    
    ------
  }	
} 
Step-3: Dispatch action
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 initialize EntitySelectors 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(); 
Now we will create our selectors as given below.
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); 
We will use our selectors in component as following.
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;
} 
Here we have created a property 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
                        }); 
Step-3: Create action class to select entity by id.
article.actions.ts
export class SelectArticle implements Action {
  readonly type = ArticleActionTypes.SELECT_ARTICLE;
  constructor(public payload: { articleId: string }) {}
} 
Step-4: Create reducer.
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 });
    }     
    ------
  }
} 
On the dispatch of 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 });
    }
    ------
  }
} 
Step-5: Create selector
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]
); 
In the above 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 using addAll 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 };
  }
}  
We will get following Web Service URL.
/api/articles 
Step-2: Before using In-Memory Web API, we need to import 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 { } 
Find the link for more information on In-Memory Web API.

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 
Now find the complete code.
article.ts
export class Article {
   id = '';
   title = '';
   category = '';
} 
article.actions.ts
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; 
app.states.ts
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;
} 
article.adapter.ts
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(); 
article.reducer.ts
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]
); 
index.ts
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] 
  : [];  
article.effects.ts
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 })) 
    );
} 
article.service.ts
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);
    }
} 
article.component.html
<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> 
article.component.ts
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 }));
	}
} 
app.component.ts
import { Component } from '@angular/core';

@Component({
   selector: 'app-root',
   template: `
		<app-article></app-article>
     `
})
export class AppComponent {
} 
app.module.ts
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 { } 
styles.css
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.
NgRx/Entity Example

13. References

Entity Adapter
Entity Interfaces
@ngrx/entity
NgRx/Store 4 + Angular 5 Tutorial
NgRx/Effects 4 Example

14. Download Source Code

POSTED BY
ARVIND RAI
ARVIND RAI
FIND MORE TUTORILAS


©2018 concretepage.com | Privacy Policy | Contact Us