From f08a0b6a7eb25b9b12603c304138020214260dd8 Mon Sep 17 00:00:00 2001 From: muhammedaltug Date: Fri, 11 Dec 2020 14:54:25 +0300 Subject: [PATCH] removed moment date adapter --- .../{blog.md => POST.md} | 948 +++++++++++------- 1 file changed, 593 insertions(+), 355 deletions(-) rename docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/{blog.md => POST.md} (65%) diff --git a/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/blog.md b/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/POST.md similarity index 65% rename from docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/blog.md rename to docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/POST.md index c55a3455de..7b1ae846af 100644 --- a/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/blog.md +++ b/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/POST.md @@ -1,127 +1,131 @@ # Using Angular Material Components With the ABP Framework ## Introduction -Angular Material library is a popular and well-known library in the community. We will implement Angular Material components with the ABP framework in this article. We will follow the Book Store tutorial which documented in [ABP Documentation](https://docs.abp.io/en/abp/latest/Tutorials/Part-1) + +Angular Material library is a popular and well-known library in the community. We will implement Angular Material components with the ABP framework in this article. We will follow the Book Store tutorial which documented in [ABP Documentation](https://docs.abp.io/en/abp/latest/Tutorials/Part-1) > This article doesn't include server-side parts except the **Author With Books Form** -section. Please follow the tutorial for server-side parts. +> section. Please follow the tutorial for server-side parts. ## Installation + Create a project with ABP CLI. Run following command in terminal: + ```bash abp new Acme.BookStore.AngularMaterial -u angular -o AcmeBookStoreAngularMaterial ``` -Add Angular Material package to the created project. Run this command in the terminal at the `angular` directory: -``` - ng add @angular/material -``` -Install the MatMomentDateAdapter recommended by AngularMaterial +Add Angular Material package to the created project. Run this command in the terminal at the `angular` directory: ``` -yarn add @angular/material-moment-adapter moment + ng add @angular/material ``` ## Book CRUD Actions > Please complete the following steps before starting this section: +> > - Follow the server-side steps at [Web Application Development Tutorial - Part 1: Creating the Server Side](https://docs.abp.io/en/abp/latest/Tutorials/Part-1) with remembering our application name is **Acme.BookStore.AngularMaterial** -> - Follow the localization part of [Web Application Development Tutorial - Part 2: The Book List Page](https://docs.abp.io/en/abp/latest/Tutorials/Part-2?UI=NG&DB=EF#localization) -> - Run following command in terminal at `angular` directory  -> ``` abp generate-proxy ``` +> - Follow the localization part of [Web Application Development Tutorial - Part 2: The Book List Page](https://docs.abp.io/en/abp/latest/Tutorials/Part-2?UI=NG&DB=EF#localization) +> - Run following command in terminal at `angular` directory  +> `abp generate-proxy` In this section, we will create the book list page, book create and update dialog using the Angular Material Modules. Add material modules to `SharedModule`'s imports and exports arrays which placed in `angular/src/app/shared/shared.module.ts`: ```typescript -import { MatCardModule } from '@angular/material/card'; -import { MatTableModule } from '@angular/material/table'; -import { MatPaginatorModule } from '@angular/material/paginator'; -import { MatSortModule } from '@angular/material/sort'; -import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from "@angular/material/card"; +import { MatTableModule } from "@angular/material/table"; +import { MatPaginatorModule } from "@angular/material/paginator"; +import { MatSortModule } from "@angular/material/sort"; +import { MatButtonModule } from "@angular/material/button"; @NgModule({ imports: [ - CoreModule, - ThemeSharedModule, - ThemeBasicModule, - NgbDropdownModule, + CoreModule, + ThemeSharedModule, + ThemeBasicModule, + NgbDropdownModule, NgxValidateCoreModule, MatCardModule, // added this line MatTableModule, // added this line MatPaginatorModule, // added this line MatSortModule, // added this line - MatButtonModule // added this line - + MatButtonModule, // added this line ], - exports: [ - CoreModule, - ThemeSharedModule, - ThemeBasicModule, + exports: [ + CoreModule, + ThemeSharedModule, + ThemeBasicModule, NgbDropdownModule, - NgxValidateCoreModule, - + NgxValidateCoreModule, + MatCardModule, // added this line MatTableModule, // added this line MatPaginatorModule, // added this line MatSortModule, // added this line - MatButtonModule // added this line - - ] + MatButtonModule, // added this line + ], }) export class SharedModule {} ``` + Run the following command in the terminal at the `angular` directory to create the book module: + ``` yarn ng generate module book --module app --routing --route books ``` + Remove `CommonModule` form and import `SharedModule` from imports array at `book.module.ts`: + ```typescript -import { NgModule } from '@angular/core'; -import { BookRoutingModule } from './book-routing.module'; -import { BookComponent } from './book.component'; -import { SharedModule } from '../shared/shared.module'; +import { NgModule } from "@angular/core"; +import { BookRoutingModule } from "./book-routing.module"; +import { BookComponent } from "./book.component"; +import { SharedModule } from "../shared/shared.module"; @NgModule({ declarations: [BookComponent], imports: [ BookRoutingModule, - SharedModule // this line added - ] + SharedModule, // this line added + ], }) -export class BookModule { } +export class BookModule {} ``` + > We deleted the `CommonModule` because `CommonModule` in `CoreModule`'s exports array and `CoreModule` in `SharedModule`'s exports array. We will add routes by adding items to the return array of the `routesProvider` created when creating a project for adding navigation elements for books route. For more information, see the [`RoutesService` document](https://docs.abp.io/en/abp/latest/UI/Angular/Modifying-the-Menu#via-routesservice). Open the `src/app/route.provider.ts` file replace the `configureRoutes` function declaration as shown below: + ```typescript -import { eLayoutType, RoutesService } from '@abp/ng.core'; +import { eLayoutType, RoutesService } from "@abp/ng.core"; function configureRoutes(routes: RoutesService) { return () => { routes.add([ { - path: '/', - name: '::Menu:Home', - iconClass: 'fas fa-home', + path: "/", + name: "::Menu:Home", + iconClass: "fas fa-home", order: 1, layout: eLayoutType.application, }, { - path: '/book-store', - name: '::Menu:BookStore', - iconClass: 'fas fa-book', + path: "/book-store", + name: "::Menu:BookStore", + iconClass: "fas fa-book", order: 2, layout: eLayoutType.application, }, { - path: '/books', - name: '::Menu:Books', - parentName: '::Menu:BookStore', + path: "/books", + name: "::Menu:Books", + parentName: "::Menu:BookStore", layout: eLayoutType.application, }, ]); @@ -130,27 +134,30 @@ function configureRoutes(routes: RoutesService) { ``` ### Book List + Replace `BookComponent` with the following code placed at `angular/src/book/book.component.ts` : -```typescript -import { Component, OnInit } from '@angular/core'; -import { ListService, PagedResultDto } from '@abp/ng.core'; -import { BookDto, BookService } from '@proxy/books'; -import { PageEvent } from '@angular/material/paginator'; -import { Sort } from '@angular/material/sort'; +```typescript +import { Component, OnInit } from "@angular/core"; +import { ListService, PagedResultDto } from "@abp/ng.core"; +import { BookDto, BookService } from "@proxy/books"; +import { PageEvent } from "@angular/material/paginator"; +import { Sort } from "@angular/material/sort"; @Component({ - selector: 'app-book', - templateUrl: './book.component.html', - styleUrls: ['./book.component.scss'], - providers: [ListService] + selector: "app-book", + templateUrl: "./book.component.html", + styleUrls: ["./book.component.scss"], + providers: [ListService], }) export class BookComponent implements OnInit { - book = { items: [], totalCount: 0 } as PagedResultDto; - columns: string[] = ['name', 'type', 'price']; + columns: string[] = ["name", "type", "price"]; - constructor(public readonly list: ListService, private bookService: BookService) { + constructor( + public readonly list: ListService, + private bookService: BookService + ) { this.list.maxResultCount = 2; } @@ -172,6 +179,7 @@ export class BookComponent implements OnInit { } } ``` + - We imported and injected the generated `BookService`. - We used the [ListService](https://docs.abp.io/en/abp/latest/UI/Angular/List-Service), a utility service of the ABP Framework which provides easy pagination, sorting and searching. - We set `this.list.maxResultCount` to 2 in the constructor, it can be changed programmatically for example changing value with the dropdown in the template @@ -182,33 +190,55 @@ Replace the `book.component.html` in the `angular/src/book/` with following code - {{ '::Menu:Books' | abpLocalization }} + {{ '::Menu:Books' | abpLocalization }} - +
- - + + - - + - +
{{'::Name' | abpLocalization}} {{element.name}} + {{'::Name' | abpLocalization}} + {{element.name}} {{'::Type' | abpLocalization}} {{ '::Enum:BookType:' + element.type | abpLocalization }} + + {{'::Type' | abpLocalization}} + + {{ '::Enum:BookType:' + element.type | abpLocalization }}
- +
``` + - We used the [Material Card](https://material.angular.io/components/card/overview) component as a container - We used [Material Table](https://material.angular.io/components/table/overview) and we made `name` and `type` columns sortable. `changeSort` method executes when sorting change. - We used the [Material Pagination](https://material.angular.io/components/paginator/overview). `changePage` method executes when the page changed ![Book List](book-list.gif) + ### Book Create In this section, we will create `BookDialogComponent` and we will display this component via [`Material Dialog`](https://material.angular.io/components/dialog/overview). We will use also [Material Input](https://material.angular.io/components/input/overview), [Material Select](https://material.angular.io/components/select/overview), [Material DatePicker](https://material.angular.io/components/datepicker/overview) modules in this component for book form. @@ -218,67 +248,72 @@ Create a new component named `BookDialogComponent` in the `angular/src/book/comp ``` yarn ng generate component book/components/BookDialog --module book ``` + > We used --module option for declaring in the component to a specific module. - + Add Material modules to `SharedModule`'s imports and exports arrays: ```typescript -import { MatDialogModule } from '@angular/material/dialog'; -import { MatDatepickerModule } from '@angular/material/datepicker'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatInputModule } from '@angular/material/input'; -import { MatSelectModule } from '@angular/material/select'; -import { MatIconModule } from '@angular/material/icon'; +import { MatDialogModule } from "@angular/material/dialog"; +import { MatDatepickerModule } from "@angular/material/datepicker"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatInputModule } from "@angular/material/input"; +import { MatSelectModule } from "@angular/material/select"; +import { MatIconModule } from "@angular/material/icon"; +import { MatNativeDateModule } from '@angular/material/core'; @NgModule({ imports: [ // other imports MatDialogModule, MatDatepickerModule, - MatMomentDateModule, + MatNativeDateModule, MatFormFieldModule, MatInputModule, MatSelectModule, - MatIconModule + MatIconModule, ], - exports: [ + exports: [ // other exports MatDialogModule, MatDatepickerModule, MatFormFieldModule, MatInputModule, MatSelectModule, - MatIconModule - ] + MatIconModule, + ], }) export class SharedModule {} ``` + Replace `book-dialog.component.ts` in `angular/src/book/` with following code: ```typescript -import { Component, Inject, OnInit } from '@angular/core'; -import { MAT_DIALOG_DATA, MAT_DIALOG_DEFAULT_OPTIONS } from '@angular/material/dialog'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { bookTypeOptions } from '@proxy/books'; +import { Component, Inject, OnInit } from "@angular/core"; +import { + MAT_DIALOG_DATA, + MAT_DIALOG_DEFAULT_OPTIONS, +} from "@angular/material/dialog"; +import { FormBuilder, FormGroup, Validators } from "@angular/forms"; +import { bookTypeOptions } from "@proxy/books"; @Component({ - selector: 'app-book-dialog', - templateUrl: './book-dialog.component.html', - styleUrls: ['./book-dialog.component.scss'], + selector: "app-book-dialog", + templateUrl: "./book-dialog.component.html", + styleUrls: ["./book-dialog.component.scss"], providers: [ - {provide: MAT_DIALOG_DEFAULT_OPTIONS, useValue: {hasBackdrop: true, width: '50vw' }} - ] + { + provide: MAT_DIALOG_DEFAULT_OPTIONS, + useValue: { hasBackdrop: true, width: "50vw" }, + }, + ], }) export class BookDialogComponent implements OnInit { - form: FormGroup; bookTypes = bookTypeOptions; - constructor( - private fb: FormBuilder - ) { - } + constructor(private fb: FormBuilder) {} ngOnInit(): void { this.buildForm(); @@ -286,7 +321,7 @@ export class BookDialogComponent implements OnInit { buildForm() { this.form = this.fb.group({ - name: [null , Validators.required], + name: [null, Validators.required], type: [null, Validators.required], publishDate: [null, Validators.required], price: [null, Validators.required], @@ -294,122 +329,141 @@ export class BookDialogComponent implements OnInit { } getFormValue() { - const {publishDate} = this.form.value; + const { publishDate } = this.form.value; return { ...this.form.value, - publishDate: `${publishDate?.getFullYear()}-${publishDate?.getMonth() + 1}-${publishDate?.getDate()}` + publishDate: `${publishDate?.getFullYear()}-${ + publishDate?.getMonth() + 1 + }-${publishDate?.getDate()}`, }; } - } ``` + - We made a form which form controls' names are the same as BookDto -- We provided the `MAT_DIALOG_DEFAULT_OPTIONS` token to change Material Dialog options for this component. Provided options are only available for this component. +- We provided the `MAT_DIALOG_DEFAULT_OPTIONS` token to change Material Dialog options for this component. Provided options are only available for this component. Replace the `book-dialog.component.html` with following code: + ```html

{{ '::NewBook' | abpLocalization }}

{{'::Name' | abpLocalization}} * - + {{'::Price' | abpLocalization}} * - + {{'::Type' | abpLocalization}} * - {{ '::Enum:BookType:' + type.value | abpLocalization }} + {{ '::Enum:BookType:' + type.value | abpLocalization }} - {{'::PublishDate' | abpLocalization}} * - + {{'::PublishDate' | abpLocalization}} * +
-
- + ``` + - We created a form with material form field components. - We added 2 buttons for closing dialog and saving form in the `mat-dialog-actions` element. Create the `createBook` method and inject `MatDialog` in `book.component.ts`. Then use the material dialog's `open` method inside the `createBook` method: ```typescript - -import {BookDialogComponent} from './components/book-dialog'; +import { BookDialogComponent } from "./components/book-dialog"; export class BookComponent { - constructor( - // ... - // inject dialog - public dialog: MatDialog) { - //... - } - //... other methods - createBook() { - const dialogRef = this.dialog.open(BookDialogComponent); - dialogRef.afterClosed().subscribe(result => { - if (result) { - this.bookService.create(result).subscribe(() => { - this.list.get(); - }); - } + constructor( + // ... + // inject dialog + public dialog: MatDialog + ) { + //... + } + //... other methods + createBook() { + const dialogRef = this.dialog.open(BookDialogComponent); + dialogRef.afterClosed().subscribe((result) => { + if (result) { + this.bookService.create(result).subscribe(() => { + this.list.get(); }); - } + } + }); + } } - ``` -- We displayed BookDialogComponent via Material Dialog. If the result has data after the dialog closes we made 2 HTTP requests for creating a book and refreshing the book list. + +- We displayed BookDialogComponent via Material Dialog. If the result has data after the dialog closes we made 2 HTTP requests for creating a book and refreshing the book list. Add create book button near the `mat-card-title` element in `book.component.html`: ```html -{{ '::Menu:Books' | abpLocalization }} - ``` + The final UI looks like below: ![Book Create](./book-create.gif) ### Edit Book -We will use the same dialog component for editing the book. And we will add the `actions` column to the book list table. The actions column is a simple dropdown. We will use [Material Menu](https://material.angular.io/components/menu/overview) for creating dropdown +We will use the same dialog component for editing the book. And we will add the `actions` column to the book list table. The actions column is a simple dropdown. We will use [Material Menu](https://material.angular.io/components/menu/overview) for creating dropdown Add `MatMenuModule` to `SharedModule` metadata's imports and exports array like this: ```typescript -import { MatMenuModule } from '@angular/material/menu'; +import { MatMenuModule } from "@angular/material/menu"; @NgModule({ imports: [ // other imports - MatMenuModule + MatMenuModule, ], - exports: [ + exports: [ // other exports - MatMenuModule - ] + MatMenuModule, + ], }) export class SharedModule {} ``` -Edit `columns` array and add `editBook` method in `book.component.ts` as shown below: +Edit `columns` array and add `editBook` method in `book.component.ts` as shown below: + ```typescript columns: string[] = ['actions', /* ... other columns*/]; @@ -428,6 +482,7 @@ editBook(id: string) { }); } ``` + - We passed the data to `BookDialogComponent` with passing the data property `open` method of the material dialog. - We checked data after closing the dialog for sending HTTP requests as in the `Create Book` section. @@ -459,7 +514,9 @@ Add actions column before name column and add `mat-menu` end of file in the `boo ``` + Get passed data which passed with material dialog's `open` method and use this data to create a form with initial values in `book-dialog.component.ts` as shown below: + ```typescript constructor( //inject data @@ -476,13 +533,19 @@ Get passed data which passed with material dialog's `open` method and use this d }); } ``` + Edit the dialog title if component has data, display **Edit Book** text otherwise **New Book** in `book-dialog.component.html`: + ```html -

