In Focus

Angular 2 Custom Controls With ASP.NET Core Web API

In this article, you will learn about Angular 2 Custom Controls in TypeScript with ASP.NET Core Web API.

Introduction

Angular 2 library contains eight TypeScript custom controls. Each control has its label and its validations. Grid and dropdown get the data dynamically using the API name. All the eight controls are listed below. 

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

Also, the library contains base classes and common HTTP services.

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

Prerequisites

  • Visual Studio Community 2015 with Update 3 – Free
  • .NET Core 1.1 SDK - Installer (dotnet-dev-win-x64.1.0.0-preview2-1-003177.exe)
  • Typescript for visual studio 2015 (TypeScript_Dev14Full_2.1.4.0.exe)
  • Node js 6.9
  • Install node_modules in client folder in the project.

    Open cmd.exe and then open client folder.
    D:\> cd D:\ Angular2WithAspNetCoreWebAPI\src\Angular2WithAspNetCoreWebAPI\client
    D:\ Angular2WithAspNetCoreWebAPI\src\Angular2WithAspNetCoreWebAPI\client> npm install

  • Start page
    http://localhost:56245/client/index.html

Using the code

Examples for using the custom form controls.

Create a new component for student form client\app\components\student\student-form.component.ts that inherits from baseFormComponent to use, save, and load methods from it.

