Revised Angular tutorial Part II

pull/1634/head
Halil İbrahim Kalkan 6 years ago
parent e53c6c73a7
commit f125b17eaf

@ -8,6 +8,7 @@ This is the first part of the Angular tutorial series. See all parts:
- **Part I: Create the project and a book list page (this tutorial)** - **Part I: Create the project and a book list page (this tutorial)**
- [Part II: Create, Update and Delete books](Part-II.md) - [Part II: Create, Update and Delete books](Part-II.md)
- [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). 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).

@ -6,6 +6,7 @@ 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 I: Create the project and a book list page](Part-I.md)
- **Part II: Create, Update and Delete books (this tutorial)** - **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). 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).
@ -37,19 +38,19 @@ Open the `books.service.ts` and add a new method, named `create` to perform an H
```typescript ```typescript
create(body: Books.CreateUpdateBookInput): Observable<Books.Book> { create(body: Books.CreateUpdateBookInput): Observable<Books.Book> {
const request: Rest.Request<Books.CreateUpdateBookInput> = { return this.rest.request<Books.CreateUpdateBookInput, Books.Book>({
method: 'POST', method: 'POST',
url: '/api/app/book', url: '/api/app/book',
body, body,
}; });
return this.rest.request<Books.CreateUpdateBookInput, Books.Book>(request);
} }
``` ```
* `rest.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 #### State Definitions
Add the `CreateUpdateBook` action to `books.actions.ts` as shown below: Add the `CreateUpdateBook` action to the `books.actions.ts` as shown below:
```typescript ```typescript
import { Books } from '../models'; import { Books } from '../models';
@ -78,7 +79,7 @@ When the `SaveBook` action dispatched, the save method is executed. It call `cre
#### Add a Modal to BookListComponent #### Add a Modal to BookListComponent
Open the `book-list.component.html` and add the `abp-modal` to show/hide the book form. Open the `book-list.component.html` and add the `abp-modal` to show/hide the modal to create a new book.
```html ```html
<abp-modal [(visible)]="isModalOpen"> <abp-modal [(visible)]="isModalOpen">
@ -98,7 +99,7 @@ Open the `book-list.component.html` and add the `abp-modal` to show/hide the boo
`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. `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. Add a button, labeled `New book` to show the modal:
```html ```html
<div class="row"> <div class="row">
@ -161,8 +162,8 @@ buildForm() {
} }
``` ```
- The `group` method of `FormBuilder` creates a `FormGroup`. - The `group` method of `FormBuilder` (`fb`) creates a `FormGroup`.
- Added `Validators.required` static method that validation of form element. - Added `Validators.required` static method that validates the related form element.
Modify the `onAdd` method as shown below: Modify the `onAdd` method as shown below:
@ -173,7 +174,7 @@ onAdd() {
} }
``` ```
### Create the DOM Elements of the Form #### Create the DOM Elements of the Form
Open `book-list.component.html` and add the form in the body template of the modal. Open `book-list.component.html` and add the form in the body template of the modal.
@ -213,28 +214,32 @@ Open `book-list.component.html` and add the form in the body template of the mod
</ng-template> </ng-template>
``` ```
TODO: Add a short description. * 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. > We've used [NgBootstrap datepicker](https://ng-bootstrap.github.io/#/components/datepicker/overview) in this component.
Open the `book-list.component.ts` and then add the `bookTypes`. Open the `book-list.component.ts` and then create an array, named `bookTypeArr`:
```typescript ```typescript
//... //...
form: FormGroup; form: FormGroup;
bookTypeArr = Object.keys(Books.BookType).filter(bookType => typeof this.booksType[bookType] === 'number'); bookTypeArr = Object.keys(Books.BookType).filter(
bookType => typeof this.booksType[bookType] === 'number'
);
``` ```
The `bookTypes` variable added to generate array from `BookType` enum. The `bookTypes` equals like this: The `bookTypeArr` contains the fields of the `BookType` enum. Resulting array is shown below:
```js ```js
['Adventure', 'Biography', 'Dystopia', 'Fantastic' ...] ['Adventure', 'Biography', 'Dystopia', 'Fantastic' ...]
``` ```
### Add the Datepicker Requirements This array was used in the previous form template (in the `ngFor` loop).
Import `NgbDatepickerModule` to the `books.module.ts`. #### Datepicker Requirements
You need to import `NgbDatepickerModule` to the `books.module.ts`:
```typescript ```typescript
import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap';
@ -248,25 +253,39 @@ import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap';
export class BooksModule {} export class BooksModule {}
``` ```
Open the `book-list.component.html` and then add `providers` as shown below: Then open the `book-list.component.html` and add `providers` as shown below:
```typescript ```typescript
@Component({ @Component({
// ... // ...
styleUrls: ['./book-list.component.scss'],
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
}) })
export class BookListComponent implements OnInit { export class BookListComponent implements OnInit {
// ... // ...
``` ```
> The `NgbDateAdapter` convert Datepicker value type to `Date` type. See the [datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview) for more details. > The `NgbDateAdapter` converts Datepicker value to `Date` type. See the [datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview) for more details.
![new-book-form](images/bookstore-new-book-form.png) ![new-book-form](images/bookstore-new-book-form.png)
### Create a New Book #### Saving the Book
Add the `save` method to `BookListComponent` Open the `book-list.component.html` and add an `abp-button` to save the form.
```html
<ng-template #abpFooter>
<button type="button" class="btn btn-secondary" #abpClose>
Cancel
</button>
<abp-button iconClass="fa fa-check" (click)="save()">Save</abp-button>
</ng-template>
```
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`:
```typescript ```typescript
save() { save() {
@ -281,56 +300,32 @@ save() {
} }
``` ```
TODO: description ?? ### Updating An Existing Book
Then, open the `book-list.component.html` and add the `abp-button` for the run `save` method. #### BooksService
```html
<ng-template #abpFooter>
<button type="button" class="btn btn-secondary" #abpClose>
Cancel
</button>
<abp-button iconClass="fa fa-check" (click)="save()">Save</abp-button>
</ng-template>
```
Now, You can add a new book.
![bookstore-new-book-form-v2](images/bookstore-new-book-form-v2.png)
### Add HTTP GET and PUT Methods
TODO: Description
Open the `books.service.ts` and then add the `getById` and `update` methods. Open the `books.service.ts` and then add the `getById` and `update` methods.
```typescript ```typescript
getById(id: string): Observable<Books.Book> { getById(id: string): Observable<Books.Book> {
const request: Rest.Request<null> = { return this.rest.request<null, Books.Book>({
method: 'GET', method: 'GET',
url: `/api/app/book/${id}`, url: `/api/app/book/${id}`,
}; });
return this.rest.request<null, Books.Book>(request);
} }
update(body: Books.CreateUpdateBookInput, id: string): Observable<Books.Book> { update(body: Books.CreateUpdateBookInput, id: string): Observable<Books.Book> {
const request: Rest.Request<Books.CreateUpdateBookInput> = { return this.rest.request<Books.CreateUpdateBookInput, Books.Book>({
method: 'PUT', method: 'PUT',
url: `/api/app/book/${id}`, url: `/api/app/book/${id}`,
body, body,
}; });
return this.rest.request<Books.CreateUpdateBookInput, Books.Book>(request);
} }
``` ```
- Added the `getById` method to get the editing book by performing an HTTP request to the related endpoint. #### CreateUpdateBook Action
- Added the `update` method to update a book with the `id` by performing an HTTP request to the related endpoint.
TODO: header ??
Open the `books.actins.ts` and add `id` parameter. Open the `books.actins.ts` and add `id` parameter to the `CreateUpdateBook` action:
```typescript ```typescript
export class CreateUpdateBook { export class CreateUpdateBook {
@ -339,9 +334,7 @@ export class CreateUpdateBook {
} }
``` ```
Added `id` parameter to reuse the `BooksSave` while updating and creating a book. Open `books.state.ts` and modify the `save` method as show below:
Open `books.state.ts` and then modify the `save` method as show below:
```typescript ```typescript
@Action(CreateUpdateBook) @Action(CreateUpdateBook)
@ -358,9 +351,9 @@ save({ dispatch }: StateContext<Books.State>, { payload, id }: CreateUpdateBook)
} }
``` ```
### Update a Book #### BookListComponent
Inject `BooksService` dependency by adding it to the `book-list.component.ts` constructor and add variable named `selectedBook`. Inject `BooksService` dependency by adding it to the `book-list.component.ts` constructor and add a variable named `selectedBook`.
```typescript ```typescript
import { BooksService } from '../shared/books.service'; import { BooksService } from '../shared/books.service';
@ -373,10 +366,7 @@ constructor(
) )
``` ```
- Added `booksService` to get the detail of selected book by `id` before creating the form `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.
- Added `selectedBook` variable to reuse detail of selected book.
Modify the `buildForm` method to reuse the same form while editing a book.
```typescript ```typescript
buildForm() { buildForm() {
@ -401,9 +391,9 @@ Add the `onEdit` method as shown below:
} }
``` ```
- Added `onEdit` method to get selected book detail, build form and then show the modal. Added `onEdit` method to get the editing book, build the form and show the modal.
Add the `selectedBook` definition to `onAdd` method for reuse same form while adding a new book. Now, add the `selectedBook` definition to `onAdd` method to reuse the same form while creating a new book:
```typescript ```typescript
onAdd() { onAdd() {
@ -412,7 +402,7 @@ Add the `selectedBook` definition to `onAdd` method for reuse same form while ad
} }
``` ```
Modify the `save` method as shown below: Modify the `save` method to pass the id of the selected book as shown below:
```typescript ```typescript
save() { save() {
@ -428,8 +418,6 @@ save() {
} }
``` ```
Added the `this.selectedBook.id` property to reuse the `CreateUpdateBook` action.
#### Add "Actions" Dropdown to the Table #### Add "Actions" Dropdown to the Table
Open the `book-list.component.html` and add modify the `p-table` as shown below: Open the `book-list.component.html` and add modify the `p-table` as shown below:
@ -471,15 +459,16 @@ Open the `book-list.component.html` and add modify the `p-table` as shown belo
</p-table> </p-table>
``` ```
Actions button added to each row of the table. * 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. > We've used to [NgbDropdown](https://ng-bootstrap.github.io/#/components/dropdown/examples) for the dropdown menu of actions.
The Actions buttons looks like this: The final UI looks like:
![actions-buttons](images/bookstore-actions-buttons.png) ![actions-buttons](images/bookstore-actions-buttons.png)
Update the modal header for reuse the same modal. Update the modal header to change the title based on the current operation:
```html ```html
<ng-template #abpHeader> <ng-template #abpHeader>
@ -489,26 +478,24 @@ Update the modal header for reuse the same modal.
![actions-buttons](images/bookstore-edit-modal.png) ![actions-buttons](images/bookstore-edit-modal.png)
TODO: description & screenshot ?? ### Deleting an Existing Book
### Delete a Book #### BooksService
Open `books.service.ts` and the the `delete` method for delete a book with the `id` by performing an HTTP request to the related endpoint. 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:
```typescript ```typescript
delete(id: string): Observable<null> { delete(id: string): Observable<void> {
const request: Rest.Request<null> = { return this.rest.request<void, void>({
method: 'DELETE', method: 'DELETE',
url: `/api/app/book/${id}`, url: `/api/app/book/${id}`
}; });
return this.rest.request<null, null>(request);
} }
``` ```
### State Definitions #### DeleteBook Action
Add an action named `BooksDelete` to `books.actions.ts` Add an action named `DeleteBook` to `books.actions.ts`:
```typescript ```typescript
export class DeleteBook { export class DeleteBook {
@ -517,7 +504,7 @@ export class DeleteBook {
} }
``` ```
Then, open the `books.state.ts` and add the `delete` method that will listen to a `CreateUpdateBook` action to create a book Open the `books.state.ts` and add the `delete` method that will listen to the `DeleteBook` action to delete a book:
```typescript ```typescript
import { ... , DeleteBook } from '../actions/books.actions'; import { ... , DeleteBook } from '../actions/books.actions';
@ -528,11 +515,12 @@ delete({ dispatch }: StateContext<Books.State>, { id }: DeleteBook) {
} }
``` ```
TODO: description?? * Added `DeleteBook` to the import list.
* Uses `bookService` to delete the book.
### Add a Delete Button #### Add a Delete Button
Open `book-list.component.html` and modify the `ngbDropdownMenu` for add the delete button as shown below: Open `book-list.component.html` and modify the `ngbDropdownMenu` to add the delete button as shown below:
```html ```html
<div ngbDropdownMenu> <div ngbDropdownMenu>
@ -543,13 +531,13 @@ Open `book-list.component.html` and modify the `ngbDropdownMenu` for add the del
</div> </div>
``` ```
The final actions dropdown UI looks like this: The final actions dropdown UI looks like below:
![bookstore-final-actions-dropdown](images/bookstore-final-actions-dropdown.png) ![bookstore-final-actions-dropdown](images/bookstore-final-actions-dropdown.png)
### Open Confirmation Popup #### Delete Confirmation Dialog
Open `book-list.component.ts` and inject the `ConfirmationService` for show confirmation popup. Open `book-list.component.ts` and inject the `ConfirmationService`.
```typescript ```typescript
import { ConfirmationService } from '@abp/ng.theme.shared'; import { ConfirmationService } from '@abp/ng.theme.shared';
@ -560,7 +548,9 @@ constructor(
) )
``` ```
Add the following method to `BookListComponent`. > `ConfirmationService` is a simple service provided by ABP framework that internally uses the PrimeNG.
Add a delete method to the `BookListComponent`:
```typescript ```typescript
import { ... , DeleteBook } from '../../store/actions'; import { ... , DeleteBook } from '../../store/actions';
@ -577,10 +567,10 @@ delete(id: string, name: string) {
} }
``` ```
The `delete` method shows confirmation popup and listens to them. When close the popup, the subscribe block runs. If confirmed this popup, it will dispatch the `DeleteBook` action. 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:
The confirmation popup looks like this:
![bookstore-confirmation-popup](images/bookstore-confirmation-popup.png) ![bookstore-confirmation-popup](images/bookstore-confirmation-popup.png)
TODO: End ### Next Part
See the [next part](Part-III.md) of this tutorial.

@ -0,0 +1,178 @@
## ASP.NET Core MVC Tutorial - Part III
### About this Tutorial
This is the third 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](Part-II.md)
- **Part III: Integration Tests (this tutorial)**
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).
### Test Projects in the Solution
There are multiple test projects in the solution:
![bookstore-test-projects-v2](images/bookstore-test-projects-v2.png)
Each project is used to test the related application project. Test projects use the following libraries for testing:
* [xunit](https://xunit.github.io/) as the main test framework.
* [Shoudly](http://shouldly.readthedocs.io/en/latest/) as an assertion library.
* [NSubstitute](http://nsubstitute.github.io/) as a mocking library.
### Adding Test Data
Startup template contains the `BookStoreTestDataSeedContributor` class in the `Acme.BookStore.TestBase` project that creates some data to run tests on.
Change the `BookStoreTestDataSeedContributor` class as show below:
````C#
using System;
using System.Threading.Tasks;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Guids;
namespace Acme.BookStore
{
public class BookStoreTestDataSeedContributor
: IDataSeedContributor, ITransientDependency
{
private readonly IRepository<Book, Guid> _bookRepository;
private readonly IGuidGenerator _guidGenerator;
public BookStoreTestDataSeedContributor(
IRepository<Book, Guid> bookRepository,
IGuidGenerator guidGenerator)
{
_bookRepository = bookRepository;
_guidGenerator = guidGenerator;
}
public async Task SeedAsync(DataSeedContext context)
{
await _bookRepository.InsertAsync(
new Book
{
Id = _guidGenerator.Create(),
Name = "Test book 1",
Type = BookType.Fantastic,
PublishDate = new DateTime(2015, 05, 24),
Price = 21
}
);
await _bookRepository.InsertAsync(
new Book
{
Id = _guidGenerator.Create(),
Name = "Test book 2",
Type = BookType.Science,
PublishDate = new DateTime(2014, 02, 11),
Price = 15
}
);
}
}
}
````
* Injected `IRepository<Book, Guid>` and used it in the `SeedAsync` to create two book entities as the test data.
* Used `IGuidGenerator` service to create GUIDs. While `Guid.NewGuid()` would perfectly work for testing, `IGuidGenerator` has additional features especially important while using real databases (see the [Guid generation document](../../Guid-Generation.md) for more).
### Testing the BookAppService
Create a test class named `BookAppService_Tests` in the `Acme.BookStore.Application.Tests` project:
````C#
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.Application.Dtos;
using Xunit;
namespace Acme.BookStore
{
public class BookAppService_Tests : BookStoreApplicationTestBase
{
private readonly IBookAppService _bookAppService;
public BookAppService_Tests()
{
_bookAppService = GetRequiredService<IBookAppService>();
}
[Fact]
public async Task Should_Get_List_Of_Books()
{
//Act
var result = await _bookAppService.GetListAsync(
new PagedAndSortedResultRequestDto()
);
//Assert
result.TotalCount.ShouldBeGreaterThan(0);
result.Items.ShouldContain(b => b.Name == "Test book 1");
}
}
}
````
* `Should_Get_List_Of_Books` test simply uses `BookAppService.GetListAsync` method to get and check the list of users.
Add a new test that creates a valid new book:
````C#
[Fact]
public async Task Should_Create_A_Valid_Book()
{
//Act
var result = await _bookAppService.CreateAsync(
new CreateUpdateBookDto
{
Name = "New test book 42",
Price = 10,
PublishDate = DateTime.Now,
Type = BookType.ScienceFiction
}
);
//Assert
result.Id.ShouldNotBe(Guid.Empty);
result.Name.ShouldBe("New test book 42");
}
````
Add a new test that tries to create an invalid book and fails:
````C#
[Fact]
public async Task Should_Not_Create_A_Book_Without_Name()
{
var exception = await Assert.ThrowsAsync<AbpValidationException>(async () =>
{
await _bookAppService.CreateAsync(
new CreateUpdateBookDto
{
Name = "",
Price = 10,
PublishDate = DateTime.Now,
Type = BookType.ScienceFiction
}
);
});
exception.ValidationErrors
.ShouldContain(err => err.MemberNames.Any(mem => mem == "Name"));
}
````
* Since the `Name` is empty, ABP throws an `AbpValidationException`.
Open the **Test Explorer Window** (use Test -> Windows -> Test Explorer menu if it is not visible) and **Run All** tests:
![bookstore-appservice-tests](images/bookstore-appservice-tests.png)
Congratulations, green icons show that tests have been successfully passed!

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Loading…
Cancel
Save