## Angular Tutorial - Part II ### About this Tutorial This is the second part of the Angular tutorial series. See all parts: - [Part I: Create the project and a book list page](Part-I.md) - **Part II: Create, Update and Delete books (this tutorial)** - [Part III: Integration Tests](Part-III.md) You can access to the **source code** of the application from the [GitHub repository](https://github.com/abpframework/abp/tree/dev/samples/BookStore-Angular-MongoDb). ### 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 Create an interface, named `CreateUpdateBookInput` in the `books.ts` as shown below: ```js export namespace Books { //... export interface CreateUpdateBookInput { name: string; type: BookType; publishDate: string; price: number; } } ``` `CreateUpdateBookInput` interface matches the `CreateUpdateBookDto` in the backend. #### Service Method Open the `books.service.ts` and add a new method, named `create` to perform an HTTP POST request to the server: ```js create(createBookInput: Books.CreateUpdateBookInput): Observable { return this.restService.request({ method: 'POST', url: '/api/app/book', body: createBookInput }); } ``` - `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 Add the `CreateUpdateBook` action to the `books.actions.ts` as shown below: ```js import { Books } from '../models'; export class CreateUpdateBook { static readonly type = '[Books] Create Update Book'; constructor(public payload: Books.CreateUpdateBookInput) {} } ``` Open `books.state.ts` and define the `save` method that will listen to a `CreateUpdateBook` action to create a book: ```js import { ... , CreateUpdateBook } from '../actions/books.actions'; import { ... , switchMap } from 'rxjs/operators'; //... @Action(CreateUpdateBook) save(ctx: StateContext, action: CreateUpdateBook) { return this.booksService .create(action.payload) .pipe(switchMap(() => ctx.dispatch(new GetBooks()))); } ``` When the `SaveBook` action dispatched, the save method is executed. It call `create` method of the `BooksService` defined before. After the service call, `BooksState` dispatches the `GetBooks` action to get books again from the server to refresh the page. #### Add a Modal to BookListComponent Open the `book-list.component.html` and add the `abp-modal` to show/hide the modal to create a new book. ```html

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. Add a button, labeled `New book` to show the modal: ```html
Books
``` Open the `book-list.component.ts` and add `isModalOpen` variable and `createBook` method to show/hide the modal. ```js isModalOpen = false; //... createBook() { this.isModalOpen = true; } ``` ![empty-modal](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. Add a `form` variable and inject a `FormBuilder` service to the `book-list.component.ts` as shown below (remember add the import statement). ```js import { FormGroup, FormBuilder, Validators } from '@angular/forms'; form: FormGroup; constructor( //... private fb: FormBuilder ) {} ``` > 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. Add the `buildForm` method to create book form. ```js buildForm() { this.form = this.fb.group({ name: ['', Validators.required], type: [null, Validators.required], publishDate: [null, Validators.required], price: [null, Validators.required], }); } ``` - The `group` method of `FormBuilder` (`fb`) creates a `FormGroup`. - Added `Validators.required` static method that validates the related form element. Modify the `createBook` method as shown below: ```js createBook() { this.buildForm(); this.isModalOpen = true; } ``` #### Create the DOM Elements of the Form Open `book-list.component.html` and add the form in the body template of the modal. ```html
*
*
*
*
``` - 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 You need to import `NgbDatepickerModule` to the `books.module.ts`: ```js import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; @NgModule({ imports: [ // ... NgbDatepickerModule, ], }) export class BooksModule {} ``` Then open the `book-list.component.ts` and add `providers` as shown below: ```js import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; @Component({ // ... providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], }) export class BookListComponent implements OnInit { // ... ``` > The `NgbDateAdapter` converts Datepicker value to `Date` type. See the [datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview) for more details. #### Create the Book Type Array Open the `book-list.component.ts` and then create an array, named `bookTypeArr`: ```js //... booksType = Books.BookType; bookTypeArr = Object.keys(Books.BookType).filter( bookType => typeof this.booksType[bookType] === 'number' ); ``` 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). ![new-book-form](images/bookstore-new-book-form.png) #### Saving the Book Open the `book-list.component.html` and add an `abp-button` to save the form. ```html ``` This adds a save button to the bottom area of the modal: ![bookstore-new-book-form-v2](images/bookstore-new-book-form-v2.png) Then define a `save` method in the `BookListComponent`: ```js //... import { ..., CreateUpdateBook } from '../../store/actions'; //... save() { if (this.form.invalid) { return; } this.store.dispatch(new CreateUpdateBook(this.form.value)).subscribe(() => { this.isModalOpen = false; this.form.reset(); }); } ``` ### Updating An Existing Book #### BooksService Open the `books.service.ts` and then add the `getById` and `update` methods. ```js getById(id: string): Observable { return this.restService.request({ method: 'GET', url: `/api/app/book/${id}` }); } update(updateBookInput: Books.CreateUpdateBookInput, id: string): Observable { return this.restService.request({ method: 'PUT', url: `/api/app/book/${id}`, body: updateBookInput }); } ``` #### CreateUpdateBook Action Open the `books.actions.ts` and add `id` parameter to the `CreateUpdateBook` action: ```js export class CreateUpdateBook { static readonly type = '[Books] Create Update Book'; constructor(public payload: Books.CreateUpdateBookInput, public id?: string) {} } ``` Open `books.state.ts` and modify the `save` method as show below: ```js @Action(CreateUpdateBook) save(ctx: StateContext, action: CreateUpdateBook) { let request; if (action.id) { request = this.booksService.update(action.payload, action.id); } else { request = this.booksService.create(action.payload); } return request.pipe(switchMap(() => ctx.dispatch(new GetBooks()))); } ``` #### BookListComponent Inject `BooksService` dependency by adding it to the `book-list.component.ts` constructor and add a variable named `selectedBook`. ```js import { BooksService } from '../shared/books.service'; //... selectedBook = {} as Books.Book; constructor( //... private booksService: BooksService ) ``` `booksService` is used to get the editing book to prepare the form. Modify the `buildForm` method to reuse the same form while editing a book. ```js buildForm() { this.form = this.fb.group({ name: [this.selectedBook.name || '', Validators.required], type: this.selectedBook.type || null, publishDate: this.selectedBook.publishDate ? new Date(this.selectedBook.publishDate) : null, price: this.selectedBook.price || null, }); } ``` Add the `editBook` method as shown below: ```js editBook(id: string) { this.booksService.getById(id).subscribe(book => { this.selectedBook = book; this.buildForm(); this.isModalOpen = true; }); } ``` Added `editBook` method to get the editing book, build the form and show the modal. Now, add the `selectedBook` definition to `createBook` method to reuse the same form while creating a new book: ```js createBook() { this.selectedBook = {} as Books.Book; //... } ``` Modify the `save` method to pass the id of the selected book as shown below: ```js save() { if (this.form.invalid) { return; } this.store.dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id)) .subscribe(() => { this.isModalOpen = false; this.form.reset(); }); } ``` #### Add "Actions" Dropdown to the Table Open the `book-list.component.html` and add modify the `p-table` as shown below: ```html Actions Book name Book type Publish date Price
{%{{{ data.name }}}%} {%{{{ booksType[data.type] }}}%} {%{{{ data.publishDate | date }}}%} {%{{{ data.price }}}%}
``` - Added a `th` for the "Actions" column. - Added `button` with `ngbDropdownToggle` to open actions when clicked the button. > We've used to [NgbDropdown](https://ng-bootstrap.github.io/#/components/dropdown/examples) for the dropdown menu of actions. The final UI looks like: ![actions-buttons](images/bookstore-actions-buttons.png) Update the modal header to change the title based on the current operation: ```html

{%{{{ selectedBook.id ? 'Edit' : 'New Book' }}}%}

``` ![actions-buttons](images/bookstore-edit-modal.png) ### Deleting an Existing Book #### BooksService Open `books.service.ts` and add a `delete` method to delete a book with the `id` by performing an HTTP request to the related endpoint: ```js delete(id: string): Observable { return this.restService.request({ method: 'DELETE', url: `/api/app/book/${id}` }); } ``` #### DeleteBook Action Add an action named `DeleteBook` to `books.actions.ts`: ```js export class DeleteBook { static readonly type = '[Books] Delete'; constructor(public id: string) {} } ``` Open the `books.state.ts` and add the `delete` method that will listen to the `DeleteBook` action to delete a book: ```js import { ... , DeleteBook } from '../actions/books.actions'; //... @Action(DeleteBook) delete(ctx: StateContext, action: DeleteBook) { return this.booksService.delete(action.id).pipe(switchMap(() => ctx.dispatch(new GetBooks()))); } ``` - Added `DeleteBook` to the import list. - Uses `bookService` to delete the book. #### Add a Delete Button Open `book-list.component.html` and modify the `ngbDropdownMenu` to add the delete button as shown below: ```html
...
``` 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` and inject the `ConfirmationService`. ```js import { ConfirmationService } from '@abp/ng.theme.shared'; //... constructor( //... private confirmationService: ConfirmationService ) ``` > `ConfirmationService` is a simple service provided by ABP framework that internally uses the PrimeNG. Add a delete method to the `BookListComponent`: ```js import { ... , DeleteBook } from '../../store/actions'; import { ... , Toaster } from '@abp/ng.theme.shared'; //... delete(id: string, name: string) { this.confirmationService .error(`${name} will be deleted. Do you confirm that?`, 'Are you sure?') .subscribe(status => { if (status === Toaster.Status.confirm) { this.store.dispatch(new DeleteBook(id)); } }); } ``` 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) ### Next Part See the [next part](Part-III.md) of this tutorial.