

Angular(v14) CRUD Example Using NgRx Data(v14)
source link: https://www.learmoreseekmore.com/2022/07/angular14-crud-example-using-ngrx-data.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

In this article, we are going to implement Angular(v14) CRUD operations using NgRX Data(v14) state management.
NgRx Data:
- NgRx Data is a good choice when our application involves a lot of 'Create', 'Read', 'Update', 'Delete' operations.
- Our Entity Model should contain a primary key value or unique key.
- No requirement to interact with Ngrx libraries.
- Using NgRx Data we never contact the store directly, every store operation is carried out implicitly.
- In NgRx Data our main point of contact to interact with the store is EntityCollectionService.
- Using EntityCollectionService for each entity can invoke API calls with default methods like 'getAll()','add()', 'delete()', 'update()'.
- So from our angular component, we need to invoke any one method from the 'EntityCollectionService'.
- Then internally EntityActions gets triggered and raises the action method to invoke the API call base on our entity model.
- On API success EntityAction raises a new action method to invoke the 'EntityCollectionReducers'.
- So from the 'EntityCollectionReducer' appropriate reducer gets invoked and updates the data to store.
- On stage change, selectors will fetch the data into the angular component from the store.
Create An Angular(14) Application:
ng new your_app_name
npm install bootstrap
Add a bootstrap 'Navbar' component in 'app.component.html'
src/app/app.component.html:
- <nav class="navbar navbar-dark bg-warning">
- <div class="container-fluid">
- <div class="navbar-brand">Bakery Store</div>
- </div>
- </nav>
Setup JSON Server:
Now to invoke the above command, run the below command in the angular application root folder in a terminal window.
Install NgRx Packages:
ng add @ngrx/store@latest
ng add @ngrx/effects@latest
ng add @ngrx/entity@latest
ng add @ngrx/data@latest
ng add @ngrx/store-devtools@latest
- import { HttpClientModule } from '@angular/common/http';
- // existing hidden for display purpose
- @NgModule({
- imports: [
- HttpClientModule
- export class AppModule { }
Create An Angular Feature Module(Ex: Cakes Module) And A Component(Ex: Home Component):
Let's create the 'Home' component in the 'Cakes' module.
- // existing code hidden for display purpose
- const routes: Routes = [{
- path:'',
- loadChildren: () => import("./cakes/cakes.module").then(_ => _.CakesModule)
Let's configure the route for the 'Home' component in the 'Cakes' routing module.
- import { HomeComponent } from './home/home.component';
- // existing code hidden for display purpose
- const routes: Routes = [
- path: '',
- component: HomeComponent,
Create A Model And A Entity Metadata In Cake Module(Feature/Child Module):
src/app/cakes/store/cakes.ts:
- export interface Cakes {
- id: number;
- name: string;
- description: string;
- cost: number;
Let's create an Entity Meta Data for our Feature Module(Cake Module) like 'cakes-entity-metadata.ts'.
- import { EntityMetadataMap } from "@ngrx/data";
- import { Cakes } from "./cakes";
- export const cakesEntityMetaData: EntityMetadataMap = {
- Cake:{
- selectId:(cake:Cakes) => cake.id
- Here 'EntityMetadataMap' loads from the '@ngrx/data'. Here we have to register all the entity models(eg: Cakes) that are related to our feature module(eg: Cakes module).
- (Line: 5) Here defined the entity name like 'Cake'. This name plays a very crucial role in our NgRx data. By pluralizing the entity name like 'Cakes' then NgRx Data will generate the URLs of our API with plural names.
- In general, our entity model should contain the primary key property mostly 'Id' to work with NgRx Data. By default 'Id' property will be recognized by the NgRx Data, but if we want to specify our custom property as the primary key then we have to use the 'selectId' property like above.
- Along with 'selectId' property, EntityMetadataMap provides so many different props, so explore them if needed.
- import { EntityDefinitionService } from '@ngrx/data';
- import { cakesEntityMetaData } from './store/cakes-entity-metadata';
- @NgModule({
- declarations: [HomeComponent],
- imports: [CommonModule, CakesRoutingModule],
- export class CakesModule {
- constructor(entityDefinitionService: EntityDefinitionService) {
- entityDefinitionService.registerMetadataMap(cakesEntityMetaData);
- Here using 'EntityDefinitionService' registered our 'cakesEntityMetaData'.
EntityCollectionService:
Implement Read Operation:
- import { Component, OnInit } from '@angular/core';
- import {
- EntityCollectionService,
- EntityCollectionServiceFactory,
- } from '@ngrx/data';
- import { Observable } from 'rxjs';
- import { Cakes } from '../store/cakes';
- @Component({
- selector: 'app-home',
- templateUrl: './home.component.html',
- styleUrls: ['./home.component.css'],
- export class HomeComponent implements OnInit {
- constructor(serviceFactory: EntityCollectionServiceFactory) {
- this.cakeService = serviceFactory.create<Cakes>('Cake');
- this.allCakes$ = this.cakeService.entities$;
- allCakes$: Observable<Cakes[]>;
- cakeService: EntityCollectionService<Cakes>;
- ngOnInit(): void {
- this.cakeService.getAll();
- (Line: 15) Here injected the 'EntityCollectionServiceFactory' that loads from the '@ngrx/data'.
- (Line: 16) Creating the instance of 'EntityCollectionService' from the 'EntityCollectionServiceFactory'. Here we used the name 'Cake' this name is nothing but the property name that we registered in the 'cakesEntityMetaData'.
- (Line: 17) Here 'EntityCollectionService.entities$' observable fetches the data that was stored in the NgRx store.
- (Line: 20) Here declared the 'allCakes$' variable which is of type 'Observable<Cakes[]>'.
- (Line: 21) Here declared the 'cakeService' variable which is of type 'EntityCollectionService<Cakes>'.
- (Line: 24) The 'EntityCollectionService.getAll()' is a default HTTP get method that will implicitly frame URL based property name(eg: 'Cake') in 'cakeEntityMetaData', implicitly invokes the actions, 'effects', 'reducer' to generate the state based on the API repsonse.
src/app/cakes/home/home.component.html:
- <div class="container mt-2">
- <div class="row row-cols-1 row-cols-md-3 g-4">
- <div class="col" *ngFor="let cake of allCakes$ | async">
- <div class="card">
- <div class="card-body">
- <h5 class="card-title">{{ cake.name }}</h5>
- <ul class="list-group list-group-flush">
- <li class="list-group-item">Price: {{ cake.cost }}</li>
- </ul>
- </div>
- <div class="card-body">
- <p>{{ cake.description }}</p>
- </div>
- </div>
- </div>
- </div>
- </div>
Now if we run the application API call will fail because the auto-generated API URL by NgRx Data is "http://localhost:4000/api/cakes/", but the actual API URL is "http://locast:3000/cakes". To update the API URL we have to implement the DefultHttpURLGenerator service.
src/app/shared/store/customurl-http-generator.ts:
- import { Injectable } from '@angular/core';
- import { DefaultHttpUrlGenerator, HttpResourceUrls, Pluralizer } from '@ngrx/data';
- @Injectable()
- export class CustomurlHttpGenerator extends DefaultHttpUrlGenerator {
- constructor(pluralizer: Pluralizer) {
- super(pluralizer);
- protected override getResourceUrls(
- entityName: string,
- root: string,
- trailingSlashEndpoints?: boolean
- ): HttpResourceUrls {
- let resourceURLs = this.knownHttpResourceUrls[entityName];
- if (entityName == 'Cake') {
- resourceURLs = {
- collectionResourceUrl: 'http://localhost:3000/cakes/',
- entityResourceUrl: 'http://localhost:3000/cakes/',
- this.registerHttpResourceUrls({ [entityName]: resourceURLs });
- return resourceURLs;
- (Line: 4) Extending the 'DefaultHttpUrlGenerator' that loads from the '@ngrx/data'
- (Line: 9) Overriding the 'getResourURLS'
- Here based on the 'entityName' we replacing the API URL. Here 'entityName' is nothing but the property name in the 'cakeEntityMetaData'.
Now register the 'CustomUrlHttpGenerator' in 'AppModule'.
- import { EntityDataModule, HttpUrlGenerator } from '@ngrx/data';
- import { CustomurlHttpGenerator } from './shared/store/customurl-http-generator';
- // existing code hidden for display purpose
- @NgModule({
- providers: [
- provide: HttpUrlGenerator,
- useClass: CustomurlHttpGenerator,
- export class AppModule { }
Now we can observe data get rendered.
Create 'Add' Component:
- import { AddComponent } from './add/add.component';
- // exisiting code removed for display purpose
- const routes: Routes = [
- path: 'add',
- component: AddComponent
Implementing Create Operation:
- import { Component, OnInit } from '@angular/core';
- import { Router } from '@angular/router';
- import { EntityCollectionService, EntityCollectionServiceFactory } from '@ngrx/data';
- import { Cakes } from '../store/cakes';
- @Component({
- selector: 'app-add',
- templateUrl: './add.component.html',
- styleUrls: ['./add.component.css']
- export class AddComponent implements OnInit {
- constructor(
- serviceFactory: EntityCollectionServiceFactory,
- private router: Router
- this.cakeService = serviceFactory.create<Cakes>('Cake');
- cakeService: EntityCollectionService<Cakes>;
- cakeForm: Cakes = {
- id: 0,
- description: '',
- name: '',
- cost: 0,
- ngOnInit(): void {}
- save() {
- this.cakeService.add(this.cakeForm).subscribe(() => {
- this.router.navigate(['/']);
- (Line: 14) Injected the 'EntityCollectionServiceFactory' that loads from the '@ngrx/data'
- (Line: 15) Injected the 'Router' service that loads from the '@angular/router'.
- (Line: 17) Creating the 'EntityCollectionService' instance using 'EntityCollectionServiceFactory'. Here service name 'Cake' is the property name in 'cakeEntityMetaData'.
- (Line: 19) Declared 'cakeService' variable of type 'EntityCollectionService'.
- (Line: 21-26) The 'cakeForm' variable binds with angular form.
- (Line: 31-33) The 'EntiyCollectionService.add()' method invokes the HTTP Post call to create a item at server.
src/app/cakes/add/add.component.html:
- <div class="container">
- <legend>Add A New Cake</legend>
- <div class="mb-3">
- <label for="txtName" class="form-label">Name of Cake</label>
- <input
- type="text"
- [(ngModel)]="cakeForm.name"
- class="form-control"
- id="txtName"
- />
- </div>
- <div class="mb-3">
- <label for="txtdescription" class="form-label">Description</label>
- <textarea
- type="text"
- [(ngModel)]="cakeForm.description"
- class="form-control"
- id="txtdescription"
- ></textarea>
- </div>
- <div class="mb-3">
- <label for="txtCost" class="form-label">Cost</label>
- <input
- type="number"
- [(ngModel)]="cakeForm.cost"
- class="form-control"
- id="txtCost"
- />
- </div>
- <button type="button" class="btn btn-warning" (click)="save()">Create</button>
- </div>
Let's import the 'FormsModule' in the 'CakesModule'.
- import { FormsModule } from '@angular/forms';
- // existing code hidden for display purpose
- @NgModule({
- imports: [FormsModule],
- export class CakesModule {
- constructor(entityDefinitionService: EntityDefinitionService) {
- entityDefinitionService.registerMetadataMap(cakesEntityMetaData);
<div class="row"> <div class="col col-md4 offset-md-4"> <a routerLink="/add" class="btn btn-warning">Add A New Book</a> </div> </div>
(step:1)
Create 'Edit' Component:
- import { EditComponent } from './edit/edit.component';
- const routes: Routes = [
- path: 'edit/:id',
- component: EditComponent
- Here ':id' represents the dynamic value part of the path.
Implement Update Operation:
- import { Component, OnInit } from '@angular/core';
- import { ActivatedRoute, Router } from '@angular/router';
- import { EntityCollectionService, EntityCollectionServiceFactory } from '@ngrx/data';
- import { combineLatest } from 'rxjs';
- import { Cakes } from '../store/cakes';
- @Component({
- selector: 'app-edit',
- templateUrl: './edit.component.html',
- styleUrls: ['./edit.component.css']
- export class EditComponent implements OnInit {
- constructor(
- serviceFactory: EntityCollectionServiceFactory,
- private router: Router,
- private route: ActivatedRoute
- this.cakeService = serviceFactory.create<Cakes>('Cake');
- cakeService: EntityCollectionService<Cakes>;
- cakeForm: Cakes = {
- id: 0,
- description: '',
- name: '',
- cost: 0,
- ngOnInit(): void {
- let fetchFormData$ = combineLatest([
- this.route.paramMap,
- this.cakeService.entities$,
- ]).subscribe(([params, cakes]) => {
- var id = Number(params.get('id'));
- var filteredCake = cakes.filter((_) => _.id == id);
- if (filteredCake) {
- this.cakeForm = { ...filteredCake[0] };
- update() {
- this.cakeService.update(this.cakeForm).subscribe(() => {
- this.router.navigate(["/"]);
- (Line: 31-40) Here 'combineLatest' that load from the 'rxjs'. The 'combineLatest' executes with the latest output from all observables registered inside of it. Here we fetch the item 'id' value from the URL and filter it against the store data to populate it on the edit form.
- (Line: 43) The 'EntityCollectionService.update()' is used to invoke the HTTP PUT API call to update the data at the server.
- <div class="card-body">
- <a class="btn btn-warning" [routerLink]="['/edit', cake.id]">Edit</a> |
- </div>
- Here generating the 'edit' button URL dynamically by adding the 'id' value.
Step1:
Step2:
Step3:
Implement Delete Operation:
- import { Component, OnInit } from '@angular/core';
- import {
- EntityCollectionService,
- EntityCollectionServiceFactory,
- } from '@ngrx/data';
- import { Observable } from 'rxjs';
- import { Cakes } from '../store/cakes';
- declare var window: any;
- @Component({
- selector: 'app-home',
- templateUrl: './home.component.html',
- styleUrls: ['./home.component.css'],
- export class HomeComponent implements OnInit {
- constructor(serviceFactory: EntityCollectionServiceFactory) {
- this.cakeService = serviceFactory.create<Cakes>('Cake');
- this.allCakes$ = this.cakeService.entities$;
- allCakes$: Observable<Cakes[]>;
- cakeService: EntityCollectionService<Cakes>;
- deleteModal: any;
- idToDelete: number = 0;
- ngOnInit(): void {
- this.deleteModal = new window.bootstrap.Modal(
- document.getElementById('deleteModal')
- this.cakeService.getAll();
- openDeleteModal(id: number) {
- this.idToDelete = id;
- this.deleteModal.show();
- confirmDelete() {
- this.cakeService.delete(this.idToDelete)
- .subscribe((data) => {
- this.deleteModal.hide();
- (Line: 9) Declare the window instance.
- (Line: 25) The 'deleteModal' variable is declared to assign the instance of the bootstrap modal.
- (Line: 26) The 'idToDelete' variable to store the item to delete.
- (Line: 29-31) The instance of the bootstrap is assigned to the 'deleteModal'.
- (Line: 36-39) The 'openDeleteModal' opens the popup to display the delete confirmation. The 'show()' method opens the bootstrap modal.
- (Line: 41-46) The 'EntityCollectionServices.delete()' method invokes the HTTP delete API call.
src/app/cakes/home/home.component.html:
- <div class="container mt-2">
- <div class="row">
- <div class="col col-md4 offset-md-4">
- <a routerLink="/add" class="btn btn-warning">Add A New Book</a>
- </div>
- </div>
- <div class="row row-cols-1 row-cols-md-3 g-4">
- <div class="col" *ngFor="let cake of allCakes$ | async">
- <div class="card">
- <div class="card-body">
- <h5 class="card-title">{{ cake.name }}</h5>
- <ul class="list-group list-group-flush">
- <li class="list-group-item">Price: {{ cake.cost }}</li>
- </ul>
- </div>
- <div class="card-body">
- <p>{{ cake.description }}</p>
- </div>
- <div class="card-body">
- <a class="btn btn-warning" [routerLink]="['/edit', cake.id]">Edit</a> |
- <button type="button" class="btn btn-danger" (click)="openDeleteModal(cake.id)">
- Delete
- </button>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div
- class="modal fade"
- id="deleteModal"
- tabindex="-1"
- aria-labelledby="exampleModalLabel"
- aria-hidden="true"
- <div class="modal-dialog">
- <div class="modal-content">
- <div class="modal-header">
- <h5 class="modal-title" id="exampleModalLabel">Delete Confirmation</h5>
- <button
- type="button"
- class="btn-close"
- data-bs-dismiss="modal"
- aria-label="Close"
- ></button>
- </div>
- <div class="modal-body">Are you sure to delete this item?</div>
- <div class="modal-footer">
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
- Close
- </button>
- <button type="button" class="btn btn-danger" (click)="confirmDelete()">
- Confirm Delete
- </button>
- </div>
- </div>
- </div>
- </div>
- (Line: 21-23) The 'Delete' button click event registered with 'openDeleteModal()' method.
- (Line: 30-51) Delete confirmation popup modal HTML.
Support Me!
Buy Me A Coffee
PayPal Me
Video Session:
Wrapping Up:
Follow Me:
Recommend
-
19
Play by Play: Angular and ngrx In this course, you’ll explore using ngrx with Angular to implement the redux pattern. ...
-
19
Reactive apps with Angular & ngrxTweet27 minutes read...
-
6
Angular 10 Universal Server Side Rendering (SSR) CRUD Example by Didin J., updated on Aug 28, 2020 The comprehensive Angular 10 tutorial on building CRUD web application using Angular Universal Server-side render...
-
12
NGRX: State management in Angular Knoldus Blog Audio Reading Time: 5 minutes Do w...
-
5
ngRx with Redux in Angular Reading Time: 4 minutes In this blog, we will see the basic pattern of Redux and some benefits and some high-level concepts of redux and then we will discuss the brief about ngRx and...
-
20
IntroState management is a key component when building applications. There are various approaches by which we can manage the state in an Angular application, each with its pros and cons. This blog post will focus on using NgRx as our...
-
8
Part-7 | NestJS(v9) | Angular(v14) | MongoDB | CRUD Example
-
6
Part-6 | NestJS(v9) | Angular(v14) | MongoDB | CRUD Example
-
15
In this article, I will share with you how to create angule 11 CRUD application with the REST Web API with example. you just follow the steps. The REST API Endpoints We'll be building an Angular 11 frontend app for...
-
8
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK