Introduction
The purpose of this article is to explain how to build a very basic custom user interface control component that can be reused in other components. I'm making the assumption that you already have knowledge of setting up an Angular 2 environment and the topic will not be covered in this article. We will write a very simple text input component that can be configured to
- Validate that a value was entered (We will be able to switch this validation on or off)
- Validate that the entered value was 2 characters or longer in length (We will be able to switch this validation on or off and make the minimum length configurable)
- Provide validation messages
- Provide a maximum length
- Provide error and ok icon classes
- Provide a place holder
Background
During my last few projects I realized that I rewrite the same user interface components over and over again. Most of my time is spent writing input controls with their associated validation rules rather than focusing on the important development such as the business rules of the application. Don't get me wrong, input controls and client validation is also of vital importance and something that a lot of developers overlook, but I firmly believe that this one of the areas where the 80/20 principle can be applied. Doing 20% of the work can save us 80% on the overall time spent during application development. And because these components are reusable it will have a tremendous time impact on future project timelines.
Before we start I have to mention an article that I used extensively in writing my first custom component, Connect your custom control to ngModel with Control Value Accessor , I learned most of the important concepts from this article and simply reworked this example into somthing that was applicable to my own unique projects.
Software Versions Used
- Angular 2.1.1
- Bootstrap 3.3.7 (Please note that Bootstrap is not required)
- Font-Awesome 4.7.0 (Please note that Font-Awesome is not required)
Angular 2 Project Structure
I use the following structure for my Angular 2 projects
TextInputComponent.ts
Let's dive right in and start with the reusable text input component code. Below is the entire TypeScript file which I will explain section by section
import { Component, forwardRef, Input } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';
const noop = () => {
};
export const textInputValueAccessor: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TextInputComponent),
multi: true
};
@Component({
selector: 'TextInput',
moduleId: module.id,
templateUrl: './Templates/TextInput.html',
providers: [textInputValueAccessor]
})
export class TextInputComponent implements ControlValueAccessor {
@Input() ValidationOkFontClass: string;
@Input() ValidationErrorFontClass: string;
@Input() ValidationRuleRequired: boolean;
@Input() ValidationRuleRequiredDescription: string;
@Input() ValidationRuleMinimumLength: boolean;
@Input() MinimumLength: number;
@Input() ValidationRuleMinimumLengthDescription: string;
@Input() MaximumLength: number;
@Input() PlaceHolder: string;
InnerValue: any = '';
private onTouchedCallback: () => void = noop;
private onChangeCallback: (_: any) => void = noop;
get value(): any {
return this.InnerValue;
};
set value(v: any) {
if (v !== this.InnerValue) {
this.InnerValue = v;
this.onChangeCallback(v);
}
}
writeValue(value: any) {
if (value !== this.InnerValue) {
this.InnerValue = value;
}
}
registerOnChange(fn: any) {
this.onChangeCallback = fn;
}
registerOnTouched(fn: any) {
this.onTouchedCallback = fn;
}
}
The first step is to inform NG_VALUE_ACCESSOR
which component to use for data bindings. We do this by importing NG_VALUE_ACCESSOR
which is part of the Angular forms package
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';
and creating a custom value accesor that can be used by our component
export const textInputValueAccessor: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TextInputComponent),
multi: true
};
Next we have to set up our component decorator where we specify our textInputValueAccessor
as a provider
@Component({
selector: 'TextInput',
moduleId: module.id,
templateUrl: './Templates/TextInput.html',
providers: [textInputValueAccessor]
})
Now we create our class implementing the ControlValueAccessor
interface which interface exposes the functions we will need to connect ngModel
to our component.
export class TextInputComponent implements ControlValueAccessor
The ControlValueAccessor
interface requires the implementation of three functions
writeValue(obj: any)
: void (Used to write a new value to the element) registerOnChange(fn: any)
: void (Register the function to be called when the change event occur) registerOnTouched(fn: any)
: void (Register the function to be called when the touch event occur)
These functions are straight forward and will not be discussed further in this article, for more information you can read the ControlValueAccessor Angular Documentation
We want to make this component as configurable as possible so that it is easy to reuse in other components or projects, so let's create the input parameters that we can configure when setting up the component
@Input() ValidationOkFontClass: string;
@Input() ValidationErrorFontClass: string;
@Input() ValidationRuleRequired: boolean;
@Input() ValidationRuleRequiredDescription: string;
@Input() ValidationRuleMinimumLength: boolean;
@Input() MinimumLength: number;
@Input() ValidationRuleMinimumLengthDescription: string;
@Input() MaximumLength: number;
@Input() PlaceHolder: string;
The input parameters will be used for
ValidationOkFontClass
(Used to specifiy the class that will be used for the icon when validation succeeds) ValidationErrorFontClass
(Used to specifiy the class that will be used for the icon when validation failed) ValidationRuleRequired
(Used to specifiy if a value is required or not) ValidationRuleRequiredDescription
(Used to specifiy the message that will be displayed if a value is required) ValidationRuleMinimumLength
(Used to specifiy if minimum lenght validation should be applied to the value provided) ValidationRuleMinimumLengthDescription
(Used to specifiy the message that will be displayed if minimun lenght validation is applied) MaximumLength
(Used to specifiy the minimum length when minimum lenght validation is applied) PlaceHolder
(Used to specifiy the place holder)
Next we have an onTouchedCallback
and onChangeCallback
that is set to the noop
function that was created as a const just after the import statements.
const noop = () => {
};
private onTouchedCallback: () => void = noop;
private onChangeCallback: (_: any) => void = noop;
I remember my confusion when i saw this for the first time. Why would we assign these callback functions to a dummy function doing nothing? Do not worry about this too much, the dummy function is simply a placeholder for the callback functions until they are registered by Angular.
The last step in the creation of our TextInputComponent
is to create the getter and setter functions that will enable us to bind our component value to ngModel.
get value(): any {
return this.InnerValue;
};
set value(v: any) {
if (v !== this.InnerValue) {
this.InnerValue = v;
this.onChangeCallback(v);
}
}
TextInput.html
Now that we have completed the TextInputComponent
, we can create the html template
This is a very simple text input control where we display the validation rules based on the input parameters we configure and I won't go into to much detail, there is however one very important binding I want to highlight
[(ngModel)]="value"
Here we bind ngModel to the value getter and setter functions of our TextInputComponent
TextInputModule.ts
Now let's take care of the boring work and create the TextInputModule
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { TextInputComponent } from '../Components/TextInputComponent';
@NgModule({
imports: [BrowserModule, FormsModule],
declarations: [TextInputComponent],
exports: [TextInputComponent],
bootstrap: [TextInputComponent]
})
export class TextInputModule { }
Make sure that you the component is exported, this will make the component reusable by other components
exports: [TextInputComponent]
How do I use the component?
First off all import the component in the module class of the component that will use it, let's call it SomeComponent
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { SomeComponent } from '../Components/SomeComponent';
import { TextInputModule } from '../Modules/TextInputModule';
@NgModule({
imports: [BrowserModule, FormsModule, HttpModule, TextInputModule],
declarations: [SomeComponent],
bootstrap: [SomeComponent]
})
export class SomeModule { }
Then configure the component in the HTML template of SomeComponent