diff --git a/docs/en/Tutorials/Part-1.md b/docs/en/Tutorials/Part-1.md index a0aa6612e9..e3f988779b 100644 --- a/docs/en/Tutorials/Part-1.md +++ b/docs/en/Tutorials/Part-1.md @@ -877,94 +877,51 @@ Run the following command in the terminal to create a new state, named `BooksSta ![Initial book list page](./images/bookstore-generate-state-books.png) ```bash -yarn ng generate ngxs-schematic:state books +npx @ngxs/cli --name books --directory src/app/books ``` -* This command creates several new files and updates `app.modules.ts` file to import the `NgxsModule` with the new state. +* 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). -#### Get books data from backend - -Create data types to map the data from the backend (you can check Swagger UI or your backend API to see the data format). - -![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: +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. ```js -export namespace Books { - export interface State { - books: Response; - } +// ... +import { BooksState } from './books/state/books.state'; //<== imported BooksState ==> - export interface Response { - items: Book[]; - totalCount: number; - } +@NgModule({ + imports: [ + // other imports - export interface Book { - name: string; - type: BookType; - publishDate: string; - price: number; - lastModificationTime: string; - lastModifierId: string; - creationTime: string; - creatorId: string; - id: string; - } + NgxsModule.forRoot([BooksState]), //<== added BooksState ==> - export enum BookType { - Undefined, - Adventure, - Biography, - Dystopia, - Fantastic, - Horror, - Science, - ScienceFiction, - Poetry, - } -} + //other imports + ], + // ... +}) +export class AppModule {} ``` -* Added `Book` interface that represents a book object and `BookType` enum which represents a book category. +#### Generate proxies -#### BooksService +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) -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 { @@ -974,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 @@ -1019,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', @@ -1032,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(); @@ -1046,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/docs/en/UI/Angular/Confirmation-Service.md b/docs/en/UI/Angular/Confirmation-Service.md new file mode 100644 index 0000000000..a8d2c4c2a7 --- /dev/null +++ b/docs/en/UI/Angular/Confirmation-Service.md @@ -0,0 +1,163 @@ +# Confirmation Popup + +You can use the `ConfirmationService` in @abp/ng.theme.shared package to display a confirmation popup by placing at the root level in your project. + + +## Getting Started + +You do not have to provide the `ConfirmationService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. + + +```js +import { ConfirmationService } from '@abp/ng.theme.shared'; + +@Component({ + /* class metadata here */ +}) +class DemoComponent { + constructor(private confirmation: ConfirmationService) {} +} +``` + +## Usage + +You can use the `success`, `warn`, `error`, and `info` methods of `ConfirmationService` to display a confirmation popup. + +### How to Display a Confirmation Popup + +```js +const confirmationStatus$ = this.confirmation.success('Message', 'Title'); +``` + +- The `ConfirmationService` methods accept three parameters that are `message`, `title`, and `options`. +- `success`, `warn`, `error`, and `info` methods return an [RxJS Subject](https://rxjs-dev.firebaseapp.com/guide/subject) to listen to confirmation popup closing event. The type of event value is [`Confirmation.Status`](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts#L24) that is an enum. + +### How to Listen Closing Event + +You can subscribe to the confirmation closing event like below: + +```js +import { Confirmation, ConfirmationService } from '@abp/ng.theme.shared'; + +constructor(private confirmation: ConfirmationService) {} + +this.confirmation + .warn('::WillBeDeleted', { key: '::AreYouSure', defaultValue: 'Are you sure?' }) + .subscribe((status: Confirmation.Status) => { + // your code here + }); +``` + + +- The `message` and `title` parameters accept a string, localization key or localization object. See the [localization document](./Localization.md) +- `Confirmation.Status` is an enum and has three properties; + - `Confirmation.Status.confirm` is a closing event value that will be emitted when the popup is closed by the confirm button. + - `Confirmation.Status.reject` is a closing event value that will be emitted when the popup is closed by the cancel button. + - `Confirmation.Status.dismiss` is a closing event value that will be emitted when the popup is closed by pressing the escape. + + +If you are not interested in the confirmation status, you do not have to subscribe to the returned observable: + +```js +this.confirmation.error('You are not authorized.', 'Error'); +``` + +### How to Display a Confirmation Popup With Given Options + +Options can be passed as the third parameter to `success`, `warn`, `error`, and `info` methods: + +```js +const options: Partial = { + hideCancelBtn: false, + hideYesBtn: false, + cancelText: 'Close', + yesText: 'Confirm', + messageLocalizationParams: ['Demo'], + titleLocalizationParams: [], +}; + +this.confirmation.warn( + 'AbpIdentity::RoleDeletionConfirmationMessage', + 'Are you sure?', + options, +); +``` + +- `hideCancelBtn` option hides the cancellation button when `true`. Default value is `false` +- `hideYesBtn` option hides the confirmation button when `true`. Default value is `false` +- `cancelText` is the text of the cancellation button. A localization key or localization object can be passed. Default value is `AbpUi::Cancel` +- `yesText` is the text of the confirmation button. A localization key or localization object can be passed. Default value is `AbpUi::Yes` +- `messageLocalizationParams` is the interpolation parameters for the localization of the message. +- `titleLocalizationParams` is the interpolation parameters for the localization of the title. + +With the options above, the confirmation popup looks like this: + +![confirmation](./images/confirmation.png) + +### How to Remove a Confirmation Popup + +The open confirmation popup can be removed manually via the `clear` method: + +```js +this.confirmation.clear(); +``` + +## API + +### success + +```js +success( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): Observable +``` + +> See the [`Config.LocalizationParam` type](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/core/src/lib/models/config.ts#L46) and [`Confirmation` namespace](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts) + + +### warn + +```js +warn( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): Observable +``` + +### error + +```js +error( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): Observable +``` + +### info + +```js +info( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): Observable +``` + +### clear + +```js +clear( + status: Confirmation.Status = Confirmation.Status.dismiss +): void +``` + +- `status` parameter is the value of the confirmation closing event. + + +## What's Next? + +- [Toast Overlay](./Toaster-Service.md) diff --git a/docs/en/UI/Angular/Http-Requests.md b/docs/en/UI/Angular/HTTP-Requests.md similarity index 99% rename from docs/en/UI/Angular/Http-Requests.md rename to docs/en/UI/Angular/HTTP-Requests.md index 71878c8045..2ac5aa1b49 100644 --- a/docs/en/UI/Angular/Http-Requests.md +++ b/docs/en/UI/Angular/HTTP-Requests.md @@ -206,4 +206,4 @@ You may find `Rest.Observe` enum [here](https://github.com/abpframework/abp/blob ## What's Next? -* [Localization](./Localization.md) \ No newline at end of file +* [Localization](./Localization.md) diff --git a/docs/en/UI/Angular/Permission-Management.md b/docs/en/UI/Angular/Permission-Management.md index ababd25e7f..d86e8c96b2 100644 --- a/docs/en/UI/Angular/Permission-Management.md +++ b/docs/en/UI/Angular/Permission-Management.md @@ -76,4 +76,4 @@ Granted Policies are stored in the `auth` property of `ConfigState`. ## What's Next? -* [Config State](./Config-State.md) \ No newline at end of file +- [Confirmation Popup](./Confirmation-Service.md) \ No newline at end of file diff --git a/docs/en/UI/Angular/Toaster-Service.md b/docs/en/UI/Angular/Toaster-Service.md new file mode 100644 index 0000000000..191f0b34fd --- /dev/null +++ b/docs/en/UI/Angular/Toaster-Service.md @@ -0,0 +1,157 @@ +# Toast Overlay + +You can use the `ToasterService` in @abp/ng.theme.shared package to display messages in an overlay by placing at the root level in your project. + + +## Getting Started + +You do not have to provide the `ToasterService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. + + +```js +import { ToasterService } from '@abp/ng.theme.shared'; + +@Component({ + /* class metadata here */ +}) +class DemoComponent { + constructor(private toaster: ToasterService) {} +} +``` + +## Usage + +You can use the `success`, `warn`, `error`, and `info` methods of `ToasterService` to display an overlay. + +### How to Display a Toast Overlay + +```js +this.toast.success('Message', 'Title'); +``` + +- The `ToasterService` methods accept three parameters that are `message`, `title`, and `options`. +- `success`, `warn`, `error`, and `info` methods return the id of opened toast overlay. The toast can be removed with this id. + +### How to Display a Toast Overlay With Given Options + +Options can be passed as the third parameter to `success`, `warn`, `error`, and `info` methods: + +```js +import { Toaster, ToasterService } from '@abp/ng.theme.shared'; +//... + +constructor(private toaster: ToasterService) {} + +//... +const options: Partial = { + life: 10000, + sticky: false, + closable: true, + tapToDismiss: true, + messageLocalizationParams: ['Demo', '1'], + titleLocalizationParams: [] + }; + + this.toaster.error('AbpUi::EntityNotFoundErrorMessage', 'AbpUi::Error', options); +``` + +- `life` option is the closing time in milliseconds. Default value is `5000`. +- `sticky` option keeps toast overlay on the screen by ignoring the `life` option when `true`. Default value is `false`. +- `closable` option displays the close icon on the toast overlay when it is `true`. Default value is `true`. +- `tapToDismiss` option, when `true`, allows closing the toast overlay by clicking over it. Default value is `false`. +- `yesText` is the text of the confirmation button. A localization key or localization object can be passed. Default value is `AbpUi::Yes`. +- `messageLocalizationParams` is the interpolation parameters for the localization of the message. +- `titleLocalizationParams` is the interpolation parameters for the localization of the title. + +With the options above, the toast overlay looks like this: + +![toast](./images/toast.png) + +### How to Remove a Toast Overlay + +The open toast overlay can be removed manually via the `remove` method by passing the `id` of toast: + +```js +const toastId = this.toast.success('Message', 'Title') + +this.toast.remove(toastId); +``` + +### How to Remove All Toasts + +The all open toasts can be removed manually via the `clear` method: + +```js +this.toast.clear(); +``` + +## API + +### success + +```js +success( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): number +``` + +- `Config` namespace can be imported from `@abp/ng.core`. +- `Toaster` namespace can be imported from `@abp/ng.theme.shared`. + +> See the [`Config.LocalizationParam` type](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/core/src/lib/models/config.ts#L46) and [`Toaster` namespace](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/toaster.ts) + + +### warn + +```js +warn( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): number +``` + +### error + +```js +error( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): number +``` + +### info + +```js +info( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): number +``` + +### remove + +```js +remove(id: number): void +``` + +Removes an open toast by the given id. + +### clear + +```js +clear(): void +``` + +Removes all open toasts. + +## See Also +- [Confirmation Popup](./Confirmation-Service.md) + +## What's Next? + +- [Config State](./Config-State.md) diff --git a/docs/en/UI/Angular/images/confirmation.png b/docs/en/UI/Angular/images/confirmation.png new file mode 100644 index 0000000000..efe4c98ea7 Binary files /dev/null and b/docs/en/UI/Angular/images/confirmation.png differ diff --git a/docs/en/UI/Angular/images/toast.png b/docs/en/UI/Angular/images/toast.png new file mode 100644 index 0000000000..24cdd0fe0c Binary files /dev/null and b/docs/en/UI/Angular/images/toast.png differ diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 8b5d41aa59..53caea554b 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -221,8 +221,11 @@ }, { "text": "Domain Driven Design", - "path": "Domain-Driven-Design.md", "items": [ + { + "text": "Overall", + "path": "Domain-Driven-Design.md" + }, { "text": "Domain Layer", "items": [ @@ -337,6 +340,14 @@ "text": "Permission Management", "path": "UI/Angular/Permission-Management.md" }, + { + "text": "Confirmation Popup", + "path": "UI/Angular/Confirmation-Service.md" + }, + { + "text": "Toast Overlay", + "path": "UI/Angular/Toaster-Service.md" + }, { "text": "Config State", "path": "UI/Angular/Config-State.md" @@ -385,8 +396,11 @@ }, { "text": "Data Access", - "path": "Data-Access.md", "items": [ + { + "text": "Overall", + "path": "Data-Access.md" + }, { "text": "Connection Strings", "path": "Connection-Strings.md" @@ -469,8 +483,11 @@ }, { "text": "Startup Templates", - "path": "Startup-Templates/Index.md", "items": [ + { + "text": "Overall", + "path": "Startup-Templates/Index.md" + }, { "text": "Application", "path": "Startup-Templates/Application.md" diff --git a/docs/zh-Hans/Specifications.md b/docs/zh-Hans/Specifications.md new file mode 100644 index 0000000000..f927c80255 --- /dev/null +++ b/docs/zh-Hans/Specifications.md @@ -0,0 +1,3 @@ +## 规约 + +TODO.. \ No newline at end of file diff --git a/docs/zh-Hans/UI/Angular/Loading-Strategy.md b/docs/zh-Hans/UI/Angular/Loading-Strategy.md new file mode 100644 index 0000000000..8e1142dd20 --- /dev/null +++ b/docs/zh-Hans/UI/Angular/Loading-Strategy.md @@ -0,0 +1,95 @@ +# LoadingStrategy + +`LoadingStrategy` 是@abp/ng.core包暴露的抽象类. 扩展它的有两种加载策略: `ScriptLoadingStrategy` 和 `StyleLoadingStrategy`. 它们实现相同的方法和属性,这两种策略都可以帮助你定义延迟加载的工作方式. + +## API + +### constructor + +```js +constructor( + public path: string, + protected domStrategy?: DomStrategy, + protected crossOriginStrategy?: CrossOriginStrategy +) +``` + +- `path` 将 `