diff --git a/docs/en/Tutorials/Part-1.md b/docs/en/Tutorials/Part-1.md index 0ff28f2c47..e3f988779b 100644 --- a/docs/en/Tutorials/Part-1.md +++ b/docs/en/Tutorials/Part-1.md @@ -880,7 +880,7 @@ Run the following command in the terminal to create a new state, named `BooksSta npx @ngxs/cli --name books --directory src/app/books ``` -* This command creates books.state.ts and books.actions.ts files in the `src/app/books/state` folder. See the [NGXS CLI documentation](https://www.ngxs.io/plugins/cli). +* This command creates `books.state.ts` and `books.actions.ts` files in the `src/app/books/state` folder. See the [NGXS CLI documentation](https://www.ngxs.io/plugins/cli). Import the `BooksState` to the `app.module.ts` in the `src/app` folder and then add the `BooksState` to `forRoot` static method of `NgxsModule` as an array element of the first parameter of the method. @@ -901,89 +901,27 @@ import { BooksState } from './books/state/books.state'; //<== imported BooksStat export class AppModule {} ``` -#### Get books data from backend +#### Generate proxies -Create data types to map the data from the backend (you can check Swagger UI or your backend API to see the data format). +ABP CLI provides `generate-proxy` command that generates client proxies for your HTTP APIs to make easy to consume your services from the client side. Before running generate-proxy command, your host must be up and running. See the [CLI documentation](../CLI.md) -![BookDto properties](./images/bookstore-swagger-book-dto-properties.png) - -Open the `books.ts` file in the `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 `Book` interface that represents a book object and `BookType` enum which represents a book category. - -#### BooksService - -Create a new service, named `BooksService` to perform `HTTP` calls to the server: +Run the following command in the `angular` folder: ```bash -yarn ng generate service books/shared/books +abp generate-proxy --module app ``` -![service-terminal-output](./images/bookstore-service-terminal-output.png) +![Generate proxy command](./images/generate-proxy-command.png) -Open the `books.service.ts` file in `app\books\shared` folder and replace the content as below: +The generated files looks like below: -```js -import { Injectable } from '@angular/core'; -import { RestService } from '@abp/ng.core'; -import { Books } from '../../store/models'; -import { Observable } from 'rxjs'; +![Generated files](./images/generated-proxies.png) -@Injectable({ - providedIn: 'root', -}) -export class BooksService { - constructor(private restService: RestService) {} - - get(): Observable { - return this.restService.request({ - method: 'GET', - url: '/api/app/book' - }); - } -} -``` +#### GetBooks Action -* We added the `get` method to get the list of books by performing an HTTP request to the related endpoint. +Actions can either be thought of as a command which should trigger something to happen, or as the resulting event of something that has already happened. [See NGXS Actions documentation](https://www.ngxs.io/concepts/actions). -Open the`books.actions.ts` file in `app\store\actions` folder and replace the content below: +Open the `books.actions.ts` file in `app/books/state` folder and replace the content below: ```js export class GetBooks { @@ -993,43 +931,48 @@ export class GetBooks { #### Implement BooksState -Open the `books.state.ts` file in `app\store\states` folder and replace the content below: +Open the `books.state.ts` file in `app/books/state` folder and replace the content below: ```js +import { PagedResultDto } from '@abp/ng.core'; import { State, Action, StateContext, Selector } from '@ngxs/store'; -import { GetBooks } from '../actions/books.actions'; -import { Books } from '../models/books'; -import { BooksService } from '../../books/shared/books.service'; +import { GetBooks } from './books.actions'; +import { BookService } from '../../app/shared/services'; import { tap } from 'rxjs/operators'; import { Injectable } from '@angular/core'; +import { BookDto } from '../../app/shared/models'; -@State({ +export class BooksStateModel { + public book: PagedResultDto; +} + +@State({ name: 'BooksState', - defaults: { books: {} } as Books.State, + defaults: { book: {} } as BooksStateModel, }) @Injectable() export class BooksState { @Selector() - static getBooks(state: Books.State) { - return state.books.items || []; + static getBooks(state: BooksStateModel) { + return state.book.items || []; } - constructor(private booksService: BooksService) {} + constructor(private bookService: BookService) {} @Action(GetBooks) - get(ctx: StateContext) { - return this.booksService.get().pipe( - tap(booksResponse => { + get(ctx: StateContext) { + return this.bookService.getListByInput().pipe( + tap((booksResponse) => { ctx.patchState({ - books: booksResponse, + book: booksResponse, }); - }), + }) ); } } ``` - -* We added the `GetBooks` action that retrieves the books data via `BooksService` and patches the state. +* We added the book property to BooksStateModel model. +* We added the `GetBooks` action that retrieves the books data via `BooksService` that generated via ABP CLI and patches the state. * `NGXS` requires to return the observable without subscribing it in the get function. #### BookListComponent @@ -1038,11 +981,12 @@ Open the `book-list.component.ts` file in `app\books\book-list` folder and repla ```js import { Component, OnInit } from '@angular/core'; -import { Store, Select } from '@ngxs/store'; -import { BooksState } from '../../store/states'; +import { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; -import { Books } from '../../store/models'; -import { GetBooks } from '../../store/actions'; +import { finalize } from 'rxjs/operators'; +import { BookDto, BookType } from '../../app/shared/models'; +import { GetBooks } from '../state/books.actions'; +import { BooksState } from '../state/books.state'; @Component({ selector: 'app-book-list', @@ -1051,13 +995,13 @@ import { GetBooks } from '../../store/actions'; }) export class BookListComponent implements OnInit { @Select(BooksState.getBooks) - books$: Observable; + books$: Observable; - booksType = Books.BookType; + booksType = BookType; loading = false; - constructor(private store: Store) { } + constructor(private store: Store) {} ngOnInit() { this.get(); @@ -1065,9 +1009,10 @@ export class BookListComponent implements OnInit { get() { this.loading = true; - this.store.dispatch(new GetBooks()).subscribe(() => { - this.loading = false; - }); + this.store + .dispatch(new GetBooks()) + .pipe(finalize(() => (this.loading = false))) + .subscribe(() => {}); } } ``` diff --git a/docs/en/Tutorials/Part-2.md b/docs/en/Tutorials/Part-2.md index 9565724cb8..ddee41325f 100644 --- a/docs/en/Tutorials/Part-2.md +++ b/docs/en/Tutorials/Part-2.md @@ -456,161 +456,76 @@ Run the application and try to delete a 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 { - return this.restService.request({ - method: 'GET', - url: '/api/app/book' - }); - } - - //<== added create method ==> - create(createBookInput: Books.CreateUpdateBookInput): Observable { - return this.restService.request({ - 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: +Open `books.action.ts` in `books\state` folder and replace the content as below: ```js -import { Books } from '../models'; //<== added this line ==> +import { CreateUpdateBookDto } from '../../app/shared/models'; //<== added this line ==> export class GetBooks { static readonly type = '[Books] Get'; } -//added CreateUpdateBook class +// added CreateUpdateBook class export class CreateUpdateBook { static readonly type = '[Books] Create Update Book'; - constructor(public payload: Books.CreateUpdateBookInput) { } + constructor(public payload: CreateUpdateBookDto) { } } ``` -* We imported the Books namespace and created the `CreateUpdateBook` action. +* We imported the `CreateUpdateBookDto` model and created the `CreateUpdateBook` action. -Open `books.state.ts` file in `app\store\states` and replace the content as below: +Open `books.state.ts` file in `books\state` folder and replace the content as below: ```js +import { PagedResultDto } from '@abp/ng.core'; 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 { GetBooks, CreateUpdateBook } from './books.actions'; // <== added CreateUpdateBook==> +import { BookService } from '../../app/shared/services'; import { tap } from 'rxjs/operators'; import { Injectable } from '@angular/core'; +import { BookDto } from '../../app/shared/models'; + +export class BooksStateModel { + public book: PagedResultDto; +} -@State({ +@State({ name: 'BooksState', - defaults: { books: {} } as Books.State, + defaults: { book: {} } as BooksStateModel, }) @Injectable() export class BooksState { @Selector() - static getBooks(state: Books.State) { - return state.books.items || []; + static getBooks(state: BooksStateModel) { + return state.book.items || []; } - constructor(private booksService: BooksService) { } + constructor(private bookService: BookService) {} @Action(GetBooks) - get(ctx: StateContext) { - return this.booksService.get().pipe( - tap(booksResponse => { + get(ctx: StateContext) { + return this.bookService.getListByInput().pipe( + tap((bookResponse) => { ctx.patchState({ - books: booksResponse, + book: bookResponse, }); - }), + }) ); } - //added CreateUpdateBook action listener + // added CreateUpdateBook action listener @Action(CreateUpdateBook) - save(ctx: StateContext, action: CreateUpdateBook) { - return this.booksService.create(action.payload); + save(ctx: StateContext, action: CreateUpdateBook) { + return this.bookService.createByInput(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`. +When the `SaveBook` action dispatched, the save method is being executed. It calls `create` method of the `BookService`. #### Add a modal to BookListComponent @@ -694,11 +609,12 @@ Open `book-list.component.ts` file in `books\book-list` folder and replace the c ```js import { Component, OnInit } from '@angular/core'; -import { Store, Select } from '@ngxs/store'; -import { BooksState } from '../../store/states'; +import { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; -import { Books } from '../../store/models'; -import { GetBooks } from '../../store/actions'; +import { finalize } from 'rxjs/operators'; +import { BookDto, BookType } from '../../app/shared/models'; +import { GetBooks } from '../state/books.actions'; +import { BooksState } from '../state/books.state'; @Component({ selector: 'app-book-list', @@ -707,15 +623,15 @@ import { GetBooks } from '../../store/actions'; }) export class BookListComponent implements OnInit { @Select(BooksState.getBooks) - books$: Observable; + books$: Observable; - booksType = Books.BookType; + booksType = BookType; loading = false; - isModalOpen = false; //<== added this line ==> + isModalOpen = false; // <== added this line ==> - constructor(private store: Store) { } + constructor(private store: Store) {} ngOnInit() { this.get(); @@ -723,12 +639,13 @@ export class BookListComponent implements OnInit { get() { this.loading = true; - this.store.dispatch(new GetBooks()).subscribe(() => { - this.loading = false; - }); + this.store + .dispatch(new GetBooks()) + .pipe(finalize(() => (this.loading = false))) + .subscribe(() => {}); } - //added createBook method + // added createBook method createBook() { this.isModalOpen = true; } @@ -749,12 +666,13 @@ Open `book-list.component.ts` file in `app\books\book-list` folder and replace t ```js import { Component, OnInit } from '@angular/core'; -import { Store, Select } from '@ngxs/store'; -import { BooksState } from '../../store/states'; +import { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; -import { Books } from '../../store/models'; -import { GetBooks } from '../../store/actions'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; //<== added this line ==> +import { finalize } from 'rxjs/operators'; +import { BookDto, BookType } from '../../app/shared/models'; +import { GetBooks } from '../state/books.actions'; +import { BooksState } from '../state/books.state'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // <== added this line ==> @Component({ selector: 'app-book-list', @@ -763,17 +681,17 @@ import { FormGroup, FormBuilder, Validators } from '@angular/forms'; //<== added }) export class BookListComponent implements OnInit { @Select(BooksState.getBooks) - books$: Observable; + books$: Observable; - booksType = Books.BookType; + booksType = BookType; loading = false; isModalOpen = false; - form: FormGroup; + form: FormGroup; // <== added this line ==> - constructor(private store: Store, private fb: FormBuilder) { } //<== added FormBuilder ==> + constructor(private store: Store, private fb: FormBuilder) {} // <== added FormBuilder ==> ngOnInit() { this.get(); @@ -781,9 +699,10 @@ export class BookListComponent implements OnInit { get() { this.loading = true; - this.store.dispatch(new GetBooks()).subscribe(() => { - this.loading = false; - }); + this.store + .dispatch(new GetBooks()) + .pipe(finalize(() => (this.loading = false))) + .subscribe(() => {}); } createBook() { @@ -791,7 +710,7 @@ export class BookListComponent implements OnInit { this.isModalOpen = true; } - //added buildForm method + // added buildForm method buildForm() { this.form = this.fb.group({ name: ['', Validators.required], @@ -804,6 +723,7 @@ export class BookListComponent implements OnInit { ``` * We imported `FormGroup, FormBuilder and Validators`. +* We added `form: FormGroup` variable. * 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`. @@ -880,35 +800,34 @@ 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 { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; -import { Books } from '../../store/models'; -import { GetBooks } from '../../store/actions'; +import { finalize } from 'rxjs/operators'; +import { BookDto, BookType } from '../../app/shared/models'; +import { GetBooks } from '../state/books.actions'; +import { BooksState } from '../state/books.state'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; -import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; //<== added this line ==> +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 ==> + providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], // <== added this line ==> }) export class BookListComponent implements OnInit { @Select(BooksState.getBooks) - books$: Observable; + books$: Observable; - booksType = Books.BookType; + booksType = BookType; //added bookTypeArr array - bookTypeArr = Object.keys(Books.BookType).filter( - bookType => typeof this.booksType[bookType] === 'number' + bookTypeArr = Object.keys(BookType).filter( + (bookType) => typeof this.booksType[bookType] === 'number' ); loading = false; @@ -917,7 +836,7 @@ export class BookListComponent implements OnInit { form: FormGroup; - constructor(private store: Store, private fb: FormBuilder) { } + constructor(private store: Store, private fb: FormBuilder) {} ngOnInit() { this.get(); @@ -925,9 +844,10 @@ export class BookListComponent implements OnInit { get() { this.loading = true; - this.store.dispatch(new GetBooks()).subscribe(() => { - this.loading = false; - }); + this.store + .dispatch(new GetBooks()) + .pipe(finalize(() => (this.loading = false))) + .subscribe(() => {}); } createBook() { @@ -981,19 +901,26 @@ Open `book-list.component.html` in `app\books\book-list` folder and add the foll ``` -* This adds a save button to the bottom area of the modal: +Find the `
` tag and replace below content: -![Save button to the modal](./images/bookstore-new-book-form-v2.png) +```html + +``` + + +* We added the `(ngSubmit)="save()"` to `` element to save a new book by pressing the enter. +* We added `abp-button` to the bottom area of the modal to save a new book. 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 { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; -import { Books } from '../../store/models'; -import { GetBooks, CreateUpdateBook } from '../../store/actions'; //<== added CreateUpdateBook ==> +import { finalize } from 'rxjs/operators'; +import { BookDto, BookType } from '../../app/shared/models'; +import { GetBooks, CreateUpdateBook } from '../state/books.actions'; // <== added CreateUpdateBook ==> +import { BooksState } from '../state/books.state'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; @@ -1001,16 +928,17 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap selector: 'app-book-list', templateUrl: './book-list.component.html', styleUrls: ['./book-list.component.scss'], - providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }] + providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], }) export class BookListComponent implements OnInit { @Select(BooksState.getBooks) - books$: Observable; + books$: Observable; - booksType = Books.BookType; + booksType = BookType; - bookTypeArr = Object.keys(Books.BookType).filter( - bookType => typeof this.booksType[bookType] === 'number' + //added bookTypeArr array + bookTypeArr = Object.keys(BookType).filter( + (bookType) => typeof this.booksType[bookType] === 'number' ); loading = false; @@ -1019,7 +947,7 @@ export class BookListComponent implements OnInit { form: FormGroup; - constructor(private store: Store, private fb: FormBuilder) { } + constructor(private store: Store, private fb: FormBuilder) {} ngOnInit() { this.get(); @@ -1027,9 +955,10 @@ export class BookListComponent implements OnInit { get() { this.loading = true; - this.store.dispatch(new GetBooks()).subscribe(() => { - this.loading = false; - }); + this.store + .dispatch(new GetBooks()) + .pipe(finalize(() => (this.loading = false))) + .subscribe(() => {}); } createBook() { @@ -1062,37 +991,20 @@ export class BookListComponent implements OnInit { ``` * We imported `CreateUpdateBook`. -* We added `save` method +* We added `save` method -### Updating an existing book +The final modal UI looks like below: -#### BooksService - -Open the `books.service.ts` in `app\books\shared` folder and add the `getById` and `update` methods. - -```js -getById(id: string): Observable { - return this.restService.request({ - method: 'GET', - url: `/api/app/book/${id}` - }); -} +![Save button to the modal](./images/bookstore-new-book-form-v2.png) -update(updateBookInput: Books.CreateUpdateBookInput, id: string): Observable { - return this.restService.request({ - method: 'PUT', - url: `/api/app/book/${id}`, - body: updateBookInput - }); -} -``` +### Updating a book #### CreateUpdateBook action -Open the `books.actions.ts` in `app\store\actions` folder and replace the content as below: +Open the `books.actions.ts` in `books\state` folder and replace the content as below: ```js -import { Books } from '../models'; +import { CreateUpdateBookDto } from '../../app/shared/models'; export class GetBooks { static readonly type = '[Books] Get'; @@ -1100,54 +1012,55 @@ export class GetBooks { export class CreateUpdateBook { static readonly type = '[Books] Create Update Book'; - constructor(public payload: Books.CreateUpdateBookInput, public id?: string) { } //<== added id parameter ==> + constructor(public payload: CreateUpdateBookDto, 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: +Open the `books.state.ts` in `books\state` folder and replace the `save` method as below: ```js @Action(CreateUpdateBook) -save(ctx: StateContext, action: CreateUpdateBook) { - if (action.id) { - return this.booksService.update(action.payload, action.id); - } else { - return this.booksService.create(action.payload); - } +save(ctx: StateContext, action: CreateUpdateBook) { + if (action.id) { + return this.bookService.updateByIdAndInput(action.payload, action.id); + } else { + return this.bookService.createByInput(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`. +Open `book-list.component.ts` in `app\books\book-list` folder and inject `BookService` 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 { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; -import { Books } from '../../store/models'; -import { GetBooks, CreateUpdateBook } from '../../store/actions'; +import { finalize } from 'rxjs/operators'; +import { BookDto, BookType } from '../../app/shared/models'; +import { GetBooks, CreateUpdateBook } from '../state/books.actions'; +import { BooksState } from '../state/books.state'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; -import { BooksService } from '../shared/books.service'; //<== imported BooksService ==> +import { BookService } from '../../app/shared/services'; // <== imported BookService ==> @Component({ selector: 'app-book-list', templateUrl: './book-list.component.html', styleUrls: ['./book-list.component.scss'], - providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }] + providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], }) export class BookListComponent implements OnInit { @Select(BooksState.getBooks) - books$: Observable; + books$: Observable; - booksType = Books.BookType; + booksType = BookType; - bookTypeArr = Object.keys(Books.BookType).filter( - bookType => typeof this.booksType[bookType] === 'number' + bookTypeArr = Object.keys(BookType).filter( + (bookType) => typeof this.booksType[bookType] === 'number' ); loading = false; @@ -1156,9 +1069,9 @@ export class BookListComponent implements OnInit { form: FormGroup; - selectedBook = {} as Books.Book; //<== declared selectedBook ==> + selectedBook = {} as BookDto; // <== declared selectedBook ==> - constructor(private store: Store, private fb: FormBuilder, private booksService: BooksService) { } + constructor(private store: Store, private fb: FormBuilder, private bookService: BookService) {} //<== injected BookService ==> ngOnInit() { this.get(); @@ -1166,39 +1079,38 @@ export class BookListComponent implements OnInit { get() { this.loading = true; - this.store.dispatch(new GetBooks()).subscribe(() => { - this.loading = false; - }); + this.store + .dispatch(new GetBooks()) + .pipe(finalize(() => (this.loading = false))) + .subscribe(() => {}); } - //<== this method is replaced ==> + // <== this method is replaced ==> createBook() { - this.selectedBook = {} as Books.Book; //<== added ==> + this.selectedBook = {} as BookDto; // <== added ==> this.buildForm(); this.isModalOpen = true; } - //<== added editBook method ==> + // <== added editBook method ==> editBook(id: string) { - this.booksService.getById(id).subscribe(book => { + this.bookService.getById(id).subscribe((book) => { this.selectedBook = book; this.buildForm(); this.isModalOpen = true; }); } - //<== this method is replaced ==> + // <== this method is replaced ==> buildForm() { this.form = this.fb.group({ - name: [this.selectedBook.name || "", Validators.required], + 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 + this.selectedBook.publishDate ? new Date(this.selectedBook.publishDate) : null, + Validators.required, ], - price: [this.selectedBook.price || null, Validators.required] + price: [this.selectedBook.price || null, Validators.required], }); } @@ -1208,7 +1120,8 @@ export class BookListComponent implements OnInit { } //<== added this.selectedBook.id ==> - this.store.dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id)) + this.store + .dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id)) .subscribe(() => { this.isModalOpen = false; this.form.reset(); @@ -1218,9 +1131,9 @@ export class BookListComponent implements OnInit { } ``` -* 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 imported `BookService`. +* We declared a variable named `selectedBook` as `BookDto`. +* We injected `BookService` to the constructor. `BookService` 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. @@ -1298,24 +1211,9 @@ Open `book-list.component.html` in `app\books\book-list` folder and find the ` { - return this.restService.request({ - 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`. +Open `books.actions.ts` in `books\state `folder and add an action named `DeleteBook`. ```js export class DeleteBook { @@ -1324,53 +1222,58 @@ export class DeleteBook { } ``` -Open the `books.state.ts` in `app\store\states` folder and replace the content as below: +Open the `books.state.ts` in `books\state` folder and replace the content as below: ```js +import { PagedResultDto } from '@abp/ng.core'; 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 { GetBooks, CreateUpdateBook, DeleteBook } from './books.actions'; // <== added DeleteBook==> +import { BookService } from '../../app/shared/services'; import { tap } from 'rxjs/operators'; import { Injectable } from '@angular/core'; +import { BookDto } from '../../app/shared/models'; -@State({ +export class BooksStateModel { + public book: PagedResultDto; +} + +@State({ name: 'BooksState', - defaults: { books: {} } as Books.State, + defaults: { book: {} } as BooksStateModel, }) @Injectable() export class BooksState { @Selector() - static getBooks(state: Books.State) { - return state.books.items || []; + static getBooks(state: BooksStateModel) { + return state.book.items || []; } - constructor(private booksService: BooksService) { } + constructor(private bookService: BookService) {} @Action(GetBooks) - get(ctx: StateContext) { - return this.booksService.get().pipe( - tap(booksResponse => { + get(ctx: StateContext) { + return this.bookService.getListByInput().pipe( + tap((booksResponse) => { ctx.patchState({ - books: booksResponse, + book: booksResponse, }); - }), + }) ); } @Action(CreateUpdateBook) - save(ctx: StateContext, action: CreateUpdateBook) { + save(ctx: StateContext, action: CreateUpdateBook) { if (action.id) { - return this.booksService.update(action.payload, action.id); + return this.bookService.updateByIdAndInput(action.payload, action.id); } else { - return this.booksService.create(action.payload); + return this.bookService.createByInput(action.payload); } } - //<== added DeleteBook ==> + // <== added DeleteBook action listener ==> @Action(DeleteBook) - delete(ctx: StateContext, action: DeleteBook) { - return this.booksService.delete(action.id); + delete(ctx: StateContext, action: DeleteBook) { + return this.bookService.deleteById(action.id); } } ``` @@ -1379,27 +1282,8 @@ export class BooksState { - 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 -
- - -
-``` - -The final actions dropdown UI looks like below: - -![bookstore-final-actions-dropdown](./images/bookstore-final-actions-dropdown.png) -#### Delete confirmation dialog +#### Delete confirmation popup Open `book-list.component.ts` in`app\books\book-list` folder and inject the `ConfirmationService`. @@ -1410,26 +1294,29 @@ import { ConfirmationService } from '@abp/ng.theme.shared'; //... constructor( - private store: Store, private fb: FormBuilder, - private booksService: BooksService, - private confirmationService: ConfirmationService // <== added this line ==> + private store: Store, + private fb: FormBuilder, + private bookService: BookService, + private confirmation: ConfirmationService // <== added this line ==> ) { } ``` * We imported `ConfirmationService`. * We injected `ConfirmationService` to the constructor. +See the [Confirmation Popup documentation](https://docs.abp.io/en/abp/latest/UI/Angular/Confirmation-Service) + In the `book-list.component.ts` add a delete method : ```js -import { GetBooks, CreateUpdateBook, DeleteBook } from '../../store/actions'; //<== added DeleteBook ==> +import { GetBooks, CreateUpdateBook, DeleteBook } from '../state/books.actions' ;// <== imported DeleteBook ==> -import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== added Confirmation ==> +import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== imported Confirmation ==> //... -delete(id: string, name: string) { - this.confirmationService +delete(id: string) { + this.confirmation .warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure') .subscribe(status => { if (status === Confirmation.Status.confirm) { @@ -1439,10 +1326,30 @@ delete(id: string, name: string) { } ``` + 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) + +#### 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 +
+ + +
+``` + +The final actions dropdown UI looks like below: + +![bookstore-final-actions-dropdown](./images/bookstore-final-actions-dropdown.png) + {{end}} ### Next Part diff --git a/docs/en/Tutorials/images/generate-proxy-command.png b/docs/en/Tutorials/images/generate-proxy-command.png new file mode 100644 index 0000000000..f850ce07a2 Binary files /dev/null and b/docs/en/Tutorials/images/generate-proxy-command.png differ diff --git a/docs/en/Tutorials/images/generated-proxies.png b/docs/en/Tutorials/images/generated-proxies.png new file mode 100644 index 0000000000..9e466e7d55 Binary files /dev/null and b/docs/en/Tutorials/images/generated-proxies.png differ diff --git a/templates/app/angular/package.json b/templates/app/angular/package.json index b4c4a8b50c..81a9940ecc 100644 --- a/templates/app/angular/package.json +++ b/templates/app/angular/package.json @@ -47,7 +47,6 @@ "karma-jasmine": "~3.0.1", "karma-jasmine-html-reporter": "^1.4.2", "ng-packagr": "^9.0.0", - "ngxs-schematic": "^1.1.9", "protractor": "~5.4.3", "ts-node": "~8.3.0", "tslint": "~6.1.0",