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
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK