## ASP.NET Core {{UI_Value}} Tutorial - Part 2
````json
//[doc-params]
{
"UI": ["MVC","NG"]
}
````
{{
if UI == "MVC"
DB="ef"
DB_Text="Entity Framework Core"
UI_Text="mvc"
else if UI == "NG"
DB="mongodb"
DB_Text="MongoDB"
UI_Text="angular"
else
DB ="?"
UI_Text="?"
end
}}
### About this tutorial
This is the second part of the ASP.NET Core {{UI_Value}} tutorial series. All parts:
* [Part I: Creating the project and book list page ](part-1.md )
* **Part II: Creating, updating and deleting books (this tutorial)**
* [Part III: Integration tests ](part-3.md )
*You can also watch [this video course ](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application ) prepared by an ABP community member, based on this tutorial.*
{{if UI == "MVC"}}
### Creating a new book
In this section, you will learn how to create a new modal dialog form to create a new book. The modal dialog will look like in the below image:
![bookstore-create-dialog ](./images/bookstore-create-dialog-2.png )
#### Create the modal form
Create a new razor page, named `CreateModal.cshtml` under the `Pages/Books` folder of the `Acme.BookStore.Web` project.
![bookstore-add-create-dialog ](./images/bookstore-add-create-dialog-v2.png )
##### CreateModal.cshtml.cs
Open the `CreateModal.cshtml.cs` file (`CreateModalModel` class) and replace with the following code:
````C#
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
namespace Acme.BookStore.Web.Pages.Books
{
public class CreateModalModel : BookStorePageModel
{
[BindProperty]
public CreateUpdateBookDto Book { get; set; }
private readonly IBookAppService _bookAppService;
public CreateModalModel(IBookAppService bookAppService)
{
_bookAppService = bookAppService;
}
public async Task< IActionResult > OnPostAsync()
{
await _bookAppService.CreateAsync(Book);
return NoContent();
}
}
}
````
* This class is derived from the `BookStorePageModel` instead of standard `PageModel` . `BookStorePageModel` inherits the `PageModel` and adds some common properties & methods that can be used in your page model classes.
* `[BindProperty]` attribute on the `Book` property binds post request data to this property.
* This class simply injects the `IBookAppService` in the constructor and calls the `CreateAsync` method in the `OnPostAsync` handler.
##### CreateModal.cshtml
Open the `CreateModal.cshtml` file and paste the code below:
````html
@page
@inherits Acme.BookStore.Web.Pages.BookStorePage
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
@model Acme.BookStore.Web.Pages.Books.CreateModalModel
@{
Layout = null;
}
< abp-dynamic-form abp-model = "Book" data-ajaxForm = "true" asp-page = "/Books/CreateModal" >
< abp-modal >
< abp-modal-header title = "@L[" NewBook " ] . Value " > < / abp-modal-header >
< abp-modal-body >
< abp-form-content / >
< / abp-modal-body >
< abp-modal-footer buttons = "@(AbpModalButtons.Cancel|AbpModalButtons.Save)" > < / abp-modal-footer >
< / abp-modal >
< / abp-dynamic-form >
````
* This modal uses `abp-dynamic-form` tag helper to automatically create the form from the model `CreateBookViewModel` .
* `abp-model` attribute indicates the model object where it's the `Book` property in this case.
* `data-ajaxForm` attribute sets the form to submit via AJAX, instead of a classic page post.
* `abp-form-content` tag helper is a placeholder to render the form controls (it is optional and needed only if you have added some other content in the `abp-dynamic-form` tag, just like in this page).
#### Add the "New book" button
Open the `Pages/Books/Index.cshtml` and set the content of `abp-card-header` tag as below:
````html
< abp-card-header >
< abp-row >
< abp-column size-md = "_6" >
< h2 > @L["Books"]< / h2 >
< / abp-column >
< abp-column size-md = "_6" class = "text-right" >
< abp-button id = "NewBookButton"
text="@L["NewBook"].Value"
icon="plus"
button-type="Primary" />
< / abp-column >
< / abp-row >
< / abp-card-header >
````
This adds a new button called **New book** to the **top-right** of the table:
![bookstore-new-book-button ](./images/bookstore-new-book-button.png )
Open the `pages/books/index.js` and add the following code just after the `Datatable` configuration:
````js
var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal');
createModal.onResult(function () {
dataTable.ajax.reload();
});
$('#NewBookButton').click(function (e) {
e.preventDefault();
createModal.open();
});
````
* `abp.ModalManager` is a helper class to manage modals in the client side. It internally uses Twitter Bootstrap's standard modal, but abstracts many details by providing a simple API.
Now, you can **run the application** and add new books using the new modal form.
### Updating a book
Create a new razor page, named `EditModal.cshtml` under the `Pages/Books` folder of the `Acme.BookStore.Web` project:
![bookstore-add-edit-dialog ](./images/bookstore-add-edit-dialog.png )
#### EditModal.cshtml.cs
Open the `EditModal.cshtml.cs` file (`EditModalModel` class) and replace with the following code:
````csharp
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
namespace Acme.BookStore.Web.Pages.Books
{
public class EditModalModel : BookStorePageModel
{
[HiddenInput]
[BindProperty(SupportsGet = true)]
public Guid Id { get; set; }
[BindProperty]
public CreateUpdateBookDto Book { get; set; }
private readonly IBookAppService _bookAppService;
public EditModalModel(IBookAppService bookAppService)
{
_bookAppService = bookAppService;
}
public async Task OnGetAsync()
{
var bookDto = await _bookAppService.GetAsync(Id);
Book = ObjectMapper.Map< BookDto , CreateUpdateBookDto > (bookDto);
}
public async Task< IActionResult > OnPostAsync()
{
await _bookAppService.UpdateAsync(Id, Book);
return NoContent();
}
}
}
````
* `[HiddenInput]` and `[BindProperty]` are standard ASP.NET Core MVC attributes. `SupportsGet` is used to be able to get `Id` value from query string parameter of the request.
* In the `GetAsync` method, we get `BookDto ` from `BookAppService` and this is being mapped to the DTO object `CreateUpdateBookDto` .
* The `OnPostAsync` uses `BookAppService.UpdateAsync()` to update the entity.
#### Mapping from BookDto to CreateUpdateBookDto
To be able to map the `BookDto` to `CreateUpdateBookDto` , configure a new mapping. To do this, open the `BookStoreWebAutoMapperProfile.cs` in the `Acme.BookStore.Web` project and change it as shown below:
````csharp
using AutoMapper;
namespace Acme.BookStore.Web
{
public class BookStoreWebAutoMapperProfile : Profile
{
public BookStoreWebAutoMapperProfile()
{
CreateMap< BookDto , CreateUpdateBookDto > ();
}
}
}
````
* We have just added `CreateMap<BookDto, CreateUpdateBookDto>();` to define this mapping.
#### EditModal.cshtml
Replace `EditModal.cshtml` content with the following content:
````html
@page
@inherits Acme.BookStore.Web.Pages.BookStorePage
@using Acme.BookStore.Web.Pages.Books
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
@model EditModalModel
@{
Layout = null;
}
< abp-dynamic-form abp-model = "Book" data-ajaxForm = "true" asp-page = "/Books/EditModal" >
< abp-modal >
< abp-modal-header title = "@L[" Update " ] . Value " > < / abp-modal-header >
< abp-modal-body >
< abp-input asp-for = "Id" / >
< abp-form-content / >
< / abp-modal-body >
< abp-modal-footer buttons = "@(AbpModalButtons.Cancel|AbpModalButtons.Save)" > < / abp-modal-footer >
< / abp-modal >
< / abp-dynamic-form >
````
This page is very similar to the `CreateModal.cshtml` , except:
* It includes an `abp-input` for the `Id` property to store `Id` of the editing book (which is a hidden input).
* It uses `Books/EditModal` as the post URL and *Update* text as the modal header.
#### Add "Actions" dropdown to the table
We will add a dropdown button to the table named *Actions* .
Open the `Pages/Books/Index.cshtml` page and change the `<abp-table>` section as shown below:
````html
< abp-table striped-rows = "true" id = "BooksTable" >
< thead >
< tr >
< th > @L["Actions"]< / th >
< th > @L["Name"]< / th >
< th > @L["Type"]< / th >
< th > @L["PublishDate"]< / th >
< th > @L["Price"]< / th >
< th > @L["CreationTime"]< / th >
< / tr >
< / thead >
< / abp-table >
````
* We just added a new `th` tag for the "*Actions*" button.
Open the `pages/books/index.js` and replace the content as below:
````js
$(function () {
var l = abp.localization.getResource('BookStore');
var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal');
var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal');
var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({
processing: true,
serverSide: true,
paging: true,
searching: false,
autoWidth: false,
scrollCollapse: true,
order: [[1, "asc"]],
ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList),
columnDefs: [
{
rowAction: {
items:
[
{
text: l('Edit'),
action: function (data) {
editModal.open({ id: data.record.id });
}
}
]
}
},
{ data: "name" },
{ data: "type" },
{ data: "publishDate" },
{ data: "price" },
{ data: "creationTime" }
]
}));
createModal.onResult(function () {
dataTable.ajax.reload();
});
editModal.onResult(function () {
dataTable.ajax.reload();
});
$('#NewBookButton').click(function (e) {
e.preventDefault();
createModal.open();
});
});
````
* Used `abp.localization.getResource('BookStore')` to be able to use the same localization texts defined on the server-side.
* Added a new `ModalManager` named `createModal` to open the create modal dialog.
* Added a new `ModalManager` named `editModal` to open the edit modal dialog.
* Added a new column at the beginning of the `columnDefs` section. This column is used for the "*Actions*" dropdown button.
* "*New Book*" action simply calls `createModal.open()` to open the create dialog.
* "*Edit*" action simply calls `editModal.open()` to open the edit dialog.
You can run the application and edit any book by selecting the edit action. The final UI looks as below:
![bookstore-books-table-actions ](./images/bookstore-edit-button.png )
### Deleting a book
Open the `pages/books/index.js` and add a new item to the `rowAction` `items` :
````js
{
text: l('Delete'),
confirmMessage: function (data) {
return l('BookDeletionConfirmationMessage', data.record.name);
},
action: function (data) {
acme.bookStore.book
.delete(data.record.id)
.then(function() {
abp.notify.info(l('SuccessfullyDeleted'));
dataTable.ajax.reload();
});
}
}
````
* `confirmMessage` option is used to ask a confirmation question before executing the `action` .
* `acme.bookStore.book.delete()` method makes an AJAX request to JavaScript proxy function to delete a book.
* `abp.notify.info()` shows a notification after the delete operation.
The final `index.js` content is shown below:
````js
$(function () {
var l = abp.localization.getResource('BookStore');
var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal');
var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal');
var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({
processing: true,
serverSide: true,
paging: true,
searching: false,
autoWidth: false,
scrollCollapse: true,
order: [[1, "asc"]],
ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList),
columnDefs: [
{
rowAction: {
items:
[
{
text: l('Edit'),
action: function (data) {
editModal.open({ id: data.record.id });
}
},
{
text: l('Delete'),
confirmMessage: function (data) {
return l('BookDeletionConfirmationMessage', data.record.name);
},
action: function (data) {
acme.bookStore.book
.delete(data.record.id)
.then(function() {
abp.notify.info(l('SuccessfullyDeleted'));
dataTable.ajax.reload();
});
}
}
]
}
},
{ data: "name" },
{ data: "type" },
{ data: "publishDate" },
{ data: "price" },
{ data: "creationTime" }
]
}));
createModal.onResult(function () {
dataTable.ajax.reload();
});
editModal.onResult(function () {
dataTable.ajax.reload();
});
$('#NewBookButton').click(function (e) {
e.preventDefault();
createModal.open();
});
});
````
Open the `en.json` in the `Acme.BookStore.Domain.Shared` project and add the following translations:
````json
"BookDeletionConfirmationMessage": "Are you sure to delete the book {0}?",
"SuccessfullyDeleted": "Successfully deleted"
````
Run the application and try to delete a book.
{{end}}
{{if UI == "NG"}}
### 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:
```js
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 `CreateUpdateBookInput` interface.
* You can see the properties of this interface from Swagger UI.
* The `CreateUpdateBookInput` interface matches with the `CreateUpdateBookDto` in the backend.
#### Service method
Open the `books.service.ts` file in `app\books\shared` folder and replace the content as below:
```js
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 `create` method to perform an HTTP Post request to the server.
- `restService.request` function gets generic parameters for the types sent to and received from the server. This example sends a `CreateUpdateBookInput` object and receives a `Book` object (you can set `void` for request or return type if not used).
#### State definitions
Open `books.action.ts` in `app\store\actions` folder and replace the content as below:
```js
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 `CreateUpdateBook` action.
Open `books.state.ts` file in `app\store\states` and replace the content as below:
```js
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 `CreateUpdateBook` action and defined the `save` method that will listen to a `CreateUpdateBook` action 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:
```html
< 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-modal` which renders a modal to allow user to create a new book.
* `abp-modal` is a pre-built component to show modals. While you could use another approach to show a modal, `abp-modal` provides additional benefits.
* We added `New book` button to the `AbpContentToolbar` .
Open `book-list.component.ts` file in `books\book-list` folder and replace the content as below:
```js
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 = false` and `createBook` method.
You can open your browser and click **New book** button to see the new modal.
![Empty modal for new book ](./images/bookstore-empty-new-book-modal.png )
#### Create a reactive form
[Reactive forms ](https://angular.io/guide/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:
```js
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: FormBuilder` service to the constructor. The [FormBuilder ](https://angular.io/api/forms/FormBuilder ) service provides convenient methods for generating controls. It reduces the amount of boilerplate needed to build complex forms.
* We added `buildForm` method to the end of the file and executed `buildForm()` in the `createBook` method. This method creates a reactive form to be able to create a new book.
* The `group` method of `FormBuilder` , `fb` creates a `FormGroup` .
* Added `Validators.required` static method which validates the relevant form element.
#### 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:
```html
< 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` , `Type` and `Publish` date fields.
- We've used [NgBootstrap datepicker ](https://ng-bootstrap.github.io/#/components/datepicker/overview ) in this component.
#### Datepicker requirements
Open `books.module.ts` file in `app\books` folder and replace the content as below:
```js
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 `NgbDatepickerModule` to 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:
```js
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, NgbDateAdapter`
* We added a new provider `NgbDateAdapter` that converts Datepicker value to `Date` type. See the [datepicker adapters ](https://ng-bootstrap.github.io/#/components/datepicker/overview ) for more details.
* We added `bookTypeArr` array to be able to use it in the combobox values. The `bookTypeArr` contains the fields of the `BookType` enum. Resulting array is shown below:
```js
['Adventure', 'Biography', 'Dystopia', 'Fantastic' ...]
```
This array was used in the previous form template in the `ngFor` loop.
Now, you can open your browser to see the changes:
![New book modal ](./images/bookstore-new-book-form.png )
#### 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.
```html
< 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:
![Save button to the modal ](./images/bookstore-new-book-form-v2.png )
Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below:
```js
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 `save` method
### Updating an existing book
#### BooksService
Open the `books.service.ts` in `app\books\shared` folder and add the `getById` and `update` methods.
```js
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:
```js
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 `id` parameter to the `CreateUpdateBook` action's constructor.
Open the `books.state.ts` in `app\store\states` folder and replace the `save` method as below:
```js
@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` .
```js
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 `selectedBook` as `Books.Book` .
* We injected `BooksService` to the constructor. `BooksService` is being used to retrieve the book data which is being edited.
* We added `editBook` method. This method fetches the book with the given `Id` and sets it to `selectedBook` object.
* We replaced the `buildForm` method so that it creates the form with the `selectedBook` data.
* We replaced the `createBook` method so it sets `selectedBook` to an empty object.
* We added `selectedBook.id` to the constructor of the new `CreateUpdateBook` .
#### 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:
```html
< 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 `th` for the "Actions" column.
- We added `button` with `ngbDropdownToggle` to open actions when clicked the button.
- We have used to [NgbDropdown ](https://ng-bootstrap.github.io/#/components/dropdown/examples ) for the dropdown menu of actions.
The final UI looks like as below:
![Action buttons ](./images/bookstore-actions-buttons.png )
Open `book-list.component.html` in `app\books\book-list` folder and find the `<ng-template #abpHeader>` tag and replace the content as below.
```html
< 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.
```js
delete(id: string): Observable< void > {
return this.restService.request< void , void > ({
method: 'DELETE',
url: `/api/app/book/${id}`
});
}
```
* `Delete` method gets `id` parameter and makes a `DELETE` HTTP request to the relevant endpoint.
#### DeleteBook action
Open `books.actions.ts` in `app\store\actions ` folder and add an action named `DeleteBook` .
```js
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:
```js
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 `DeleteBook` action 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:
```html
< 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:
![bookstore-final-actions-dropdown ](./images/bookstore-final-actions-dropdown.png )
#### Delete confirmation dialog
Open `book-list.component.ts` in`app\books\book-list` folder and inject the `ConfirmationService` .
Replace the constructor as below:
```js
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 `ConfirmationService` to the constructor.
In the `book-list.component.ts` add a delete method :
```js
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:
![bookstore-confirmation-popup ](./images/bookstore-confirmation-popup.png )
{{end}}
### Next Part
See the [next part ](part-3.md ) of this tutorial.