This post currently reflects an earlier version of Angular, and may not be up to date or reflect the current release. Learn more about Angular migration, here.
I’ve heard a lot of people express frustration with building forms in Angular 2. I think in large part this derives from bad experiences with FormBuilder, FormGroup and FormControl. This blog post explores a new way of building complex forms in Angular 2, using ngModel, the ControlValueAccessor contract, and some clever validation code.
Interested in staying up to date on the latest in innovation? Never miss an update by signing up for our newsletter, here.
Suppositions
- You are building complex forms that have custom validators, styling, and behaviours that make using plain <input> tags all over the place unsuitable
- You therefore want custom form input controls that bind to ngModel and have complex validation behaviour
- You dislike FormBuiler, FormControl and FormGroup and find that they reduce maintainability and understandability of the code: you want to define your forms in your template only, not in code
- You perhaps think that ngModel is only suitable for binding plain jane <input> controls to values, and that ngModel does not work for custom form components
ngModel in fact can be used to build great, template-driven forms that allow you put more focus on your data model and less focus on creating the right FormBuilder structures. You can have custom components and complex validation, too.
Therefore, we are going to build some form input controls that behave like core input elements like <select>, <input> and <textarea>, and in fact look like normal base input elements as far as the Angular forms implementation is concerned. If we follow this contract correctly, we will be able to apply any validators or bindings that you can apply to a base input control like <input>. To Angular, there will be no difference whatsoever between the standard <input> control and our custom <form-text> control. The difference will be that our custom input will also display inline validation failure messages, and will dynamically style itself based on whether the value inside is valid or invalid (i.e. we will set a red border on ourselves when we fail to validate).
Standard form element usage:
<input type="text" [(ngModel)]="myValue" required />
Our custom form element component usage:
<form-text [(ngModel)]="myValue" required />
But our control will, as mentioned, also render inline validation failure messages, whereas the standard <input> control will not.
To summarize:
- We are going to build a custom form input with complex validators and two-way data binding with ngModel.
- Our controls will not know ahead of time what validators will be applied to them, but they will style themselves with red borders if they fail to validate. Validators will be specified at the place that you use the control, not inside of the control itself. They will be queried through the hierarchical dependency injection system. We will ask our parent scope for them through @Inject(), not @Input().
- Our controls will be good citizens and will output data that is appropriate to the element. For example, a checkbox control should supply booleans to its parent, and a text input will supply strings.
Getting started with the implementation
Let’s lay out what our requirements are first. We want custom form components with clean styles and advanced functionality like:
- The ability to apply multiple arbitrary validators to the same control
- Red outlines and warnings when the inputs do not validate
- Detailed list of validation errors that notify the user what is wrong with their inputs
- Reusability in a variety of contexts
- A single source of form structure information with no duplication (everything about the forms should be in the template, not the TypeScript code)
Our two form controls, a text input and a dropdown control, will come together to look like the image below. (The complete source code and unit tests are available at this link.)
A Form Container Element
First, let’s create a form context which will be responsible for aggregating our form inputs into a single form instance (). When we call form.valid on this instance, it will give us the aggregate validity state of all its inputs. When we call form.value, it will collect values from every input in the form and give it back to us in the form of a simple object.
The beauty of this is that we never have to tell NgForm what our inputs are. It simply will query the dependency injector and retrieve all instances of NG_VALUE_ACCESSOR and add them to our form control. Then it will automatically create and maintain a object for us! We never have to do new FormGroup({}) at any time, but we still have access to the form property of our NgForm instance. NgForm::form is a reference to a FormGroup object that we can write code around. Beautiful!
Our custom components will have full and complete integration with the Angular forms library, and will be indistinguishable from standard basic <input> elements, at least as far as @angular/forms is concerned.
There are a couple other things we are going to manage at the <form> level:
- We want to disable our “Submit” button if the form fails validation.
- We want to have a form-level validation state that tells us if all of the inputs in our control are currently valid. This part will be mostly taken care of by the Angular forms library implementation, but we will at least demonstrate how to use it.
- Since we are building our form template, let’s also use this opportunity to determine what we want our contract to be for our form input controls. I often find that trying to create some code that uses my component before it has even been defined can clarify my thinking about what I want my component contract to look like. I create the ideal usage scenario of my component, and then I implement the component in such a way that it will meet that contract. This ensures that my focus remains on creating a good, sensical contract for my component, instead of focusing on the implementation details and creating a component contract that ends up being difficult to use but easy to implement.
So let’s create our entire form container now, along with usages of our nonexistent input components:
<form #form="ngForm" (ngSubmit)="onSubmit(form.value)">
<form-text
required
hexadecimal
name="hexadecimalValue"
label="Value 1"
placeholder="Enter a hexadecimal value"
[(ngModel)]="hexadecimalValue">
</form-text>
<form-select
required
name="dropdownValue"
label="Value 2"
placeholder="Select a dropdown value"
[(ngModel)]="dropdownValue">
<option [value]="1">Option One</option>
<option [value]="2">Option Two</option>
<option [value]="3">Option Three</option>
<option [value]="4">Option Four</option>
</form-select>
<button type="Submit" [disabled]="!form.valid">Submit</button>
</form>
First, we have the <form> element, which is the container element of all of our form inputs. The Angular forms library is smart enough to understand that all <form> elements should have an NgForm directive attached to them. It will also collect all of the input controls inside of that form and associate them with the automatically-managed FormGroup object on NgForm, such that when you ask for form.value it will give you an object that is the same shape of your form (using name attributes to determine property names). In our case, form.value will provide an object that looks like this:
{email: 'email@address.com', gender: 'male'}
This object is a great representation of our data, extracted right out of the NgForm object. But the best thing about this form template is that it requires almost no TypeScript code in order to implement it. Our entire form is delineated using template components, and even the values we get back are constructed using the shape of the form in the template.
A couple more points
Notice the required attribute on both of our form inputs. required is a basic HTML5 form validation attribute which Angular also understands. We are essentially saying that both of our inputs must have input or selection from the user in order to be considered valid. In the first input, we also use a custom validator, hexadecimal, which will validate that the user input is a valid base-16 number (af02c3f, for example). We will explain more about the custom validator directive implementation later.
In the case of our form-select input, we are projecting the options into that control from the actual usage of the control. This allows us to avoid a nasty hack like using @Input() to provide an array of all of the possible options the user can select. We just ask our parent to inject the options into form-select, and it will render them in the correct spot using <ng-content>.
Now that we have an idea of how we want to use our controls and validators, let’s start figuring out how to implement them.
NgModule
The first thing we need to do is ensure that is part of our imports list in our @NgModule() declaration. This ensures that the various form-related directives will be available to our application.
ValidationComponent
Next let’s build a validation failure component, which will be shared amongst all of our form inputs and will be used when we have some validation failures. This component will display actual validation failure messages inline, next to the input elements themselves. We’ll call it ValidationComponent. It will print things like Please enter a value next to the control if it is empty. It will be a basic dumb component:
import {Component, Input} from '@angular/core';
@Component({
selector: 'validation-messages',
template: `
<div class="container">
<ul>
<li *ngFor="let message of messages">{{message}}</li>
</ul>
</div>
`
})
export class ValidationMessages {
@Input() messages: Array<string>;
}
Perfect. We will come back and style this component a bit later, but this will do for now.
Implementing our first form control
Next let us begin with our text input component, form-text (or FormTextComponent). The first thing we have to think about is the fact that our component must integrate with ngModel. Why? Because we want to be able to bind the input value of our control to a two-way ngModel directive, the same way we would bind a regular <input> element. How do we do this? In a word: . Our component must implement the ControlValueAccessor contract. With that in mind, let us take a look at our basic component definition:
import {Component, Input} from '@angular/core';
import {
NG_VALUE_ACCESSOR,
} from '@angular/forms';
@Component({
selector: 'form-text',
template: `
<div>
<input type="text" [(ngModel)]="value" />
</div>
`,
providers: [
{provide: NG_VALUE_ACCESSOR, useExisting: FormTextComponent, multi: true}
],
})
export class FormTextComponent {}
- We are telling the dependency injector that we implement ControlValueAccessor, (NG_VALUE_ACCESSOR), but we do not actually implement it. We have to add implementations for those methods and abstract them into something we can share amongst all form components.
- There is no validation happening on our input control
So we need to remedy these problems before we can move on. The first thing we need to do is implement the ControlValueAccessor interface. But we are going to be implementing this interface on all of our input elements, so rather than copy and paste the same code into all of our form elements, we will create an abstract base class that manages the ControlValueAccessor code. We can then simply extends that base class and make use of it in all our form elements.
Common ValueAccessor abstract base
Let’s implement this base class. First let’s take a look at ControlValueAccessor:
interface ControlValueAccessor {
writeValue(obj: any): void
registerOnChange(fn: any): void
registerOnTouched(fn: any): void
setDisabledState(isDisabled: boolean): void
}
In order to implement this interface, we are going to need to have implementations for each of these methods. But before we do, let’s understand what’s going on here. writeValue is used to update the value of our control, but it takes an argument of type any. In fact almost all of these methods just accept arguments of type any and therefore it’s a poor interface declaration. But we know what type of arguments these functions are going to take, so let’s use real type declarations in our implementation, with the magic of a TypeScript generic class definition.
The second thing to understand is that our form elements are going to bind to ngModel, and should bind values that are appropriate for the control itself. For example, a checkbox control ought to provide a boolean value to ngModel (checked or not checked). A textbox control should provide a string value (the text). A number control should provide a number object to ngModel. A radio group should provide an enum type to ngModel. This ensures that the form values we get back from our controls are appropriate to the data types they are working with. Therefore our first abstract base class will be a generic <T> class that will accept a type argument that matches the data type that class will provide to ngModel. It will look like this:
import {ControlValueAccessor} from '@angular/forms';
export class ValueAccessorBase<T> implements ControlValueAccessor {
private innerValue: T;
private changed = new Array<(value: T) => void>();
private touched = new Array<() => void>();
get value(): T {
return this.innerValue;
}
set value(value: T) {
if (this.innerValue !== value) {
this.innerValue = value;
this.changed.forEach(f => f(value));
}
}
touch() {
this.touched.forEach(f => f());
}
writeValue(value: T) {
this.innerValue = value;
}
registerOnChange(fn: (value: T) => void) {
this.changed.push(fn);
}
registerOnTouched(fn: () => void) {
this.touched.push(fn);
}
}
We need a couple more things: validate method, and invalid and failures methods. These methods need to be implemented in such a way that they will work for any type of input control. In theory, you could also add this code to ValueAccessorBase<T>, but I chose to create another abstract base class called ElementBase<T> which extends ValueAccessorBase<T>. We will inherit our various input controls from ElementBase<T>. It is responsible for supplying validity state and methods to the derived component, whereas ValueAccessorBase is responsible for implementing the control value accessor pattern from Angular. Now once we create a control and use extends ElementBase<T>, we already have all of the state information that we need in order to render ourselves. This allows us to have zero implementation code in the actual control implementations themselves -- just templates and a constructor. Therefore our controls will be dead simple to implement and will all share the same logic relating to model changes and validity. Each element will need to know its validity state for rendering purposes, so it makes sense to put this functionality into an abstract class that can be shared amongst all form inputs component implementations.
To summarize:
- ValueAccessor<T>
- Implements the Control Value Accessor pattern from Angular
- ElementBase<T> extends ValueAccessor<T>
- Provides observable values to its parent about its validity state
- Provides a validate method which can also be used in the control and which works off of validators injected through hierarchical dependency injection (eg. a required or minlength directive).
- Actual @Component for input controls will extend ElementBase<T> where T is the type of data that control deals with ( string for text box, boolean for checkbox, etc).
Let’s break down the ElementBase<T> implementation itself:
import {NgModel} from '@angular/forms';
import {Observable} from 'rxjs';
import {ValueAccessorBase} from './value-accessor';
import {
AsyncValidatorArray,
ValidatorArray,
ValidationResult,
message,
validate,
} from './validate';
export abstract class ElementBase<T> extends ValueAccessorBase<T> {
protected abstract model: NgModel;
// we will ultimately get these arguments from @Inject on the derived class
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)));
}
}
Validate() returns an Observable of an object containing keys for any failed validatorsinvalid is an Observable<boolean> which will be true when any of the validators attached to the control through the dependency injector are failingfailures is an Observable<ValidationResult> which is an object containing a property and a message (or a boolean, for Angular-supplied validators) for each validator that is failing.
With that done, we will inherit from this class in our FormTextComponent and bind our internal ngModel. Not the ngModel which the control itself will bind to in usages of <form-text>, but the internal ngModel binding we will use to bind our <input> control to our internal value state. Our internal input control, <input>, will be bound to this ngModel. value itself is defined as a getter/setter on the ValueAccessor<T> base, and it will call Angular callback functions when value is changed.
<input type="text" [(ngModel)]="value" />
And:
export class FormTextComponent extends ValueAccessorBase<string> {}
Now <form-text> can be bound to ngModel through the magic of ControlValueAccessor, and which uses ngModel internally to manage its own state. So this usage has become valid:
<form-text [(ngModel)]="myValue"></form-text>
But we still have not implemented any validation. We are calling it from ElementBase but it is not defined yet. Let’s do that. It’s just a free-standing function which itself returns a function that accepts a control argument. That nested function is responsible for doing the ultimate validation of the control. We therefore need a validate method of this signature:
validate(
validators: Array<Validator | ValidatorFn>,
asyncValidators: Array<Validator | AsyncValidatorFn>
): (control: AbstractControl) => ({[failure: string]: boolean | string});
It will be called like this when we need to validate our controls, and it will be given validators that we acquired from the hierarchical dependency injector (i.e., we got them from the usage of our component):
constructor(
@Optional() @Inject(NG_VALIDATORS) private validators: Array<any>,
@Optional() @Inject(NG_ASYNC_VALIDATORS) private asyncValidators: Array<any>,
) {
super(); // ValueAccessor base
}
validate() {
return validate(this.validators, this.asyncValidators)(this.model.control);
}
Our validate method will be implemented like so:
import {
AbstractControl,
AsyncValidatorFn,
Validator,
Validators,
ValidatorFn,
} from '@angular/forms';
import {Observable} from 'rxjs';
export type ValidationResult = {[validator: string]: string | boolean};
export type AsyncValidatorArray = Array<Validator | AsyncValidatorFn>;
export type ValidatorArray = Array<Validator | ValidatorFn>;
const normalizeValidator =
(validator: Validator | ValidatorFn): ValidatorFn | AsyncValidatorFn => {
const func = (validator as Validator).validate.bind(validator);
if (typeof func === 'function') {
return (c: AbstractControl) => func(c);
} else {
return <ValidatorFn | AsyncValidatorFn> validator;
}
};
export const composeValidators =
(validators: ValidatorArray): AsyncValidatorFn | ValidatorFn => {
if (validators == null || validators.length === 0) {
return null;
}
return Validators.compose(validators.map(normalizeValidator));
};
export const validate =
(validators: ValidatorArray, asyncValidators: AsyncValidatorArray) => {
return (control: AbstractControl) => {
const synchronousValid = () => composeValidators(validators)(control);
if (asyncValidators) {
const asyncValidator = composeValidators(asyncValidators);
return asyncValidator(control).map(v => {
const secondary = synchronousValid();
if (secondary || v) { // compose async and sync validator results
return Object.assign({}, secondary, v);
}
});
}
if (validators) {
return Observable.of(synchronousValid());
}
return Observable.of(null);
};
};
Essentially what we are doing here is we are accepting validators of all shapes and sizes, and producing a single asynchronous validator function. We can accept:
- Synchronous validator objects or functions
- Asynchronous validator objects or functions
And we will always produce an AsyncValidatorFn which we can use to collect the results of any and all validators attached to our inputs, and produce a single resulting validation object containing all validation failures in one place.
We also need the messages function below because internally, Angular validators only return boolean values, not error messages. Our validators will always return error messages, but for the built in validator directives, we need to translate them into messages ourselves. Your code should go the extra distance to produce good validation messages, unlike mine above.
export const message = (validator: ValidationResult, key: string): string => {
switch (key) {
case 'required':
return 'Please enter a value';
case 'pattern':
return 'Value does not match required pattern';
case 'minlength':
return 'Value must be N characters';
case 'maxlength':
return 'Value must be a maximum of N characters';
}
switch (typeof validator[key]) {
case 'string':
return <string> validator[key];
default:
return `Validation failed: ${key}`;
}
};
Let’s edit our FormTextComponent control and add support for validation. First we need to add a constructor which will query the dependency injector for all synchronous and asynchronous validators associated with our component. Therefore when someone attaches eg. required and minlength directives to our <form-text> control, we will get a copy of their instances in our control constructor. Then we can use it to determine our own validity state.
import {
NgModel,
NG_VALUE_ACCESSOR,
NG_VALIDATORS,
NG_ASYNC_VALIDATORS,
} from '@angular/forms';
/// …
constructor(
@Optional() @Inject(NG_VALIDATORS) private validators: Array<any>,
@Optional() @Inject(NG_ASYNC_VALIDATORS) private asyncValidators: Array<any>,
) {
super(); // ValueAccessor base
}
Now let’s edit our template and add a style class when our component is not validating (invalid) and an instance of the <validation-messages> component we created above, which will show all validation failure messages:
<div class="container">
<input
type="text"
[(ngModel)]="value"
[ngClass]="{invalid: (invalid | async)}" />
<validation *ngIf="invalid | async" [messages]="failures | async">
</validation-messages>
</div>
In a real application, you would also add model.control.touched and make it short-circuit the invalid logic in the event the control was untouched. In essence, we would not want to show validation failures until the user has interacted with the control in some way (typically by focusing the control). This way we would not show the user a big wall of red messages as soon as they load the form. But for the purposes of our example app we can skip that part.
To match those classes, we must add a style class (invalid) to our component as well:
styles: [`.invalid { border: 1px solid red; }`],
Now we have a form control which is able to accept validators, but only the usage of our component will specify the validators: the control itself doesn’t have to know about them ahead of time. For example we may want to use a required validator:
<form-text required [(ngModel)]="myText"></form-text>
Custom validator implementation
Now that our control is able to handle validation gracefully, I will show you how to implement a custom validator directive. It’s very simple! The recipe looks like this:
import {Directive} from '@angular/core';
import {
NG_VALIDATORS,
AbstractControl,
} from '@angular/forms';
@Directive({
selector: '[hexadecimal][ngModel]',
providers: [
{ provide: NG_VALIDATORS, useExisting: HexadecimalValueValidator, multi: true }
]
})
export class HexadecimalValueValidator {
validate(control: AbstractControl): {[validator: string]: string} {
const expression = /^([0-9a-fA-F]+)$/i;
if (!control.value) { // the [required] validator will check presence, not us
return null;
}
const value = control.value.trim();
if (expression.test(value)) {
return null;
}
return {hexadecimal: 'Please enter a hexadecimal value (alphanumeric, 0-9 and A-F)'};
}
}
Note that we are returning a validation failure message, not a boolean. This allows us to display the message inside our form element - even though we don’t know ahead of time what that validator or its message will be.
Now that we have all that done, creating a new form input component is dead simple
For example, here is a form-select component. It’s defined in a single file and is very easy to understand:
import {
Component,
Optional,
Inject,
Input,
ViewChild,
} from '@angular/core';
import {
NgModel,
NG_VALUE_ACCESSOR,
NG_VALIDATORS,
NG_ASYNC_VALIDATORS,
} from '@angular/forms';
import {ElementBase, animations} from '../form';
@Component({
selector: 'form-select',
template: `
<div>
<label *ngIf="label" [attr.for]="identifier">{{label}}</label>
<select
[(ngModel)]="value"
[ngClass]="{invalid: (invalid | async)}"
[id]="identifier">
<option value="" disabled selected *ngIf="placeholder">{{placeholder}}</option>
<ng-content></ng-content>
</select>
<validation
[@flyInOut]="'in,out'"
*ngIf="invalid | async"
[messages]="failures | async">
</validation>
</div>
`,
animations,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: FormSelectComponent,
multi: true,
}],
})
export class FormSelectComponent extends ElementBase<string> {
@Input() public label: string;
@Input() public placeholder: string;
@ViewChild(NgModel) model: NgModel;
public identifier = `form-select-${identifier++}`;
constructor(
@Optional() @Inject(NG_VALIDATORS) validators: Array<any>,
@Optional() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<any>,
) {
super(validators, asyncValidators);
}
}
let identifier = 0;
That’s it! Now you have a great dropdown control you can use all over your application. It would be extremely simple to apply this style of component to also create radio groups, checkboxes, date pickers, payment forms -- just about anything you could think of. But first, maybe take a look at the existing code repository below and make sure you understand the various bits of code.
Putting it all together into a repository
The code for this post is available at this GitHub repository, and demonstrates working ngModel-based forms.
I encourage you to check the code out and to consider writing your forms using this pattern. I promise it will result in more maintainable code than passing FormControl objects around.
Good luck!