Angular 2 到 Angular 4,使用 Angular Material UI 组件 - 第三部分






4.90/5 (20投票s)
在本文中,我们将把我们的 Angular 2 应用程序转换为 Angular 4,并用 Angular Material 组件替换传统的 HTML 和第三方组件。
系列文章
- 第 1 部分:在 Visual Studio 2017 中设置 Angular2,基本的 CRUD 应用程序,第三方模态弹出窗口控件
- 第 2 部分:使用 Angular2 管道进行过滤/搜索,全局错误处理,客户端调试
- 第 3 部分:Angular 2 到 Angular 4,包含 Angular Material 组件
- 第 4 部分:Angular 4 数据网格,支持导出到 Excel、排序和过滤
引言
在本文中,我们将继续增强用户管理应用程序,将 Angular 版本从 Angular 2 升级到 Angular 4,并使用 Angular Material 2 组件代替传统的 HTML 和第三方组件(例如,我们在添加/更新/删除用户时使用的 `ng2-bs3-modal` 模态弹出框)。
Angular 4 有一些很酷的新功能,我不想一一赘述,因为你可以在网上轻松找到它们。快速回顾请查看 这里。我们将在用户管理应用程序中使用 **电子邮件验证器** 和 `If-else` 模板条件。
Angular Material 是与 Angular 兼容的组件,我们主要用它来设计 Web 应用程序的 UI,例如 `Input`、`Autocomplete`、`dropdown`、`checkbox` 等。点击 这里 查看 Angular Material 组件。我们将用 Angular Material 组件替换所有现有的 HTML 和第三方组件。
背景
本文是 Angular2 in ASP.NET MVC & Web API - Part 2 的第三部分。在之前的文章中,我们使用了 `ng3-bs3-modal` 第三方模态弹出组件和传统的 HTML 控件。在本文中,我们将用 **Angular Material 组件** 替换所有控件。强烈建议你在阅读本文之前先阅读 第一部分 和 第二部分。
让我们开始
在开始实际开发之前,我将向你展示本文的最终输出。你可以将其与 Angular2 in ASP.NET MVC & Web API - Part 2 进行比较,以了解我们要构建的内容。
你可以看到,我们在 **添加用户** 模态弹出框中添加了一些额外的表单控件,并且每个控件的外观与 第一部分 和 第二部分 中相比有很大不同。让我们开始开发吧!
由于本文是 Angular2 in ASP.NET MVC & Web API - Part 2 文章的延续,让我们从 这里 下载附带的项目。
恢复 Angular 4 包
- 在 Visual Studio 2017 社区版中打开 `Angular2MVC_p2.sln`(最好),也建议重命名解决方案。
- 编辑 `package.json` 文件,并用以下包替换文件内容
{ "name": "angular-quickstart", "version": "1.0.0", "description": "QuickStart package.json from the documentation for Visual Studio 2017 & WebApi", "scripts": { "start": "tsc && concurrently \"tsc -w\" \"lite-server\" ", "lint": "tslint ./app/**/*.ts -t verbose", "lite": "lite-server", "pree2e": "webdriver-manager update", "test": "tsc && concurrently \"tsc -w\" \"karma start karma.conf.js\"", "test-once": "tsc && karma start karma.conf.js --single-run", "tsc": "tsc", "tsc:w": "tsc -w" }, "keywords": [], "author": "", "license": "MIT", "dependencies": { "@angular/common": "4.0.2", "@angular/compiler": "4.0.2", "@angular/core": "4.0.2", "@angular/forms": "4.0.2", "@angular/http": "4.0.2", "@angular/platform-browser": "4.0.2", "@angular/platform-browser-dynamic": "4.0.2", "@angular/router": "4.0.2", "angular-in-memory-web-api": "0.2.4", "systemjs": "0.19.40", "core-js": "2.4.1", "rxjs": "5.0.1", "zone.js": "0.7.4" }, "devDependencies": { "concurrently": "3.2.0", "lite-server": "2.2.2", "typescript": "2.0.10", "canonical-path": "0.0.2", "tslint": "3.15.1", "lodash": "4.16.4", "jasmine-core": "2.4.1", "karma": "1.3.0", "karma-chrome-launcher": "2.0.0", "karma-cli": "1.0.1", "karma-jasmine": "1.0.2", "karma-jasmine-html-reporter": "0.2.2", "protractor": "4.0.14", "rimraf": "2.5.4", "@types/node": "6.0.46", "@types/jasmine": "2.5.36", "@angular/material": "2.0.0-beta.6", "@angular/animations": "4.1.3" }, "repository": {} }
- 在 `dependencies` 部分,你可以看到我们将 Angular 和其他辅助包的版本升级到了 `4.0.2`。
- 在 `devDependencies` 部分,你可以看到我们正在导入 Angular Material 包。
- 右键单击 `package.json` 文件,选择 **恢复包** 选项,这将需要几分钟时间来下载所有包。等待直到收到包恢复完成的消息。
- 保存文件,然后单击菜单 **生成** -> **重新生成解决方案** 选项。这将花费几秒钟或一分钟时间来下载所有包(.NET 和 Angular)。Angular 4 包恢复就完成了。
升级数据库中的用户表
- 在之前的文章中,我们在 `User` 表中只有三个字段,现在让我们添加更多字段,以便更好地理解 Angular Material 组件。
- 转到 `App_Data` 文件夹,右键单击并选择 `打开` 或双击 `UserDB.mdf` 文件进行编辑。
- 在 **数据连接** -> **UserDBEntities** 层级中展开 `Tables`,右键单击 `TblUser` 并选择 **打开表定义** 选项。
- 你会看到只有四个字段(`Id`、`FirstName`、`LastName`、`Gender`),让我们根据以下屏幕截图手动更新表。
- 完成后,单击顶部的 **更新** 按钮。这将需要一些时间,然后你将看到以下屏幕,单击 **更新数据库** 按钮。
- 接下来,让我们更新 `User` 实体,转到 `DBContext` 文件夹,右键单击 `UserDBEntities.edmx` 并选择 **打开** 或双击进行编辑。
- 右键单击屏幕上的任意位置,然后选择 **从数据库更新模型...** 选项。
- 在 **更新向导** 屏幕上,转到 **刷新** 选项卡,选择 **表**,然后单击 **完成** 按钮。
- 片刻之后,你会看到 `TblUser` 已更新如下:
数据库更新已完成,让我们进入下一步。
Angular 4 & Angular Material 组件更新
让我们更新我们的用户管理应用程序,以使用 Angular Material 组件和 Angular 4 的一些功能。
- 编辑 `app -> app.module.ts` 文件,并为 Angular Material 添加以下 `import` 语句。同时在 `import` 部分添加 `BrowserAnimationsModule`、`MaterialModule`、`MdNativeDateModule` 模块。
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { MaterialModule, MdNativeDateModule } from '@angular/material';
- 接下来,在 `systemjs.config.js` 文件中添加 Material 和 Animation 的引用。
'@angular/animations': 'npm:@angular/animations/bundles/animations.umd.js', '@angular/animations/browser': 'npm:@angular/animations/bundles/animations-browser.umd.js', '@angular/platform-browser/animations': 'npm:@angular/platform-browser/bundles/platform-browser-animations.umd.js', '@angular/material': 'npm:@angular/material/bundles/material.umd.js',
- 现在我们已经在 `AppModule` 中添加了 Angular Material 模块,我们的应用程序就可以使用 Angular Material 组件了。首先,让我们修改 `home` 页面。编辑 `app -> Components -> home.component.ts` 文件。
- 与其使用普通的图像,不如使用 Angular Material 的 `Card` 组件。转到 Angular Material Card 页面,了解 Card 是什么,在哪里以及如何使用它?
- 在 Angular Material `Card` 页面以及其他任何组件页面,你都可以看到三个选项卡:**概览**、**API 参考** 和 **示例**,它们为我们提供了以下信息:
- **概览**:组件描述、架构及其用法。HTML、TS 和 CSS 代码以及工作 Plunker 链接。
- **API 参考**:如何导入组件、模块和指令信息。
- **示例**:包含该组件几乎所有功能的运行示例,以及包含 HTML、TS 和 CSS 代码的 Plunker 链接。
- 既然我们在 Card 页面,请单击 **示例** 选项卡,然后单击查看源代码 `< >` 链接。
- 你将进入以下视图,其中包含三个选项卡:**HTML**、**TS** 和 **CSS**。
- 我们将从这里复制 **HTML**、**TypeScript**(如果需要)和 **CSS** 到我们的应用程序中,所有我们需要它们的组件。当然,我们会根据我们的需求进行修改,但**不要**害怕复制,这真的可以帮助我们毫不费力地充分利用 Angular Material 组件。所以,复制整个 `md-card` HTML,并用 `home.component.ts` 模板的 HTML 替换它。
- 对于 Angular Material 组件,我们可能需要许多组件的 **CSS**,所以让我们创建一个样式表。右键单击 `Content` 文件夹,然后选择 `添加 -> 样式表`。
- 输入名称 `style.css`,然后单击 **确定** 按钮。
- 将 `style.css` 的内容替换为以下 **CSS**。
.example-card { width: 400px; } .example-header-image { background-image: url('../images/users.png'); background-size: cover; }
- 接下来,编辑 `App_Start -> BundleConfig.cs` 并按照以下方式更新 `Content/css` 包,以便在我们的应用程序中添加和使用新添加的 `style.css`。
bundles.Add(new StyleBundle("~/Content/css").Include( "~/Content/style.css", "~/Content/bootstrap.css"));
- 现在,我们的样式表已准备就绪,随着我们开发进程的推进,我们将继续为其他组件添加 CSS 类。
- 让我们回到 `home.component.ts` 并进行修改。更改 `md-card-title`,例如“`Users`”,`md-card-subtitle` 为“`Sample Image`”等。在 `md-card-image` 中更新图像 URL 为 `src="../../images/users.png"`。最终的 `home.component.ts` 模板应如下所示:
- 编译并运行项目,你的主页应如下所示:
- 太好了,我们成功使用了第一个 Angular Material 组件。
- 接下来,让我们转到 `user.component.ts` 并用 Angular Material 组件进行更新。为了简化操作并使用 Dialog 组件,我们需要将其拆分为两个组件。`UserComponent` 将只包含用户列表,而 `Add`、`Update` 和 `Delete` 功能将移至我们将在后续步骤中创建的 `ManageUser` 组件。
- 在之前的文章中,我们使用了 `ng2-bs3-modal` 模态弹出控件来处理添加、更新和删除屏幕,现在不再需要它了,因此请从 `app.module.ts`、`systemjs.config.js` 和 `user.component.ts` 文件中删除所有对其的引用。
- 接下来,让我们创建 `ManageUser` 组件,然后回到 `UserComponent` 进行清理。右键单击 `app -> Components`,然后选择 `添加 -> TypeScript 文件`。
- 输入名称 `manageuser.component.ts`,然后单击 **确定** 按钮。同时创建 `manageuser.component.html` 文件。
- 在前面的步骤中,我们用几个额外的列更新了 `User` 表,让我们相应地更新我们的 `user.ts`。编辑 `app-> Model -> user.ts` 并进行如下更新:
export interface IUser { Id: number, FirstName: string, LastName: string, Email:string, Gender: string, DOB: string, City: string, State: string, Zip: string, Country:string }
- 在 `manageuser.component.html` 中,我们将添加 Angular Material Dialog。我们将简单地访问 Dialog Plunker,并将 `dialog-result-example-dialog.html` 文件内容复制到 `manageuser.component.html`。从这个文件中,你可以看到 Dialog 的三个主要部分:`title`、`content` 和 `action` 按钮,这些都比较直观。其他 Angular Material 组件对应于 `user.ts` 模型字段,它们将放在 `md-dialog-content` div 内,如下所示:
- mdInput:用于 `FirstName`、`LastName`、`Email`、`City` 和 `Zip`。
- md-radio-button:用于 `Gender`(`Male`/Female)。
- md-datepicker:生日(`DOB`)的日期选择器控件。
- md-autocomplete:用于状态的自动完成下拉列表,只要我们开始输入就会过滤状态。(与我们通过管道开发的搜索用户功能相同)。
- md-select:国家的下拉列表。
- md-raised-button:取消和添加/更新/删除按钮。
- 你可以访问每个组件的 `Plunker` 链接来查看运行示例,我个人更喜欢 Plunker 来获取每个组件的代码。
- 将以下 HTML 复制到 `manageuser.component.html` 文件中。
<form novalidate (ngSubmit)="onSubmit(userFrm)" [formGroup]="userFrm"> <div> <h1 md-dialog-title><span> <md-icon>create</md-icon>{{modalTitle}}</span></h1> </div> <div style="padding-bottom:1px;background-color:#d8d8d8"></div> <div md-dialog-content class="md-dialog-container"> <div class="frm-ctrl"> <md-input-container> <input mdInput placeholder="First Name" formControlName="FirstName"> </md-input-container> <div *ngIf="formErrors.FirstName" class="text-danger"> {{ formErrors.FirstName }} </div> </div> <div class="frm-ctrl"> <md-input-container> <input mdInput placeholder="Last Name" formControlName="LastName"> </md-input-container> <div *ngIf="formErrors.LastName" class="text-danger"> {{ formErrors.LastName }} </div> </div> <div class="frm-ctrl"> <md-input-container> <input type="email" mdInput placeholder="Email" formControlName="Email"> </md-input-container> <div *ngIf="formErrors.Email" class="text-danger"> {{ formErrors.Email }} </div> </div> <div class="frm-ctrl"> <md-radio-group formControlName="Gender"> <md-radio-button *ngFor="let gndr of gender" [value]="gndr"> {{gndr}} </md-radio-button> </md-radio-group> <div *ngIf="formErrors.Gender" class="text-danger"> {{ formErrors.Gender }} </div> </div> <div class="frm-ctrl"> <md-input-container style="width:50%"> <input mdInput [mdDatepicker]="picker" placeholder="Date of Birth" formControlName="DOB"> <button mdSuffix [mdDatepickerToggle]="picker"></button> </md-input-container> <md-datepicker #picker></md-datepicker> <div *ngIf="formErrors.DOB" class="text-danger"> {{ formErrors.DOB }} </div> </div> <div class="frm-ctrl"> <div class="line_ctrl"> <md-input-container class="example-full-width"> <input mdInput placeholder="City" formControlName="City"> </md-input-container> <div *ngIf="formErrors.City" class="text-danger"> {{ formErrors.City }} </div> </div> <div class="line_ctrl"> <md-input-container> <input mdInput placeholder="State" [mdAutocomplete]="auto" formControlName="State"> </md-input-container> <md-autocomplete #auto="mdAutocomplete"> <md-option *ngFor="let state of filteredStates | async" [value]="state"> {{ state }} </md-option> </md-autocomplete> <div *ngIf="formErrors.State" class="text-danger"> {{ formErrors.State }} </div> </div> <div class="line_ctrl"> <md-input-container class="example-full-width"> <input mdInput #postalCode maxlength="5" placeholder="Zip" formControlName="Zip"> <md-hint align="end">{{postalCode.value.length}} / 5</md-hint> </md-input-container> <div *ngIf="formErrors.Zip" class="text-danger"> {{ formErrors.Zip }} </div> </div> </div> <div class="frm-ctrl"> <md-select placeholder="Country" style="width:50%" formControlName="Country"> <md-option *ngFor="let ctry of country" [value]="ctry.value"> {{ ctry.viewValue }} </md-option> </md-select> <div *ngIf="formErrors.Country" class="text-danger"> {{ formErrors.Country }} </div> </div> </div> <md-dialog-actions class="md-dialog-footer" align="end" > <button color="warn" type="button" md-raised-button (click)="dialogRef.close()">Cancel</button> <button type="submit" color="primary" [disabled]="userFrm.invalid" md-raised-button>{{modalBtnTitle}}</button> </ md-dialog-actions> </form>
- 这是我们在上一篇文章中使用的相同的响应式(模型驱动)表单,但增加了额外的字段以及 Angular Material 组件,我在上一步中已简要解释过。此表单将加载到 `Dialog` 框中,正如你所见,它位于 Dialog `content` 标签内。我们将在 `UserComponent` 中看到如何显示 Dialog 框。在 `style.css` 文件中添加以下 CSS。欢迎你根据自己的喜好更改 CSS。
.md-dialog-container { width: 550px; height: 480px; padding-bottom: 20px; padding-top: 20px; } .md-dialog-footer { padding: 20px; width: 100%; } md-input-container { width: 100%; } .frm-ctrl { padding-top: 20px; width: 100%; } .line_ctrl { float: left; width: 145px; text-align: right; margin: 2px; display: inline; }
- 接下来,将以下代码复制到 `manageuser.component.ts` 文件中,让我们来理解它。
import { Component, OnInit, ViewChild } from '@angular/core'; import { UserService } from '../Service/user.service'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ModalComponent } from 'ng2-bs3-modal/ng2-bs3-modal'; import { IUser } from '../Model/user'; import { DBOperation } from '../Shared/enum'; import { Observable } from 'rxjs/Rx'; import { Global } from '../Shared/global'; import { MdDialog, MdDialogRef } from '@angular/material'; import { FormControl } from '@angular/forms'; @Component({ templateUrl: 'app/Components/manageuser.component.html', }) export class ManageUser implements OnInit { msg: string; indLoading: boolean = false; userFrm: FormGroup; dbops: DBOperation; modalTitle: string; modalBtnTitle: string; listFilter: string; selectedOption: string; user: IUser; country = [ { value: 'USA', viewValue: 'USA' }, { value: 'Canada', viewValue: 'Canada' } ]; gender = [ 'Male', 'Female' ]; states = ['Alabama','Alaska','Arizona','Arkansas', 'California','Colorado','Connecticut','Delaware', 'Florida','Georgia','Hawaii', 'Idaho','Illinois','Indiana','Iowa', 'Kansas','Kentucky','Louisiana','Maine', 'Maryland','Massachusetts','Michigan','Minnesota', 'Mississippi','Missouri','Montana','Nebraska', 'Nevada','New Hampshire','New Jersey', 'New Mexico','New York','North Carolina', 'North Dakota','Ohio','Oklahoma','Oregon', 'Pennsylvania','Rhode Island','South Carolina', 'South Dakota','Tennessee','Texas', 'Utah','Vermont','Virginia','Washington', 'West Virginia','Wisconsin','Wyoming' ]; stateCtrl: FormControl; filteredStates: any; constructor(private fb: FormBuilder, private _userService: UserService, public dialogRef: MdDialogRef<ManageUser>) { } filterStates(val: string) { return val ? this.states.filter(s => new RegExp(`^${val}`, 'gi').test(s)) : this.states; } ngOnInit(): void { this.userFrm = this.fb.group({ Id: [''], FirstName: ['', [Validators.required, Validators.maxLength(50)]], LastName: ['', [Validators.required, Validators.maxLength(50)]], Email: ['', [Validators.required, Validators.email]], Gender: ['', Validators.required], DOB: ['', Validators.required], City: ['', Validators.required], State: ['', Validators.required], Zip: ['', Validators.required], Country: ['', Validators.required] }); this.filteredStates = this.userFrm.controls["State"]. valueChanges.startWith(null).map(name => this.filterStates(name)); this.userFrm.valueChanges.subscribe(data => this.onValueChanged(data)); this.onValueChanged(); if (this.dbops == DBOperation.create) this.userFrm.reset(); else this.userFrm.setValue(this.user); this.SetControlsState(this.dbops == DBOperation.delete ? false : true); } onValueChanged(data?: any) { if (!this.userFrm) { return; } const form = this.userFrm; for (const field in this.formErrors) { // clear previous error message (if any) this.formErrors[field] = ''; const control = form.get(field); if (control && control.dirty && !control.valid) { const messages = this.validationMessages[field]; for (const key in control.errors) { this.formErrors[field] += messages[key] + ' '; } } } } formErrors = { 'FirstName': '', 'LastName': '', 'Email': '', 'Gender': '', 'DOB': '', 'City': '', 'State': '', 'Zip': '', 'Country': '' }; validationMessages = { 'FirstName': { 'maxlength': 'First Name cannot be more than 50 characters long.', 'required': 'First Name is required.' }, 'LastName': { 'maxlength': 'Last Name cannot be more than 50 characters long.', 'required': 'Last Name is required.' }, 'Email': { 'email': 'Invalid email format.', 'required': 'Email is required.' }, 'Gender': { 'required': 'Gender is required.' } , 'DOB': { 'required': 'DOB is required.' } , 'City': { 'required': 'City is required.' } , 'State': { 'required': 'State is required.' } , 'Zip': { 'required': 'Zip is required.' } , 'Country': { 'required': 'Country is required.' } }; onSubmit(formData: any) { switch (this.dbops) { case DBOperation.create: this._userService.post(Global.BASE_USER_ENDPOINT, formData.value).subscribe( data => { if (data == 1) //Success { this.dialogRef.close("success"); } else { this.dialogRef.close("error"); } }, error => { this.dialogRef.close("error"); } ); break; case DBOperation.update: this._userService.put(Global.BASE_USER_ENDPOINT, formData._value.Id, formData._value).subscribe( data => { if (data == 1) //Success { this.dialogRef.close("success"); } else { this.dialogRef.close("error"); } }, error => { this.dialogRef.close("error"); } ); break; case DBOperation.delete: this._userService.delete(Global.BASE_USER_ENDPOINT, formData._value.Id).subscribe( data => { if (data == 1) //Success { this.dialogRef.close("success"); } else { this.dialogRef.close("error"); } }, error => { this.dialogRef.close("error"); } ); break; } } SetControlsState(isEnable: boolean) { isEnable ? this.userFrm.enable() : this.userFrm.disable(); } }
- 为了提高可读性,你可以收起上面的代码以显示滚动条。
- 大部分代码是从上一篇文章的 `user.component.ts` 中复制的,例如用户表单的创建、初始化和错误消息。唯一区别是增加了几个字段和验证规则,包括 **电子邮件验证器**,这是 **Angular 4** 的一项功能。有用于 `Country`、`Gender` 和 `States` 的本地变量,它们分别填充 `md-select`、`md-radio-button` 和 `md-autocomplete` 控件。关于 `States` `md-autocomplete` 控件的一个有趣之处在于它是如何工作的?如果你在 `ngOnInit()` 事件中看到,在创建和初始化 `userFrm` 响应式表单后,我们添加了以下一行:
this.filteredStates = this.userFrm.controls["State"]. valueChanges.startWith(null).map(name => this.filterStates(name));
- 在上面的行中,每次用户在 `States md-autocomplete` 组件中开始键入任何字符时,都会发生 `valueChanges` 事件,该事件调用 `filterStates` 方法,该方法将用户输入的字符串与状态数组进行比较,并在运行时返回匹配结果。`filterStates` 方法的主体如下:
filterStates(val: string) { return val ? this.states.filter (s => new RegExp(`^${val}`, 'gi').test(s)): this.states; }
- 其余代码已在之前的文章中解释过。
- 现在让我们转到 `user.component.ts` 文件,并将现有代码替换为以下内容:
import { Component, OnInit, ViewChild } from '@angular/core'; import { UserService } from '../Service/user.service'; import { IUser } from '../Model/user'; import { DBOperation } from '../Shared/enum'; import { Observable } from 'rxjs/Rx'; import { Global } from '../Shared/global'; import { ManageUser} from './manageuser.component'; import { MdDialog, MdDialogRef } from '@angular/material'; @Component({ templateUrl: 'app/Components/user.component.html' }) export class UserComponent implements OnInit { users: IUser[]; user: IUser; msg: string; dbops: DBOperation; modalTitle: string; modalBtnTitle: string; listFilter: string; searchTitle: string = "Search: "; selectedOption: string; constructor(private _userService: UserService, private dialog: MdDialog) { } openDialog() { let dialogRef = this.dialog.open(ManageUser); dialogRef.componentInstance.dbops = this.dbops; dialogRef.componentInstance.modalTitle = this.modalTitle; dialogRef.componentInstance.modalBtnTitle = this.modalBtnTitle; dialogRef.componentInstance.user = this.user; dialogRef.afterClosed().subscribe(result => { if (result == "success") { this.LoadUsers(); switch (this.dbops) { case DBOperation.create: this.msg = "Data successfully added."; break; case DBOperation.update: this.msg = "Data successfully updated."; break; case DBOperation.delete: this.msg = "Data successfully deleted."; break; } } else if (result == "error") this.msg = "There is some issue in saving records, please contact to system administrator!" else this.msg = result; }); } ngOnInit(): void { this.LoadUsers(); } LoadUsers(): void { this._userService.get(Global.BASE_USER_ENDPOINT) .subscribe(users => { this.users = users; } //,error => this.msg = <any>error ); } addUser() { this.dbops = DBOperation.create; this.modalTitle = "Add New User"; this.modalBtnTitle = "Add"; this.openDialog(); } editUser(id: number) { this.dbops = DBOperation.update; this.modalTitle = "Edit User"; this.modalBtnTitle = "Update"; this.user = this.users.filter(x => x.Id == id)[0]; this.openDialog(); } deleteUser(id: number) { this.dbops = DBOperation.delete; this.modalTitle = "Confirm to Delete?"; this.modalBtnTitle = "Delete"; this.user = this.users.filter(x => x.Id == id)[0]; this.openDialog(); } criteriaChange(value: string): void { if (value != '[object Event]') this.listFilter = value; } }
- 由于大量代码已移至 `ManageUser` 组件,因此 `UserComponent` 现在变得非常精简。一个新方法是 `openDialog`,它打开 Dialog 框并发送参数。让我们一步一步地理解它:
- ``let dialogRef = this.dialog.open(ManageUser)``:`dialog.open` 将我们刚刚在前面步骤中创建的 `ManageUser` 组件作为参数。
- ``dialogRef.componentInstance.dbops = this.dbops;`` 用于将参数发送到 Dialog 组件(`ManageUser` 组件)。我们正在发送 `dpops`(我们要执行的操作类型,Add/Update/Delete 以相应地更改视图)。`modalTitle` 和 `modalBtnTitle` 是每个 DB 操作视图的标签。`user` 参数是我们为编辑和删除视图发送的单个用户记录。在参数语句之后,我们订阅 Dialog 的 `afterClosed` 事件,以检查从 `ManageUser` 组件发送的结果。检查 `ManageUser` 组件的 `onSubmit` 函数,我们明确发送了“success”和“error”字符串。根据相应 DB 操作的成功和失败,我们在用户列表下方显示消息。
- 在 `addUser`、`editUser` 和 `deleteUser` 方法中,我们只设置 `dbops`、`modalTitle` 和 `modalBtnTitle` 变量的值,然后调用 `openDialog` 函数,如前所述,这些变量被发送到 `ManageUser` 组件。其余代码不言自明。
- 接下来,编辑 `user.component.html` 并将代码替换为以下内容:
<div class='panel panel-primary'> <div class='panel-heading'> User Management </div> <div class='panel-body'> <div> <search-list [title]='searchTitle' (change)="criteriaChange($event)"></search-list> </div> <div class='table-responsive'> <div style="padding-bottom:10px"> <button class="btn btn-primary" (click)="addUser()">Add</button></div> <div *ngIf='users && users.length==0' class="alert alert-info" role="alert">No record found!</div> <table class='table table-striped' *ngIf='users; else loadingScreen;'> <thead> <tr> <th>First Name</th> <th>Last Name</th> <th>Gender</th> <th></th> </tr> </thead> <tbody> <tr *ngFor="let user of users | userFilter:listFilter"> <td>{{user.FirstName}}</td> <td>{{user.LastName}}</td> <td>{{user.Gender}}</td> <td> <button title="Edit" class="btn btn-primary" (click)="editUser(user.Id)">Edit</button> <button title="Delete" class="btn btn-danger" (click)="deleteUser(user.Id)">Delete</button> </td> </tr> </tbody> </table> <ng-template #loadingScreen><div class="alert alert-info" role="alert"><md-progress-spinner mode="indeterminate" style="width:50px; height:50px"></md-progress-spinner>loading...</div></ng-template> </div> <div *ngIf="msg" role="alert" class="alert alert-info alert-dismissible"> <button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">×</span></button> <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> <span class="sr-only">Error:</span> {{msg}} </div> </div> </div>
- 由于其功能已移至 `ManageUser` 组件(我们正在使用 Angular Material 的 Dialog 组件),因此我们从 HTML 源中删除了 `ng2-bs3-modal`。这里,我们使用了另一个 Angular 4 功能,ngIf – else。找到 `<table class='table table-striped' *ngIf='users; else loadingScreen;'>` 语句。`*ngIf='users; else loadingScreen;'` 表示在加载用户列表之前,显示 `loadingScreen` 模板。`loadingScreen` 模板在后续语句中如下所示:
<ng-template #loadingScreen><div class="alert alert-info" role="alert"><md-progress-spinner mode="indeterminate" style="width:50px; height:50px"></md-progress-spinner>loading...</div></ng-template>
- md-progress-spinner 是 Angular Material 的 spinner,我用它替换了加载图像。`ngIf-else` 是一个很棒的功能,有助于优化代码。因此,我删除了不再需要的 `isLoading` 变量。
- 现在 `ManageUser` 组件已完成,让我们将其添加到 `AppModule.ts` 中。编辑 `app-> app.module.ts`,导入 `ManageUser` 组件,并将其添加到 `declarations` 部分。
import { ManageUser } from './components/manageuser.component'; //Declaration Statement declarations: [AppComponent, UserComponent, HomeComponent, UserFilterPipe, SearchComponent, ManageUser],
- 还有一步是将 `ManageUser` 添加到 `entryComponents` 部分,如下所示:
entryComponents: [ManageUser]
- Angular Material 还提供了 `预构建主题`,浏览 `node_modules\@angular\material\prebuilt-themes` 文件夹中的 `themes` 文件夹。你可以使用任何主题,我将使用 `indigo-pink.css`。将主题引用添加到 `Views -> Shared -> _Layout.cshtml` 页面。
<link href="/node_modules/%40angular/material/prebuilt-themes/indigo-pink.css" rel="stylesheet" /> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
- 在第二条语句中,我从 Google CDN 导入 Material 图标,我们将在 `AppComponent` 中重新设计菜单时使用它们。Spinner、按钮和其他组件的颜色方案、可见性和布局取决于你选择的主题。删除主题并运行应用程序,你会看到应用程序的 UI 会被弄乱。
- 重新生成,清除浏览器缓存(浏览器历史记录),然后运行应用程序。通过添加、更新和删除用户来测试应用程序。
- 在最后一步,我想使用 Angular Material Menu 组件。编辑 `app -> app.component.ts` 并将其内容替换为以下内容:
import { Component } from "@angular/core" import { Router, // import as RouterEvent to avoid confusion with the DOM Event Event as RouterEvent, NavigationStart, NavigationEnd, NavigationCancel, NavigationError } from '@angular/router'; @Component({ selector: "user-app", template: ` <div> <nav class='navbar navbar-default'> <div class='container-fluid'> <button md-icon-button [mdMenuTriggerFor]="menu"> <md-icon>more_vert</md-icon> Menu </button> <md-menu #menu="mdMenu"> <button md-menu-item [routerLink]="['home']"> <md-icon>home</md-icon> <span>Home</span> </button> <button md-menu-item [routerLink]="['user']"> <md-icon>group</md-icon> <span>Users Management</span> </button> </md-menu> </div> </nav> <div class='container'> <router-outlet><div class="loading-overlay" *ngIf="loading"> <!-- show something fancy here, here with Angular 2 Material's loading bar or circle --> <md-progress-bar mode="indeterminate"></md-progress-bar> </div></router-outlet> </div> </div> ` }) export class AppComponent { loading: boolean = true; constructor(private router: Router) { router.events.subscribe((event: RouterEvent) => { this.navigationInterceptor(event); }); } // Shows and hides the loading spinner during RouterEvent changes navigationInterceptor(event: RouterEvent): void { if (event instanceof NavigationStart) { this.loading = true; } if (event instanceof NavigationEnd) { setTimeout(() => { this.loading = false; }, 1000) // this.loading = false; } // Set loading state to false in both of the // below events to hide the spinner in case a request fails if (event instanceof NavigationCancel) { this.loading = false; } if (event instanceof NavigationError) { this.loading = false; } } }
- 在模板的 HTML 中,我们使用了 `md-menu`,它有 `md-menu-item` 按钮,带有指向相应视图(`home` 和 `user`)的 `routerLink`。另一件事是 `md-icon`,它会在菜单项之前显示,我们已经在 `_Layout.cshtml` 页面中添加了图标引用。要获取完整的图标列表,请单击 这里。
- 我们在 `AppComponent` 中更新的另一件事是视图之间的加载进度条。这可以通过拦截路由更改并基于 `loading` 变量的布尔值显示 `md-progress-bar` 来实现。我特意添加了 `setTimeout(() => { this.loading = false; }, 1000)` 延迟,以便在切换视图时向你显示 spinner。
- 重新生成,清除浏览器缓存,然后运行应用程序,你应该会看到以下屏幕:
- 你可以点击顶部菜单的 **User Management** 按钮,在顶部菜单下方看到蓝色进度条。
关注点
我喜欢 Angular Material 组件,因为它节省了我使用许多第三方组件的时间和精力,所有组件都兼容模板驱动和模型驱动(响应式表单)。
Angular 4 还有一些更有用的功能,其中两个(`ngIf-else` 和 `Email Validator`)已在本篇文章中使用。其余部分可以根据适用情况使用。
你可以改进的地方
有一些功能可以用来改进这个应用程序。例如,出生日期不能是未来的日期,尝试添加 DOB 的验证规则,在用户列表中显示比名字、姓氏和性别更多的字段,直到用户列表完全加载后才显示搜索过滤器,并为成功的添加/更新/删除消息添加 snack-bar 组件,而不是当前的 Bootstrap 消息。试试看!
历史
- 2017 年 6 月 12 日:文章创建