ASP.NET Core Angular Tutorial - Part 2
About this tutorial
This is the second part of the ASP.NET Core Angular tutorial series. All parts:
- Part I: Creating the project and book list page
- Part II: Creating, updating and deleting books (this tutorial)
- Part III: Integration tests
You can also watch this video course prepared by an ABP community member, based on this tutorial.
Creating a new book
In this section, you will learn how to create a new modal dialog form to create a new book.
Type definition
Open books.ts file in app\store\models folder and replace the content as below:
export namespace Books {
export interface State {
books: Response;
}
export interface Response {
items: Book[];
totalCount: number;
}
export interface Book {
name: string;
type: BookType;
publishDate: string;
price: number;
lastModificationTime: string;
lastModifierId: string;
creationTime: string;
creatorId: string;
id: string;
}
export enum BookType {
Undefined,
Adventure,
Biography,
Dystopia,
Fantastic,
Horror,
Science,
ScienceFiction,
Poetry,
}
//<== added CreateUpdateBookInput interface ==>
export interface CreateUpdateBookInput {
name: string;
type: BookType;
publishDate: string;
price: number;
}
}
- We added
CreateUpdateBookInputinterface. - You can see the properties of this interface from Swagger UI.
- The
CreateUpdateBookInputinterface matches with theCreateUpdateBookDtoin the backend.
Service method
Open the books.service.ts file in app\books\shared folder and replace the content as below:
import { Injectable } from '@angular/core';
import { RestService } from '@abp/ng.core';
import { Books } from '../../store/models';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class BooksService {
constructor(private restService: RestService) {}
get(): Observable<Books.Response> {
return this.restService.request<void, Books.Response>({
method: 'GET',
url: '/api/app/book'
});
}
//<== added create method ==>
create(createBookInput: Books.CreateUpdateBookInput): Observable<Books.Book> {
return this.restService.request<Books.CreateUpdateBookInput, Books.Book>({
method: 'POST',
url: '/api/app/book',
body: createBookInput
});
}
}
- We added the
createmethod to perform an HTTP Post request to the server. restService.requestfunction gets generic parameters for the types sent to and received from the server. This example sends aCreateUpdateBookInputobject and receives aBookobject (you can setvoidfor request or return type if not used).
State definitions
Open books.action.ts in app\store\actions folder and replace the content as below:
import { Books } from '../models'; //<== added this line ==>
export class GetBooks {
static readonly type = '[Books] Get';
}
//added CreateUpdateBook class
export class CreateUpdateBook {
static readonly type = '[Books] Create Update Book';
constructor(public payload: Books.CreateUpdateBookInput) { }
}
- We imported the Books namespace and created the
CreateUpdateBookaction.
Open books.state.ts file in app\store\states and replace the content as below:
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks, CreateUpdateBook } from '../actions/books.actions'; //<== added CreateUpdateBook==>
import { Books } from '../models/books';
import { BooksService } from '../../books/shared/books.service';
import { tap } from 'rxjs/operators';
@State<Books.State>({
name: 'BooksState',
defaults: { books: {} } as Books.State,
})
export class BooksState {
@Selector()
static getBooks(state: Books.State) {
return state.books.items || [];
}
constructor(private booksService: BooksService) { }
@Action(GetBooks)
get(ctx: StateContext<Books.State>) {
return this.booksService.get().pipe(
tap(booksResponse => {
ctx.patchState({
books: booksResponse,
});
}),
);
}
//added CreateUpdateBook action listener
@Action(CreateUpdateBook)
save(ctx: StateContext<Books.State>, action: CreateUpdateBook) {
return this.booksService.create(action.payload);
}
}
- We imported
CreateUpdateBookaction and defined thesavemethod that will listen to aCreateUpdateBookaction to create a book.
When the SaveBook action dispatched, the save method is being executed. It calls create method of the BooksService.
Add a modal to BookListComponent
Open book-list.component.html file in books\book-list folder and replace the content as below:
<div class="card">
<div class="card-header">
<div class="row">
<div class="col col-md-6">
<h5 class="card-title">
{{ '::Menu:Books' | abpLocalization }}
</h5>
</div>
<!--Added new book button -->
<div class="text-right col col-md-6">
<div class="text-lg-right pt-2">
<button
id="create"
class="btn btn-primary"
type="button"
(click)="createBook()"
>
<i class="fa fa-plus mr-1"></i>
<span>{{ "::NewBook" | abpLocalization }}</span>
</button>
</div>
</div>
</div>
</div>
<div class="card-body">
<abp-table
[value]="books$ | async"
[abpLoading]="loading"
[headerTemplate]="tableHeader"
[bodyTemplate]="tableBody"
[rows]="10"
[scrollable]="true"
>
</abp-table>
<ng-template #tableHeader>
<tr>
<th>{{ "::Name" | abpLocalization }}</th>
<th>{{ "::Type" | abpLocalization }}</th>
<th>{{ "::PublishDate" | abpLocalization }}</th>
<th>{{ "::Price" | abpLocalization }}</th>
</tr>
</ng-template>
<ng-template #tableBody let-data>
<tr>
<td>{{ data.name }}</td>
<td>{{ booksType[data.type] }}</td>
<td>{{ data.publishDate | date }}</td>
<td>{{ data.price }}</td>
</tr>
</ng-template>
</div>
</div>
<!--added modal-->
<abp-modal [(visible)]="isModalOpen">
<ng-template #abpHeader>
<h3>{{ '::NewBook' | abpLocalization }}</h3>
</ng-template>
<ng-template #abpBody> </ng-template>
<ng-template #abpFooter>
<button type="button" class="btn btn-secondary" #abpClose>
{{ 'AbpAccount::Close' | abpLocalization }}
</button>
</ng-template>
</abp-modal>
- We added the
abp-modalwhich renders a modal to allow user to create a new book. abp-modalis a pre-built component to show modals. While you could use another approach to show a modal,abp-modalprovides additional benefits.- We added
New bookbutton to theAbpContentToolbar.
Open book-list.component. file in books\book-list folder and replace the content as below:
import { Component, OnInit } from '@angular/core';
import { Store, Select } from '@ngxs/store';
import { BooksState } from '../../store/states';
import { Observable } from 'rxjs';
import { Books } from '../../store/models';
import { GetBooks } from '../../store/actions';
@Component({
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
})
export class BookListComponent implements OnInit {
@Select(BooksState.getBooks)
books$: Observable<Books.Book[]>;
booksType = Books.BookType;
loading = false;
isModalOpen = false; //<== added this line ==>
constructor(private store: Store) { }
ngOnInit() {
this.get();
}
get() {
this.loading = true;
this.store.dispatch(new GetBooks()).subscribe(() => {
this.loading = false;
});
}
//added createBook method
createBook() {
this.isModalOpen = true;
}
}
- We added
isModalOpen = falseandcreateBookmethod.
You can open your browser and click New book button to see the new modal.

Create a reactive form
Reactive forms provide a model-driven approach to handling form inputs whose values change over time.
Open book-list.component.ts file in app\books\book-list folder and replace the content as below:
import { Component, OnInit } from '@angular/core';
import { Store, Select } from '@ngxs/store';
import { BooksState } from '../../store/states';
import { Observable } from 'rxjs';
import { Books } from '../../store/models';
import { GetBooks } from '../../store/actions';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; //<== added this line ==>
@Component({
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
})
export class BookListComponent implements OnInit {
@Select(BooksState.getBooks)
books$: Observable<Books.Book[]>;
booksType = Books.BookType;
loading = false;
isModalOpen = false;
form: FormGroup;
constructor(private store: Store, private fb: FormBuilder) { } //<== added FormBuilder ==>
ngOnInit() {
this.get();
}
get() {
this.loading = true;
this.store.dispatch(new GetBooks()).subscribe(() => {
this.loading = false;
});
}
createBook() {
this.buildForm(); //<== added this line ==>
this.isModalOpen = true;
}
//added buildForm method
buildForm() {
this.form = this.fb.group({
name: ['', Validators.required],
type: [null, Validators.required],
publishDate: [null, Validators.required],
price: [null, Validators.required],
});
}
}
- We imported
FormGroup, FormBuilder and Validators. - We injected
fb: FormBuilderservice to the constructor. The FormBuilder service provides convenient methods for generating controls. It reduces the amount of boilerplate needed to build complex forms. - We added
buildFormmethod to the end of the file and executedbuildForm()in thecreateBookmethod. This method creates a reactive form to be able to create a new book.- The
groupmethod ofFormBuilder,fbcreates aFormGroup. - Added
Validators.requiredstatic method which validates the relevant form element.
- The
Create the DOM elements of the form
Open book-list.component.html in app\books\book-list folder and replace <ng-template #abpBody> </ng-template> with the following code part:
<ng-template #abpBody>
<form [formGroup]="form">
<div class="form-group">
<label for="book-name">Name</label><span> * </span>
<input type="text" id="book-name" class="form-control" formControlName="name" autofocus />
</div>
<div class="form-group">
<label for="book-price">Price</label><span> * </span>
<input type="number" id="book-price" class="form-control" formControlName="price" />
</div>
<div class="form-group">
<label for="book-type">Type</label><span> * </span>
<select class="form-control" id="book-type" formControlName="type">
<option [ngValue]="null">Select a book type</option>
<option [ngValue]="booksType[type]" *ngFor="let type of bookTypeArr"> {{ type }}</option>
</select>
</div>
<div class="form-group">
<label>Publish date</label><span> * </span>
<input
#datepicker="ngbDatepicker"
class="form-control"
name="datepicker"
formControlName="publishDate"
ngbDatepicker
(click)="datepicker.toggle()"
/>
</div>
</form>
</ng-template>
- This template creates a form with
Name,Price,TypeandPublishdate fields. - We've used NgBootstrap datepicker in this component.
Datepicker requirements
Open books.module.ts file in app\books folder and replace the content as below:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BooksRoutingModule } from './books-routing.module';
import { BooksComponent } from './books.component';
import { BookListComponent } from './book-list/book-list.component';
import { SharedModule } from '../shared/shared.module';
import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; //<== added this line ==>
@NgModule({
declarations: [BooksComponent, BookListComponent],
imports: [
CommonModule,
BooksRoutingModule,
SharedModule,
NgbDatepickerModule //<== added this line ==>
]
})
export class BooksModule { }
- We imported
NgbDatepickerModuleto be able to use the date picker.
Open book-list.component.ts file in app\books\book-list folder and replace the content as below:
import { Component, OnInit } from '@angular/core';
import { Store, Select } from '@ngxs/store';
import { BooksState } from '../../store/states';
import { Observable } from 'rxjs';
import { Books } from '../../store/models';
import { GetBooks } from '../../store/actions';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; //<== added this line ==>
@Component({
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }] //<== added this line ==>
})
export class BookListComponent implements OnInit {
@Select(BooksState.getBooks)
books$: Observable<Books.Book[]>;
booksType = Books.BookType;
//added bookTypeArr array
bookTypeArr = Object.keys(Books.BookType).filter(
bookType => typeof this.booksType[bookType] === 'number'
);
loading = false;
isModalOpen = false;
form: FormGroup;
constructor(private store: Store, private fb: FormBuilder) { }
ngOnInit() {
this.get();
}
get() {
this.loading = true;
this.store.dispatch(new GetBooks()).subscribe(() => {
this.loading = false;
});
}
createBook() {
this.buildForm();
this.isModalOpen = true;
}
buildForm() {
this.form = this.fb.group({
name: ['', Validators.required],
type: [null, Validators.required],
publishDate: [null, Validators.required],
price: [null, Validators.required],
});
}
}
We imported
NgbDateNativeAdapter, NgbDateAdapterWe added a new provider
NgbDateAdapterthat converts Datepicker value toDatetype. See the datepicker adapters for more details.We added
bookTypeArrarray to be able to use it in the combobox values. ThebookTypeArrcontains the fields of theBookTypeenum. Resulting array is shown below:['Adventure', 'Biography', 'Dystopia', 'Fantastic' ...]This array was used in the previous form template in the
ngForloop.
Now, you can open your browser to see the changes:

