Advanced Approaches to Angular Form Validations
Tags: webdev, angular, tutorial
Published: 2022-06-03 12:28:06

Validations in and out of the ControlValueAccessor

{% embed https://youtu.be/Vm-tlaJ4nug %}
_See this and many other articles at [lucaspaganini.com](lucaspaganini.com)_

Chances are you've already used Angular form validators. In this article, I'll show you how they work and how to create your own, but there's already plenty of content teaching that.

What I want to do here is to take it a step further. Instead of just teaching you how to use validators from outside, I'll teach you how to use them from inside.

## Angular Validators

Let's start with the basics. When you create a `FormControl`, you can optionally give it an array of validators. Some validators are synchronous and others are asynchronous.

Some needed to be implemented by the angular team to comply with the native HTML specification, like `[min]`, `[max]`, `[required]`, `[email]`, so on… Those can be found in the Angular forms library.

```ts
import { Validators } from '@angular/forms';

new FormControl(5, [Validators.min(0), Validators.max(10)]);

new FormControl('test@example.com', [Validators.required, Validators.email]);
```

## Reactive vs Template

If you declare an input element with the `required` attribute while using the `FormsModule`, Angular will turn that input into a `ControlValueAccessor` (again, read the first article if you haven't done yet), it will create a `FormControl` with the required validator and attach the `FormControl` to the `ControlValueAccessor`

```html
<input type="text" name="email" [(ngModel)]="someObject.email" required />
```

That all happens in the background and with no type safety. That's why I avoid the `FormsModule`, it's too magical and untyped for my taste, I'd rather work with something more explicit, and that's where the `ReactiveFormsModule` comes into play.

Instead of using the banana syntax that does all that magic for you, in the reactive forms way, you'd:

1. Instantiate your `FormControl` manually;
2. Attach the validators manually;
3. Listen to changes manually;
4. And attach it to the `ControlValueAccessor` semi-manually.

Apart from that last step, all of that's done in your TypeScript file, not in the HTML template. And that gives you a lot more type safety. It's not perfect, it does treat the inner values as `any`, but they're working to change that and there's also a good library to work around that issue in the meantime.

## ValidatorFn

Enough theory, let's see some actual coding.

In the last article, we implemented a date input. But as mentioned at the end of the article, I want to change it so that it only accepts business days. That means:

- No weekends
- No holidays
- No nonexistent dates (like February 31)

Let's start by handling the weekends. I have a simple function that receives a `Date` and returns a boolean indicating if that date is a weekend.

```ts
enum WeekDay {
  Sunday = 0,
  Monday,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday
}

export const isWeekend = (date: Date): boolean => {
  const weekDay = date.getDay();
  switch (weekDay) {
    case WeekDay.Monday:
    case WeekDay.Saturday:
      return true;
    default:
      return false;
  }
};
```

That's good, but we need a different function signature for this to work. What Angular expects from a `ValidatorFn` is for it to return `null` if everything's fine and an object when something's wrong.

The properties of the returned object are ids for the errors. For example, if the date is a weekend, I'll return an object with the property weekend set to true. That means the `FormControl` now has an error, called `"weekend"` and its value is `true`. If I do `FormControl.getError('weekend')`, I get `true`. And if I do `FormControl.valid`, I get `false`, because it has an error, so it's not valid.

You could give any value to the error property. For example, you could give it `"Saturday"`, and when you call `FormControl.getError('weekend')`, you'll get `"Saturday"`.

By the way, the validator function doesn't receive the value as a parameter, it receives the `AbstractControl` that's wrapping the value. An `AbstractControl` could be a `FormControl`, a `FormArray`, or a `FormGroup`, you just have to take the value from it before doing your validation.

```ts
export const weekendValidator: ValidatorFn = (
  control: AbstractControl
): null | { weekend: true } => {
  const value = control.value;
  if (isDate(value) === false) return null;
  if (isWeekend(value)) return { weekend: true };
  return null;
};
```

Also, don't forget that the value could be `null` or something different instead of a `Date`, so it's always good to handle those edge cases. For this weekend's validator function, I'll just bypass it if the value is not a date.

Ok, now that it's done, you just have to use it like you would with `Validators.required`.

```ts
export class AppComponent {
  public readonly dateControl = new FormControl(new Date(), [weekendValidator]);
}
```

## AsyncValidatorFn

Now let's tackle the holiday validator.

This is a different case because we'll need to hit an external API to consult if the given date is a holiday or not. And that means it is not synchronous, so we can't possibly return `null` or an object. We'll need to rely on `Promise`s or `Observable`s.

Now, I don't know about you, but I prefer to use `Promise`s when possible. I like `Observable`s and I happen to know a lot about them, but they are uncomfortable for a lot of people. I find `Promise`s to be much more widely understood and overall simpler.

The same applies for `fetch` versus Angular's `HTTPClient`. If I'm not dealing with server-side rendering, I'll skip the `HTTPClient` and go with `fetch`.

So I've made a function that receives a `Date` and returns a `Promise` of a `boolean`, indicating if that date is a holiday. To make it work, I'm using a free API that gives me a list of holidays for a given date.

<!-- #### Version 1 -->

I'm using their free plan, so I am limited to one request per second and only holidays from this year. But for our purposes, that'll do just fine.

<!-- #### Version 2

I have to say I was quite impressed with the quality of their API, it is extremely fast, supports multiple countries, and has a very comprehensive list of holidays. It has much more than I was expecting. So, being the content creator that I am, I've reached out to them, and they decided to sponsor this article. This is great because it's a product I have used, and I can attest to its quality. -->

```ts
export const isHoliday = async (date: Date): Promise<boolean> => {
  const year = date.getFullYear();
  const month = date.getMonth() + 1;
  const day = date.getDate();

  const currentYear = new Date().getFullYear();
  if (year < currentYear) {
    console.warn(
      `We're using a free API plan to see if the date is a holiday and in this free plan we can only check for dates in the current year`
    );
    return false;
  }

  // This is to make sure I only make one request per second
  await holidayQueue.push();

  const queryParams = new URLSearchParams({
    api_key: environment.abstractApiKey,
    country: 'US',
    year: year.toString(),
    month: month.toString(),
    day: day.toString()
  });

  const url = `https://holidays.abstractapi.com/v1/?${queryParams.toString()}`;
  const rawRes = await fetch(url);
  const jsonRes = await rawRes.json();

  return (
    isArray(jsonRes) &&
    isEmpty(jsonRes) === false &&
    // They return multiple holidays and I only care if it's a national one
    jsonRes.some((holiday) => holiday.type === 'National')
  );
};
```

Just like our previous case, this signature won't do. What Angular expects from an `AsyncValidatorFn` is for it to receive an `AbstractControl` and return `null` or an object wrapped in a `Promise` or an `Observable`.

```ts
export const holidayValidator: AsyncValidatorFn = async (
  control: AbstractControl
): Promise<null | { holiday: true }> => {
  const value = control.value;
  if (isDate(value) === false) return null;
  if (await isHoliday(value)) return { holiday: true };
  return null;
};
```

Again, don't forget to handle edge cases if the value is not a `Date`.

And now we can use it in our `FormControl`. Note that the `AsyncValidatorFn`s are the third parameter to a `FormControl`, not the second.

```ts
export class AppComponent {
  public readonly dateControl = new FormControl(
    new Date(),
    [weekendValidator],
    [holidayValidator]
  );
}
```

## Validator

So far so good, now there's only one check left: see if the date exists.

I have a function here that receives the day, month, and year and returns a boolean indicating if that date exists. It's a rather simple function, I create a `Date`object from the given values and check if the year, month, and day of the newly created date are the same as the ones used to construct it.

```ts
export const dateExists = (
  year: number,
  month: number,
  day: number
): boolean => {
  const date = new Date(year, month - 1, day);
  return (
    date.getFullYear() === year &&
    date.getMonth() === month - 1 &&
    date.getDate() === day
  );
};
```

You might think that's so obvious that it's almost useless. To you, I say: you don't know the `Date` constructor, it is tricky…

See, you might think that instantiating a `Date` with February 31 would throw an error. But it does not., it gives you March 03 (please ignore leap years for the sake of this example).

```ts
new Date(2021, 1, 31);
//=> March 03, 2021
```

Because of that, we can't take a `Date` object and tell if it's an existing date or not because we can't see what day, month, and year were used to instantiate it. But if you have that information, you can try to create a date and see if the day, month, and year of the created date are what you were expecting.

Unfortunately, our date input doesn't give us that information, it only handles back the already instantiated `Date` object. We could do a bunch of hacks here, like creating a public method in the date input component that gives us those properties, and then we would grab the component instance and do our check.

That seems wrong though, we would be exposing internal details of our component and that's never a good idea, it should be a black box. There must be a better solution, and there is one. We can validate from inside the component.

There's an interface called `Validator` exported in the Angular forms library, and it's very similar to our `ControlValueAccessor` pattern. You implement the interface in your component and provide the component itself in a specific multi-token. `NG_VALIDATORS`, in this case.

To comply with the `Validator` interface, you just need a single method called `validate()`. This method is a `ValidatorFn`. It receives an `AbstractControl` and returns `null` or an object with the occurred errors.

But since we're inside the component, we don't really need the `AbstractControl`, we can grab the value ourselves.

```ts
public validate(): { invalid: true } | null {
  if (
    this.dayControl.invalid ||
    this.monthControl.invalid ||
    this.yearControl.invalid
  )
    return { invalid: true };

  const day = this.dayControl.value;
  const month = this.monthControl.value;
  const year = this.yearControl.value;
  if (dateExists(year, month, day)) return { invalid: true };

  const date = new Date(year, month - 1, day);
  if (isWeekend(date)) return { weekend: true };
  if (await isHoliday(date)) return { holiday: true };

  return null;
}
```

This works just like the `ValidatorFn`s we were passing to the `FormControl`, but it works from inside. And it has two benefits:

1. It would be a nightmare to implement this check from outside the component;
2. We don't need to declare it every time we create a `FormControl`, it'll be present in the component by default.

That second benefit really appeals to me, I think it makes total sense for our date component to be responsible for its own validation. If we wanted to customize it, we could create `@Input`s, like `[holiday]="true"` means we're ok with the date being a holiday and that this check should be skipped.

I won't implement those customizations because they're outside the scope of this article, but now you know how I would do it.

As I've said, I think it makes total sense for our date component to be responsible for its own validation. So let's bring our other synchronous validator inside too.

```ts
public validate(): {
  invalid?: true;
  weekend?: true;
} | null {
  if (
    this.dayControl.invalid ||
    this.monthControl.invalid ||
    this.yearControl.invalid
  )
    return { invalid: true };

  const day = this.dayControl.value;
  const month = this.monthControl.value;
  const year = this.yearControl.value;
  if (dateExists(year, month, day)) return { invalid: true };

  const date = new Date(year, month - 1, day);
  if (isWeekend(date)) return { weekend: true };

  return null;
}
```

## AsyncValidator

The last thing missing is to also bring our asynchronous validator inside. And that'll be easy, we just need a few adjustments.

Instead of implementing the `Validator` interface, we'll implement the `AsyncValidator` interface. And instead of providing our component in the `NG_VALIDATORS` token, we'll provide it in the `NG_ASYNC_VALIDATORS` token.

Now our `validate()` method expects to be an `AsyncValidatorFn`, so we'll need to wrap its return value in a `Promise`.

```ts
public async validate(): Promise<{
  invalid?: true;
  holiday?: true;
  weekend?: true;
} | null> {
  if (
    this.dayControl.invalid ||
    this.monthControl.invalid ||
    this.yearControl.invalid
  )
    return { invalid: true };

  const day = this.dayControl.value;
  const month = this.monthControl.value;
  const year = this.yearControl.value;
  if (dateExists(year, month, day)) return { invalid: true };

  const date = new Date(year, month - 1, day);
  if (isWeekend(date)) return { weekend: true };
  if (await isHoliday(date)) return { holiday: true };

  return null;
}
```

Now that all validators are implemented inside the component, we can remove them from outside.

```ts
export class AppComponent {
  public readonly dateControl = new FormControl(new Date());
}
```

## Conclusion

I'll leave a link for the repository in the references below.

Have a great day, and I'll see you soon!

## References

1. [Repository _GitHub_ ](https://github.com/LucasPaganini/published-content/tree/master/custom-validators)
2. [Introduction to ControlValueAccessors _Lucas Paganini Channel_](https://youtu.be/HiW5CYOw6-Q)
3. [Pull request to make Angular forms strictly typed _GitHub_](https://github.com/angular/angular/pull/40772)
4. [Library for typed forms in the meantime _npm_ ](https://www.npmjs.com/package/ngx-forms-typed)
5. [Article explaining how the typed forms library was created _Indepth_](https://indepth.dev/posts/1198/angular-forms-story-strong-types)
6. [Angular form validation from outside _Angular docs_](https://angular.io/guide/form-validation)
7. [Angular validation from inside _Angular docs_](https://angular.io/api/forms/Validator)
8. [Angular async validation from inside _Angular docs_](https://angular.io/api/forms/AsyncValidator)