Using Custom Async Validators with Angular

Ville Lahdenvuo
Grano
Published in
5 min readJan 30, 2018

In this post I explain what went wrong when we needed to add some backend validation to our admin panel because we wanted to get quick feedback for the operators filling the forms — and how we got it working.

We’re currently working on a new E-Commerce solution for Grano, aiming to provide a better customer experience by modernizing our platform, improving automation and integration to our production services. Our tech stack is reasonably cutting edge, considering that we’re working with an enterprise-level product and environment: Node.js, Angular, Redux, AWS accompanied with high code quality and modern development standards.

A little bit about debounce

If the term debounce doesn’t sound familiar to you, don’t worry. It simply means that the action is being deferred until a certain time has passed since the last call. It is used to reduce the amount of useless processing while keeping the application seem real-time.

When it comes to forms and validation, it means that we wait a certain time after every time the form is updated (a key is pressed) before running the validation. That way the validation doesn’t necessarily run on every single key press, but once the user has finished typing the validation kicks in.

How custom validation works in Angular

In Angular a custom validator is just a function that has the signature of (control: AbstractControl) => null | { [key: string]: any } . The function is called every time the form is updated and if it returns null the control is valid and if it returns an object the control is considered invalid and the returned keys are added to it’s errors object. An async validator works exactly the same, except the return value is a Promise or an Observable — so you can take advantage of all the RxJS magic.

So how do we debounce? Well, the simple answer would be to use the RxJS debounceTime operator:

But you will find out that this approach does not work. There are a couple of reasons for this: One is that the returned Observable is hot. To know the difference you really should read this article. Now let’s imagine we use this code. Now every time the user presses a key the validator is called, an observable is created and the timer is started. The user types “foo”, myServiceCall is called three times and after 500 ms the validation status is updated.

You see, Angular is smart: When you update the form, it unsubscribes from the validation observable, but since it’s a hot observable we have already sent the request to the backend. Oops! That is because the debounceTime operator is meant to debounce an Observable, for example an Observable that emits every time a key is pressed, but in our case we create a new observable that emits once every time, there is nothing to debounce. The way to fix this is to create a cold observable. That is, defer the myServiceCall until a certain time has passed. This can be achieved using the RxJS timer and switchMap operators:

That works, great! Now when the user types “foo”, we get three observables like before, however, all but the last observable are unsubscribed right away and they never actually call myServiceCall. Once the 500 ms delay has passed we switch to a new observable using switchMap and after our service call, the form errors are updated.

Well… It kind of works.

There is only one problem. If you check myForm.invalid, it says false — great, it must be working, right? Then you check myForm.valid and it’s also false! Why is that? To see what’s going on, we must check myForm.status, and lo and behold, it says PENDING . Obviously our service has been called and the errors object has been updated, so why is Angular still waiting? The answer is in the documentation of RxJS:

timer: After given duration, emit numbers in sequence every specified duration.

The RxJS timer operator is actually similar to setInterval, and it will keep emitting values until we tell it to stop. To fix this, we just tell it to take the first value and stop. We can do this by using the RxJS first operator:

Now, our service is only being called once and the form status doesn’t get stuck to PENDING. It works like a charm! 🎉

That extra little bug 🐛

While we got the async validation working like a charm, we had another problem: The error messages weren’t updating in the view. At first, we thought there was a bug in Angular, but after creating a minimal reproduction in a jsFiddle, we realized the problem was somewhere in our code.

We found out that our little helper that we use to aggregate form errors to simplify templates was actually listening to myForm.valueChanges, and it had a debounce of 300 ms. When you would type something, it would check the errors, and after 500 ms our async validation would kick in and see that there are actually errors, but the error messages were updated before this happened.

To fix this, we simply change the helper to use the myForm.statusChanges Observable, which is called every time the form’s validation status changes, therefore it is called after our async validators finish. Phew!

The takeaway

First of all, credit where credit is due. I found this solution while desperately browsing Github issues, and n00dl3 had commented his solution. To summarize:

  • Learn RxJS properly and don’t just copy & paste from tutorials — it’s awesome
  • If you think there is a bug in a library, create a minimal repro case
  • Someone else is probably struggling with the same problem—if you benefit from the community, it’s nice to contribute back
P.S. We’re hiring!

Ville works as a Lead Developer at Grano, the most versatile content service provider in Finland and the leading graphic industry company in the Nordics. Currently he is working on Grano’s E-Commerce solutions.

--

--