Saving the book
Open book-list.component.html in app\books\book-list folder and add the following abp-button to save the new book.
<ng-template #abpFooter>
<button type="button" class="btn btn-secondary" #abpClose>
{{ 'AbpAccount::Close' | abpLocalization }}
</button>
<!--added save button-->
<button class="btn btn-primary" (click)="save()" [disabled]="form.invalid">
<i class="fa fa-check mr-1"></i>
{{ 'AbpAccount::Save' | abpLocalization }}
</button>
</ng-template>
- This adds a save button to the bottom area of the modal:

Open book-list.component.ts file in app\books\book-list folder and replace the content as below:
import { Component, OnInit } from '@angular/core';
import { Store, Select } from '@ngxs/store';
import { BooksState } from '../../store/states';
import { Observable } from 'rxjs';
import { Books } from '../../store/models';
import { GetBooks, CreateUpdateBook } from '../../store/actions'; //<== added CreateUpdateBook ==>
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
@Component({
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }]
})
export class BookListComponent implements OnInit {
@Select(BooksState.getBooks)
books$: Observable<Books.Book[]>;
booksType = Books.BookType;
bookTypeArr = Object.keys(Books.BookType).filter(
bookType => typeof this.booksType[bookType] === 'number'
);
loading = false;
isModalOpen = false;
form: FormGroup;
constructor(private store: Store, private fb: FormBuilder) { }
ngOnInit() {
this.get();
}
get() {
this.loading = true;
this.store.dispatch(new GetBooks()).subscribe(() => {
this.loading = false;
});
}
createBook() {
this.buildForm();
this.isModalOpen = true;
}
buildForm() {
this.form = this.fb.group({
name: ['', Validators.required],
type: [null, Validators.required],
publishDate: [null, Validators.required],
price: [null, Validators.required],
});
}
//<== added save ==>
save() {
if (this.form.invalid) {
return;
}
this.store.dispatch(new CreateUpdateBook(this.form.value)).subscribe(() => {
this.isModalOpen = false;
this.form.reset();
this.get();
});
}
}
- We imported
CreateUpdateBook. - We added
savemethod
Updating an existing book
BooksService
Open the books.service.ts in app\books\shared folder and add the getById and update methods.
getById(id: string): Observable<Books.Book> {
return this.restService.request<void, Books.Book>({
method: 'GET',
url: `/api/app/book/${id}`
});
}
update(updateBookInput: Books.CreateUpdateBookInput, id: string): Observable<Books.Book> {
return this.restService.request<Books.CreateUpdateBookInput, Books.Book>({
method: 'PUT',
url: `/api/app/book/${id}`,
body: updateBookInput
});
}
CreateUpdateBook action
Open the books.actions.ts in app\store\actions folder and replace the content as below:
import { Books } from '../models';
export class GetBooks {
static readonly type = '[Books] Get';
}
export class CreateUpdateBook {
static readonly type = '[Books] Create Update Book';
constructor(public payload: Books.CreateUpdateBookInput, public id?: string) { } //<== added id parameter ==>
}
- We added
idparameter to theCreateUpdateBookaction's constructor.
Open the books.state.ts in app\store\states folder and replace the save method as below:
@Action(CreateUpdateBook)
save(ctx: StateContext<Books.State>, action: CreateUpdateBook) {
if (action.id) {
return this.booksService.update(action.payload, action.id);
} else {
return this.booksService.create(action.payload);
}
}
BookListComponent
Open book-list.component.ts in app\books\book-list folder and inject BooksService dependency by adding it to the constructor and add a variable named selectedBook.
import { Component, OnInit } from '@angular/core';
import { Store, Select } from '@ngxs/store';
import { BooksState } from '../../store/states';
import { Observable } from 'rxjs';
import { Books } from '../../store/models';
import { GetBooks, CreateUpdateBook } from '../../store/actions';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
import { BooksService } from '../shared/books.service'; //<== imported BooksService ==>
@Component({
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }]
})
export class BookListComponent implements OnInit {
@Select(BooksState.getBooks)
books$: Observable<Books.Book[]>;
booksType = Books.BookType;
bookTypeArr = Object.keys(Books.BookType).filter(
bookType => typeof this.booksType[bookType] === 'number'
);
loading = false;
isModalOpen = false;
form: FormGroup;
selectedBook = {} as Books.Book; //<== declared selectedBook ==>
constructor(private store: Store, private fb: FormBuilder, private booksService: BooksService) { }
ngOnInit() {
this.get();
}
get() {
this.loading = true;
this.store.dispatch(new GetBooks()).subscribe(() => {
this.loading = false;
});
}
//<== this method is replaced ==>
createBook() {
this.selectedBook = {} as Books.Book; //<== added ==>
this.buildForm();
this.isModalOpen = true;
}
//<== added editBook method ==>
editBook(id: string) {
this.booksService.getById(id).subscribe(book => {
this.selectedBook = book;
this.buildForm();
this.isModalOpen = true;
});
}
//<== this method is replaced ==>
buildForm() {
this.form = this.fb.group({
name: [this.selectedBook.name || "", Validators.required],
type: [this.selectedBook.type || null, Validators.required],
publishDate: [
this.selectedBook.publishDate
? new Date(this.selectedBook.publishDate)
: null,
Validators.required
],
price: [this.selectedBook.price || null, Validators.required]
});
}
save() {
if (this.form.invalid) {
return;
}
//<== added this.selectedBook.id ==>
this.store.dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id))
.subscribe(() => {
this.isModalOpen = false;
this.form.reset();
this.get();
});
}
}
- We imported
BooksService. - We declared a variable named
selectedBookasBooks.Book. - We injected
BooksServiceto the constructor.BooksServiceis being used to retrieve the book data which is being edited. - We added
editBookmethod. This method fetches the book with the givenIdand sets it toselectedBookobject. - We replaced the
buildFormmethod so that it creates the form with theselectedBookdata. - We replaced the
createBookmethod so it setsselectedBookto an empty object. - We added
selectedBook.idto the constructor of the newCreateUpdateBook.
Add "Actions" dropdown to the table
Open the book-list.component.html in app\books\book-list folder and replace the <div class="card-body"> tag as below:
<div class="card-body">
<abp-table
[value]="books$ | async"
[abpLoading]="loading"
[headerTemplate]="tableHeader"
[bodyTemplate]="tableBody"
[rows]="10"
[scrollable]="true"
>
</abp-table>
<ng-template #tableHeader>
<tr>
<th>{{ "::Actions" | abpLocalization }}</th>
<th>{{ "::Name" | abpLocalization }}</th>
<th>{{ "::Type" | abpLocalization }}</th>
<th>{{ "::PublishDate" | abpLocalization }}</th>
<th>{{ "::Price" | abpLocalization }}</th>
</tr>
</ng-template>
<ng-template #tableBody let-data>
<tr>
<td>
<div ngbDropdown container="body" class="d-inline-block">
<button
class="btn btn-primary btn-sm dropdown-toggle"
data-toggle="dropdown"
aria-haspopup="true"
ngbDropdownToggle
>
<i class="fa fa-cog mr-1"></i>{{ "::Actions" | abpLocalization }}
</button>
<div ngbDropdownMenu>
<button ngbDropdownItem (click)="editBook(data.id)">
{{ "::Edit" | abpLocalization }}
</button>
</div>
</div>
</td>
<td>{{ data.name }}</td>
<td>{{ booksType[data.type] }}</td>
<td>{{ data.publishDate | date }}</td>
<td>{{ data.price }}</td>
</tr>
</ng-template>
</div>
- We added a
thfor the "Actions" column. - We added
buttonwithngbDropdownToggleto open actions when clicked the button. - We have used to NgbDropdown for the dropdown menu of actions.
The final UI looks like as below:

