Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Angular 2 Custom Controls with ASP.NET Core WebAPI

4.83/5 (3 votes)
25 Jan 2017CPOL7 min read 20.2K   485  
Angular 2 Type Script Custom Controls Integrated with ASP.NET Core web API

Introduction

This library contains eight Angular2 custom controls. Each control has its label and its validations. Grid and dropdown get data dynamically using API name.

  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 service:

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

Prerequisites

Using the Code

Example for Using the Custom Form Controls

Create a new component for student form client\app\components\student\student-form.component.ts that inherit from baseFormComponent to use save and load methods from it, then pass ASP.NET Core API name to base form constructor to use API name in get and post data. In this example, we pass "Students" as API name.

TypeScript
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");              
    }
}

Add the 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 controls, 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".

HTML
<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>
HTML
<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 new component for students list client\app\components\student\student-list.component.ts, then add grid columns array contains displayed columns. Each column has name, modelName and label properties. Sorting, paging and filtering features are included in the grid component.

HTML
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' }),
         ]
    }
}

Add the custom grid to a list template client\app\components\student\student-list.template.html then set apiname and column property (it will get it from the StudentListComponent component). The grid control has sorting, paging and filtering features.

TypeScript
<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 and it is 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 patterns object for regular expression validations for email and url or any extra validations by adding regular expression for each textType (email, url, tel) which will be used in child controls' html template.

HTML
<input  type="{{textType}}"  pattern="{{patterns[textType]}}">
JavaScript
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

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.

JavaScript
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

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 .

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

Add Route Configuration for the New Component (Student form, student list)

The pages 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

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

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]:

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>

Note: Edit link will be in list form in the grid control.

HTML
<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>

Adding controls and forms components to the main module client\app\app.module.ts

TypeScript
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 Control

TextboxComponent overrides NgModel to pass the validation from custom control to the original input.

JavaScript
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 has label, input and required, maxlength, minlength and pattern validations.

HTML
<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 Control

TextboxMultilineComponent to override NgModel to pass the validation from custom control to the original input:

JavaScript
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 has label, textarea to support multi line text and required, maxlength, minlength and pattern validations.

HTML
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 Control

DropdownComponent 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 select options, filed name for option value and field name for option text. On component init, it call http common service and fill items arrays which will used in the template to load the select options.

JavaScript
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 has label, select, required and the logic for fill select options from items array and bind value and text using valueFieldName and textFieldName.

HTML
<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 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 radio list, field name for its value and field name for its text. On component init, it call http common service and fill items arrays which will be used in the template to load the radio list.

JavaScript
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 has label, inputs of type radio, required and the logic for fill radio list from items array and bind value and text using valueFieldName and textFieldName.

HTML
<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 Control

GridComponent has apiName for the webapi service which will be used 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 call http common service and fill data in the grid. Also, it will handle the filtering feature using query property and getdata method.

JavaScript
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 has name for module which is used in edit and new link, input for filter data and table for display data on it used 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 loops on column array to load grid header, then loops on grid data to draw the grid rows.

HTML
<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 Control

DatePickerComponent overrides NgModel to pass the validation from custom control to the original input:

JavaScript
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 has label, input and required. The input type is data and it should be in string yyyy-MM-dd format.

HTML
<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 Control

RadioComponent overrides NgModel to pass the validation from custom control to the original input and it contains checked and val properties.

JavaScript
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 has label, input with radio type. It has val property to pass value to 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:

JavaScript
<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 Control

CheckboxComponent overrides NgModel to pass the validation from custom control to the original input and it contains checked property.

JavaScript
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 has label, input with checkbox type. It has checked property.

HTML
<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> 

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.

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

Note

The connection string is in appsettings.json:

JavaScript
"ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\mssqllocaldb;
Database=aspnet-Angular2CodeProject;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 at the top of this post.

Points of Interest

Use type script 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, fix binding issue in base control, and add 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. Moreover, adding common service to handle all http crud operations.

References

Getting started with ASP.NET Core MVC and Entity Framework Core using Visual Studio (1 of 10)

The Tour of Heroes tutorial takes you through the steps of creating an Angular application in TypeScript.

Passing data to and from a nested component in Angular 2

TWO-WAY DATA BINDING IN ANGULAR

Table component with sorting and pagination for Angular2

History

  • 25th January, 2017: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)