5
votes

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.
2

2 Answers

7
votes

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">
1
votes

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;
});