{{ (data ? '::EditBook' : '::NewBook' )| abpLocalization }}

+

+ {{ (data ? '::EditBook' : '::NewBook' )| abpLocalization }} +

``` ![Book Edit](book-edit.gif) + ### Delete Book + In the ABP Framework, a confirmation popup displays when the delete button is clicked. We will create a Confirmation Dialog and display this dialog with Material Dialog. Create `ConfirmationDialogComponent` in `angular/src/shared/components` directory with following command: @@ -490,44 +553,58 @@ Create `ConfirmationDialogComponent` in `angular/src/shared/components` director ``` yarn ng generate component shared/components/ConfirmationDialog --module shared ``` -Replace `ConfirmationDialogComponent` with following code: + +Replace `ConfirmationDialogComponent` with following code: + ```typescript -import { Component, Inject } from '@angular/core'; -import { MAT_DIALOG_DATA, MAT_DIALOG_DEFAULT_OPTIONS } from '@angular/material/dialog'; +import { Component, Inject } from "@angular/core"; +import { + MAT_DIALOG_DATA, + MAT_DIALOG_DEFAULT_OPTIONS, +} from "@angular/material/dialog"; export interface ConfirmationDialogData { title: string; - description: string + description: string; } @Component({ - selector: 'app-confirmation-dialog', - templateUrl: './confirmation-dialog.component.html', - styleUrls: ['./confirmation-dialog.component.scss'], + selector: "app-confirmation-dialog", + templateUrl: "./confirmation-dialog.component.html", + styleUrls: ["./confirmation-dialog.component.scss"], providers: [ - {provide: MAT_DIALOG_DEFAULT_OPTIONS, useValue: {hasBackdrop: true, width: "450px" }} - ] + { + provide: MAT_DIALOG_DEFAULT_OPTIONS, + useValue: { hasBackdrop: true, width: "450px" }, + }, + ], }) export class ConfirmationDialogComponent { - constructor(@Inject(MAT_DIALOG_DATA) public data: ConfirmationDialogData) { } + constructor(@Inject(MAT_DIALOG_DATA) public data: ConfirmationDialogData) {} } ``` + Replace `confirmation-dialog.component.html` with the following code: ```html -
- warning -

