使用 ASP.NET Core WebAPI 构建 Angular 2 自定义控件






4.83/5 (3投票s)
Angular 2 TypeScript 自定义控件与 ASP.NET Core Web API 集成
引言
此库包含八个 Angular2 自定义控件。每个控件都有自己的标签和验证。Grid 和 dropdown 使用 API 名称动态获取数据。
textBox
textbox-multiline
date-picker
dropdown
grid
checkbox
radio
radio list
此外,该库还包含基类和通用 http 服务
base-control-component
base-form-component
http-common-service
必备组件
- Visual Studio Community 2015 Update 3 – 免费 https://download.microsoft.com/download/b/e/d/bedddfc4-55f4-4748-90a8-ffe38a40e89f/vs2015.3.com_enu.iso
- .NET Core 1.1 SDK - 安装程序 (dotnet-dev-win-x64.1.0.0-preview2-1-003177.exe)
https://www.microsoft.com/net/download/core#/current - Visual Studio 2015 的 Typescript (TypeScript_Dev14Full_2.1.4.0.exe)
https://www.microsoft.com/en-us/download/details.aspx?id=48593 - Node js 6.9
https://node.org.cn/dist/v6.9.4/node-v6.9.4-x64.msi - 在项目中的 client 文件夹中安装 node_modules
打开 cmd.exe,然后打开 client 文件夹
D:\> cd D:\Angular2CodeProject\src\Angular2CodeProject\client<br /> D:\Angular2CodeProject\src\Angular2CodeProject\client> npm install
- 启动页面
https://:56245/client/index.html
Using the Code
使用自定义表单控件的示例
为 student
表单创建新组件 client\app\components\student\student-form.component.ts
,使其继承自 baseFormComponent
以便使用其中的 save 和 load 方法,然后将 ASP.NET Core API 名称传递给基表单构造函数,以便在 get 和 post 数据时使用 API 名称。在此示例中,我们传递 "Students
" 作为 API 名称。
import { Component, OnDestroy,OnInit } from '@angular/core';
import { FlashMessagesService } from 'angular2-flash-messages';
import { HttpCommonService } from '../../shared/services/http-common.service';
import { BaseFormComponent } from '../../shared/controls/base/base-form.component';
import { ActivatedRoute } from '@angular/router';
@Component({
moduleId: module.id,
selector: 'student-form',
templateUrl: 'student-form.template.html',
providers: [HttpCommonService]
})
export class StudentFormComponent extends BaseFormComponent{
//public strDate:string = "2010-10-25";
constructor( _httpCommonService: HttpCommonService,
flashMessagesService: FlashMessagesService,
route: ActivatedRoute) {
super(_httpCommonService, flashMessagesService, route, "Students");
}
}
将自定义控件添加到表单模板 client\app\components\student\student-form.template.html。在此模板中,我们添加 form
标签,然后在 ngSubmit
事件上调用基表单组件的 save
方法,并设置表单别名 #from="ngForm.
。对于每个自定义表单输入控件,我们使用双向绑定设置其 id
、label
值、required
和 ngModelName
。对于 radio list 和 dropdown 控件,我们传递额外的属性 apiName
来填充列表,valueFieldName
和 textFieldName
来设置列表元素的文本和值字段。将提交按钮的 disable
属性设置为 [disabled]="!from.form.valid"
。
<div class="container">
<div>
<!--[hidden]="submitted"-->
<h1>Student</h1>
<form (ngSubmit)="save()" #from="ngForm">
<textbox id="txtFirstMidName" name="txtFirstMidName"
label="First-Mid Name" [(ngModel)]="model.firstMidName"
required="true"></textbox>
<textbox id="txtLastName" name="txtLastName"
label="Last Name" [(ngModel)]="model.lastName"
required="true"></textbox>
<date-picker name="dpEnrollmentDate" id="dpEnrollmentDate"
label="EnrollmentDate" [(ngModel)]="model.enrollmentDate"
required="true"></date-picker>
<dropdown name="ddlCourse" id="ddlCourse"
label="Course" [(ngModel)]="model.course1ID"
apiName="Courses"
valueFieldName="courseID" textFieldName="title"
required="true"></dropdown>
<textbox-multiline id="txtStudentDescription"
name="txtStudentDescription"
label="Description" [(ngModel)]="model.description"
required="true"></textbox-multiline>
<radio-list name="elStudentType"
id="studentType" [(ngModel)]="model.course2ID"
valueFieldName="courseID" textFieldName="title"
apiName="Courses" required="true"></radio-list>
<radio id="rbMale" label="Male"
name="rbgender" [(ngModel)]="model.gender"
checked="true" val="1"></radio>
<radio id="rbFemale" label="Female"
name="rbgender" [(ngModel)]="model.gender" val="0"></radio>
<checkbox id="chkHasCar" label="HasCar"
name="chkHasCar" [(ngModel)]="model.hasCar"></checkbox>
<button type="submit" class="btn btn-default"
[disabled]="!from.form.valid">Save</button>
<button type="button" class="btn btn-default"
[disabled]="model.id>0"
(click)="from.reset()">New</button>
</form>
</div>
</div>
<button type="submit" class="btn btn-default" [disabled]="!from.form.valid">Save</button>
当控件为空且必填时,保存按钮将被禁用,控件上会出现红色标记。
注意:文本框中的 textType
属性可以是 number、email、url、tel。
当控件不为空时,保存按钮将被启用,控件上会出现绿色标记。
所有这些控件都具有通用属性,这些属性包含在 client\app\shared\controls\base\base-control.component.ts 中。
label
名称
id
required
hidden
textType
minLength
maxLength
使用自定义 Grid 的示例
为学生列表 client\app\components\student\student-list.component.ts 创建新组件,然后添加包含显示列的 grid 列数组。每列都有 name
、modelName
和 label
属性。Grid 组件包含排序、分页和过滤功能。
import { Component, Input } from '@angular/core';
import { GridColumnModel } from '../../shared/controls/grid/grid-column.model';
@Component({
moduleId: module.id,
selector: 'student-list',
templateUrl: 'student-list.template.html'
})
export class StudentListComponent {
@Input() columns: Array<GridColumnModel>;
constructor() {
this.columns = [
new GridColumnModel({ name: 'LastName',
modelName: 'lastName', label: 'Last Name' }),
new GridColumnModel({ name: 'FirstMidName',
modelName: 'firstMidName', label: 'FirstMid Name' }),
new GridColumnModel({ name: 'EnrollmentDate',
modelName: 'enrollmentDate', label: 'Enrollment Date' }),
]
}
}
将自定义 Grid 添加到列表模板 client\app\components\student\student-list.template.html,然后设置 apiname
和 column
属性(它将从 StudentListComponent
组件获取)。Grid 控件具有排序、分页和过滤功能。
<grid [columns]="columns" apiName="Students" label="Student" name="student"></grid>
控件源代码
BaseControlComponent
所有自定义控件都继承自 BaseControlComponent
,以获取通用属性,如 label
、name
、id
、required
、hidden
、texttype
,并用于通过 http://blog.rangle.io/angular-2-ngmodel-and-custom-form-components/ 中的步骤修复嵌套控件 ngmodel
绑定问题。
它还包含用于电子邮件和 URL 或任何其他自定义验证的正则表达式的 patterns 对象,通过为每个 textType
(email
、url
、tel
)添加正则表达式,这些表达式将在子控件的 HTML 模板中使用。
<input type="{{textType}}" pattern="{{patterns[textType]}}">
import { Component, Optional,Inject,OnInit, Output, Input,
AfterViewInit, AfterViewChecked, EventEmitter } from '@angular/core';
import { NgModel } from '@angular/forms';
import { Observable } from 'rxjs/Observable';
import { ValueAccessorBase } from './value-accessor';
import {
AsyncValidatorArray,
ValidatorArray,
ValidationResult,
message,
validate,
} from './validate';
@Component({
})
export abstract class BaseControlComponent<T> extends ValueAccessorBase<T> implements OnInit{
protected abstract model: NgModel;
@Input() label: string;
@Input() name: string;
@Input() id: string;
@Input() required: boolean = false;
@Input() hidden: boolean = false;
@Input() textType: string;
@Input() minLength: string;
@Input() maxLength: string;
public patterns = {
email: "([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+",
url: "(https?)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]"
};
ngOnInit() {
}
constructor(
private validators: ValidatorArray,
private asyncValidators: AsyncValidatorArray,
) {
super();
}
protected validate(): Observable<ValidationResult> {
return validate
(this.validators, this.asyncValidators)
(this.model.control);
}
protected get invalid(): Observable<boolean> {
return this.validate().map(v => Object.keys(v || {}).length > 0);
}
protected get failures(): Observable<Array<string>> {
return this.validate().map(v => Object.keys(v).map(k => message(v, k)));
}
}
base-form.component
所有输入表单都应继承自 BaseFormComponent
,以获取所有 CRUD 操作,例如保存(创建或更新)、删除、重置表单数据以进入新模式以及在路由具有 id 参数时加载模型以进行编辑。
import { Component, OnDestroy, OnInit ,Input} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { HttpCommonService } from '../../services/http-common.service';
import { FlashMessagesService } from 'angular2-flash-messages';
@Component({
moduleId: module.id,
providers: [HttpCommonService]
})
export class BaseFormComponent {
public apiName:string
protected model = {};
protected submitted = false;
private sub: any;
id: number;
// onSubmit() { this.submitted = true; }
// TODO: Remove this when we're done
// get diagnostic() { return JSON.stringify(this.model); }
constructor(private _httpCommonService: HttpCommonService,
private flashMessagesService: FlashMessagesService,
private route: ActivatedRoute, _apiName: string) {
this.apiName = _apiName;
this.sub = this.route.params.subscribe(params => {
this.id = +params['id']; // (+) converts string 'id' to a number
if (this.id > 0) {
this._httpCommonService.getItem(this.apiName, this.id).subscribe(data => {
this.model = data
this.model["enrollmentDate"] = this.model["enrollmentDate"].substring(0, 10);
});
}
});
}
ngOnInit() {
//this.sub = this.route.params.subscribe(params => {
// this.id = +params['id']; // (+) converts string 'id' to a number
//this._httpCommonService.getItem("Accounts", this.id).subscribe(data => {
// this.model = data
//});
// });
}
ngOnDestroy() {
this.sub.unsubscribe();
}
reset() {
this.id = 0;
this.model = {};
}
save() {
alert(JSON.stringify(this.model));
if (this.id > 0) {
this._httpCommonService.update(this.apiName, this.model).subscribe();
}
else {
this._httpCommonService.create(this.apiName, this.model).subscribe();
}
this.flashMessagesService.show('success', { cssClass: 'alert-success' });//{ cssClass: 'alert-success', timeout: 1000 }
//this.flashMessagesService.grayOut(true);
this.submitted = true;
}
delete () {
this._httpCommonService.delete("Accounts", this.model["id"]);
}
}
http-common.service
它用于集中所有 http 方法,并作为任何请求的入口点。它包含 create
、update
、delete
、getlist
和 getItem
方法。我们必须设置 apiBaseUrl
属性才能将所有这些方法与此服务一起使用。
import { Injectable } from "@angular/core";
import { Http, Response, ResponseContentType, Headers, RequestOptions,
RequestOptionsArgs, Request, RequestMethod, URLSearchParams } from "@angular/http";
//import { Observable } from 'rxjs/Observable';
//import { Observable } from "rxjs/Rx";
import { Observable } from 'rxjs/Rx'
@Injectable()
export class HttpCommonService {
public apiBaseUrl: string;
constructor(public http: Http) {
this.http = http;
this.apiBaseUrl = "/api/";
}
PostRequest(apiName: string, model: any) {
let headers = new Headers();
headers.append("Content-Type", 'application/json');
let requestOptions = new RequestOptions({
method: RequestMethod.Post,
url: this.apiBaseUrl + apiName,
headers: headers,
body: JSON.stringify(model)
})
return this.http.request(new Request( requestOptions))
.map((res: Response) => {
if (res) {
return [{ status: res.status, json: res.json() }]
}
});
}
requestOptions()
{
let contentType = 'application/json';//"x-www-form-urlencoded";
let headers = new Headers({ 'Content-Type': contentType});
let options = new RequestOptions({
headers: headers,
//body: body,
// url: this.apiBaseUrl + apiName,
// method: requestMethod,
//responseType: ResponseContentType.Json
});
return options;
}
stringifyModel(model: any)
{
return JSON.stringify(model);
}
create(apiName: string, model: any) {
// let headers = new Headers({ 'Content-Type': 'application/json' });
// let options = new RequestOptions({ headers: headers });
// let body = JSON.stringify(model);
return this.http.post(this.apiBaseUrl + apiName,
this.stringifyModel(model),
this.requestOptions())
.map(this.extractData) //.map((res: Response) => res.json())
.catch(this.handleError)
// .subscribe()
;
}
update(apiName:string,model: any) {
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
let body = JSON.stringify(model);
return this.http.put(this.apiBaseUrl + apiName + '/' +
model.id, body, options).map((res: Response) => res.json());//.subscribe();
}
delete(apiName:string,id:any) {
return this.http.delete(this.apiBaseUrl + apiName + '/' + id);//.subscribe();;
}
getList(apiName: string) {
return this.http.get(this.apiBaseUrl + apiName, { search: null })
.map((responseData) => responseData.json());
}
getItem(apiName: string,id:number) {
return this.http.get(this.apiBaseUrl + apiName + "/" + id, { search: null })
.map((responseData) => responseData.json());
}
getLookup(lookupName: string, parentId: number, parentName: string) {
var params = null;
if (parentId != null) {
params = new URLSearchParams();
params.set(parentName, parentId.toString());
}
return this.http.get(this.apiBaseUrl +"lookup/" + lookupName, { search: params })
.map((responseData) => responseData.json());
}
private extractData(res: Response) {
let body = res.json();
return body || {};
}
private handleError(error: Response | any) {
// In a real world app, we might use a remote logging infrastructure
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
//console.error(errMsg);
return Observable.throw(errMsg);
}
}
为新组件(学生表单、学生列表)添加路由配置
页面路由应在 app.routes
中添加,用于添加、编辑和列表。在编辑时,我们将 id 放在 url 中。
client\app\app.routes.ts
import { ModuleWithProviders } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { StudentFormComponent } from './components/student/student-form.component';
import { StudentListComponent } from './components/student/student-list.component';
// Route Configuration
export const routes: Routes = [
{ path: 'student', component: StudentFormComponent },
{ path: 'student/:id', component: StudentFormComponent },
{ path: 'students', component: StudentListComponent},
];
export const routing: ModuleWithProviders = RouterModule.forRoot(routes);
为 App 组件模板中的新组件(学生表单、学生列表)添加页面链接
使用 [routerLink]
将新页面链接添加到 app 组件 client\app\app.component.template.html
<div id="wrapper">
<!-- Sidebar -->
<div id="sidebar-wrapper">
<nav class="mdl-navigation">
<ul class="sidebar-nav">
<li class="sidebar-brand">
<!--<a href="#">-->
Accounting System
<!--</a>-->
</li>
<li>
<a class="mdl-navigation__link"
[routerLink]="['/']">Dashboard</a>
</li>
<li>
<a class="mdl-navigation__link"
[routerLink]="['/student']">Add Student</a>
</li>
<li>
<a class="mdl-navigation__link"
[routerLink]="['/students']">List Students</a>
</li>
</ul>
</nav>
</div>
<!-- /#sidebar-wrapper -->
<!-- Page Content -->
<div id="page-content-wrapper">
<div class="container-fluid">
<div class="row">
<div class="col-lg-12">
<router-outlet></router-outlet>
</div>
</div>
</div>
</div>
<!-- /#page-content-wrapper -->
</div>
注意:编辑链接将显示在 Grid 控件的列表表单中。
<td><a class="mdl-navigation__link"
[routerLink]="['/'+name+'',item.id]">Edit</a></td>
<td><a class="mdl-navigation__link"
[routerLink]="['/'+name+'Details',item.id]">Details</a></td>
将控件和表单组件添加到主模块client\app\app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule, NG_VALIDATORS,
NG_ASYNC_VALIDATORS, FormControl } from '@angular/forms';
import { HttpModule, JsonpModule } from '@angular/http';
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { requestOptionsProvider } from './shared/services/default-request-options.service';
import { FlashMessagesModule } from 'angular2-flash-messages';
import { DataTableModule } from "angular2-datatable";
import { routing } from './app.routes';
//validation-test
import { DropdownComponent } from './shared/controls/dropdown/dropdown.component';
import { RadioListComponent } from './shared/controls/radio/radio-list.component';
import { TextboxComponent } from './shared/controls/textbox/textbox.component';
import { TextboxMultilineComponent } from './shared/controls/textbox/textbox-multiline.component';
import { DatePickerComponent } from './shared/controls/date/date-picker.component';
import { CheckboxComponent } from './shared/controls/checkbox/checkbox.component';
import { RadioComponent } from './shared/controls/radio/radio.component';
import { GridComponent } from './shared/controls/grid/grid.component';
import { ValidationComponent } from './shared/controls/validators/validation';
import { StudentFormComponent } from './components/student/student-form.component';
import { StudentListComponent } from './components/student/student-list.component';
import { AppComponent } from './app.component';
@NgModule({
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule,
HttpModule,
JsonpModule,
routing,
FlashMessagesModule,
DataTableModule,
],
declarations: [
AppComponent,
TextboxComponent,
TextboxMultilineComponent,
DatePickerComponent,
CheckboxComponent,
DropdownComponent,
RadioListComponent,
RadioComponent,
GridComponent,
ValidationComponent,
StudentFormComponent,
StudentListComponent,
],
providers: [requestOptionsProvider],
bootstrap: [AppComponent]
})
export class AppModule { }
Textbox 控件
TextboxComponent
覆盖 NgModel
,将验证从自定义控件传递到原始输入。
import { Component, ViewChild, Optional, Inject} from '@angular/core';
import { BaseControlComponent } from '../base/base-control.component'
import { NG_VALUE_ACCESSOR, NgModel, NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '@angular/forms';
import { animations } from '../validators/animations';
@Component({
moduleId: module.id,
selector: 'textbox',
templateUrl: 'textbox.template.html'
, animations
, providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: TextboxComponent, multi: true}
]
})
export class TextboxComponent extends BaseControlComponent<string> {
@ViewChild(NgModel) model: NgModel;
constructor(
@Optional() @Inject(NG_VALIDATORS) validators: Array<any>,
@Optional() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<any>,
) {
super(validators, asyncValidators);
}
}
textbox.template.html 包含 label
、input
以及 required
、maxlength
、minlength
和 pattern
验证。
<div class="form-group">
<label for="{{name}}">{{label}}</label>
<input type="{{textType}}" class="form-control" id="{{name}}"
required="{{required}}"
[(ngModel)]="value" name="{{name}}"
#txt="ngModel"
pattern="{{patterns[textType]}}"
maxlength="{{maxLength}}"
minlength="{{minLength}}"
hidden="{{hidden}}">
<div *ngIf="txt.errors && (txt.dirty || txt.touched)"
class="alert alert-danger">
<div [hidden]="(!txt.errors.required)">
{{label}} is required
</div>
<div [hidden]="!txt.errors.minlength">
{{label}} must be at least 4 characters long.
</div>
<div [hidden]="!txt.errors.maxlength">
{{label}} cannot be more than 24 characters long.
</div>
</div>
<validation [@flyInOut]="'in,out'"
*ngIf="invalid | async"
[messages]="failures | async">
</validation>
</div>
Textbox Multiline 控件
TextboxMultilineComponent
覆盖 NgModel
,将验证从自定义控件传递到原始输入。
import { Component, ViewChild, Optional, Inject} from '@angular/core';
import { NgModel, NG_VALUE_ACCESSOR, NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '@angular/forms';
import { BaseControlComponent } from '../base/base-control.component'
@Component({
moduleId: module.id,
selector: 'textbox-multiline',
templateUrl: 'textbox-multiline.template.html', providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: TextboxMultilineComponent, multi: true }
]
})
export class TextboxMultilineComponent extends BaseControlComponent<string> {
@ViewChild(NgModel) model: NgModel;
constructor(
@Optional() @Inject(NG_VALIDATORS) validators: Array<any>,
@Optional() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<any>,
) {
super(validators, asyncValidators);
}
}
textbox-multiline.template.html 包含 label
、textarea
以支持多行文本,以及 required
、maxlength
、minlength
和 pattern
验证。
div class="form-group"><!--#txt="ngModel" (blur)="setValid(txt)"-->
<label for="{{name}}">{{label}}</label>
<textarea class="form-control" id="{{name}}"
required="{{required}}"
[(ngModel)]="value" name="{{name}}"
pattern="{{patterns[textType]}}"
#txt="ngModel"
maxlength="{{maxLength}}"
minlength="{{minLength}}"
></textarea>
<div *ngIf="txt.errors && (txt.dirty || txt.touched)"
class="alert alert-danger">
<div [hidden]="(!txt.errors.required)">
{{label}} is required
</div>
<div [hidden]="!txt.errors.minlength">
{{label}} must be at least {{minlength}} characters long.
</div>
<div [hidden]="!txt.errors.maxlength">
{{label}} cannot be more than {{maxlength}} characters long.
</div>
</div>
</div>
Drop Down 控件
DropdownComponent
覆盖 NgModel
,将验证从自定义控件传递到原始输入。它具有用于 webapi
服务的 apiName
,该服务将用于加载 select 选项,以及用于选项值的字段名和用于选项文本的字段名。在组件 init
时,它会调用 http common service 并填充 items 数组,该数组将用于模板中加载 select
选项。
import { Component, OnInit, Inject, Output, Optional,Input,
AfterViewInit, AfterViewChecked, EventEmitter, ViewChild} from '@angular/core';
import { HttpCommonService } from '../../services/http-common.service';
import { BaseControlComponent } from '../base/base-control.component'
import { DropdownModel } from './dropdown.model';
import { NgModel, NG_VALUE_ACCESSOR, NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '@angular/forms';
import { animations } from '../validators/animations';
@Component({
moduleId: module.id,
selector: 'dropdown',
templateUrl: 'dropdown.template.html', animations,
providers: [ {
provide: NG_VALUE_ACCESSOR,
useExisting: DropdownComponent,
multi: true
},HttpCommonService]
})
export class DropdownComponent extends BaseControlComponent<string> {
@ViewChild(NgModel) model: NgModel;
items: DropdownModel[];
@Input() apiName: string;
@Input() valueFieldName: string;
@Input() textFieldName: string;
@Input() parentName: string;
@Input() parentId: string;
// constructor(private _httpCommonService: HttpCommonService) { super(); }
ngOnInit() {
super.ngOnInit();
if (this.apiName !=null){
// this.items = [new DropdownModel(1, "a"),
// new DropdownModel(2, "b"), new DropdownModel(3, "c")]
this._httpCommonService.getList(this.apiName).subscribe(data => {
this.items = data
});
}
}
constructor(private _httpCommonService: HttpCommonService,
@Optional() @Inject(NG_VALIDATORS) validators: Array<any>,
@Optional() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<any>,
) {
super(validators, asyncValidators);
}
}
dropdown.template.html 包含 label
、select
、required
,以及用于从 items 数组填充 select 选项并使用 valueFieldName
和 textFieldName
绑定值和文本的逻辑。
<div class="form-group">
<label for="{{name}}">{{label}} </label>
<select class="form-control" id="{{ name}}"
name="{{ name}}"
[(ngModel)]="value"
hidden="{{hidden}}"
#ddl="ngModel"
required="{{required}}">
<option value="">--Select--</option>
<ng-content></ng-content>
<option *ngFor="let item of items"
[value]="item[valueFieldName]">{{item[textFieldName]}}</option>
</select>
<div [hidden]="(ddl.valid || ddl.pristine)" class="alert alert-danger">
{{name}} is required
</div>
<validation [@flyInOut]="'in,out'"
*ngIf="invalid | async"
[messages]="failures | async">
</validation>
</div>
Radio List
RadioListComponent
覆盖 NgModel
,将验证从自定义控件传递到原始输入。它具有用于 webapi
服务的 apiName
,该服务将用于加载单选列表,以及用于其值的字段名和用于其文本的字段名。在组件 init 时,它会调用 http common service 并填充 items 数组,该数组将用于模板中加载单选列表。
import { Component, OnInit, Optional, Input, ViewChild, Inject} from '@angular/core';
import { HttpCommonService } from '../../services/http-common.service';
import { RadioListModel } from './radio-list.model';
import { BaseControlComponent } from '../base/base-control.component'
import { NgModel, NG_VALUE_ACCESSOR, NG_VALIDATORS, NG_ASYNC_VALIDATORS } from '@angular/forms';
@Component({
moduleId :module.id ,
selector: 'radio-list',
templateUrl: 'radio-list.template.html',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: RadioListComponent,
multi: true
},HttpCommonService]
})
export class RadioListComponent extends BaseControlComponent<string>{
@ViewChild(NgModel) model: NgModel;
items: RadioListModel[];
@Input() apiName: string;
@Input() valueFieldName: string;
@Input() textFieldName: string;
constructor(private _httpCommonService: HttpCommonService,
@Optional() @Inject(NG_VALIDATORS) validators: Array<any>,
@Optional() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<any>,
) {
super(validators, asyncValidators);
}
ngOnInit() {
super.ngOnInit();
if (this.apiName != null) {
this._httpCommonService.getList(this.apiName).subscribe(data => {
this.items = data
});
}
}
}
export class RadioListModel {
constructor(public id: number, public name: string,public checked:boolean) { }
}
radio-list.template.html 包含 label、type 为 radio 的 inputs、required,以及用于从 items 数组填充单选列表并使用 valueFieldName
和 textFieldName
绑定值和文本的逻辑。
<div class="form-group"> <!--#rbl="ngModel"-->
<label for="{{name}}">{{label}} </label>
<div *ngFor="let item of items">
<label>
<input type="radio" id="{{name}}"
name="{{name}}"
[value]="item[valueFieldName]"
[(ngModel)]="value"
required="{{required}}"
[checked]="item[valueFieldName] === value"
#rbl="ngModel"
>
<span>{{ item[textFieldName] }}</span>
</label>
<div [hidden]="rbl.valid || rbl.pristine" class="alert alert-danger">
{{name}} is required
</div>
</div>
</div>
Grid 控件
GridComponent
具有用于 webapi
服务的 apiName
,该服务将用于在 Grid 中加载数据,以及一个 Grid 列数组,其中每列都具有 name
、label
、model
名称属性。在组件 init
时,它会调用 http common service 并填充 Grid 中的数据。此外,它还将使用 query
属性和 getdata
方法来处理过滤功能。
import { Component, OnInit, Output, Input, AfterViewInit,
AfterViewChecked, EventEmitter } from '@angular/core';
import { HttpCommonService } from '../../services/http-common.service';
import { GridColumnModel } from './grid-column.model';
@Component({
moduleId: module.id,
selector: 'grid',
templateUrl: 'grid.template.html',
providers: [HttpCommonService]
})
export class GridComponent implements OnInit {
data: any;
@Input() name: string;
@Input() apiName: string;
@Input() columns: Array<GridColumnModel>;
@Input() enableFilter = true;
query = "";
filteredList:any;
constructor(private _httpCommonService: HttpCommonService) {
}
getData() {
if (this.query !== "") {
return this.filteredList;
} else {
return this.data;
}
}
filter() {
this.filteredList = this.data.filter(function (el:any) {
var result = "";
for (var key in el) {
result += el[key];
}
return result.toLowerCase().indexOf(this.query.toLowerCase()) > -1;
}.bind(this));
}
ngOnInit() {
if (this.columns == null)
{
this.columns = [
new GridColumnModel({ name: 'name', modelName: 'name', label: 'name' }),
]
}
this._httpCommonService.getList(this.apiName).subscribe(data => {
this.data = data
});
}
}
export class GridColumnModel {
// value: T;
name: string;
label: string;
order: number;
modelName: string;
constructor(options: {
// value?: T,
name?: string,
label?: string,
order?: number,
modelName?: string,
} = {}) {
//this.value = options.value;
this.name = options.name || '';
this.label = options.label || '';
this.order = options.order === undefined ? 1 : options.order;
this.modelName = options.modelName || '';
}
}
grid.template.html 包含用于模块的 name,该 name 在 edit 和 new 链接中使用,用于过滤数据的 input,以及用于显示数据的 table,它使用 https://npmjs.net.cn/package/angular2-data-table 中的 angular2-datatable
来处理排序和分页。mfData
属性从 getData()
方法获取数据,然后循环遍历列数组以加载 Grid 标题,然后循环遍历 Grid 数据以绘制 Grid 行。
<div>
<a class="mdl-navigation__link"
[routerLink]="['/'+name]">New {{name}}</a>
</div>
<label for="filter">Filter</label>
<input name="filter" id="filter" type="text"
class="form-control" *ngIf=enableFilter [(ngModel)]=query
(keyup)=filter() placeholder="Filter" />
<table class="table table-striped" [mfData]="getData()" #mf="mfDataTable"
[mfRowsOnPage]="5" hidden="{{hidden}}">
<thead>
<tr>
<th *ngFor="let colum of columns">
<mfDefaultSorter by="{{colum.modelName}}">{{colum.label}}</mfDefaultSorter>
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of mf.data">
<td *ngFor="let colum of columns">
{{item[colum.modelName] ? (item[colum.modelName].name?
item[colum.modelName].name : item[colum.modelName]): 'N/A'}}
</td>
<td><a class="mdl-navigation__link"
[routerLink]="['/'+name+'',item.id]">Edit</a></td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="4">
<mfBootstrapPaginator [rowsOnPageSet]="[5,10,25]"></mfBootstrapPaginator>
</td>
</tr>
</tfoot>
</table>
datepicker 控件
DatePickerComponent
覆盖 NgModel
,将验证从自定义控件传递到原始输入。
import { Component, Optional, Inject, OnInit, ViewChild} from '@angular/core';
import { BaseControlComponent } from '../base/base-control.component'
import { NgModel, NG_VALIDATORS, NG_ASYNC_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
moduleId: module.id,
selector: 'date-picker',
templateUrl: 'date-picker.template.html' ,
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: DatePickerComponent, multi: true }
]
})
export class DatePickerComponent extends BaseControlComponent<string> {
@ViewChild(NgModel) model: NgModel;
constructor(
@Optional() @Inject(NG_VALIDATORS) validators: Array<any>,
@Optional() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<any>,
) {
super(validators, asyncValidators);
}
}
date-picker.template.html 包含 label
、input
和 required
。input
类型为 date
,并且应该是 string yyyy-MM-dd
格式。
<div class="form-group" >
<label for="name">{{label}}</label>
<input type="date" class="form-control" id="{{name}}"
required="{{required}}"
[(ngModel)]="value" name="{{name}}"
>
</div>
Radio 控件
RadioComponent
覆盖 NgModel
,将验证从自定义控件传递到原始输入,并包含 checked
和 val
属性。
import { Component, Optional,Inject,ViewChild, OnInit, Output,
Input, AfterViewInit, AfterViewChecked, EventEmitter } from '@angular/core';
import { BaseControlComponent } from '../base/base-control.component'
import { NgModel, NG_VALIDATORS, NG_ASYNC_VALIDATORS, NG_VALUE_ACCESSOR} from '@angular/forms'
@Component({
moduleId: module.id,
selector: 'radio',
templateUrl: 'radio.template.html',
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: RadioComponent, multi: true }
]
})
export class RadioComponent extends BaseControlComponent<string>{
@ViewChild(NgModel) model: NgModel;
@Input() checked: boolean = false;
@Input() val:string
constructor(
@Optional() @Inject(NG_VALIDATORS) validators: Array<any>,
@Optional() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<any>,
) {
super(validators, asyncValidators);
}
}
radio.template.html 包含 label、type 为 radio 的 input。它具有 val
属性,可以通过原始 value
属性和 checked
属性将值传递给原始控件。
注意:当我将 val
属性名设置为 value
时,它返回 on
而不是正确的值,因此我将其名称更改为 val
。
<div class="form-group">
<label for="{{name}}">{{label}}</label>
<input #rb
id="{{id}}"
name="{{name}}"
[value]="val"
type="radio"
[checked]="value == rb.value"
(click)="value = rb.value"
>
</div>
Checkbox 控件
CheckboxComponent
覆盖 NgModel
,将验证从自定义控件传递到原始输入,并包含 checked
属性。
import { Component, OnInit, Inject,Optional,Output, ViewChild, Input,
AfterViewInit, AfterViewChecked, EventEmitter } from '@angular/core';
import { BaseControlComponent } from '../base/base-control.component'
import { NgModel, NG_VALIDATORS, NG_ASYNC_VALIDATORS, NG_VALUE_ACCESSOR} from '@angular/forms'
@Component({
moduleId: module.id,
selector: 'checkbox',
templateUrl: 'checkbox.template.html',
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: CheckboxComponent, multi: true }
]
})
export class CheckboxComponent extends BaseControlComponent<string>{
@ViewChild(NgModel) model: NgModel;
constructor(
@Optional() @Inject(NG_VALIDATORS) validators: Array<any>,
@Optional() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<any>,
) {
super(validators, asyncValidators);
}
}
checkbox.template.html 包含 label、type 为 checkbox
的 input。它具有 checked
属性。
<div class="form-group">
<label for="{{name}}">{{label}}</label>
<input type="checkbox"
id="{{name}}"
[(ngModel)]="value" name="{{name}}"
#chk="ngModel"
hidden="{{hidden}}"
>
<div [hidden]="chk.valid || chk.pristine"
class="alert alert-danger">
{{name}} is required
</div>
</div>
服务器端
ASP.NET Core Web API 的使用遵循 https://docs.microsoft.com/en-us/aspnet/core/data/ef-mvc/intro 中的步骤,将 MVC 控制器更改为 Web API 控制器,并将以下配置添加到 Startup.cs。
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<SchoolContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory, SchoolContext context)
{
app.UseStaticFiles(new StaticFileOptions()
{
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), @"client")),
RequestPath = new PathString("/client")
});
DbInitializer.Initialize(context);
注意
连接 string
在 appsettings.json 中。
"ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\mssqllocaldb;
Database=aspnet-Angular2CodeProject;Trusted_Connection=True;MultipleActiveResultSets=true" }
DbInitializer.Initialize
用于在数据库中添加虚拟数据以测试 Grid。
该项目的源代码附在本帖顶部。
关注点
使用 TypeScript 和 Angular 2 构建自定义控件库,这比在屏幕上为每个控件编写标签和验证消息要容易。此外,通过添加基类控件和基类表单来使用继承概念。此外,修复基类控件中的绑定问题,并在基类表单中一次性添加 CRUD 操作,通过设置 Grid 的 API 名称和列列表来填充 Grid,以及通过设置其 API 名称、textFieldName
和 valueFieldName
来填充下拉列表。此外,添加通用服务来处理所有 http CRUD 操作。
参考文献
使用 Visual Studio 入门 ASP.NET Core MVC 和 Entity Framework Core(1/10)
“英雄之旅”教程将指导你完成在 TypeScript 中创建 Angular 应用程序的步骤。
在 Angular 2 中将数据传递给嵌套组件和从嵌套组件传递数据
- http://blog.rangle.io/angular-2-ngmodel-and-custom-form-components/
- https://www.themarketingtechnologist.co/building-nested-components-in-angular-2/
Angular 中的双向数据绑定
适用于 Angular2 的带有排序和分页的表组件
历史
- 2017 年 1 月 25 日:初始版本