Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular2 ngModel validator just in the component

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:

  • Why should we create a new local variables - with same structure - for FormGroups, FormControls when the data model already exists?
  • The component itself implements the required business logic so all business rule validators must be also implemented here.
  • There can be many complicated validation logics. These cannot be implemented just using the pure text values of inputs and simple checks like required, length, pattern etc.
  • Because of all above I think we ultimately need our whole component object to solve all real business rule validations.
like image 407
ggabor Avatar asked Aug 06 '16 17:08

ggabor


People also ask

How do you validate in ngModel?

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.

What is [( ngModel )]?

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.

How do I add validation in template driven form?

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.


2 Answers

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">
like image 59
ggabor Avatar answered Sep 22 '22 21:09

ggabor


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;
});
like image 34
wutzebaer Avatar answered Sep 19 '22 21:09

wutzebaer