{{ data.title | abpLocalization }}

-

{{ data.description | abpLocalization }}

-
- +
+ warning +

{{ data.title | abpLocalization }}

+

{{ data.description | abpLocalization }}

+
- - + + ``` + Replace `confirmation-dialog.component.scss` with following code: + ```scss :host { .dialog-container { @@ -548,7 +625,9 @@ Replace `confirmation-dialog.component.scss` with following code: } } ``` + Add `deleteBook` method to `BookComponent`: + ```typescript import { BookDialogComponent } from './components/book-dialog/book-dialog.component'; import { ConfirmationDialogComponent } from '../shared/components/confirmation-dialog/confirmation-dialog.component'; @@ -567,7 +646,9 @@ deleteBook(id: string) { }); } ``` + Add delete button to actions' button menu template in `book.component.html`: + ```html @@ -582,6 +663,7 @@ Add delete button to actions' button menu template in `book.component.html`: ``` + ![Book Delete](./book-delete.gif) ## Authorization @@ -589,12 +671,14 @@ Add delete button to actions' button menu template in `book.component.html`: You can follow steps for authorization at the [Web Application Development Tutorial - Part 5: Authorization](https://docs.abp.io/en/abp/latest/Tutorials/Part-5?UI=NG&DB=EF) ## Author CRUD Actions + > Please complete the following steps before starting this section +> > - [Web Application Development Tutorial - Part 6: Authors: Domain Layer](https://docs.abp.io/en/abp/latest/Tutorials/Part-6) > - [Web Application Development Tutorial - Part 7: Authors: Database Integration](https://docs.abp.io/en/abp/latest/Tutorials/Part-7) > - [Web Application Development Tutorial - Part 8: Authors: Application Layer](https://docs.abp.io/en/abp/latest/Tutorials/Part-8) -> - Run following command in terminal at `angular` directory: -> ``` abp generate-proxy ``` +> - Run following command in terminal at `angular` directory: +> `abp generate-proxy` In this section, we will create an author list page and author create/update dialog by following the same steps in the `Book CRUD Actions` section @@ -605,37 +689,37 @@ yarn ng generate module author --module app --routing --route authors ``` Create `AuthorDialogComponent` in `angular/src/app/author/components` directory with following command: + ``` yarn ng generate component author/components/AuthorDialog -m author ``` Add `SharedModule` to `AuthorModule`'s imports array: + ```typescript -import { NgModule } from '@angular/core'; -import { AuthorWithBooksRoutingModule } from './author-with-books-routing.module'; -import { AuthorWithBooksComponent } from './author-with-books.component'; -import { SharedModule } from '../shared/shared.module'; +import { NgModule } from "@angular/core"; +import { AuthorWithBooksRoutingModule } from "./author-with-books-routing.module"; +import { AuthorWithBooksComponent } from "./author-with-books.component"; +import { SharedModule } from "../shared/shared.module"; @NgModule({ declarations: [AuthorComponent, AuthorDialogComponent], - imports: [ - SharedModule, - AuthorRoutingModule - ] + imports: [SharedModule, AuthorRoutingModule], }) -export class AuthorModule { } +export class AuthorModule {} ``` Open the `src/app/route.provider.ts` file replace the `configureRoutes` function declaration as shown below: + ```typescript function configureRoutes(routes: RoutesService) { return () => { routes.add([ // other routes { - path: '/authors', - name: '::Menu:Authors', - parentName: '::Menu:BookStore', + path: "/authors", + name: "::Menu:Authors", + parentName: "::Menu:BookStore", layout: eLayoutType.application, }, ]); @@ -644,28 +728,34 @@ function configureRoutes(routes: RoutesService) { ``` Replace `AuthorDialogComponent` with following code below: + ```typescript -import { Component, Inject, OnInit } from '@angular/core'; -import { MAT_DIALOG_DATA, MAT_DIALOG_DEFAULT_OPTIONS } from '@angular/material/dialog'; -import { AuthorDto } from '@proxy/authors'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Component, Inject, OnInit } from "@angular/core"; +import { + MAT_DIALOG_DATA, + MAT_DIALOG_DEFAULT_OPTIONS, +} from "@angular/material/dialog"; +import { AuthorDto } from "@proxy/authors"; +import { FormBuilder, FormGroup, Validators } from "@angular/forms"; @Component({ - selector: 'app-author-dialog', - templateUrl: './author-dialog.component.html', - styleUrls: ['./author-dialog.component.scss'], + selector: "app-author-dialog", + templateUrl: "./author-dialog.component.html", + styleUrls: ["./author-dialog.component.scss"], providers: [ - {provide: MAT_DIALOG_DEFAULT_OPTIONS, useValue: {hasBackdrop: true, width: "50vw" }} - ] + { + provide: MAT_DIALOG_DEFAULT_OPTIONS, + useValue: { hasBackdrop: true, width: "50vw" }, + }, + ], }) -export class AuthorDialogComponent implements OnInit{ - +export class AuthorDialogComponent implements OnInit { form: FormGroup; constructor( @Inject(MAT_DIALOG_DATA) public data: AuthorDto, - private fb: FormBuilder, - ) { } + private fb: FormBuilder + ) {} ngOnInit(): void { this.buildForm(); } @@ -673,61 +763,71 @@ export class AuthorDialogComponent implements OnInit{ buildForm() { this.form = this.fb.group({ name: [this.data?.name, Validators.required], - birthDate: [this.data?.birthDate ? new Date(this.data.birthDate) : null, Validators.required], + birthDate: [ + this.data?.birthDate ? new Date(this.data.birthDate) : null, + Validators.required, + ], }); } getFormValue() { - const {birthDate} = this.form.value; + const { birthDate } = this.form.value; return { ...this.form.value, - publishDate: `${birthDate?.getFullYear()}-${birthDate?.getMonth() + 1}-${birthDate?.getDate()}` + publishDate: `${birthDate?.getFullYear()}-${ + birthDate?.getMonth() + 1 + }-${birthDate?.getDate()}`, }; } } ``` Replace `author-dialog.component.html` with the following code below: + ```html -

{{ (data ? '::EditAuthor' : '::NewAuthor' )| abpLocalization }}

+

+ {{ (data ? '::EditAuthor' : '::NewAuthor' )| abpLocalization }} +

{{'::Name' | abpLocalization}} * - + {{'::BirthDate' | abpLocalization}} * - +
-
- + ``` Replace `author.component.ts` with following code below: + ```typescript -import { Component, OnInit } from '@angular/core'; -import { ListService, PagedResultDto } from '@abp/ng.core'; -import { AuthorDto, AuthorService } from '@proxy/authors'; -import { FormGroup } from '@angular/forms'; -import { PageEvent } from '@angular/material/paginator'; -import { Sort } from '@angular/material/sort'; -import { MatDialog } from '@angular/material/dialog'; -import { AuthorDialogComponent } from './components/author-dialog/author-dialog.component'; -import { ConfirmationDialogComponent } from '../shared/components/confirmation-dialog/confirmation-dialog.component'; +import { Component, OnInit } from "@angular/core"; +import { ListService, PagedResultDto } from "@abp/ng.core"; +import { AuthorDto, AuthorService } from "@proxy/authors"; +import { FormGroup } from "@angular/forms"; +import { PageEvent } from "@angular/material/paginator"; +import { Sort } from "@angular/material/sort"; +import { MatDialog } from "@angular/material/dialog"; +import { AuthorDialogComponent } from "./components/author-dialog/author-dialog.component"; +import { ConfirmationDialogComponent } from "../shared/components/confirmation-dialog/confirmation-dialog.component"; @Component({ - selector: 'app-author', - templateUrl: './author.component.html', - styleUrls: ['./author.component.scss'], + selector: "app-author", + templateUrl: "./author.component.html", + styleUrls: ["./author.component.scss"], providers: [ListService], }) export class AuthorComponent implements OnInit { @@ -735,7 +835,7 @@ export class AuthorComponent implements OnInit { form: FormGroup; - columns = ['actions', 'name', 'birthDate']; + columns = ["actions", "name", "birthDate"]; constructor( public readonly list: ListService, @@ -755,7 +855,6 @@ export class AuthorComponent implements OnInit { this.list.page = pageEvent.pageIndex; } - changeSort(sort: Sort) { this.list.sortKey = sort.active; this.list.sortOrder = sort.direction; @@ -763,7 +862,7 @@ export class AuthorComponent implements OnInit { createAuthor() { const dialogRef = this.dialog.open(AuthorDialogComponent); - dialogRef.afterClosed().subscribe(result => { + dialogRef.afterClosed().subscribe((result) => { if (result) { this.authorService.create(result).subscribe(() => { this.list.get(); @@ -775,9 +874,9 @@ export class AuthorComponent implements OnInit { editAuthor(id: any) { this.authorService.get(id).subscribe((author) => { const dialogRef = this.dialog.open(AuthorDialogComponent, { - data: author + data: author, }); - dialogRef.afterClosed().subscribe(result => { + dialogRef.afterClosed().subscribe((result) => { if (result) { this.authorService.update(id, result).subscribe(() => { this.list.get(); @@ -788,13 +887,16 @@ export class AuthorComponent implements OnInit { } deleteAuthor(id: string) { - const confirmationDialogRef = this.dialog.open(ConfirmationDialogComponent, { - data: { - title: '::AreYouSure', - description: '::AreYouSureToDelete' + const confirmationDialogRef = this.dialog.open( + ConfirmationDialogComponent, + { + data: { + title: "::AreYouSure", + description: "::AreYouSureToDelete", + }, } - }); - confirmationDialogRef.afterClosed().subscribe(confirmationResult => { + ); + confirmationDialogRef.afterClosed().subscribe((confirmationResult) => { if (confirmationResult) { this.authorService.delete(id).subscribe(() => this.list.get()); } @@ -809,9 +911,17 @@ Replace `author.component.html` with the following code: - {{ '::Menu:Authors' | abpLocalization }} + {{ '::Menu:Authors' | abpLocalization }}
- @@ -819,29 +929,51 @@ Replace `author.component.html` with the following code: - +
- + - - - - + + + + - - +
{{'::Actions' | abpLocalization}} + {{'::Actions' | abpLocalization}} + - - {{'::Name' | abpLocalization}} {{element.name}} + {{'::Name' | abpLocalization}} + {{element.name}} {{'::BirthDate' | abpLocalization}} {{ element.birthDate | date }} + + {{'::BirthDate' | abpLocalization}} + + {{ element.birthDate | date }}
- +
@@ -857,6 +989,7 @@ Replace `author.component.html` with the following code: ``` Open the `en.json` file under the `Localization/BookStore` folder of the `Acme.BookStore.AngularMaterial.Domain.Shared project` and add the following entries: + ```json "Menu:Authors": "Authors", "Authors": "Authors", @@ -867,11 +1000,14 @@ Open the `en.json` file under the `Localization/BookStore` folder of the `Acme.B ``` ![Author](./author-crud.gif) + ## Author And Book Relation + > Please complete the following steps before starting this section +> > - Complete server-side parts [Web Application Development Tutorial - Part 10: Book to Author Relation](https://docs.abp.io/en/abp/latest/Tutorials/Part-10) until [The User Interface](https://docs.abp.io/en/abp/latest/Tutorials/Part-10?UI=NG&DB=EF#the-user-interface) section -> - Run following command in terminal at `angular` directory: -> ``` abp generate-proxy ``` +> - Run following command in terminal at `angular` directory: +> `abp generate-proxy` In this section, we will add author selection to the book creation form, create one form for adding an author with books using Material Stepper and display the author's name in the book list page. @@ -880,24 +1016,35 @@ In this section, we will add author selection to the book creation form, create We will add the author select box using Material Select and we will get authors from the server in `BookDialogComponent`. Replace `book-dialog.component.ts` in `app/src/book/components/book-dialog` with following code: + ```typescript -import { Component, Inject, OnInit } from '@angular/core'; -import { MAT_DIALOG_DATA, MAT_DIALOG_DEFAULT_OPTIONS } from '@angular/material/dialog'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { AuthorLookupDto, BookDto, BookService, bookTypeOptions } from '@proxy/books'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { Component, Inject, OnInit } from "@angular/core"; +import { + MAT_DIALOG_DATA, + MAT_DIALOG_DEFAULT_OPTIONS, +} from "@angular/material/dialog"; +import { FormBuilder, FormGroup, Validators } from "@angular/forms"; +import { + AuthorLookupDto, + BookDto, + BookService, + bookTypeOptions, +} from "@proxy/books"; +import { Observable } from "rxjs"; +import { map } from "rxjs/operators"; @Component({ - selector: 'app-book-dialog', - templateUrl: './book-dialog.component.html', - styleUrls: ['./book-dialog.component.scss'], + selector: "app-book-dialog", + templateUrl: "./book-dialog.component.html", + styleUrls: ["./book-dialog.component.scss"], providers: [ - {provide: MAT_DIALOG_DEFAULT_OPTIONS, useValue: {hasBackdrop: true, width: '50vw' }} - ] + { + provide: MAT_DIALOG_DEFAULT_OPTIONS, + useValue: { hasBackdrop: true, width: "50vw" }, + }, + ], }) export class BookDialogComponent implements OnInit { - form: FormGroup; bookTypes = bookTypeOptions; @@ -907,7 +1054,7 @@ export class BookDialogComponent implements OnInit { constructor( private fb: FormBuilder, @Inject(MAT_DIALOG_DATA) public data: BookDto, - bookService: BookService, // inject bookService + bookService: BookService // inject bookService ) { this.authors$ = bookService.getAuthorLookup().pipe(map((r) => r.items)); // this line added } @@ -922,47 +1069,60 @@ export class BookDialogComponent implements OnInit { type: [this.data?.type, Validators.required], publishDate: [this.data?.publishDate, Validators.required], price: [this.data?.price, Validators.required], - authorId: [this.data?.authorId, Validators.required] // this line added + authorId: [this.data?.authorId, Validators.required], // this line added }); } } ``` Add author select box before name field in `book-dialog.component.html` as shown below: + ```html - {{'::Author' | abpLocalization}} * - - {{ author.name }} - + {{'::Author' | abpLocalization}} * + + {{ author.name }} + - {{'::Name' | abpLocalization}} * - + {{'::Name' | abpLocalization}} * + ``` -### Author Name Column +### Author Name Column Add the `authorName` item to columns array in `BookComponent`: + ```typescript columns: string[] = [/* ...other columns*/, 'authorName']; ``` Add the `authorName` column after price column in `book.component.html`: + ```html - + - + ``` ### Author With Books Form + In this section, we will create an endpoint that takes author information and book list in the request body for creating an author and books by one request. Create a class named `CreateBookDto` in `Application.Contracts/Books` folder: + ```csharp using System; using System.ComponentModel.DataAnnotations; @@ -987,7 +1147,7 @@ namespace Acme.BookStore.AngularMaterial.Books [Required] public float Price { get; set; } } -} +} ``` Create a class named `CreateAuthorWithBookDto` in `Application.Contracts/Books` folder: @@ -999,7 +1159,7 @@ using Acme.BookStore.AngularMaterial.Authors; namespace Acme.BookStore.AngularMaterial.Books { public class CreateAuthorWithBookDto: CreateAuthorDto - { + { public List Books { get; set; } public CreateAuthorWithBookDto() @@ -1007,7 +1167,7 @@ namespace Acme.BookStore.AngularMaterial.Books Books = new List(); } } -} +} ``` Create a class named `AuthorWithDetailsDto` in `Application.Contracts/Books` folder: @@ -1021,7 +1181,7 @@ namespace Acme.BookStore.AngularMaterial.Books { public class AuthorWithDetailsDto: AuthorDto { - [Required] + [Required] public List Books { get; set; } public AuthorWithDetailsDto() @@ -1029,10 +1189,11 @@ namespace Acme.BookStore.AngularMaterial.Books Books = new List(); } } -} +} ``` Add following line to `IBookAppService` interface which placed in `Application.Contracts/Books`: + ```csharp Task CreateAuthorWithBooksAsync(CreateAuthorWithBookDto input); ``` @@ -1057,13 +1218,13 @@ namespace Acme.BookStore.AngularMaterial.Books BookDto, Guid, PagedAndSortedResultRequestDto, - CreateUpdateBookDto>, - IBookAppService + CreateUpdateBookDto>, + IBookAppService { private readonly IAuthorRepository _authorRepository; private readonly AuthorManager _authorManager; // this line added public BookAppService( - IRepository repository, + IRepository repository, IAuthorRepository authorRepository, // inject AuthorManager AuthorManager authorManager) @@ -1108,6 +1269,7 @@ namespace Acme.BookStore.AngularMaterial.Books ``` Open the `en.json` file under the `Localization/BookStore` folder of the `Acme.BookStore.AngularMaterial.Domain.Shared project` and add the following entries: + ```json "AuthorInfo": "Author Info", "BookInfo": "Book Info", @@ -1117,6 +1279,7 @@ Open the `en.json` file under the `Localization/BookStore` folder of the `Acme.B ``` Run generate-proxy command in the terminal at `angular` directory: + ``` abp generate-proxy ``` @@ -1124,37 +1287,40 @@ abp generate-proxy We will create `AuthorWithBooksModule` with components and we will use Material Stepper inside the component. Run the following command for creating `AuthorWithBooksModule`: + ``` yarn ng generate module author-with-books --module app --routing --route author-with-books ``` Add `SharedModule` to `AuthorWithBooksModule`'s imports array as shown below: + ```typescript -import { SharedModule } from '../shared/shared.module'; +import { SharedModule } from "../shared/shared.module"; @NgModule({ declarations: [AuthorWithBooksComponent], imports: [ - SharedModule,// this line added - AuthorWithBooksRoutingModule - ] + SharedModule, // this line added + AuthorWithBooksRoutingModule, + ], }) -export class AuthorWithBooksModule { } +export class AuthorWithBooksModule {} ``` Add `MatStepperModule` to `SharedModule`'s imports array as shown below: + ```typescript -import { MatStepperModule } from '@angular/material/stepper'; +import { MatStepperModule } from "@angular/material/stepper"; @NgModule({ imports: [ // other imports - MatStepperModule + MatStepperModule, ], - exports: [ + exports: [ // other exports - MatStepperModule - ] + MatStepperModule, + ], }) export class SharedModule {} ``` @@ -1164,82 +1330,84 @@ We will create one form which includes author form group and book form array. We Replace `author-with-books.component.ts` with the following code: ```typescript -import { Component, OnInit } from '@angular/core'; -import { bookTypeOptions } from '@proxy/books'; -import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { STEPPER_GLOBAL_OPTIONS } from '@angular/cdk/stepper'; +import { Component, OnInit } from "@angular/core"; +import { bookTypeOptions } from "@proxy/books"; +import { FormArray, FormBuilder, FormGroup, Validators } from "@angular/forms"; +import { STEPPER_GLOBAL_OPTIONS } from "@angular/cdk/stepper"; @Component({ - selector: 'app-author-with-book', - templateUrl: './author-with-book.component.html', - styleUrls: ['./author-with-book.component.scss'], + selector: "app-author-with-book", + templateUrl: "./author-with-book.component.html", + styleUrls: ["./author-with-book.component.scss"], providers: [ { provide: STEPPER_GLOBAL_OPTIONS, - useValue: { displayDefaultIndicatorType: false } - } - ] + useValue: { displayDefaultIndicatorType: false }, + }, + ], }) export class AuthorWithBooksComponent implements OnInit { + form: FormGroup; - form: FormGroup; - - bookTypes = bookTypeOptions; + bookTypes = bookTypeOptions; - get bookFormArray(): FormArray { - return this.form.get('books') as FormArray; - } + get bookFormArray(): FormArray { + return this.form.get("books") as FormArray; + } - constructor( - private fb: FormBuilder, - private bookService: BookService, - private toasterService: ToasterService) { - } + constructor( + private fb: FormBuilder, + private bookService: BookService, + private toasterService: ToasterService + ) {} - ngOnInit(): void { - this.form = this.fb.group({ - author: this.fb.group({ - name: [null, Validators.required], - birthDate: [null, Validators.required] - }), - books: this.fb.array([this.getBookForm()]) - }); - } + ngOnInit(): void { + this.form = this.fb.group({ + author: this.fb.group({ + name: [null, Validators.required], + birthDate: [null, Validators.required], + }), + books: this.fb.array([this.getBookForm()]), + }); + } - getBookForm(){ - return this.fb.group({ - name: [null, Validators.required], - type: [null, Validators.required], - publishDate: [null, Validators.required], - price: [null, Validators.required], - }); - } + getBookForm() { + return this.fb.group({ + name: [null, Validators.required], + type: [null, Validators.required], + publishDate: [null, Validators.required], + price: [null, Validators.required], + }); + } - addBook(){ - this.bookFormArray.push(this.getBookForm()); - } + addBook() { + this.bookFormArray.push(this.getBookForm()); + } - deleteBook(i: number) { - this.bookFormArray.removeAt(i); - } + deleteBook(i: number) { + this.bookFormArray.removeAt(i); + } - save() { - if (this.form.invalid) { - return; - } - const authorWithBook: CreateAuthorWithBookDto = { - ...this.form.value.author, - books: this.form.value.books - }; - this.bookService.createAuthorWithBooks(authorWithBook).subscribe((res) => { - this.toasterService.success('::AuthorWithBook:Success', '', {messageLocalizationParams: [res.name]}); - }); + save() { + if (this.form.invalid) { + return; } + const authorWithBook: CreateAuthorWithBookDto = { + ...this.form.value.author, + books: this.form.value.books, + }; + this.bookService.createAuthorWithBooks(authorWithBook).subscribe((res) => { + this.toasterService.success("::AuthorWithBook:Success", "", { + messageLocalizationParams: [res.name], + }); + }); + } } ``` + - We created form until component initialization in the `ngOnInit` method -- In the `getBookForm` method, returned -- In the `addBook` method, we pushed a form group instance created in the `getBookForm` method to the book form array. We will execute this method when clicked on the **Add Book** button +- In the `getBookForm` method, returned +- In the `addBook` method, we pushed a form group instance created in the `getBookForm` method to the book form array. We will execute this method when clicked on the **Add Book** button - We deleted a form group instance by index from the book form array in the `deleteBook` method - In the `save` method, we send an HTTP request for creating author and books if creation will be successful toaster message will be display @@ -1247,54 +1415,108 @@ Replace the `author-with-books.component.html` content with following code: ```html - +
{{'::Name' | abpLocalization}} * - + - {{'::BirthDate' | abpLocalization}} * - + {{'::BirthDate' | abpLocalization}} * +
- +
{{'::Name' | abpLocalization}} * - + - {{'::Price' | abpLocalization}} * - + {{'::Price' | abpLocalization}} * + {{'::Type' | abpLocalization}} * - {{'::SelectBookType' | abpLocalization}} - {{ '::Enum:BookType:' + type.value | abpLocalization }} + {{'::SelectBookType' | abpLocalization}} + {{ '::Enum:BookType:' + type.value | abpLocalization + }} - {{'::PublishDate' | abpLocalization}} * - - + {{'::PublishDate' | abpLocalization}} * + +
- +
- - + +
@@ -1305,10 +1527,12 @@ Replace the `author-with-books.component.html` content with following code:
``` + - We created the same author form where we created in `AuthorDialogComponent` and we gave the author form group to mat-step's stepConrol input - We created the same book form which we created at `BookDialogComponent` except the author selection - + Replace `author-with-books.component.scss` content with following code: + ```scss .book-form { margin-top: 20px; @@ -1320,24 +1544,38 @@ Replace `author-with-books.component.scss` content with following code: margin-top: 25px; } ``` + Finally add Create Author With Books button near the Create Author button in `author.component.html`: + ```html
- - + +
- ``` + Final UI looks as shown below: ![Author With Books](./author-with-books.gif) ## Conclusion -We implemented Angular Material Components to our angular application which was created with ABP Framework. There is no blocker case of using angular libraries with the ABP framework. \ No newline at end of file +We implemented Angular Material Components to our angular application which was created with ABP Framework. There is no blocker case of using angular libraries with the ABP framework.