Browse Source

Merge branch 'master' into dev

pull/3737/head
mehmet-erim 7 years ago
parent
commit
3dced6e58b
  1. 150
      docs/en/Tutorials/Part-1.md
  2. 495
      docs/en/Tutorials/Part-2.md
  3. BIN
      docs/en/Tutorials/images/generate-proxy-command.png
  4. BIN
      docs/en/Tutorials/images/generated-proxies.png
  5. 15
      docs/en/docs-nav.json
  6. 1
      templates/app/angular/package.json

150
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) ![Initial book list page](./images/bookstore-generate-state-books.png)
```bash ```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 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.
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:
```js ```js
export namespace Books { // ...
export interface State { import { BooksState } from './books/state/books.state'; //<== imported BooksState ==>
books: Response;
}
export interface Response { @NgModule({
items: Book[]; imports: [
totalCount: number; // other imports
}
export interface Book { NgxsModule.forRoot([BooksState]), //<== added BooksState ==>
name: string;
type: BookType;
publishDate: string;
price: number;
lastModificationTime: string;
lastModifierId: string;
creationTime: string;
creatorId: string;
id: string;
}
export enum BookType { //other imports
Undefined, ],
Adventure, // ...
Biography, })
Dystopia, export class AppModule {}
Fantastic,
Horror,
Science,
ScienceFiction,
Poetry,
}
}
``` ```
* 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 ```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 ![Generated files](./images/generated-proxies.png)
import { Injectable } from '@angular/core';
import { RestService } from '@abp/ng.core';
import { Books } from '../../store/models';
import { Observable } from 'rxjs';
@Injectable({ #### GetBooks Action
providedIn: 'root',
})
export class BooksService {
constructor(private restService: RestService) {}
get(): Observable<Books.Response> {
return this.restService.request<void, Books.Response>({
method: 'GET',
url: '/api/app/book'
});
}
}
```
* 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 ```js
export class GetBooks { export class GetBooks {
@ -974,43 +931,48 @@ export class GetBooks {
#### Implement BooksState #### 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 ```js
import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store'; import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks } from '../actions/books.actions'; import { GetBooks } from './books.actions';
import { Books } from '../models/books'; import { BookService } from '../../app/shared/services';
import { BooksService } from '../../books/shared/books.service';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { BookDto } from '../../app/shared/models';
@State<Books.State>({ export class BooksStateModel {
public book: PagedResultDto<BookDto>;
}
@State<BooksStateModel>({
name: 'BooksState', name: 'BooksState',
defaults: { books: {} } as Books.State, defaults: { book: {} } as BooksStateModel,
}) })
@Injectable() @Injectable()
export class BooksState { export class BooksState {
@Selector() @Selector()
static getBooks(state: Books.State) { static getBooks(state: BooksStateModel) {
return state.books.items || []; return state.book.items || [];
} }
constructor(private booksService: BooksService) {} constructor(private bookService: BookService) {}
@Action(GetBooks) @Action(GetBooks)
get(ctx: StateContext<Books.State>) { get(ctx: StateContext<BooksStateModel>) {
return this.booksService.get().pipe( return this.bookService.getListByInput().pipe(
tap(booksResponse => { tap((booksResponse) => {
ctx.patchState({ ctx.patchState({
books: booksResponse, book: booksResponse,
}); });
}), })
); );
} }
} }
``` ```
* We added the book property to BooksStateModel model.
* We added the `GetBooks` action that retrieves the books data via `BooksService` and patches the state. * 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. * `NGXS` requires to return the observable without subscribing it in the get function.
#### BookListComponent #### BookListComponent
@ -1019,11 +981,12 @@ Open the `book-list.component.ts` file in `app\books\book-list` folder and repla
```js ```js
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Store, Select } from '@ngxs/store'; import { Select, Store } from '@ngxs/store';
import { BooksState } from '../../store/states';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Books } from '../../store/models'; import { finalize } from 'rxjs/operators';
import { GetBooks } from '../../store/actions'; import { BookDto, BookType } from '../../app/shared/models';
import { GetBooks } from '../state/books.actions';
import { BooksState } from '../state/books.state';
@Component({ @Component({
selector: 'app-book-list', selector: 'app-book-list',
@ -1032,13 +995,13 @@ import { GetBooks } from '../../store/actions';
}) })
export class BookListComponent implements OnInit { export class BookListComponent implements OnInit {
@Select(BooksState.getBooks) @Select(BooksState.getBooks)
books$: Observable<Books.Book[]>; books$: Observable<BookDto[]>;
booksType = Books.BookType; booksType = BookType;
loading = false; loading = false;
constructor(private store: Store) { } constructor(private store: Store) {}
ngOnInit() { ngOnInit() {
this.get(); this.get();
@ -1046,9 +1009,10 @@ export class BookListComponent implements OnInit {
get() { get() {
this.loading = true; this.loading = true;
this.store.dispatch(new GetBooks()).subscribe(() => { this.store
this.loading = false; .dispatch(new GetBooks())
}); .pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
} }
} }
``` ```

495
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. 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<Books.Response> {
return this.restService.request<void, Books.Response>({
method: 'GET',
url: '/api/app/book'
});
}
//<== added create method ==>
create(createBookInput: Books.CreateUpdateBookInput): Observable<Books.Book> {
return this.restService.request<Books.CreateUpdateBookInput, Books.Book>({
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 #### 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 ```js
import { Books } from '../models'; //<== added this line ==> import { CreateUpdateBookDto } from '../../app/shared/models'; //<== added this line ==>
export class GetBooks { export class GetBooks {
static readonly type = '[Books] Get'; static readonly type = '[Books] Get';
} }
//added CreateUpdateBook class // added CreateUpdateBook class
export class CreateUpdateBook { export class CreateUpdateBook {
static readonly type = '[Books] Create Update Book'; 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 ```js
import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store'; import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks, CreateUpdateBook } from '../actions/books.actions'; //<== added CreateUpdateBook==> import { GetBooks, CreateUpdateBook } from './books.actions'; // <== added CreateUpdateBook==>
import { Books } from '../models/books'; import { BookService } from '../../app/shared/services';
import { BooksService } from '../../books/shared/books.service';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { BookDto } from '../../app/shared/models';
export class BooksStateModel {
public book: PagedResultDto<BookDto>;
}
@State<Books.State>({ @State<BooksStateModel>({
name: 'BooksState', name: 'BooksState',
defaults: { books: {} } as Books.State, defaults: { book: {} } as BooksStateModel,
}) })
@Injectable() @Injectable()
export class BooksState { export class BooksState {
@Selector() @Selector()
static getBooks(state: Books.State) { static getBooks(state: BooksStateModel) {
return state.books.items || []; return state.book.items || [];
} }
constructor(private booksService: BooksService) { } constructor(private bookService: BookService) {}
@Action(GetBooks) @Action(GetBooks)
get(ctx: StateContext<Books.State>) { get(ctx: StateContext<BooksStateModel>) {
return this.booksService.get().pipe( return this.bookService.getListByInput().pipe(
tap(booksResponse => { tap((bookResponse) => {
ctx.patchState({ ctx.patchState({
books: booksResponse, book: bookResponse,
}); });
}), })
); );
} }
//added CreateUpdateBook action listener // added CreateUpdateBook action listener
@Action(CreateUpdateBook) @Action(CreateUpdateBook)
save(ctx: StateContext<Books.State>, action: CreateUpdateBook) { save(ctx: StateContext<BooksStateModel>, action: CreateUpdateBook) {
return this.booksService.create(action.payload); 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. * 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 #### 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 ```js
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Store, Select } from '@ngxs/store'; import { Select, Store } from '@ngxs/store';
import { BooksState } from '../../store/states';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Books } from '../../store/models'; import { finalize } from 'rxjs/operators';
import { GetBooks } from '../../store/actions'; import { BookDto, BookType } from '../../app/shared/models';
import { GetBooks } from '../state/books.actions';
import { BooksState } from '../state/books.state';
@Component({ @Component({
selector: 'app-book-list', selector: 'app-book-list',
@ -707,15 +623,15 @@ import { GetBooks } from '../../store/actions';
}) })
export class BookListComponent implements OnInit { export class BookListComponent implements OnInit {
@Select(BooksState.getBooks) @Select(BooksState.getBooks)
books$: Observable<Books.Book[]>; books$: Observable<BookDto[]>;
booksType = Books.BookType; booksType = BookType;
loading = false; loading = false;
isModalOpen = false; //<== added this line ==> isModalOpen = false; // <== added this line ==>
constructor(private store: Store) { } constructor(private store: Store) {}
ngOnInit() { ngOnInit() {
this.get(); this.get();
@ -723,12 +639,13 @@ export class BookListComponent implements OnInit {
get() { get() {
this.loading = true; this.loading = true;
this.store.dispatch(new GetBooks()).subscribe(() => { this.store
this.loading = false; .dispatch(new GetBooks())
}); .pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
} }
//added createBook method // added createBook method
createBook() { createBook() {
this.isModalOpen = true; this.isModalOpen = true;
} }
@ -749,12 +666,13 @@ Open `book-list.component.ts` file in `app\books\book-list` folder and replace t
```js ```js
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Store, Select } from '@ngxs/store'; import { Select, Store } from '@ngxs/store';
import { BooksState } from '../../store/states';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Books } from '../../store/models'; import { finalize } from 'rxjs/operators';
import { GetBooks } from '../../store/actions'; import { BookDto, BookType } from '../../app/shared/models';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; //<== added this line ==> import { GetBooks } from '../state/books.actions';
import { BooksState } from '../state/books.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // <== added this line ==>
@Component({ @Component({
selector: 'app-book-list', selector: 'app-book-list',
@ -763,17 +681,17 @@ import { FormGroup, FormBuilder, Validators } from '@angular/forms'; //<== added
}) })
export class BookListComponent implements OnInit { export class BookListComponent implements OnInit {
@Select(BooksState.getBooks) @Select(BooksState.getBooks)
books$: Observable<Books.Book[]>; books$: Observable<BookDto[]>;
booksType = Books.BookType; booksType = BookType;
loading = false; loading = false;
isModalOpen = 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() { ngOnInit() {
this.get(); this.get();
@ -781,9 +699,10 @@ export class BookListComponent implements OnInit {
get() { get() {
this.loading = true; this.loading = true;
this.store.dispatch(new GetBooks()).subscribe(() => { this.store
this.loading = false; .dispatch(new GetBooks())
}); .pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
} }
createBook() { createBook() {
@ -791,7 +710,7 @@ export class BookListComponent implements OnInit {
this.isModalOpen = true; this.isModalOpen = true;
} }
//added buildForm method // added buildForm method
buildForm() { buildForm() {
this.form = this.fb.group({ this.form = this.fb.group({
name: ['', Validators.required], name: ['', Validators.required],
@ -804,6 +723,7 @@ export class BookListComponent implements OnInit {
``` ```
* We imported `FormGroup, FormBuilder and Validators`. * 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 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. * 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`. * 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. * 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: Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below:
```js ```js
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Store, Select } from '@ngxs/store'; import { Select, Store } from '@ngxs/store';
import { BooksState } from '../../store/states';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Books } from '../../store/models'; import { finalize } from 'rxjs/operators';
import { GetBooks } from '../../store/actions'; 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 { 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({ @Component({
selector: 'app-book-list', selector: 'app-book-list',
templateUrl: './book-list.component.html', templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'], 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 { export class BookListComponent implements OnInit {
@Select(BooksState.getBooks) @Select(BooksState.getBooks)
books$: Observable<Books.Book[]>; books$: Observable<BookDto[]>;
booksType = Books.BookType; booksType = BookType;
//added bookTypeArr array //added bookTypeArr array
bookTypeArr = Object.keys(Books.BookType).filter( bookTypeArr = Object.keys(BookType).filter(
bookType => typeof this.booksType[bookType] === 'number' (bookType) => typeof this.booksType[bookType] === 'number'
); );
loading = false; loading = false;
@ -917,7 +836,7 @@ export class BookListComponent implements OnInit {
form: FormGroup; form: FormGroup;
constructor(private store: Store, private fb: FormBuilder) { } constructor(private store: Store, private fb: FormBuilder) {}
ngOnInit() { ngOnInit() {
this.get(); this.get();
@ -925,9 +844,10 @@ export class BookListComponent implements OnInit {
get() { get() {
this.loading = true; this.loading = true;
this.store.dispatch(new GetBooks()).subscribe(() => { this.store
this.loading = false; .dispatch(new GetBooks())
}); .pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
} }
createBook() { createBook() {
@ -981,19 +901,26 @@ Open `book-list.component.html` in `app\books\book-list` folder and add the foll
</ng-template> </ng-template>
``` ```
* This adds a save button to the bottom area of the modal: Find the `<form [formGroup]="form">` tag and replace below content:
![Save button to the modal](./images/bookstore-new-book-form-v2.png) ```html
<form [formGroup]="form" (ngSubmit)="save()"> <!-- added the ngSubmit -->
```
* We added the `(ngSubmit)="save()"` to `<form>` 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: Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below:
```js ```js
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Store, Select } from '@ngxs/store'; import { Select, Store } from '@ngxs/store';
import { BooksState } from '../../store/states';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Books } from '../../store/models'; import { finalize } from 'rxjs/operators';
import { GetBooks, CreateUpdateBook } from '../../store/actions'; //<== added CreateUpdateBook ==> 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 { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
@ -1001,16 +928,17 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap
selector: 'app-book-list', selector: 'app-book-list',
templateUrl: './book-list.component.html', templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'], styleUrls: ['./book-list.component.scss'],
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }] providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
}) })
export class BookListComponent implements OnInit { export class BookListComponent implements OnInit {
@Select(BooksState.getBooks) @Select(BooksState.getBooks)
books$: Observable<Books.Book[]>; books$: Observable<BookDto[]>;
booksType = Books.BookType; booksType = BookType;
bookTypeArr = Object.keys(Books.BookType).filter( //added bookTypeArr array
bookType => typeof this.booksType[bookType] === 'number' bookTypeArr = Object.keys(BookType).filter(
(bookType) => typeof this.booksType[bookType] === 'number'
); );
loading = false; loading = false;
@ -1019,7 +947,7 @@ export class BookListComponent implements OnInit {
form: FormGroup; form: FormGroup;
constructor(private store: Store, private fb: FormBuilder) { } constructor(private store: Store, private fb: FormBuilder) {}
ngOnInit() { ngOnInit() {
this.get(); this.get();
@ -1027,9 +955,10 @@ export class BookListComponent implements OnInit {
get() { get() {
this.loading = true; this.loading = true;
this.store.dispatch(new GetBooks()).subscribe(() => { this.store
this.loading = false; .dispatch(new GetBooks())
}); .pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
} }
createBook() { createBook() {
@ -1062,37 +991,20 @@ export class BookListComponent implements OnInit {
``` ```
* We imported `CreateUpdateBook`. * We imported `CreateUpdateBook`.
* We added `save` method * We added `save` method
### Updating an existing book The final modal UI looks like below:
#### BooksService ![Save button to the modal](./images/bookstore-new-book-form-v2.png)
Open the `books.service.ts` in `app\books\shared` folder and add the `getById` and `update` methods.
```js
getById(id: string): Observable<Books.Book> {
return this.restService.request<void, Books.Book>({
method: 'GET',
url: `/api/app/book/${id}`
});
}
update(updateBookInput: Books.CreateUpdateBookInput, id: string): Observable<Books.Book> { ### Updating a book
return this.restService.request<Books.CreateUpdateBookInput, Books.Book>({
method: 'PUT',
url: `/api/app/book/${id}`,
body: updateBookInput
});
}
```
#### CreateUpdateBook action #### 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 ```js
import { Books } from '../models'; import { CreateUpdateBookDto } from '../../app/shared/models';
export class GetBooks { export class GetBooks {
static readonly type = '[Books] Get'; static readonly type = '[Books] Get';
@ -1100,54 +1012,55 @@ export class GetBooks {
export class CreateUpdateBook { export class CreateUpdateBook {
static readonly type = '[Books] Create Update Book'; 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. * 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 ```js
@Action(CreateUpdateBook) @Action(CreateUpdateBook)
save(ctx: StateContext<Books.State>, action: CreateUpdateBook) { save(ctx: StateContext<BooksStateModel>, action: CreateUpdateBook) {
if (action.id) { if (action.id) {
return this.booksService.update(action.payload, action.id); return this.bookService.updateByIdAndInput(action.payload, action.id);
} else { } else {
return this.booksService.create(action.payload); return this.bookService.createByInput(action.payload);
} }
} }
``` ```
#### BookListComponent #### 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 ```js
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Store, Select } from '@ngxs/store'; import { Select, Store } from '@ngxs/store';
import { BooksState } from '../../store/states';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Books } from '../../store/models'; import { finalize } from 'rxjs/operators';
import { GetBooks, CreateUpdateBook } from '../../store/actions'; 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 { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; 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({ @Component({
selector: 'app-book-list', selector: 'app-book-list',
templateUrl: './book-list.component.html', templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'], styleUrls: ['./book-list.component.scss'],
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }] providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
}) })
export class BookListComponent implements OnInit { export class BookListComponent implements OnInit {
@Select(BooksState.getBooks) @Select(BooksState.getBooks)
books$: Observable<Books.Book[]>; books$: Observable<BookDto[]>;
booksType = Books.BookType; booksType = BookType;
bookTypeArr = Object.keys(Books.BookType).filter( bookTypeArr = Object.keys(BookType).filter(
bookType => typeof this.booksType[bookType] === 'number' (bookType) => typeof this.booksType[bookType] === 'number'
); );
loading = false; loading = false;
@ -1156,9 +1069,9 @@ export class BookListComponent implements OnInit {
form: FormGroup; 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() { ngOnInit() {
this.get(); this.get();
@ -1166,39 +1079,38 @@ export class BookListComponent implements OnInit {
get() { get() {
this.loading = true; this.loading = true;
this.store.dispatch(new GetBooks()).subscribe(() => { this.store
this.loading = false; .dispatch(new GetBooks())
}); .pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
} }
//<== this method is replaced ==> // <== this method is replaced ==>
createBook() { createBook() {
this.selectedBook = {} as Books.Book; //<== added ==> this.selectedBook = {} as BookDto; // <== added ==>
this.buildForm(); this.buildForm();
this.isModalOpen = true; this.isModalOpen = true;
} }
//<== added editBook method ==> // <== added editBook method ==>
editBook(id: string) { editBook(id: string) {
this.booksService.getById(id).subscribe(book => { this.bookService.getById(id).subscribe((book) => {
this.selectedBook = book; this.selectedBook = book;
this.buildForm(); this.buildForm();
this.isModalOpen = true; this.isModalOpen = true;
}); });
} }
//<== this method is replaced ==> // <== this method is replaced ==>
buildForm() { buildForm() {
this.form = this.fb.group({ this.form = this.fb.group({
name: [this.selectedBook.name || "", Validators.required], name: [this.selectedBook.name || '', Validators.required],
type: [this.selectedBook.type || null, Validators.required], type: [this.selectedBook.type || null, Validators.required],
publishDate: [ publishDate: [
this.selectedBook.publishDate this.selectedBook.publishDate ? new Date(this.selectedBook.publishDate) : null,
? new Date(this.selectedBook.publishDate) Validators.required,
: 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 ==> //<== 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(() => { .subscribe(() => {
this.isModalOpen = false; this.isModalOpen = false;
this.form.reset(); this.form.reset();
@ -1218,9 +1131,9 @@ export class BookListComponent implements OnInit {
} }
``` ```
* We imported `BooksService`. * We imported `BookService`.
* We declared a variable named `selectedBook` as `Books.Book`. * We declared a variable named `selectedBook` as `BookDto`.
* We injected `BooksService` to the constructor. `BooksService` is being used to retrieve the book data which is being edited. * 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 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 `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 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 `<n
### Deleting a book ### Deleting a book
#### BooksService
Open `books.service.ts` in `app\books\shared` folder and add the below `delete` method to delete a book.
```js
delete(id: string): Observable<void> {
return this.restService.request<void, void>({
method: 'DELETE',
url: `/api/app/book/${id}`
});
}
```
* `Delete` method gets `id` parameter and makes a `DELETE` HTTP request to the relevant endpoint.
#### DeleteBook action #### 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 ```js
export class DeleteBook { 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 ```js
import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store'; import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks, CreateUpdateBook, DeleteBook } from '../actions/books.actions'; //<== added DeleteBook==> import { GetBooks, CreateUpdateBook, DeleteBook } from './books.actions'; // <== added DeleteBook==>
import { Books } from '../models/books'; import { BookService } from '../../app/shared/services';
import { BooksService } from '../../books/shared/books.service';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { BookDto } from '../../app/shared/models';
@State<Books.State>({ export class BooksStateModel {
public book: PagedResultDto<BookDto>;
}
@State<BooksStateModel>({
name: 'BooksState', name: 'BooksState',
defaults: { books: {} } as Books.State, defaults: { book: {} } as BooksStateModel,
}) })
@Injectable() @Injectable()
export class BooksState { export class BooksState {
@Selector() @Selector()
static getBooks(state: Books.State) { static getBooks(state: BooksStateModel) {
return state.books.items || []; return state.book.items || [];
} }
constructor(private booksService: BooksService) { } constructor(private bookService: BookService) {}
@Action(GetBooks) @Action(GetBooks)
get(ctx: StateContext<Books.State>) { get(ctx: StateContext<BooksStateModel>) {
return this.booksService.get().pipe( return this.bookService.getListByInput().pipe(
tap(booksResponse => { tap((booksResponse) => {
ctx.patchState({ ctx.patchState({
books: booksResponse, book: booksResponse,
}); });
}), })
); );
} }
@Action(CreateUpdateBook) @Action(CreateUpdateBook)
save(ctx: StateContext<Books.State>, action: CreateUpdateBook) { save(ctx: StateContext<BooksStateModel>, action: CreateUpdateBook) {
if (action.id) { if (action.id) {
return this.booksService.update(action.payload, action.id); return this.bookService.updateByIdAndInput(action.payload, action.id);
} else { } else {
return this.booksService.create(action.payload); return this.bookService.createByInput(action.payload);
} }
} }
//<== added DeleteBook ==> // <== added DeleteBook action listener ==>
@Action(DeleteBook) @Action(DeleteBook)
delete(ctx: StateContext<Books.State>, action: DeleteBook) { delete(ctx: StateContext<BooksStateModel>, action: DeleteBook) {
return this.booksService.delete(action.id); return this.bookService.deleteById(action.id);
} }
} }
``` ```
@ -1379,27 +1282,8 @@ export class BooksState {
- We added `DeleteBook` action listener to the end of the file. - 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
<div ngbDropdownMenu>
<!-- added Delete button -->
<button ngbDropdownItem (click)="delete(data.id, data.name)">
{%{{{ 'AbpAccount::Delete' | abpLocalization }}}%}
</button>
</div>
```
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`. 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( constructor(
private store: Store, private fb: FormBuilder, private store: Store,
private booksService: BooksService, private fb: FormBuilder,
private confirmationService: ConfirmationService // <== added this line ==> private bookService: BookService,
private confirmation: ConfirmationService // <== added this line ==>
) { } ) { }
``` ```
* We imported `ConfirmationService`. * We imported `ConfirmationService`.
* We injected `ConfirmationService` to the constructor. * 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 : In the `book-list.component.ts` add a delete method :
```js ```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) { delete(id: string) {
this.confirmationService this.confirmation
.warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure') .warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure')
.subscribe(status => { .subscribe(status => {
if (status === Confirmation.Status.confirm) { 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: 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) ![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
<div ngbDropdownMenu>
<!-- added Delete button -->
<button ngbDropdownItem (click)="delete(data.id)">
{%{{{ 'AbpAccount::Delete' | abpLocalization }}}%}
</button>
</div>
```
The final actions dropdown UI looks like below:
![bookstore-final-actions-dropdown](./images/bookstore-final-actions-dropdown.png)
{{end}} {{end}}
### Next Part ### Next Part

BIN
docs/en/Tutorials/images/generate-proxy-command.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
docs/en/Tutorials/images/generated-proxies.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

15
docs/en/docs-nav.json

@ -221,8 +221,11 @@
}, },
{ {
"text": "Domain Driven Design", "text": "Domain Driven Design",
"path": "Domain-Driven-Design.md",
"items": [ "items": [
{
"text": "Overall",
"path": "Domain-Driven-Design.md"
},
{ {
"text": "Domain Layer", "text": "Domain Layer",
"items": [ "items": [
@ -393,8 +396,11 @@
}, },
{ {
"text": "Data Access", "text": "Data Access",
"path": "Data-Access.md",
"items": [ "items": [
{
"text": "Overall",
"path": "Data-Access.md"
},
{ {
"text": "Connection Strings", "text": "Connection Strings",
"path": "Connection-Strings.md" "path": "Connection-Strings.md"
@ -477,8 +483,11 @@
}, },
{ {
"text": "Startup Templates", "text": "Startup Templates",
"path": "Startup-Templates/Index.md",
"items": [ "items": [
{
"text": "Overall",
"path": "Startup-Templates/Index.md"
},
{ {
"text": "Application", "text": "Application",
"path": "Startup-Templates/Application.md" "path": "Startup-Templates/Application.md"

1
templates/app/angular/package.json

@ -47,7 +47,6 @@
"karma-jasmine": "~3.0.1", "karma-jasmine": "~3.0.1",
"karma-jasmine-html-reporter": "^1.4.2", "karma-jasmine-html-reporter": "^1.4.2",
"ng-packagr": "^9.0.0", "ng-packagr": "^9.0.0",
"ngxs-schematic": "^1.1.9",
"protractor": "~5.4.3", "protractor": "~5.4.3",
"ts-node": "~8.3.0", "ts-node": "~8.3.0",
"tslint": "~6.1.0", "tslint": "~6.1.0",

Loading…
Cancel
Save