Then, pass ASP.NET Core API name to the base form constructor to use it in get and post data. In this example, we pass "Students" as API name.
  1. import { Component, OnDestroy,OnInit } from '@angular/core';  
  2. import { FlashMessagesService } from 'angular2-flash-messages';  
  3. import { HttpCommonService } from '../../shared/services/http-common.service';  
  4. import { BaseFormComponent } from '../../shared/controls/base/base-form.component';  
  5. import { ActivatedRoute } from '@angular/router';  
  6.    
  7.    
  8. @Component({  
  9.     moduleId: module.id,  
  10.     selector: 'student-form',  
  11.     templateUrl: 'student-form.template.html',  
  12.     providers: [HttpCommonService]  
  13.   })    
  14. export class StudentFormComponent extends  BaseFormComponent{  
  15.     //public strDate:string  = "2010-10-25";  
  16.   
  17.     constructor(  _httpCommonService: HttpCommonService,  
  18.           flashMessagesService: FlashMessagesService,  
  19.           route: ActivatedRoute) {  
  20.         super(_httpCommonService, flashMessagesService, route, "Students"  
Add custom controls to a form template client\app\components\student\student-form.template.html. In this template, we add form tag then call "Save" method in base form compnent on ngSubmit event and set form alias name #from="ngForm.
 
For each custom form input contros, we set its id, label value, required, and ngModelName using two way binding. For radio list and dropdown controls, we pass extra properties apiName to fill the list, valueFieldName, and textFieldName to set text and value fields for the list elements. Set disable property for submit button to be [disabled]="!from.form.valid".
  1. <div class="container">  
  2.     <div>  
  3.         <!--[hidden]="submitted"-->  
  4.         <h1>Student</h1>  
  5.         <form (ngSubmit)="save()" #from="ngForm">  
  6.   
  7. <textbox id="txtFirstMidName" name="txtFirstMidName" label="First-Mid Name" [(ngModel)]="model.firstMidName" required="true"></textbox>  
  8.               
  9. <textbox id="txtLastName" name="txtLastName"  label="Last Name" [(ngModel)]="model.lastName" required="true"></textbox>  
  10.               
  11. <date-picker name="dpEnrollmentDate" id="dpEnrollmentDate" label="EnrollmentDate" [(ngModel)]="model.enrollmentDate" required="true"></date-picker>  
  12.               
  13. <dropdown name="ddlCourse" id="ddlCourse" label="Course" [(ngModel)]="model.course1ID" apiName="Courses" valueFieldName="courseID" textFieldName="title" required="true"></dropdown>   
  14.               
  15. <textbox-multiline id="txtStudentDescription" name="txtStudentDescription" label="Description" [(ngModel)]="model.description" required="true"></textbox-multiline>  
  16.               
  17. <radio-list name="elStudentType" id="studentType" [(ngModel)]="model.course2ID" valueFieldName="courseID" textFieldName="title" apiName="Courses" required="true"></radio-list>   
  18.               
  19. <radio id="rbMale" label="Male" name="rbgender" [(ngModel)]="model.gender" checked="true" val="1"></radio>  
  20.              
  21. <radio id="rbFemale" label="Female" name="rbgender" [(ngModel)]="model.gender" val="0"></radio>  
  22.               
  23. <checkbox id="chkHasCar" label="HasCar" name="chkHasCar" [(ngModel)]="model.hasCar"></checkbox>   
  24.               
  25. <button type="submit" class="btn btn-default" [disabled]="!from.form.valid">Save</button>  
  26.               
  27. <button type="button" class="btn btn-default" [disabled]="model.id>0" (click)="from.reset()">New</button>   
  28.   
  29.         </form>  
  30.     </div>  
  31.   
  32. </div>  
  1. <button type="submit" class="btn btn-default" [disabled]="!from.form.valid">Save</button>  
When the controls are empty and required, the Save button will be disabled and red sign will appear in the control.

Note - textType property in text box could be number, email, URL, tel.



When the controls are not empty, the "Save" button will be enabled and a green sign will appear in the control.



All these controls have common properties which are included in client\app\shared\controls\base\base-control.component.ts.
  • label
  • name
  • id
  • required
  • hidden
  • textType
  • minLength
  • maxLength

Example for using custom grid

Create a new component for students list client\app\components\student\student-list.component.ts.

Then, add grid columns array. Each column has name, modelName, and label properties. Sorting, paging, and filtering features are included in the grid component.
  1. import { Component, Input } from '@angular/core';  
  2. import { GridColumnModel } from '../../shared/controls/grid/grid-column.model';  
  3.   
  4.   
  5. @Component({  
  6.     moduleId: module.id,  
  7.     selector: 'student-list',  
  8.     templateUrl: 'student-list.template.html'  
  9. })  
  10. export class StudentListComponent {  
  11.     @Input() columns: Array<GridColumnModel>;  
  12.     constructor() {  
  13.    
  14.         this.columns = [  
  15.             new GridColumnModel({ name: 'LastName', modelName: 'lastName', label: 'Last Name' }),  
  16.             new GridColumnModel({ name: 'FirstMidName', modelName: 'firstMidName', label: 'FirstMid Name' }),  
  17.             new GridColumnModel({ name: 'EnrollmentDate', modelName: 'enrollmentDate', label: 'Enrollment Date' }),  
  18.          ]  
  19.     }  
  20.   
  21. }  
Add the custom grid to a list template client\app\components\student\student-list.template.html and then set APIname and column property (it will get it from the StudentListComponent component).The grid control has sorting, paging, and filtering features.
  1. <grid  [columns]="columns" apiName="Students" label="Student" name="student"></grid>   


Controls Source code

BaseControlComponent

All custom controls inherit from BaseControlComponent to get common properties such as label, name, id, required, hidden, texttype, which are used for fixing nested controls' ngmodel binding issue in validation using steps in http://blog.rangle.io/angular-2-ngmodel-and-custom-form-components/

It also contains pattern objects for regular expression validations for email and url or any extra validations by adding regular expression for each textType (emial, URL, tel) which will be used in child controls html template.
  1. <input type="{{textType}}"  pattern="{{patterns[textType]}}">  
  1. import { Component, Optional,Inject,OnInit, Output, Input, AfterViewInit, AfterViewChecked, EventEmitter } from '@angular/core';  
  2. import { NgModel } from '@angular/forms';  
  3. import { Observable } from 'rxjs/Observable';  
  4. import { ValueAccessorBase } from './value-accessor';  
  5. import {  
  6.     AsyncValidatorArray,  
  7.     ValidatorArray,  
  8.     ValidationResult,  
  9.     message,  
  10.     validate,  
  11. } from './validate';  
  12.   
  13. @Component({  
  14.      
  15. })  
  16. export abstract   class BaseControlComponent<T> extends ValueAccessorBase<T> implements OnInit{  
  17.     
  18.     protected abstract  model: NgModel;  
  19.    
  20.     @Input() label: string;  
  21.     @Input() name: string;  
  22.     @Input() id: string;  
  23.     @Input() required: boolean = false;  
  24.     @Input() hidden: boolean = false;  
  25.     @Input() textType: string;  
  26.     @Input() minLength: string;  
  27.     @Input() maxLength: string;  
  28.    
  29.     public patterns = {  
  30.         email: "([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+",  
  31.         url: "(https?)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]"  
  32.     };  
base-form.component

All input forms should be inherited from BaseFormComponent to get all CRUD operations, such as save [create or update], delete, reset form data to be in new mode and load model in edit mode if router has id param.
  1. import { Component, OnDestroy, OnInit ,Input} from '@angular/core';  
  2. import { ActivatedRoute } from '@angular/router';  
  3. import { HttpCommonService } from '../../services/http-common.service';  
  4. import { FlashMessagesService } from 'angular2-flash-messages';  
  5.   
  6.   
  7. @Component({  
  8.     moduleId: module.id,  
  9.     providers: [HttpCommonService]  
  10. })  
  11. export  class BaseFormComponent {  
  12.     public apiName:string    
  13.     protected  model = {};  
  14.     protected submitted = false;  
  15.     private sub: any;  
  16.     id: number;  
  17.   
  18.     // onSubmit() { this.submitted = true; }  
  19.   
  20.     // TODO: Remove this when we're done  
  21.     // get diagnostic() { return JSON.stringify(this.model); }  
  22.   
  23.     constructor(private _httpCommonService: HttpCommonService,  
  24.         private flashMessagesService: FlashMessagesService,  
  25.         private route: ActivatedRoute, _apiName: string) {  
  26.         this.apiName = _apiName;  
  27.         this.sub = this.route.params.subscribe(params => {  
  28.             this.id = +params['id']; // (+) converts string 'id' to a number  
  29.             if (this.id > 0) {  
  30.                 this._httpCommonService.getItem(this.apiName, this.id).subscribe(data => {  
  31.                     this.model = data  
  32.                     this.model["enrollmentDate"] = this.model["enrollmentDate"].substring(0, 10);  
  33.                 });  
  34.             }  
  35.   
  36.         });  
  37.     }  
  38.     ngOnInit() {  
  39.   
  40.         //this.sub = this.route.params.subscribe(params => {  
  41.         //    this.id = +params['id']; // (+) converts string 'id' to a number  
  42.         //this._httpCommonService.getItem("Students", this.id).subscribe(data => {  
  43.         //    this.model = data  
  44.         //});  
  45.         // });  
  46.     }  
  47.     ngOnDestroy() {  
  48.          this.sub.unsubscribe();  
  49.     }  
  50.     reset() {  
  51.         this.id = 0;  
  52.         this.model = {};  
  53.     }  
  54.     save() {  
  55.         alert(JSON.stringify(this.model));  
  56.         if (this.id > 0) {  
  57.             this._httpCommonService.update(this.apiName, this.model).subscribe();  
  58.               
  59.         }  
  60.         else {  
  61.             this._httpCommonService.create(this.apiName, this.model).subscribe();  
  62.              
  63.         }  
  64.   
  65.         this.flashMessagesService.show('success', { cssClass: 'alert-success' });//{ cssClass: 'alert-success', timeout: 1000 }  
  66.         //this.flashMessagesService.grayOut(true);  
  67.         this.submitted = true;  
  68.   
  69.     }  
  70.     delete () {  
  71.         this._httpCommonService.delete(this.apiNamethis.model["id"]);  
  72.     }    
  73. }  
http-common.service

It is used to centralize all http methods and to be an entry point for any request. It contains create, update, delete, getlist, and getItem methods. We have to set apiBaseUrl property to use it for all these methods.
  1. import { Injectable } from "@angular/core";  
  2. import { Http, Response, ResponseContentType, Headers, RequestOptions, RequestOptionsArgs, Request, RequestMethod, URLSearchParams } from "@angular/http";   
  3. //import { Observable } from 'rxjs/Observable';  
  4. //import { Observable } from "rxjs/Rx";  
  5. import { Observable } from 'rxjs/Rx'  
  6.    
  7.   
  8.   
  9. @Injectable()  
  10. export class HttpCommonService {  
  11.     public apiBaseUrl: string;  
  12.     
  13.     constructor(public http: Http) {  
  14.         this.http = http;  
  15.          this.apiBaseUrl = "/api/";  
  16.     }  
  17.       
  18.     PostRequest(apiName: string, model: any) {  
  19.   
  20.        let headers = new Headers();  
  21.         headers.append("Content-Type"'application/json');  
  22.         let requestOptions = new RequestOptions({  
  23.             method: RequestMethod.Post,  
  24.             url: this.apiBaseUrl + apiName,  
  25.             headers:  headers,  
  26.             body: JSON.stringify(model)  
  27.         })  
  28.   
  29.         return this.http.request(new Request( requestOptions))  
  30.             .map((res: Response) => {  
  31.                 if (res) {  
  32.                     return [{ status: res.status, json: res.json() }]  
  33.                 }  
  34.             });  
  35.     }  
  36.   
  37.    requestOptions()  
  38.     {  
  39.         let contentType = 'application/json';//"x-www-form-urlencoded";  
  40.         let headers = new Headers({ 'Content-Type': contentType});  
  41.         let options = new RequestOptions({  
  42.             headers: headers,  
  43.             //body: body,  
  44.            // url: this.apiBaseUrl + apiName,  
  45.            // method: requestMethod,  
  46.             //responseType: ResponseContentType.Json  
  47.         });  
  48.         return options;  
  49.   
  50.     }  
  51.    stringifyModel(model: any)  
  52.    {  
  53.        return JSON.stringify(model);  
  54.    }  
  55.    create(apiName: string, model: any) {  
  56.          
  57.        // let headers = new Headers({ 'Content-Type': 'application/json' });  
  58.        // let options = new RequestOptions({ headers: headers });  
  59.        // let body = JSON.stringify(model);  
  60.         return this.http.post(this.apiBaseUrl + apiName,  
  61.             this.stringifyModel(model),  
  62.             this.requestOptions())  
  63.             .map(this.extractData)  //.map((res: Response) => res.json())   
  64.             .catch(this.handleError)  
  65.             // .subscribe()  
  66.             ;  
  67.           
  68.     }  
  69.     update(apiName:string,model: any) {  
  70.         let headers = new Headers({ 'Content-Type''application/json' });  
  71.         let options = new RequestOptions({ headers: headers });  
  72.         let body = JSON.stringify(model);  
  73.         return this.http.put(this.apiBaseUrl + apiName + '/' + model.id, body, options).map((res: Response) => res.json());//.subscribe();  
  74.     }  
  75.     delete(apiName:string,id:any) {  
  76.         return this.http.delete(this.apiBaseUrl + apiName + '/' + id);//.subscribe();;  
  77.     }  
  78.   
  79.     getList(apiName: string) {  
  80.         return this.http.get(this.apiBaseUrl + apiName, { search: null })  
  81.             .map((responseData) => responseData.json());  
  82.     }  
  83.     getItem(apiName: string,id:number) {  
  84.          
  85.         return this.http.get(this.apiBaseUrl + apiName + "/" + id, { search: null })  
  86.             .map((responseData) => responseData.json());  
  87.     }  
  88.   
  89.     getLookup(lookupName: string, parentId: number, parentName: string) {  
  90.         var params = null;  
  91.         if (parentId != null) {  
  92.             params = new URLSearchParams();  
  93.             params.set(parentName, parentId.toString());  
  94.         }  
  95.         return this.http.get(this.apiBaseUrl +"lookup/" + lookupName, { search: params })  
  96.             .map((responseData) => responseData.json());  
  97.     }  
  98.   
  99.   
  100.     private extractData(res: Response) {  
  101.         let body = res.json();  
  102.         return body || {};  
  103.     }  
  104.     private handleError(error: Response | any) {  
  105.         // In a real world app, we might use a remote logging infrastructure  
  106.         let errMsg: string;  
  107.         if (error instanceof Response) {  
  108.             const body = error.json() || '';  
  109.             const err = body.error || JSON.stringify(body);  
  110.             errMsg = `${error.status} - ${error.statusText || ''} ${err}`;  
  111.         } else {  
  112.             errMsg = error.message ? error.message : error.toString();  
  113.         }  
  114.        //console.error(errMsg);  
  115.   
  116.         
  117.          return Observable.throw(errMsg);  
  118.     }  
  119.   
  120. }  
Add Route Configuration for the new component (studeint form, student list).

The page routes should be added in app.routes for add, edit and list. In edit, we path the id in the url client\app\app.routes.ts
  1. import { ModuleWithProviders } from '@angular/core';  
  2. import { Routes, RouterModule } from '@angular/router';  
  3.    
  4. import { StudentFormComponent } from './components/student/student-form.component';  
  5. import { StudentListComponent } from './components/student/student-list.component';  
  6.   
  7.     
  8. // Route Configuration  
  9. export const routes: Routes = [  
  10.     { path: 'student', component: StudentFormComponent  },  
  11.     { path: 'student/:id', component: StudentFormComponent },  
  12.     { path: 'students', component: StudentListComponent},  
  13.  ];    
  14.   
  15. export const routing: ModuleWithProviders = RouterModule.forRoot(routes);  
Add pages Links for the new component (student form, student list) in app component template.

Add new pages links to app component client\app\app.component.template.html using [routerLink].
  1. <div id="wrapper">  
  2.       <!-- Sidebar -->  
  3.       <div id="sidebar-wrapper">  
  4.           <nav class="mdl-navigation">  
  5.               <ul class="sidebar-nav">  
  6.                   <li class="sidebar-brand">  
  7.                       <!--<a href="#">-->  
  8.                       Student System  
  9.                       <!--</a>-->  
  10.                   </li>  
  11.   
  12.                   <li>  
  13.                       <a class="mdl-navigation__link" [routerLink]="['/']">Dashboard</a>  
  14.                   </li>  
  15.                   <li>  
  16.                       <a class="mdl-navigation__link" [routerLink]="['/student']">Add Student</a>  
  17.                   </li>  
  18.                   <li>  
  19.                       <a class="mdl-navigation__link" [routerLink]="['/students']">List Students</a>  
  20.             </li>  
  21.                     
  22.               </ul>  
  23.           </nav>  
  24.       </div>  
  25.       <!-- /#sidebar-wrapper -->  
  26.       <!-- Page Content -->  
  27.       <div id="page-content-wrapper">  
  28.           <div class="container-fluid">  
  29.               <div class="row">  
  30.                   <div class="col-lg-12">  
  31.                      
  32.                       <router-outlet></router-outlet>  
  33.   
  34.                        
  35.                   </div>  
  36.               </div>  
  37.           </div>  
  38.       </div>  
  39.       <!-- /#page-content-wrapper -->  
  40.   </div>  
Note

Edit link will be in list form in the grid control.
  1. <td><a class="mdl-navigation__link" [routerLink]="['/'+name+'',item.id]">Edit</a></td>  
  2. <td><a class="mdl-navigation__link" [routerLink]="['/'+name+'Details',item.id]">Details</a></td>  
Adding controls and forms components to the main module client\app\app.module.ts.
  1. import { NgModule } from '@angular/core';  
  2. import { BrowserModule } from '@angular/platform-browser';  
  3. import { FormsModule, ReactiveFormsModule, NG_VALIDATORS, NG_ASYNC_VALIDATORS, FormControl   } from '@angular/forms';  
  4. import { HttpModule, JsonpModule } from '@angular/http';  
  5. import { InMemoryWebApiModule } from 'angular-in-memory-web-api';  
  6. import { requestOptionsProvider } from './shared/services/default-request-options.service';  
  7. import { FlashMessagesModule } from 'angular2-flash-messages';  
  8. import { DataTableModule } from "angular2-datatable";  
  9.  import { routing } from './app.routes';  
  10.    
  11.   
  12. //validation-test  
  13. import { DropdownComponent } from './shared/controls/dropdown/dropdown.component';  
  14. import { RadioListComponent } from './shared/controls/radio/radio-list.component';  
  15.   
  16. import { TextboxComponent } from './shared/controls/textbox/textbox.component';  
  17. import { TextboxMultilineComponent } from './shared/controls/textbox/textbox-multiline.component';  
  18.   
  19. import { DatePickerComponent } from './shared/controls/date/date-picker.component';  
  20.   
  21. import { CheckboxComponent } from './shared/controls/checkbox/checkbox.component';  
  22. import { RadioComponent } from './shared/controls/radio/radio.component';  
  23.   
  24. import { GridComponent } from './shared/controls/grid/grid.component';  
  25.   
  26. import { ValidationComponent } from './shared/controls/validators/validation';  
  27.   
  28.    
  29. import { StudentFormComponent } from './components/student/student-form.component';  
  30. import { StudentListComponent } from './components/student/student-list.component';  
  31.   
  32. import { AppComponent } from './app.component';  
  33.    
  34. @NgModule({  
  35.     imports: [  
  36.         BrowserModule,  
  37.         FormsModule,  
  38.         ReactiveFormsModule,  
  39.         HttpModule,  
  40.         JsonpModule,  
  41.         routing,  
  42.         FlashMessagesModule,  
  43.         DataTableModule,  
  44.    
  45.     ],  
  46.     declarations: [  
  47.         AppComponent,  
  48.         TextboxComponent,  
  49.         TextboxMultilineComponent,  
  50.         DatePickerComponent,  
  51.         CheckboxComponent,  
  52.         DropdownComponent,  
  53.         RadioListComponent,  
  54.         RadioComponent,  
  55.         GridComponent,  
  56.         ValidationComponent,  
  57.   
  58.         StudentFormComponent,  
  59.        StudentListComponent,  
  60.     ],  
  61.     providers: [requestOptionsProvider],  
  62.     bootstrap: [AppComponent]  
  63. })  
  64. export class AppModule { }  
Textbox control

TextboxComponent overrides NgModel to pass the validation from custom control to the original input.
  1. import { Component, ViewChild, Optional, Inject} from '@angular/core';  
  2. import { BaseControlComponent } from '../base/base-control.component'  
  3.   
  4. import { NG_VALUE_ACCESSOR, NgModel, NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '@angular/forms';  
  5. import { animations } from '../validators/animations';  
  6.   
  7. @Component({  
  8.     moduleId: module.id,  
  9.     selector: 'textbox',  
  10.     templateUrl: 'textbox.template.html'  
  11.     , animations   
  12.     , providers: [  
  13.         { provide: NG_VALUE_ACCESSOR, useExisting: TextboxComponent, multi: true}  
  14.     ]  
  15. })  
  16.   
  17. export class TextboxComponent extends BaseControlComponent<string>   {  
  18.       
  19.     @ViewChild(NgModel) model: NgModel;  
  20.   
  21.     constructor(  
  22.         @Optional() @Inject(NG_VALIDATORS) validators: Array<any>,  
  23.         @Optional() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<any>,  
  24.     ) {  
  25.         super(validators, asyncValidators);  
  26.     }  
  27. }  
textbox.template.html has label, input and required, maxlength, minlength, and paterrn validations.
  1. <div class="form-group">  
  2.       <label for="{{name}}">{{label}}</label>  
  3.     <input  type="{{textType}}" class="form-control" id="{{name}}"  
  4.            required="{{required}}"  
  5.            [(ngModel)]="value" name="{{name}}"   
  6.             #txt="ngModel"  
  7.            pattern="{{patterns[textType]}}"  
  8.            maxlength="{{maxLength}}"  
  9.            minlength="{{minLength}}"   
  10.            hidden="{{hidden}}">  
  11.            
  12.      <div *ngIf="txt.errors && (txt.dirty || txt.touched)"  
  13.          class="alert alert-danger">  
  14.         <div [hidden]="(!txt.errors.required)">  
  15.             {{label}} is required  
  16.               
  17.         </div>  
  18.         <div [hidden]="!txt.errors.minlength">  
  19.             {{label}} must be at least 4 characters long.  
  20.         </div>  
  21.         <div [hidden]="!txt.errors.maxlength">  
  22.             {{label}} cannot be more than 24 characters long.  
  23.         </div>  
  24.     </div>  
  25.     <validation [@flyInOut]="'in,out'"  
  26.                 *ngIf="invalid | async"  
  27.                 [messages]="failures | async">  
  28.     </validation>  
  29. </div>  
Textbox Multiline control

TextboxMultilineComponent overrides NgModel to pass the validation from custom control to the original input.
  1. import { Component, ViewChild, Optional, Inject} from '@angular/core';  
  2. import { NgModel, NG_VALUE_ACCESSOR, NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '@angular/forms';  
  3. import { BaseControlComponent } from '../base/base-control.component'  
  4.   
  5. @Component({  
  6.     moduleId: module.id,  
  7.     selector: 'textbox-multiline',  
  8.     templateUrl: 'textbox-multiline.template.html', providers: [  
  9.         { provide: NG_VALUE_ACCESSOR, useExisting: TextboxMultilineComponent, multi: true }  
  10.     ]  
  11. })  
  12. export class TextboxMultilineComponent  extends BaseControlComponent<string>  {  
  13.     @ViewChild(NgModel) model: NgModel;  
  14.   
  15.     constructor(  
  16.         @Optional() @Inject(NG_VALIDATORS) validators: Array<any>,  
  17.         @Optional() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<any>,  
  18.     ) {  
  19.         super(validators, asyncValidators);  
  20.     }  
  21.   
  22. }  
textbox-multiline.template.html has label ,textarea, to support multi line text and required, maxlength, minlength, and pattern validations.
  1. div class="form-group"><!--#txt="ngModel"  (blur)="setValid(txt)"-->   
  2.     <label for="{{name}}">{{label}}</label>  
  3.     <textarea   class="form-control" id="{{name}}"  
  4.            required="{{required}}"  
  5.            [(ngModel)]="value" name="{{name}}"   
  6.             pattern="{{patterns[textType]}}"  
  7.            #txt="ngModel"  
  8.            maxlength="{{maxLength}}"  
  9.            minlength="{{minLength}}"   
  10.            ></textarea>  
  11.     
  12.     <div *ngIf="txt.errors && (txt.dirty || txt.touched)"  
  13.          class="alert alert-danger">  
  14.         <div [hidden]="(!txt.errors.required)">  
  15.             {{label}} is required  
  16.         </div>  
  17.         <div [hidden]="!txt.errors.minlength">  
  18.             {{label}} must be at least {{minlength}} characters long.  
  19.         </div>  
  20.         <div [hidden]="!txt.errors.maxlength">  
  21.             {{label}} cannot be more than {{maxlength}} characters long.  
  22.         </div>  
  23.     </div>  
  24. </div>  
Drop Down control

DropdownComponent is used to override NgModel to pass the validation from custom control to the original input. It has apiName for the WebAPI service which will be used to load the select options, field name for option value, and field name for option text. On component init, it calls http common service and fills items arrays which is used in the template to load the select options.
  1. import { Component, OnInit, Inject, Output, Optional,Input, AfterViewInit, AfterViewChecked, EventEmitter, ViewChild} from '@angular/core';  
  2. import { HttpCommonService } from '../../services/http-common.service';  
  3. import { BaseControlComponent } from '../base/base-control.component'  
  4. import { DropdownModel } from './dropdown.model';  
  5. import { NgModel, NG_VALUE_ACCESSOR, NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '@angular/forms';  
  6. import { animations } from '../validators/animations';  
  7.   
  8. @Component({  
  9.     moduleId: module.id,  
  10.     selector: 'dropdown',  
  11.     templateUrl: 'dropdown.template.html', animations,  
  12.     providers:  [ {  
  13.         provide: NG_VALUE_ACCESSOR,  
  14.         useExisting: DropdownComponent,  
  15.         multi: true  
  16.     },HttpCommonService]  
  17.      
  18. })  
  19. export class DropdownComponent extends BaseControlComponent<string> {  
  20.     @ViewChild(NgModel) model: NgModel;  
  21.      items: DropdownModel[];  
dropdown.template.html has label, select, required, and tho logic for filling select options from items array, and to bind value and text, using valueFieldName and textFieldName.
  1. <div class="form-group">  
  2.      
  3.         <label for="{{name}}">{{label}}  </label>  
  4.      
  5.         <select class="form-control" id="{{ name}}"  
  6.                 name="{{ name}}"  
  7.                 [(ngModel)]="value"  
  8.                 hidden="{{hidden}}"  
  9.                 #ddl="ngModel"  
  10.                 required="{{required}}">  
  11.             <option value="">--Select--</option>  
  12.              <ng-content></ng-content>  
  13.             <option *ngFor="let item of items" [value]="item[valueFieldName]">{{item[textFieldName]}}</option>   
  14.         </select>  
  15.          <div [hidden]="(ddl.valid || ddl.pristine)" class="alert alert-danger">  
  16.             {{name}} is required  
  17.           </div>   
  18.     <validation [@flyInOut]="'in,out'"  
  19.                 *ngIf="invalid | async"  
  20.                 [messages]="failures | async">  
  21.     </validation>  
  22. </div>  
Radio List

RadioListComponent is used to override NgModel to pass the validation from custom control to the original input. It has apiName for the WebAPI service which will used to load the radio list, field name for its value, and field name for its text. On component init, it calls http common service and fills items arrays which is used in the template to load the radio list.
  1. import { Component, OnInit, Optional, Input, ViewChild, Inject} from '@angular/core';  
  2. import { HttpCommonService } from '../../services/http-common.service';  
  3. import { RadioListModel } from './radio-list.model';  
  4. import { BaseControlComponent } from '../base/base-control.component'  
  5. import { NgModel, NG_VALUE_ACCESSOR, NG_VALIDATORS, NG_ASYNC_VALIDATORS } from '@angular/forms';  
  6. @Component({  
  7.     moduleId :module.id ,  
  8.     selector: 'radio-list',  
  9.     templateUrl: 'radio-list.template.html',  
  10.     providers: [{  
  11.         provide: NG_VALUE_ACCESSOR,  
  12.         useExisting: RadioListComponent,  
  13.         multi: true  
  14.     },HttpCommonService]   
  15. })  
  16. export class RadioListComponent extends BaseControlComponent<string>{  
  17.     @ViewChild(NgModel) model: NgModel;  
  18.      items: RadioListModel[];  
  19.     @Input() apiName: string;  
  20.     @Input() valueFieldName: string;  
  21.     @Input() textFieldName: string;  
  22.      
  23.     constructor(private _httpCommonService: HttpCommonService,  
  24.     @Optional() @Inject(NG_VALIDATORS) validators: Array<any>,  
  25.     @Optional() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<any>,  
  26.     ) {  
  27.      super(validators, asyncValidators);  
  28.  }  
  29.    
  30.    ngOnInit() {  
  31.        super.ngOnInit();  
  32.        if (this.apiName != null) {  
  33.            this._httpCommonService.getList(this.apiName).subscribe(data => {  
  34.                this.items = data  
  35.            });  
  36.        }  
  37.    }  
  38.     
  39. }  
  40. export class RadioListModel {  
  41.     constructor(public id: number, public name: string,public checked:boolean) { }  
  42.        
  43. }  
radio-list.template.html has label, inputs of type radio, required, and the logic for filling the radio list from items array, and binds value and text, using valueFieldName and textFieldName.
  1. <div class="form-group"> <!--#rbl="ngModel"-->  
  2.     <label for="{{name}}">{{label}}  </label>  
  3.     <div   *ngFor="let item of items">  
  4.         <label>  
  5.             <input type="radio" id="{{name}}"  
  6.                    name="{{name}}"  
  7.                    [value]="item[valueFieldName]"  
  8.                    [(ngModel)]="value"  
  9.                    required="{{required}}"    
  10.                    [checked]="item[valueFieldName] === value"   
  11.                    #rbl="ngModel"  
  12.                   >  
  13.             <span>{{ item[textFieldName] }}</span>  
  14.         </label>  
  15.         <div [hidden]="rbl.valid || rbl.pristine" class="alert alert-danger">  
  16.             {{name}} is required  
  17.         </div>   
  18.     </div>  
  19.   
  20. </div>  
Grid control

GridComponent has apiName for the WebAPI service which issued to load the data in the grid and array of grid columns and each column has name, label, model name properties. On component init, it calls http common service and fills data in the grid. Also, it handles the filtering feature using query property and getdata method.
  1. import { Component, OnInit, Output, Input, AfterViewInit, AfterViewChecked, EventEmitter } from '@angular/core';  
  2. import { HttpCommonService } from '../../services/http-common.service';  
  3. import { GridColumnModel } from './grid-column.model';  
  4.   
  5.  @Component({  
  6.     moduleId: module.id,  
  7.     selector: 'grid',  
  8.     templateUrl: 'grid.template.html',  
  9.         providers: [HttpCommonService]   
  10. })  
  11. export class GridComponent implements OnInit {  
  12.      data: any;  
  13.     @Input() name: string;  
  14.     @Input() apiName: string;  
  15.     @Input() columns: Array<GridColumnModel>;  
  16.   
  17.     @Input() enableFilter = true;  
  18.     query = "";  
  19.     filteredList:any;  
  20.   
  21.     constructor(private _httpCommonService: HttpCommonService) {  
  22.     }  
  23.   
  24.     getData() {  
  25.         if (this.query !== "") {  
  26.             return this.filteredList;  
  27.         } else {  
  28.             return this.data;  
  29.         }  
  30.     }  
  31.   
  32.     filter() {  
  33.         this.filteredList = this.data.filter(function (el:any) {  
  34.             var result = "";  
  35.             for (var key in el) {  
  36.                 result += el[key];  
  37.             }  
  38.             return result.toLowerCase().indexOf(this.query.toLowerCase()) > -1;  
  39.         }.bind(this));  
  40.     }  
  41.   
  42.   
  43.     ngOnInit() {  
  44.         if (this.columns == null)  
  45.         {  
  46.             this.columns =     [  
  47.                 new GridColumnModel({ name: 'name', modelName: 'name', label: 'name' }),  
  48.               ]  
  49.         }  
  50.         this._httpCommonService.getList(this.apiName).subscribe(data => {  
  51.             this.data = data  
  52.         });  
  53.     }  
  54.       
  55. }   
  56. export class GridColumnModel {  
  57.     //   value: T;  
  58.     name: string;  
  59.     label: string;  
  60.     order: number;  
  61.     modelName: string;  
  62.   
  63.   
  64.     constructor(options: {  
  65.         // value?: T,  
  66.         name?: string,  
  67.         label?: string,  
  68.         order?: number,  
  69.         modelName?: string,  
  70.           
  71.     } = {}) {  
  72.         //this.value = options.value;  
  73.         this.name = options.name || '';  
  74.         this.label = options.label || '';  
  75.         this.order = options.order === undefined ? 1 : options.order;  
  76.         this.modelName = options.modelName || '';  
  77.      }  
  78. }  
grid.template.html has name for module which is used in edit and new link, input for filter data, and table for display data on. It uses angular2-datatable from https://www.npmjs.com/package/angular2-data-table which handles sorting and paging. mfData property gets its data from getData() method, then loop on column array to load grid header. Then, loop on grid data to draw the grid rows.
  1. <div>   
  2.     <a class="mdl-navigation__link" [routerLink]="['/'+name]">New {{name}}</a>  
  3. </div>  
  4.   
  5. <label for="filter">Filter</label>  
  6. <input name="filter" id="filter" type="text" class="form-control" *ngIf=enableFilter [(ngModel)]=query  
  7.   
  8.        (keyup)=filter() placeholder="Filter" />  
  9.   
  10. <table class="table table-striped"  [mfData]="getData()" #mf="mfDataTable" [mfRowsOnPage]="5"  hidden="{{hidden}}">  
  11.     <thead>  
  12.         <tr>  
  13.             <th *ngFor="let colum of columns">  
  14.                 <mfDefaultSorter by="{{colum.modelName}}">{{colum.label}}</mfDefaultSorter>  
  15.             </th>  
  16.         </tr>  
  17.     </thead>  
  18.     <tbody>  
  19.         <tr *ngFor="let item of mf.data">  
  20.             
  21.             <td *ngFor="let colum of columns">  
  22.                    {{item[colum.modelName] ? (item[colum.modelName].name? item[colum.modelName].name : item[colum.modelName]): 'N/A'}}   
  23.             </td>  
  24.             <td><a class="mdl-navigation__link" [routerLink]="['/'+name+'',item.id]">Edit</a></td>  
  25.          </tr>  
  26.     </tbody>  
  27.     <tfoot>  
  28.         <tr>  
  29.             <td colspan="4">  
  30.                 <mfBootstrapPaginator [rowsOnPageSet]="[5,10,25]"></mfBootstrapPaginator>  
  31.             </td>  
  32.         </tr>  
  33.     </tfoot>  
  34. </table>  
datepicker control

DatePickerComponent overrides NgModel to pass the validation from custom control to the original input.
  1. import { Component, Optional, Inject, OnInit, ViewChild} from '@angular/core';  
  2. import { BaseControlComponent } from '../base/base-control.component'  
  3. import { NgModel, NG_VALIDATORS, NG_ASYNC_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms';  
  4.   
  5.   
  6. @Component({  
  7.     moduleId: module.id,  
  8.     selector: 'date-picker',  
  9.     templateUrl: 'date-picker.template.html' ,  
  10.      providers: [  
  11.          { provide: NG_VALUE_ACCESSOR, useExisting: DatePickerComponent, multi: true }  
  12.     ]  
  13. })  
  14. export class DatePickerComponent extends BaseControlComponent<string>   {  
  15.   
  16.   
  17.       
  18.     @ViewChild(NgModel) model: NgModel;  
  19.     
  20.       
  21.     constructor(  
  22.         @Optional() @Inject(NG_VALIDATORS) validators: Array<any>,  
  23.         @Optional() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<any>,  
  24.     ) {  
  25.         super(validators, asyncValidators);  
  26.     }  
  27. }  
date-picker.template.html has label, input, and required. The input type is data and it should be in string yyyy-MM-dd format.
  1. <div class="form-group" >  
  2.    
  3.     <label for="name">{{label}}</label>  
  4.     <input type="date" class="form-control" id="{{name}}"  
  5.            required="{{required}}"  
  6.            [(ngModel)]="value" name="{{name}}"  
  7.                
  8.            >  
  9.    
  10. </div>  
Radio control

RadioComponent overrides NgModel to pass the validation from custom control to the original input and it contains checked and val properties.
  1. import { Component, Optional,Inject,ViewChild, OnInit, Output, Input, AfterViewInit, AfterViewChecked, EventEmitter } from '@angular/core';  
  2. import { BaseControlComponent } from '../base/base-control.component'  
  3. import { NgModel, NG_VALIDATORS, NG_ASYNC_VALIDATORS, NG_VALUE_ACCESSOR} from '@angular/forms'  
  4.   
  5. @Component({  
  6.     moduleId: module.id,  
  7.     selector: 'radio',  
  8.     templateUrl: 'radio.template.html',  
  9.     providers: [  
  10.         { provide: NG_VALUE_ACCESSOR, useExisting: RadioComponent, multi: true }  
  11.     ]  
  12. })  
  13. export class RadioComponent extends BaseControlComponent<string>{  
  14.     @ViewChild(NgModel) model: NgModel;  
  15.     @Input() checked: boolean = false;  
  16.     @Input() val:string  
  17.     constructor(  
  18.         @Optional() @Inject(NG_VALIDATORS) validators: Array<any>,  
  19.         @Optional() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<any>,  
  20.     ) {  
  21.         super(validators, asyncValidators);  
  22.     }  
  23.  }  
radio.template.html has label, input with radio type. It has val property to pass value the the original control through original value property and checked property.

Note

When I set the val property name to be value, it returns on, instead of the right value, so I changed its name to be val.
  1. <div class="form-group">  
  2.     <label for="{{name}}">{{label}}</label>  
  3.       
  4.   
  5.     <input #rb    
  6.             id="{{id}}"  
  7.             name="{{name}}"   
  8.             [value]="val"  
  9.             type="radio"  
  10.             [checked]="value == rb.value"   
  11.            (click)="value = rb.value"  
  12.              
  13.            >      
  14. </div>  
Checkbox control

CheckboxComponent overrides NgModel to pass the validation from custom control to the original input and it contains checked property.
  1. import { Component, OnInit, Inject,Optional,Output, ViewChild, Input, AfterViewInit, AfterViewChecked, EventEmitter } from '@angular/core';  
  2. import { BaseControlComponent } from '../base/base-control.component'  
  3. import { NgModel, NG_VALIDATORS, NG_ASYNC_VALIDATORS, NG_VALUE_ACCESSOR} from '@angular/forms'  
  4.   
  5. @Component({  
  6.     moduleId: module.id,  
  7.     selector: 'checkbox',  
  8.     templateUrl: 'checkbox.template.html',  
  9.     providers: [  
  10.         { provide: NG_VALUE_ACCESSOR, useExisting: CheckboxComponent, multi: true }  
  11.     ]  
  12. })  
  13. export class CheckboxComponent extends BaseControlComponent<string>{  
  14.   
  15.     @ViewChild(NgModel) model: NgModel;  
  16.     constructor(  
  17.         @Optional() @Inject(NG_VALIDATORS) validators: Array<any>,  
  18.         @Optional() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<any>,  
  19.     ) {  
  20.         super(validators, asyncValidators);  
  21.     }  
  22.  }  
checkbox.template.html has label, input with checkbox type. It has checked property.
  1. <div class="form-group">  
  2.     <label for="{{name}}">{{label}}</label>  
  3.     <input  type="checkbox"  
  4.             id="{{name}}"  
  5.            [(ngModel)]="value" name="{{name}}"   
  6.            #chk="ngModel"   
  7.             hidden="{{hidden}}"  
  8.            >  
  9.     <div [hidden]="chk.valid || chk.pristine"  
  10.          class="alert alert-danger">  
  11.         {{name}} is required   
  12.     </div>  
  13.       
  14. </div>  
For Server side

The ASP.NET Core Web API is used by following the steps in https://docs.microsoft.com/en-us/aspnet/core/data/ef-mvc/intro and changing MVC Controllers to Web API Controllers and add the below configurations to Startup.cs.
  1.  public void ConfigureServices(IServiceCollection services)  
  2.        {  
  3.   
  4.  services.AddDbContext<SchoolContext>(options =>  
  5.    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));  
  6.   
  7.   
  8. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, SchoolContext context)  
  9.        {  
  10.   
  11.   
  12.  app.UseStaticFiles(new StaticFileOptions()  
  13.            {  
  14.                FileProvider = new PhysicalFileProvider(  
  15.           Path.Combine(Directory.GetCurrentDirectory(), @"client")),  
  16.                RequestPath = new PathString("/client")  
  17.            });  
  18.   
  19.   
  20.   
  21.   
  22. DbInitializer.Initialize(context);  
Note - The connection string is in appsettings.json.

"ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database= Angular2WithAspNetCoreWebAPI;Trusted_Connection=True;MultipleActiveResultSets=true" }

DbInitializer.Initialize used add dummy data in database to test the grid

The source code for the project is attached on top .

Points of Interest

Using TypeScript and Angular 2 to build custom controls library that will be easier than writing label and validation message for each control on the screen. Also, use the inheritance concept by adding base control and base form. In addition, fixing binding issue in base control, and adding crud operations once in base form, fill grid by setting its API name and its columns list, as well as fill dropdown list by setting its API name, textFieldName, and valueFieldName.

References

 
The Tour of Heroes tutorial takes you through the steps of creating an Angular application in Typescript.
  • https://angular.io/docs/ts/latest/tutorial/
  • https://angular.io/docs/ts/latest/tutorial/toh-pt1.html
Passing data to and from a nested component in Angular 2,
  • http://blog.rangle.io/angular-2-ngmodel-and-custom-form-components/
  • https://www.themarketingtechnologist.co/building-nested-components-in-angular-2/
TWO-WAY DATA BINDING IN ANGULAR
  • https://blog.thoughtram.io/angular/2016/10/13/two-way-data-binding-in-angular-2.html
Table component with sorting and pagination for Angular2
  • https://github.com/mariuszfoltak/angular2-datatable