65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (3投票s)

2017年1月25日

CPOL

7分钟阅读

viewsIcon

20704

downloadIcon

520

Angular 2 TypeScript 自定义控件与 ASP.NET Core Web API 集成

引言

此库包含八个 Angular2 自定义控件。每个控件都有自己的标签和验证。Grid 和 dropdown 使用 API 名称动态获取数据。

  1. textBox
  2. textbox-multiline
  3. date-picker
  4. dropdown
  5. grid
  6. checkbox
  7. radio
  8. radio list

此外,该库还包含基类和通用 http 服务

  1. base-control-component
  2. base-form-component
  3. http-common-service

必备组件

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.。对于每个自定义表单输入控件,我们使用双向绑定设置其 idlabel 值、requiredngModelName。对于 radio list 和 dropdown 控件,我们传递额外的属性 apiName 来填充列表,valueFieldNametextFieldName 来设置列表元素的文本和值字段。将提交按钮的 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 列数组。每列都有 namemodelNamelabel 属性。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,然后设置 apinamecolumn 属性(它将从 StudentListComponent 组件获取)。Grid 控件具有排序、分页和过滤功能。

<grid  [columns]="columns" apiName="Students" label="Student" name="student"></grid> 

控件源代码

BaseControlComponent

所有自定义控件都继承自 BaseControlComponent,以获取通用属性,如 labelnameidrequiredhiddentexttype,并用于通过 http://blog.rangle.io/angular-2-ngmodel-and-custom-form-components/ 中的步骤修复嵌套控件 ngmodel 绑定问题。

它还包含用于电子邮件和 URL 或任何其他自定义验证的正则表达式的 patterns 对象,通过为每个 textTypeemailurltel)添加正则表达式,这些表达式将在子控件的 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 方法,并作为任何请求的入口点。它包含 createupdatedeletegetlistgetItem 方法。我们必须设置 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 包含 labelinput 以及 requiredmaxlengthminlengthpattern 验证。

<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 包含 labeltextarea 以支持多行文本,以及 requiredmaxlengthminlengthpattern 验证。

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 包含 labelselectrequired,以及用于从 items 数组填充 select 选项并使用 valueFieldNametextFieldName 绑定值和文本的逻辑。

<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 数组填充单选列表并使用 valueFieldNametextFieldName 绑定值和文本的逻辑。

<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 列数组,其中每列都具有 namelabelmodel 名称属性。在组件 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 包含 labelinputrequiredinput 类型为 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,将验证从自定义控件传递到原始输入,并包含 checkedval 属性。

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); 

注意

连接 stringappsettings.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 名称、textFieldNamevalueFieldName 来填充下拉列表。此外,添加通用服务来处理所有 http CRUD 操作。

参考文献

使用 Visual Studio 入门 ASP.NET Core MVC 和 Entity Framework Core(1/10)

“英雄之旅”教程将指导你完成在 TypeScript 中创建 Angular 应用程序的步骤。

在 Angular 2 中将数据传递给嵌套组件和从嵌套组件传递数据

Angular 中的双向数据绑定

适用于 Angular2 的带有排序和分页的表组件

历史

  • 2017 年 1 月 25 日:初始版本
© . All rights reserved.