diff --git a/docs/en/Tutorials/Part-1.md b/docs/en/Tutorials/Part-1.md index 835c74385f..a0f970c75f 100644 --- a/docs/en/Tutorials/Part-1.md +++ b/docs/en/Tutorials/Part-1.md @@ -754,8 +754,8 @@ It's end of this part. The final UI of this work is shown as below: It's time to create something visible and usable! There are some tools that we will use when developing ABP Angular frontend application: - [Angular CLI](https://angular.io/cli) will be used to create modules, components and services. -- [NGXS](https://ngxs.gitbook.io/ngxs/) will be used as the state management library. - [Ng Bootstrap](https://ng-bootstrap.github.io/#/home) will be used as the UI component library. +- [ngx-datatable](https://swimlane.gitbook.io/ngx-datatable/) will be used as the datatable library. - [Visual Studio Code](https://code.visualstudio.com/) will be used as the code editor (you can use your favorite editor). #### Install NPM packages @@ -778,26 +778,62 @@ yarn ng generate module book --routing true #### Routing -Open the `app-routing.module.ts` file in `src\app` folder. Add the new `import` and add a route as shown below +Open the `app-routing.module.ts` file in `src\app` folder and add a route as shown below: ```js -import { ApplicationLayoutComponent } from '@abp/ng.theme.basic'; //==> added this line to imports <== - -//...added books path with the below to the routes array +const routes: Routes = [ +// ... +// added a new route to the routes array { path: 'books', - component: ApplicationLayoutComponent, - loadChildren: () => import('./book/book.module').then(m => m.BookModule), - data: { - routes: { - name: '::Menu:Books', - iconClass: 'fas fa-book' - } as ABP.Route - }, + loadChildren: () => import('./book/book.module').then(m => m.BookModule) +}] +``` + +* We added a lazy-loaded route. See the [Lazy-Loading Feature Modules](https://angular.io/guide/lazy-loading-ngmodules#lazy-loading-feature-modules). + +Open the `route.provider.ts` file in `src\app` folder and replace the content as below: + +```js +import { RoutesService, eLayoutType } from '@abp/ng.core'; +import { APP_INITIALIZER } from '@angular/core'; + +export const APP_ROUTE_PROVIDER = [ + { provide: APP_INITIALIZER, useFactory: configureRoutes, deps: [RoutesService], multi: true }, +]; + +function configureRoutes(routes: RoutesService) { + return () => { + routes.add([ + { + path: '/', + name: '::Menu:Home', + iconClass: 'fas fa-home', + order: 1, + layout: eLayoutType.application, + }, + // added below element + { + path: '/books', + name: '::Menu:Books', + iconClass: 'fas fa-book', + order: 101, + layout: eLayoutType.application, + }, + ]); + }; } + ``` -* The `ApplicationLayoutComponent` configuration sets the application layout to the new page. We added the `data` object. The `name` is the menu item name and the `iconClass` is the icon of the menu item. +* We added a new route element to show a navigation element labeled "Books" on the menu. + * `path` is the URL of the route. + * `name` is the menu item name. A Localization key can be passed. + * `iconClass` is the icon of the menu item. + * `order` is the order of the menu item. We define 101 to show the route after the "Administration" item. + * `layout` is the layout of the BooksModule's routes. `eLayoutType.application`, `eLayoutType.account` or `eLayoutType.empty` can be defined. + + #### Book list component @@ -865,35 +901,6 @@ Open the browser and navigate to http://localhost:4200/books. We'll see **book-l ![Initial book list page](./images/bookstore-initial-book-list-page.png) -#### Create BookState - -Run the following command in the terminal to create a new state, named `BooksState`: - -```bash -npx @ngxs/cli --name book --directory src/app/book -``` - -* This command creates `book.state.ts` and `book.actions.ts` files in the `src/app/book/state` folder. See the [NGXS CLI documentation](https://www.ngxs.io/plugins/cli). - -Import the `BookState` to the `app.module.ts` in the `src/app` folder and then add the `BookState` to `forRoot` static method of `NgxsModule` as an array element of the first parameter of the method. - -```js -// ... -import { BookState } from './books/state/book.state'; //<== imported BookState ==> - -@NgModule({ - imports: [ - // other imports - - NgxsModule.forRoot([BookState]), //<== added BookState ==> - - //other imports - ], - // ... -}) -export class AppModule {} -``` - #### Generate proxies 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) @@ -910,109 +917,40 @@ The generated files looks like below: ![Generated files](./images/generated-proxies.png) -#### GetBooks Action - -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 `book.actions.ts` file in `app/book/state` folder and replace the content below: - -```js -export class GetBooks { - static readonly type = '[Book] Get'; -} -``` - -#### Implement BookState - -Open the `book.state.ts` file in `app/book/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 './book.actions'; -import { BookService } from '../services'; -import { tap } from 'rxjs/operators'; -import { Injectable } from '@angular/core'; -import { BookDto } from '../models'; - -export class BookStateModel { - public book: PagedResultDto; -} - -@State({ - name: 'BookState', - defaults: { book: {} } as BookStateModel, -}) -@Injectable() -export class BookState { - @Selector() - static getBooks(state: BookStateModel) { - return state.book.items || []; - } - - constructor(private bookService: BookService) {} - - @Action(GetBooks) - get(ctx: StateContext) { - return this.bookService.getListByInput().pipe( - tap((booksResponse) => { - ctx.patchState({ - book: booksResponse, - }); - }) - ); - } -} -``` - -* We added the book property to BookStateModel model. -* We added the `GetBooks` action that retrieves the book data via `BookService` that generated via ABP CLI and patches the state. -* `NGXS` requires to return the observable without subscribing it in the get function. - #### BookListComponent Open the `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below: ```js +import { ListService, PagedResultDto } from '@abp/ng.core'; import { Component, OnInit } from '@angular/core'; -import { Select, Store } from '@ngxs/store'; -import { Observable } from 'rxjs'; -import { finalize } from 'rxjs/operators'; import { BookDto, BookType } from '../models'; -import { GetBooks } from '../state/book.actions'; -import { BookState } from '../state/book.state'; +import { BookService } from '../services'; @Component({ selector: 'app-book-list', templateUrl: './book-list.component.html', styleUrls: ['./book-list.component.scss'], + providers: [ListService], }) export class BookListComponent implements OnInit { - @Select(BookState.getBooks) - books$: Observable; + book = { items: [], totalCount: 0 } as PagedResultDto; booksType = BookType; - loading = false; - - constructor(private store: Store) {} + constructor(public readonly list: ListService, private bookService: BookService) {} ngOnInit() { - this.get(); - } + const bookStreamCreator = (query) => this.bookService.getListByInput(query); - get() { - this.loading = true; - this.store - .dispatch(new GetBooks()) - .pipe(finalize(() => (this.loading = false))) - .subscribe(() => {}); + this.list.hookToQuery(bookStreamCreator).subscribe((response) => { + this.book = response; + }); } } ``` - -* We added the `get` function that updates store to get the books. -* See the [Dispatching actions](https://ngxs.gitbook.io/ngxs/concepts/store#dispatching-actions) and [Select](https://ngxs.gitbook.io/ngxs/concepts/select) on the `NGXS` documentation for more information on these `NGXS` features. +* We imported and injected the generated `BookService`. +* We implemented the [ListService](https://docs.abp.io/en/abp/latest/UI/Angular/List-Service)` that is a utility service to provide easy pagination, sorting, and search implementation. Open the `book-list.component.html` file in `app\book\book-list` folder and replace the content as below: @@ -1022,38 +960,31 @@ Open the `book-list.component.html` file in `app\book\book-list` folder and repl
- {%{{{ "::Menu:Books" | abpLocalization }}}%} + {%{{{ '::Menu:Books' | abpLocalization }}}%}
- - - - - {%{{{ "::Name" | abpLocalization }}}%} - {%{{{ "::Type" | abpLocalization }}}%} - {%{{{ "::PublishDate" | abpLocalization }}}%} - {%{{{ "::Price" | abpLocalization }}}%} - - - - - {%{{{ data.name }}}%} - {%{{{ booksType[data.type] }}}%} - {%{{{ data.publishDate | date }}}%} - {%{{{ data.price }}}%} - - + + + + + {%{{{ booksType[row.type] }}}%} + + + + + {%{{{ row.publishDate | date }}}%} + + + + + {%{{{ row.price | currency }}}%} + + +
``` @@ -1074,4 +1005,4 @@ In this tutorial we have applied the rules of official [Angular Style Guide](htt ### Next Part -See the [part 2](part-2.md) for creating, updating and deleting books. +See the [part 2](./Part-2.md) for creating, updating and deleting books. diff --git a/docs/en/Tutorials/Part-2.md b/docs/en/Tutorials/Part-2.md index 8ac9e19615..d296335cfa 100644 --- a/docs/en/Tutorials/Part-2.md +++ b/docs/en/Tutorials/Part-2.md @@ -454,78 +454,50 @@ 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. -#### State definitions - -Open `book.action.ts` in `app\book\state` folder and replace the content as below: +#### Add a modal to BookListComponent -```js -import { CreateUpdateBookDto } from '../models'; //<== added this line ==> -export class GetBooks { - static readonly type = '[Book] Get'; -} +Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below: -// added CreateUpdateBook class -export class CreateUpdateBook { - static readonly type = '[Book] Create Update Book'; - constructor(public payload: CreateUpdateBookDto) { } -} -``` +```js +import { ListService, PagedResultDto } from '@abp/ng.core'; +import { Component, OnInit } from '@angular/core'; +import { BookDto, BookType } from '../models'; +import { BookService } from '../services'; -* We imported the `CreateUpdateBookDto` model and created the `CreateUpdateBook` action. +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.component.html', + styleUrls: ['./book-list.component.scss'], + providers: [ListService], +}) +export class BookListComponent implements OnInit { + book = { items: [], totalCount: 0 } as PagedResultDto; -Open `book.state.ts` file in `app\book\state` folder and replace the content as below: + booksType = BookType; -```js -import { PagedResultDto } from '@abp/ng.core'; -import { State, Action, StateContext, Selector } from '@ngxs/store'; -import { GetBooks, CreateUpdateBook } from './book.actions'; // <== added CreateUpdateBook==> -import { BookService } from '../services'; -import { tap } from 'rxjs/operators'; -import { Injectable } from '@angular/core'; -import { BookDto } from '../models'; + isModalOpen = false; // <== added this line ==> -export class BookStateModel { - public book: PagedResultDto; -} + constructor(public readonly list: ListService, private bookService: BookService) {} -@State({ - name: 'BookState', - defaults: { book: {} } as BookStateModel, -}) -@Injectable() -export class BookState { - @Selector() - static getBooks(state: BookStateModel) { - return state.book.items || []; - } + ngOnInit() { + const bookStreamCreator = (query) => this.bookService.getListByInput(query); - constructor(private bookService: BookService) {} - - @Action(GetBooks) - get(ctx: StateContext) { - return this.bookService.getListByInput().pipe( - tap((bookResponse) => { - ctx.patchState({ - book: bookResponse, - }); - }) - ); + this.list.hookToQuery(bookStreamCreator).subscribe((response) => { + this.book = response; + }); } - // added CreateUpdateBook action listener - @Action(CreateUpdateBook) - save(ctx: StateContext, action: CreateUpdateBook) { - return this.bookService.createByInput(action.payload); + // added createBook method + createBook() { + this.isModalOpen = true; } } ``` -* 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 `createByInput` method of the `BookService`. +* We defined a variable called `isModalOpen` and `createBook` method. +* We added the `createBook` method. -#### Add a modal to BookListComponent Open `book-list.component.html` file in `books\book-list` folder and replace the content as below: @@ -534,19 +506,12 @@ Open `book-list.component.html` file in `books\book-list` folder and replace the
-
- {%{{{ '::Menu:Books' | abpLocalization }}}%} -
+
{%{{{ '::Menu:Books' | abpLocalization }}}%}
- +
- @@ -555,47 +520,40 @@ Open `book-list.component.html` file in `books\book-list` folder and replace the
- - - - - {%{{{ "::Name" | abpLocalization }}}%} - {%{{{ "::Type" | abpLocalization }}}%} - {%{{{ "::PublishDate" | abpLocalization }}}%} - {%{{{ "::Price" | abpLocalization }}}%} - - - - - {%{{{ data.name }}}%} - {%{{{ booksType[data.type] }}}%} - {%{{{ data.publishDate | date }}}%} - {%{{{ data.price }}}%} - - + + + + + {%{{{ booksType[row.type] }}}%} + + + + + {%{{{ row.publishDate | date }}}%} + + + + + {%{{{ row.price | currency }}}%} + + +
- -

{%{{{ '::NewBook' | abpLocalization }}}%}

-
+ +

{%{{{ '::NewBook' | abpLocalization }}}%}

+
- + - - - + + +
``` @@ -603,55 +561,6 @@ Open `book-list.component.html` file in `books\book-list` folder and replace the * `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 `app\book\book-list` folder and replace the content as below: - -```js -import { Component, OnInit } from '@angular/core'; -import { Select, Store } from '@ngxs/store'; -import { Observable } from 'rxjs'; -import { finalize } from 'rxjs/operators'; -import { BookDto, BookType } from '../models'; -import { GetBooks } from '../state/book.actions'; -import { BookState } from '../state/book.state'; - -@Component({ - selector: 'app-book-list', - templateUrl: './book-list.component.html', - styleUrls: ['./book-list.component.scss'], -}) -export class BookListComponent implements OnInit { - @Select(BookState.getBooks) - books$: Observable; - - booksType = BookType; - - loading = false; - - isModalOpen = false; // <== added this line ==> - - constructor(private store: Store) {} - - ngOnInit() { - this.get(); - } - - get() { - this.loading = true; - this.store - .dispatch(new GetBooks()) - .pipe(finalize(() => (this.loading = false))) - .subscribe(() => {}); - } - - // 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) @@ -663,48 +572,43 @@ You can open your browser and click **New book** button to see the new modal. Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below: ```js +import { ListService, PagedResultDto } from '@abp/ng.core'; import { Component, OnInit } from '@angular/core'; -import { Select, Store } from '@ngxs/store'; -import { Observable } from 'rxjs'; -import { finalize } from 'rxjs/operators'; import { BookDto, BookType } from '../models'; -import { GetBooks } from '../state/book.actions'; -import { BookState } from '../state/book.state'; +import { BookService } from '../services'; 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'], + providers: [ListService], }) export class BookListComponent implements OnInit { - @Select(BookState.getBooks) - books$: Observable; + book = { items: [], totalCount: 0 } as PagedResultDto; booksType = BookType; - loading = false; - isModalOpen = false; form: FormGroup; // <== added this line ==> - constructor(private store: Store, private fb: FormBuilder) {} // <== added FormBuilder ==> + constructor( + public readonly list: ListService, + private bookService: BookService, + private fb: FormBuilder // <== injected FormBuilder ==> + ) {} ngOnInit() { - this.get(); - } + const bookStreamCreator = (query) => this.bookService.getListByInput(query); - get() { - this.loading = true; - this.store - .dispatch(new GetBooks()) - .pipe(finalize(() => (this.loading = false))) - .subscribe(() => {}); + this.list.hookToQuery(bookStreamCreator).subscribe((response) => { + this.book = response; + }); } createBook() { - this.buildForm(); //<== added this line ==> + this.buildForm(); // <== added this line ==> this.isModalOpen = true; } @@ -799,13 +703,10 @@ export class BookModule {} Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below: ```js +import { ListService, PagedResultDto } from '@abp/ng.core'; import { Component, OnInit } from '@angular/core'; -import { Select, Store } from '@ngxs/store'; -import { Observable } from 'rxjs'; -import { finalize } from 'rxjs/operators'; import { BookDto, BookType } from '../models'; -import { GetBooks } from '../state/book.actions'; -import { BookState } from '../state/book.state'; +import { BookService } from '../services'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; // <== added this line ==> @@ -813,37 +714,34 @@ 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 }], // <== added this line ==> + providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], // <== added a provide ==> }) export class BookListComponent implements OnInit { - @Select(BookState.getBooks) - books$: Observable; + book = { items: [], totalCount: 0 } as PagedResultDto; booksType = BookType; - //added bookTypeArr array + // <== added bookTypeArr array ==> bookTypeArr = Object.keys(BookType).filter( (bookType) => typeof this.booksType[bookType] === 'number' ); - loading = false; - isModalOpen = false; form: FormGroup; - constructor(private store: Store, private fb: FormBuilder) {} + constructor( + public readonly list: ListService, + private bookService: BookService, + private fb: FormBuilder + ) {} ngOnInit() { - this.get(); - } + const bookStreamCreator = (query) => this.bookService.getListByInput(query); - get() { - this.loading = true; - this.store - .dispatch(new GetBooks()) - .pipe(finalize(() => (this.loading = false))) - .subscribe(() => {}); + this.list.hookToQuery(bookStreamCreator).subscribe((response) => { + this.book = response; + }); } createBook() { @@ -884,13 +782,10 @@ Now, you can open your browser to see the changes: Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below: ```js +import { ListService, PagedResultDto } from '@abp/ng.core'; import { Component, OnInit } from '@angular/core'; -import { Select, Store } from '@ngxs/store'; -import { Observable } from 'rxjs'; -import { finalize } from 'rxjs/operators'; import { BookDto, BookType } from '../models'; -import { GetBooks, CreateUpdateBook } from '../state/book.actions'; // <== added CreateUpdateBook ==> -import { BookState } from '../state/book.state'; +import { BookService } from '../services'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; @@ -898,11 +793,10 @@ 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: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], }) export class BookListComponent implements OnInit { - @Select(BookState.getBooks) - books$: Observable; + book = { items: [], totalCount: 0 } as PagedResultDto; booksType = BookType; @@ -910,24 +804,22 @@ export class BookListComponent implements OnInit { (bookType) => typeof this.booksType[bookType] === 'number' ); - loading = false; - isModalOpen = false; form: FormGroup; - constructor(private store: Store, private fb: FormBuilder) {} + constructor( + public readonly list: ListService, + private bookService: BookService, + private fb: FormBuilder + ) {} ngOnInit() { - this.get(); - } + const bookStreamCreator = (query) => this.bookService.getListByInput(query); - get() { - this.loading = true; - this.store - .dispatch(new GetBooks()) - .pipe(finalize(() => (this.loading = false))) - .subscribe(() => {}); + this.list.hookToQuery(bookStreamCreator).subscribe((response) => { + this.book = response; + }); } createBook() { @@ -950,19 +842,18 @@ export class BookListComponent implements OnInit { return; } - this.store.dispatch(new CreateUpdateBook(this.form.value)).subscribe(() => { + this.bookService.createByInput(this.form.value).subscribe(() => { this.isModalOpen = false; this.form.reset(); - this.get(); + this.list.get(); }); } } ``` -* We imported `CreateUpdateBook`. * We added `save` method -Open `book-list.component.html` in `app\book\book-list` folder and add the following `abp-button` to save the new book. +Open `book-list.component.html` in `app\book\book-list` folder, find the `` element and replace this element with the following to create a new book. ```html @@ -994,63 +885,24 @@ The final modal UI looks like below: ### Updating a book -#### CreateUpdateBook action - -Open the `book.actions.ts` in `app\book\state` folder and replace the content as below: - -```js -import { CreateUpdateBookDto } from '../models'; - -export class GetBooks { - static readonly type = '[Book] Get'; -} - -export class CreateUpdateBook { - static readonly type = '[Book] Create Update Book'; - constructor(public payload: CreateUpdateBookDto, public id?: string) {} // <== added id parameter ==> -} -``` - -* We added `id` parameter to the `CreateUpdateBook` action's constructor. - -Open the `book.state.ts` in `app\book\state` folder and replace the `save` method as below: - -```js -@Action(CreateUpdateBook) -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\book\book-list` folder and inject `BookService` dependency by adding it to the constructor and add a variable named `selectedBook`. +Open `book-list.component.ts` in `app\book\book-list` folder and add a variable named `selectedBook`. ```js +import { ListService, PagedResultDto } from '@abp/ng.core'; import { Component, OnInit } from '@angular/core'; -import { Select, Store } from '@ngxs/store'; -import { Observable } from 'rxjs'; -import { finalize } from 'rxjs/operators'; import { BookDto, BookType } from '../models'; -import { GetBooks, CreateUpdateBook } from '../state/book.actions'; -import { BookState } from '../state/book.state'; +import { BookService } from '../services'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; -import { BookService } from '../services'; // <== imported BookService ==> @Component({ selector: 'app-book-list', templateUrl: './book-list.component.html', styleUrls: ['./book-list.component.scss'], - providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], + providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], }) export class BookListComponent implements OnInit { - @Select(BookState.getBooks) - books$: Observable; + book = { items: [], totalCount: 0 } as PagedResultDto; booksType = BookType; @@ -1058,26 +910,24 @@ export class BookListComponent implements OnInit { (bookType) => typeof this.booksType[bookType] === 'number' ); - loading = false; - isModalOpen = false; form: FormGroup; selectedBook = {} as BookDto; // <== declared selectedBook ==> - constructor(private store: Store, private fb: FormBuilder, private bookService: BookService) {} //<== injected BookService ==> + constructor( + public readonly list: ListService, + private bookService: BookService, + private fb: FormBuilder + ) {} ngOnInit() { - this.get(); - } + const bookStreamCreator = (query) => this.bookService.getListByInput(query); - get() { - this.loading = true; - this.store - .dispatch(new GetBooks()) - .pipe(finalize(() => (this.loading = false))) - .subscribe(() => {}); + this.list.hookToQuery(bookStreamCreator).subscribe((response) => { + this.book = response; + }); } // <== this method is replaced ==> @@ -1109,30 +959,31 @@ export class BookListComponent implements OnInit { }); } + // <== this method is replaced ==> 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(); - }); + // <== added request ==> + const request = this.selectedBook.id + ? this.bookService.updateByIdAndInput(this.form.value, this.selectedBook.id) + : this.bookService.createByInput(this.form.value); + + request.subscribe(() => { + this.isModalOpen = false; + this.form.reset(); + this.list.get(); + }); } } ``` -* 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. -* We added `selectedBook.id` to the constructor of the new `CreateUpdateBook`. +* We replaced the `save` method. #### Add "Actions" dropdown to the table @@ -1140,27 +991,14 @@ Open the `book-list.component.html` in `app\book\book-list` folder and replace ```html
- - - - - {%{{{ "::Actions" | abpLocalization }}}%} - {%{{{ "::Name" | abpLocalization }}}%} - {%{{{ "::Type" | abpLocalization }}}%} - {%{{{ "::PublishDate" | abpLocalization }}}%} - {%{{{ "::Price" | abpLocalization }}}%} - - - - - + + + +
-
- - {%{{{ data.name }}}%} - {%{{{ booksType[data.type] }}}%} - {%{{{ data.publishDate | date }}}%} - {%{{{ data.price }}}%} - -
+
+ + + + + {%{{{ booksType[row.type] }}}%} + + + + + {%{{{ row.publishDate | date }}}%} + + + + + {%{{{ row.price | currency }}}%} + + +
``` -- We added a `th` for the "Actions" column. +- We added a `ngx-datatable-column` 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. @@ -1198,7 +1048,7 @@ Open `book-list.component.html` in `app\book\book-list` folder and find the ` -

{%{{{ (selectedBook.id ? 'AbpIdentity::Edit' : '::NewBook' ) | abpLocalization }}}%}

+

{%{{{ (selectedBook.id ? '::Edit' : '::NewBook' ) | abpLocalization }}}%}

``` @@ -1206,81 +1056,9 @@ Open `book-list.component.html` in `app\book\book-list` folder and find the ` -import { BookService } from '../services'; -import { tap } from 'rxjs/operators'; -import { Injectable } from '@angular/core'; -import { BookDto } from '../models'; - -export class BookStateModel { - public book: PagedResultDto; -} - -@State({ - name: 'BookState', - defaults: { book: {} } as BookStateModel, -}) -@Injectable() -export class BookState { - @Selector() - static getBooks(state: BookStateModel) { - return state.book.items || []; - } - - constructor(private bookService: BookService) {} - - @Action(GetBooks) - get(ctx: StateContext) { - return this.bookService.getListByInput().pipe( - tap((booksResponse) => { - ctx.patchState({ - book: booksResponse, - }); - }) - ); - } - - @Action(CreateUpdateBook) - save(ctx: StateContext, action: CreateUpdateBook) { - if (action.id) { - return this.bookService.updateByIdAndInput(action.payload, action.id); - } else { - return this.bookService.createByInput(action.payload); - } - } - - // <== added DeleteBook action listener ==> - @Action(DeleteBook) - delete(ctx: StateContext, action: DeleteBook) { - return this.bookService.deleteById(action.id); - } -} -``` - -- We imported `DeleteBook` . - -- We added `DeleteBook` action listener to the end of the file. - - #### Delete confirmation popup -Open `book-list.component.ts` in`app\book\book-list` folder and inject the `ConfirmationService`. +Open `book-list.component.ts` in `app\book\book-list` folder and inject the `ConfirmationService`. Replace the constructor as below: @@ -1289,11 +1067,11 @@ import { ConfirmationService } from '@abp/ng.theme.shared'; //... constructor( - private store: Store, - private fb: FormBuilder, - private bookService: BookService, - private confirmation: ConfirmationService // <== added this line ==> -) { } + public readonly list: ListService, + private bookService: BookService, + private fb: FormBuilder, + private confirmation: ConfirmationService // <== added this line ==> +) {} ``` * We imported `ConfirmationService`. @@ -1301,28 +1079,24 @@ 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 : +In the `book-list.component.ts` add a delete method: ```js -import { GetBooks, CreateUpdateBook, DeleteBook } from '../state/book.actions' ;// <== imported DeleteBook ==> - -import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== imported Confirmation ==> +import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== imported Confirmation namespace ==> //... delete(id: string) { - this.confirmation - .warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure') - .subscribe(status => { - if (status === Confirmation.Status.confirm) { - this.store.dispatch(new DeleteBook(id)).subscribe(() => this.get()); - } - }); + this.confirmation.warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure').subscribe((status) => { + if (status === Confirmation.Status.confirm) { + this.bookService.deleteById(id).subscribe(() => this.list.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: +The `delete` method shows a confirmation popup and subscribes for the user response. The `deleteById` method of `BookService` called only if user clicks to the `Yes` button. The confirmation popup looks like below: ![bookstore-confirmation-popup](./images/bookstore-confirmation-popup.png) @@ -1335,7 +1109,7 @@ Open `book-list.component.html` in `app\book\book-list` folder and modify the `n ```html
-
diff --git a/docs/en/Tutorials/images/bookstore-actions-buttons.png b/docs/en/Tutorials/images/bookstore-actions-buttons.png index e8243fedc7..f5d8f0870b 100644 Binary files a/docs/en/Tutorials/images/bookstore-actions-buttons.png and b/docs/en/Tutorials/images/bookstore-actions-buttons.png differ diff --git a/docs/en/Tutorials/images/bookstore-angular-file-tree.png b/docs/en/Tutorials/images/bookstore-angular-file-tree.png index ffa8dcd7e2..d921f146fc 100644 Binary files a/docs/en/Tutorials/images/bookstore-angular-file-tree.png and b/docs/en/Tutorials/images/bookstore-angular-file-tree.png differ diff --git a/docs/en/Tutorials/images/bookstore-book-list.png b/docs/en/Tutorials/images/bookstore-book-list.png index d402895c9b..ecdb87c737 100644 Binary files a/docs/en/Tutorials/images/bookstore-book-list.png and b/docs/en/Tutorials/images/bookstore-book-list.png differ diff --git a/docs/en/Tutorials/images/bookstore-confirmation-popup.png b/docs/en/Tutorials/images/bookstore-confirmation-popup.png index a80b180f1c..b17e70638d 100644 Binary files a/docs/en/Tutorials/images/bookstore-confirmation-popup.png and b/docs/en/Tutorials/images/bookstore-confirmation-popup.png differ diff --git a/docs/en/Tutorials/images/bookstore-empty-new-book-modal.png b/docs/en/Tutorials/images/bookstore-empty-new-book-modal.png index 2a02802bb9..f4e969ae22 100644 Binary files a/docs/en/Tutorials/images/bookstore-empty-new-book-modal.png and b/docs/en/Tutorials/images/bookstore-empty-new-book-modal.png differ