Angular Component – part 3: Validation

This is part 3 of my series about Angular components.
If you haven’t already, check out part 1 (one-way and two-way binding) and part 2 (ngModel) to see how and why to write an Angular component following the ngModel pattern.

Our goal for this part of the series is to learn how to validate your angular component’s user inputs to disable the submit of a form surrounding our form-control.

Currently, our component has two input fields and a button for demo purposes. It’s a 24-hour daytime input control, so there is definitely some potential for users to enter data into the input fields that wouldn’t result in a valid daytime. To recapitulate, let’s take a look at our component:

import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { DayTime } from './typeDefinitions';

@Component({
  selector: 'time-input',
  template: `
    <input [ngModel]="value?.hours" name="hours" type="number" [disabled]="disabled" />
    <input [ngModel]="value?.minutes" name="minutes" type="number" [disabled]="disabled" />
    <button type="button" name="addHour" (click)="onAddHourButtonClicked()" [disabled]="disabled" (blur)="onBlur()">Add hour</button>
  `,
    providers: [
        { useExisting: forwardRef(() => TimeInputComponent), provide: NG_VALUE_ACCESSOR, multi: true }
    ]
})
export class TimeInputComponent implements ControlValueAccessor  {
  value: DayTime;
    
  writeValue(obj: DayTime) {
    this.value = obj;
  }

  private onChangeFunctions = [];
  registerOnChange(fn: (v: DayTime) => void) {
    this.onChangeFunctions.push(fn);
  }

  private onTouchedFunctions = [];
  registerOnTouched(fn: () => void) {
    this.onTouchedFunctions.push(fn);
  }

  disabled = false;
  setDisabledState?(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  onAddHourButtonClicked() {
    const newValue: DayTime = {
      hours: this.value.hours + 1,
      minutes: this.value.minutes
    };
    this.onChangeFunctions.forEach(fn => fn(newValue));
    this.value = newValue;
  }

  onBlur() {
    this.onTouchedFunctions.forEach(fn => fn());
  }
}

Putting power in people’s hands!

Right now, if the user enters a number into the "hours" or "minutes" field, our control data doesn’t change because we only use property binding (to display the data). We should change that to give the user more power:

@Component({
  template: `
    <input [ngModel]="value?.hours" (ngModelChange)="onUserInput($event, value?.minutes)" name="hours" type="number" [disabled]="disabled" />
    <input [ngModel]="value?.minutes" (ngModelChange)="onUserInput(value?.hours, $event)" name="minutes" type="number" [disabled]="disabled" />
    <button type="button" name="addHour" (click)="onAddHourButtonClicked()" [disabled]="disabled" (blur)="onBlur()">Add hour</button>
  ` //...
})
export class TimeInputComponent implements ControlValueAccessor  {
  // ...
  onUserInput(hours: number, minutes: number) {
    this.onValueChanged({
      hours: hours,
      minutes: minutes
    });
  }
  onAddHourButtonClicked() {
    this.onValueChanged({
      hours: this.value.hours + 1,
      minutes: this.value.minutes
    });
  }
  onValueChanged(newValue: DayTime) {
    this.onChangeFunctions.forEach(fn => fn(newValue));
    this.value = newValue;
  }
  // ...
}

But as some famous uncle once said: “With great power comes great responsibility.” Now that the user can enter whatever he/she wishes, it is possible to type in utter nonsense. Nothing stops the user from putting in 23:61 (11:61 p.m.). Nothing, really?

Doing your own thing

Of course, we could just modify our onValueChanged method and check the data for correctness. If the user input is outside the scope of valid values (before 00:00 and after 23:59), we could just disregard the new value and keep the old. At TimeRocket found out that this approach leads to frustrated users because from a user’s perspective, it might not be obvious why the values he/she entered a second ago were invalid and therefore disregarded. For most of our users, such an approach just felt like a bug. It is far more intuitive if the values are visible to the user (so he/she can see that the program registered her/his inputs) but are accompanied by a visual indication of what was wrong.
Ok, so let’s make invalid user inputs visible:

<input [ngModel]="value?.hours" (ngModelChange)="onUserInput($event, value?.minutes)" name="hours" type="number" [disabled]="disabled" />
<input [ngModel]="value?.minutes" (ngModelChange)="onUserInput(value?.hours, $event)" name="minutes" type="number" [disabled]="disabled" />
<button type="button" name="addHour" (click)="onAddHourButtonClicked()" [disabled]="disabled" (blur)="onBlur()">Add hour</button>
<span *ngIf="invalidData">time outside of scope</span>
onValueChanged(newValue: DayTime) {
  this.invalidData = newValue?.hours == undefined || newValue?.minutes == undefined 
                      || newValue?.hours > 23 || newValue?.hours < 0
                      || newValue?.minutes > 59 || newValue?.minutes < 0;
  this.onChangeFunctions.forEach(fn => fn(newValue));
  this.value = newValue;
}

(This might be the right time to tell you that I DO know there are min and max attributes for input fields with type="number" but that isn’t the point of this tutorial…)
Ok, that works somehow. At least the user sees that the entered data is outside of the expected scope. However, as we discussed in part 2 of this series, our component acts as a control inside an ngForm. This means, usually there is a submit <button> in that <form> and we want to disable that submit button when our component contains invalid data.

Now, we could, of course, create another event binding field (@Output(...)) for invalidData and our submit button could listen to this and decide if it should be disabled or not. But imagine a form with 5+ controls in it. The submit button would have to listen to the respective invalidData field of every single control. I think we can agree this would be a mess. So, how to solve this properly?

NG_VALIDATORS

The idea behind NG_VALIDATORS is to create a validator directive for your component that validates the data and gives the surrounding ngForm a hint if the control data is valid. Then we can observe the status of ngForm and decide if the submit button should be disabled or not.
Here’s the template of the view that will be using our component alongside our directive that we want to build:

<form #exampleForm="ngForm" (ngSubmit)="performAction()">
  <time-input [(ngModel)]="timeInMyApp" name="time" withinTimeScope></time-input>
  <button type="submit" [disabled]="!exampleForm.valid">
    perform action
  </button>
</form>

withinTimeScope will be the selector of our directive and exampleForm.valid will only be true if withinTimeScope says so. The benefit of using NG_VALIDATORS is that if we’d have more controls in our form, every control with one or even multiple validators would affect exampleForm.valid. This keeps our form clean without sacrificing functionality.

Let’s take a look at how to build such a validator:

import { Directive, forwardRef } from "@angular/core";
import { NG_VALIDATORS, FormControl } from "@angular/forms";
import { DayTime } from './typeDefinitions';

@Directive({
  selector: "time-input[withinTimeScope]",
  providers: [
    { provide: NG_VALIDATORS, useExisting: forwardRef(() => WithinTimeScopeDirective), multi: true }
  ]
})
export class WithinTimeScopeDirective {
  validate(formControl: FormControl) {
    const controlValue: DayTime = formControl.value;
    console.log("ControlValue", controlValue);
    return null;
  }
}
  • On line 5, our @Directive(…) annotation starts. We use the same logic for our provider (line 8) as in part 2 of this series with one difference: This time, the token we want to provide an implementation for is NG_VALIDATORS.
  • On line 6, we use a special selector syntax. What we’re saying with this is: This directive should be applied if we have a time-input component that has the withinTimeScope attribute set.
  • Because we’re providing an implementation for NG_VALIDATORS, we have to implement a validate method that takes a FormControl as an argument (line 12).
    This method will get called every time the value (ngModel) of time-input changes, and we find that value in formControl.value (line 13).

Now, the only question remaining is: Why are we returning null? You might think this is only a placeholder, but actually null has a special purpose in a validate method. It indicates that the value is valid! As long as every validator of every control in an ngForm returns null, ngForm.valid will be true.

Actually validating

That’s great and all, but we want to tell the form if the data is invalid, not always act like everything was honky-dory. To achieve that, let’s first see how we would represent invalid data to the caller of validate:

validate(formControl: FormControl) {
  // const controlValue: DayTime = formControl.value;
  return {
    "ourErrorCode": {
      valid: false
    }
  };
}

With this implementation of the validate method, the data of our control will always be viewed as invalid. This has the following effects:

