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.
textBox
textbox-multiline
date-picker
dropdown
grid
checkbox
radio
radio list
Also, the library contains base classes and common http service:
base-control-component
base-form-component
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.
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{
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"
.
<div class="container">
<div>
<!--
<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>
When the controls are empty and required, the save button will be disabled and red sign will appear in the control.
Note: t
extType
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.
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.
<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.
<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
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.
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;
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'];
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() {
}
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' });
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 .
import { Injectable } from "@angular/core";
import { Http, Response, ResponseContentType, Headers, RequestOptions,
RequestOptionsArgs, Request, RequestMethod, URLSearchParams } from "@angular/http";
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';
let headers = new Headers({ 'Content-Type': contentType});
let options = new RequestOptions({
headers: headers,
});
return options;
}
stringifyModel(model: any)
{
return JSON.stringify(model);
}
create(apiName: string, model: any) {
return this.http.post(this.apiBaseUrl + apiName,
this.stringifyModel(model),
this.requestOptions())
.map(this.extractData)
.catch(this.handleError)
;
}
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());
}
delete(apiName:string,id:any) {
return this.http.delete(this.apiBaseUrl + apiName + '/' + id);
}
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) {
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();
}
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
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';
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]
:
<div id="wrapper">
<!--
<div id="sidebar-wrapper">
<nav class="mdl-navigation">
<ul class="sidebar-nav">
<li class="sidebar-brand">
<!--
Accounting System
<!--
</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>
<!--
<!--
<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>
<!--
</div>
Note: Edit link will be in list form in the grid control.
<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
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';
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.
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.
<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:
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.
div class="form-group"><!--
<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.
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;
ngOnInit() {
super.ngOnInit();
if (this.apiName !=null){
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
.
<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.
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
.
<div class="form-group"> <!--
<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.
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 {
name: string;
label: string;
order: number;
modelName: string;
constructor(options: {
name?: string,
label?: string,
order?: number,
modelName?: string,
} = {}) {
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.
<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:
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.
<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.
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
:
<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.
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.
<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.
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:
"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