I try to figure out the simplest way to implement a custom validator logic for ngModel. I have a pre-defined model (interface) which stores current data so I don't want to deal with the new FormGroup/FormControl (model-driven) approach.
Why should I build an exactly same schema with FormControls if I already have all data I need?
Here is my code (https://plnkr.co/edit/fPEdbMihRSVqQ5LZYBHO):
import { Component, Input } from '@angular/core';
export interface MyWidgetModel {
title:string;
description:string;
}
@Component({
selector: 'my-widget',
template: `
<h4 *ngIf="!editing">{{data.title}}</h4>
<input *ngIf="editing" type="text" name="title" [(ngModel)]="data.title">
<p *ngIf="!editing">{{data.description}}</p>
<textarea *ngIf="editing" name="description" [(ngModel)]="data.description" (ngModelChange)="customValidator($event)"></textarea>
<button (click)="clickEditing()">{{editing ? 'save' : 'edit'}}</button>
`
styles: [
':host, :host > * { display: block; margin: 5px; }',
':host { margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #eee; }',
'.ng-invalid { background-color: #FEE; }'
]
})
export class MyWidgetComponent {
@Input() data:MyWidgetModel;
constructor() {
this.editing = false;
}
clickEditing() {
this.editing = !this.editing;
}
customValidator(value:string) {
console.log(this, value); //should be: MyWidgetComponent
//How to set 'invalid' state here?
}
}
As you can see I can quickly turn on/off editing mode and I can edit my data in-place directly.
My question is how to manage ng-valid/ng-invalid states of the ngModel directly from my component? The idea behind this consists multiple points:
The ngModel property is used to bind the form control to the model. For validating the user name availability we will use the appValidateUserName directive on the username field. Similarly, we will use the appPasswordPattern directive on the password field to validate the password pattern.
The ngModel directive is a directive that is used to bind the values of the HTML controls (input, select, and textarea) or any custom form controls, and stores the required user value in a variable and we can use that variable whenever we require that value. It also is used during form validations.
To add validation to a template-driven form, you add the same validation attributes as you would with native HTML form validation. Angular uses directives to match these attributes with validator functions in the framework.
Finally I figured out a way to do. I think this is the simplest. I also updated the plunker: https://plnkr.co/edit/fPEdbMihRSVqQ5LZYBHO
Let's see step-by-step.
1 - Create a simple, minimal directive which implements a Validator interface - just as usual - but do not write any validation logic. Instead provide an Input() field of function type - same name as the selector. This will allow us to implement the real logic outside of this validator. Inside the validate(...) function just call that external Input() function.
import { Directive, forwardRef, Input } from '@angular/core';
import { AbstractControl, NG_VALIDATORS, Validator, ValidatorFn } from '@angular/forms';
@Directive({
selector: '[myvalidator][ngModel],[myvalidator][ngFormControl]',
providers: [{
multi: true,
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MyValidator)
}]
})
export class MyValidator implements Validator {
@Input() myvalidator:ValidatorFn; //same name as the selector
validate(control: AbstractControl):{ [key: string]: any; } {
return this.myvalidator(control);
}
}
2 - To use the custom validator just import it and add to the directives array of the component. In the template markup use it like any other directive:
<input type="text" name="title" [(ngModel)]="data.title" [myvalidator]="validateTitle()">
The trick is right here. The value passed to the validator's Input() function is a function call - which will returns a validator function. Here it is:
validateTitle() {
return <ValidatorFn>((control:FormControl) => {
//implement a custom validation logic here.
//the 'this' points the component instance here thanks to the arrow syntax.
return null; //null means: no error.
});
All of above fully compatible with official Angular2 validators - required, pattern etc. - so our custom validator can be combined without any further tricks .
Edit: It can be implemented more simple and effective way if a local variable created in the constructor of the component for each validation:
private validateTitle:ValidatorFn;
constructor() {
this.validateTitle = (control:FormControl) => {
//implement a custom validation logic here.
//the 'this' points the component instance here thanks to the arrow syntax.
return null; //null means: no error.
};
}
Using this approach we created a ValidatorFn function only once instead of for every validation request. 1 function call eleminated: validateTitle(). So in the template we can just bind our variable:
<input type="text" name="title" [(ngModel)]="data.title" [myvalidator]="validateTitle">
If you dont want a directive for a onetime template driven form validation:
Make input accessible
#receiverInput="ngModel"
Bind in controller
@ViewChild(NgModel, { static: true }) receiverInput: NgModel;
validate
this.receiverInput.control.setValidators((control: AbstractControl) => {
if (!this.receiver.kundenNr) {
// invalid
return { receiver: false };
}
// valid
return null;
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With