I am using a template-driven approach to building forms in Angular 2 and I have successfully created custom validators that I can use in the template.
However, I can't find a way to display specific error message bound to specific errors. I want to differentiate why the form is not valid. How do I achive that?
import { Component } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Site } from './../site';
import { BackendService } from './../backend.service';
import { email } from './../validators';
import { CustomValidators } from './../validators';
@Component({
templateUrl: 'app/templates/form.component.html',
styleUrls: ['app/css/form.css'],
directives: [CustomValidators.Email, CustomValidators.Url, CustomValidators.Goof],
providers: [BackendService]
})
export class FormComponent {
active = true;
submitted = false;
model = new Site();
onSubmit() {
this.submitted = true;
console.log(this.model);
}
resetForm() {
this.model = new Site();
this.submitted = false;
this.active = false;
setTimeout(() => this.active = true, 0);
}
get diagnostics() {
return JSON.stringify(this.model)
}
}
import { Directive, forwardRef } from '@angular/core';
import { NG_VALIDATORS, FormControl } from '@angular/forms';
import { BackendService } from './backend.service';
function validateEmailFactory(backend:BackendService) {
return (c:FormControl) => {
let EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
return EMAIL_REGEXP.test(c.value) ? null : {
validateEmail: {
valid: false
}
};
};
}
export module CustomValidators {
@Directive({
selector: '[email][ngModel],[email][formControl]',
providers: [
{provide: NG_VALIDATORS, useExisting: forwardRef(() => CustomValidators.Email), multi: true}
]
})
export class Email {
validator:Function;
constructor(backend:BackendService) {
this.validator = validateEmailFactory(backend);
}
validate(c:FormControl) {
return this.validator(c);
}
}
@Directive({
selector: '[url][ngModel],[url][formControl]',
providers: [
{provide: NG_VALIDATORS, useExisting: forwardRef(() => CustomValidators.Url), multi: true}
]
})
export class Url {
validator:Function;
constructor(backend:BackendService) {
this.validator = validateEmailFactory(backend);
}
validate(c:FormControl) {
var pattern = /(https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/;
return pattern.test(c.value) ? null : {
validateEmail: {
valid: false
}
};
}
}
@Directive({
selector: '[goof][ngModel],[goof][formControl]',
providers: [
{provide: NG_VALIDATORS, useExisting: forwardRef(() => CustomValidators.Goof), multi: true}
]
})
export class Goof {
validate(c:FormControl) {
return {
validateGoof: {
valid: false
}
};
}
}
}
You can just check the AbstractControl#hasError(...)
method to see if the control has a specific error. FormGroup
and FormControl
are both AbstractControl
s. for FormControl
you just pass as an argument the error name. For example
function regexValidator(control: FormControl): {[key:string]: boolean} {
if (!control.value.match(/^pee/)) {
return { 'badName': true };
}
}
<div *ngIf="!nameCtrl.valid && nameCtrl.hasError('badName')"
class="error">Name must start with <tt>pee</tt>.
</div>
The validator method should return a string/boolean map, where the key is the name of the error. This is the name that you check for in hasError
method.
For FormGroup
you can pass as an extra parameter the path to the FormControl
.
<div *ngIf="!form.valid && form.hasError('required', ['name'])"
class="error">Form name is required.</div>
name
is simply the identifier of the FormControl
for the input.
Here is an example with both the FormControl
and the FormGroup
check.
import { Component } from '@angular/core';
import {
FormGroup,
FormBuilder,
FormControl,
Validators,
AbstractControl,
REACTIVE_FORM_DIRECTIVES
} from '@angular/forms';
function regexValidator(control: FormControl): {[key:string]: boolean} {
if (!control.value.match(/^pee/)) {
return { 'badName': true };
}
}
@Component({
selector: 'validation-errors-demo',
template: `
<div>
<h2>Differentiate Validation Errors</h2>
<h4>Type in "peeskillet"</h4>
<form [formGroup]="form">
<label for="name">Name: </label>
<input type="text" [formControl]="nameCtrl"/>
<div *ngIf="!nameCtrl.valid && nameCtrl.hasError('required')"
class="error">Name is required.</div>
<div *ngIf="!nameCtrl.valid && nameCtrl.hasError('badName')"
class="error">Name must start with <tt>pee</tt>.</div>
<div *ngIf="!form.valid && form.hasError('required', ['name'])"
class="error">Form name is required.</div>
</form>
</div>
`,
styles: [`
.error {
border-radius: 3px;
border: 1px solid #AB4F5B;
color: #AB4F5B;
background-color: #F7CBD1;
margin: 5px;
padding: 10px;
}
`],
directives: [REACTIVE_FORM_DIRECTIVES],
providers: [FormBuilder]
})
export class ValidationErrorsDemoComponent {
form: FormGroup;
nameCtrl: AbstractControl;
constructor(formBuilder: FormBuilder) {
let name = new FormControl('', Validators.compose([
Validators.required, regexValidator
]));
this.form = formBuilder.group({
name: name
});
this.nameCtrl = this.form.controls['name'];
}
}
Ok so I got it working, but it's a little verbose. I couldn't figure out how to properly get access to the individual FormControl
of the inputs. So what I did was just create a reference to the FormGroup
<form #f="ngForm" novalidate>
Then then to check for validity, I just use the hasError
overload that passed the path of the form control name. For <input>
that use name
and ngModel
, the name
value gets added to the main FormGroup
with that name as the FormControl
name. So you can access it like
`f.form.hasError('require', ['nameCtrl'])`
assuming name=nameCtrl
. Notice the f.form
. The f
is the NgForm
instance which has a FormGroup
member variable form
.
Below is the refactored example
import { Component, Directive } from '@angular/core';
import {
FormControl,
Validators,
AbstractControl,
NG_VALIDATORS,
REACTIVE_FORM_DIRECTIVES
} from '@angular/forms';
function validateRegex(control: FormControl): {[key:string]: boolean} {
if (!control.value || !control.value.match(/^pee/)) {
return { 'badName': true };
}
}
@Directive({
selector: '[validateRegex]',
providers: [
{ provide: NG_VALIDATORS, useValue: validateRegex, multi: true }
]
})
export class RegexValidator {
}
@Component({
moduleId: module.id,
selector: 'validation-errors-template-demo',
template: `
<div>
<h2>Differentiate Validation Errors</h2>
<h4>Type in "peeskillet"</h4>
<form #f="ngForm" novalidate>
<label for="name">Name: </label>
<input type="text" name="nameCtrl" ngModel validateRegex required />
<div *ngIf="!f.form.valid && f.form.hasError('badName', ['nameCtrl'])"
class="error">Name must start with <tt>pee</tt>.</div>
<div *ngIf="!f.form.valid && f.form.hasError('required', ['nameCtrl'])"
class="error">Name is required.</div>
</form>
</div>
`,
styles: [`
.error {
border-radius: 3px;
border: 1px solid #AB4F5B;
color: #AB4F5B;
background-color: #F7CBD1;
margin: 5px;
padding: 10px;
}
`],
directives: [REACTIVE_FORM_DIRECTIVES, RegexValidator]
})
export class ValidationErrorsTemplateDemoComponent {
}