Open book-list.component.html in app\books\book-list folder and find the <ng-template #abpHeader> tag and replace the content as below.
<ng-template #abpHeader>
<h3>{{ (selectedBook.id ? 'AbpIdentity::Edit' : '::NewBook' ) | abpLocalization }}</h3>
</ng-template>
- This template will show Edit text for edit record operation, New Book for new record operation in the title.
Deleting a book
BooksService
Open books.service.ts in app\books\shared folder and add the below delete method to delete a book.
delete(id: string): Observable<void> {
return this.restService.request<void, void>({
method: 'DELETE',
url: `/api/app/book/${id}`
});
}
Deletemethod getsidparameter and makes aDELETEHTTP request to the relevant endpoint.
DeleteBook action
Open books.actions.ts in app\store\actions folder and add an action named DeleteBook.
export class DeleteBook {
static readonly type = '[Books] Delete';
constructor(public id: string) {}
}
Open the books.state.ts in app\store\states folder and replace the content as below:
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks, CreateUpdateBook, DeleteBook } from '../actions/books.actions'; //<== added DeleteBook==>
import { Books } from '../models/books';
import { BooksService } from '../../books/shared/books.service';
import { tap } from 'rxjs/operators';
@State<Books.State>({
name: 'BooksState',
defaults: { books: {} } as Books.State,
})
export class BooksState {
@Selector()
static getBooks(state: Books.State) {
return state.books.items || [];
}
constructor(private booksService: BooksService) { }
@Action(GetBooks)
get(ctx: StateContext<Books.State>) {
return this.booksService.get().pipe(
tap(booksResponse => {
ctx.patchState({
books: booksResponse,
});
}),
);
}
@Action(CreateUpdateBook)
save(ctx: StateContext<Books.State>, action: CreateUpdateBook) {
if (action.id) {
return this.booksService.update(action.payload, action.id);
} else {
return this.booksService.create(action.payload);
}
}
//<== added DeleteBook ==>
@Action(DeleteBook)
delete(ctx: StateContext<Books.State>, action: DeleteBook) {
return this.booksService.delete(action.id);
}
}
We imported
DeleteBook.We added
DeleteBookaction listener to the end of the file.
Add a delete button
Open book-list.component.html in app\books\book-list folder and modify the ngbDropdownMenu to add the delete button as shown below:
<div ngbDropdownMenu>
<!-- added Delete button -->
<button ngbDropdownItem (click)="delete(data.id, data.name)">
{{ 'AbpAccount::Delete' | abpLocalization }}
</button>
</div>
The final actions dropdown UI looks like below:

Delete confirmation dialog
Open book-list.component.ts inapp\books\book-list folder and inject the ConfirmationService.
Replace the constructor as below:
import { ConfirmationService } from '@abp/ng.theme.shared';
//...
constructor(
private store: Store, private fb: FormBuilder,
private booksService: BooksService,
private confirmationService: ConfirmationService // <== added this line ==>
) { }
- We imported
ConfirmationService. - We injected
ConfirmationServiceto the constructor.
In the book-list.component.ts add a delete method :
import { GetBooks, CreateUpdateBook, DeleteBook } from '../../store/actions'; //<== added DeleteBook ==>
import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== added Confirmation ==>
//...
delete(id: string, name: string) {
this.confirmationService
.warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure')
.subscribe(status => {
if (status === Confirmation.Status.confirm) {
this.store.dispatch(new DeleteBook(id)).subscribe(() => this.get());
}
});
}
The delete method shows a confirmation popup and subscribes for the user response. DeleteBook action dispatched only if user clicks to the Yes button. The confirmation popup looks like below:

Next Part
See the next part of this tutorial.