  1. ngForm.valid is always false
  2. The CSS class ng-invalid got assigned to time-input. That’s great for us, because we can use this to render our component in a special way to signal to the user that the data is invalid.
  3. If we take a look at ngForm.controls['the-name-of-our-control'].errors (in our view: exampleForm.controls['time'].errors), we find our error code object.

You might be asking yourself: Why do I have to return an object with an error code name as a property that contains another object that simply says if the value is valid or not? Why can’t we just return true or false in the validate method? Well, there are good reasons for that:

  1. Returning a specific error code instead of just true or false gives one single validator the possibility to give more context for the error. In our case, we could return "exceeds24Hours" or "isNegative" or what have you.
  2. The error context can be further enhanced if we’re returning an error code and some data within that error. For example: We could return { "exceedsHourDuration": { minutesEntered: 65, maxAllowedMinutes: 59 } }

Bring it all together

Now there’s only one thing left: Moving the validation code from our component into the validator:

import { Directive, forwardRef } from "@angular/core";
import { NG_VALIDATORS, FormControl } from "@angular/forms";
import { DayTime } from './typeDefinitions';

@Directive({
  selector: "time-input[withinTimeScope]",
  providers: [
    { provide: NG_VALIDATORS, useExisting: forwardRef(() => WithinTimeScopeDirective), multi: true }
  ]
})
export class WithinTimeScopeDirective {
  validate(formControl: FormControl) {
    const controlValue: DayTime = formControl.value;
    const isInvalid = controlValue?.hours == undefined || controlValue?.minutes == undefined 
                        || controlValue?.hours > 23 || controlValue?.hours < 0
                        || controlValue?.minutes > 59 || controlValue?.minutes < 0;
    return isInvalid
      ? {
        "ourErrorCode": {
          valid: false
        }
      }
      : null;
  }
}

Finally, we have a component that can be used within an ngForm as a control and that influences the behavior of said <form> (and its submit button). In the next article of this series, I will discuss why sometimes there might be an even better way to handle validation, especially if you’re building custom controls only for your app and not for a library.

See you there!

About the author

Domenic Helfenstein

2 comments

  • Hello Domenic, will you still complete these series? I have been enjoying it so far.

  • Hi Chukwuma
    Thanks for reaching out. I didn’t continue the series (and do not plan to do so atm) because there was little interest and my team would not have profited much from the next parts.
    However, I can give you the gist of what I planned to write:
    – Part 4: Move the validation from a directive into the component itself
    – Part 5: Use a directive to load async data into a component

Recent Posts