Briebug Blog

Sharing our thoughts with the community

Advanced Async Reactive Angular Validators

Advanced Async Reactive Angular Validators


Validators! A long-standing feature of Angular forms; they are probably those things you try to avoid getting into and, when you do, you find and copy code off the internet when it serves your needs and muddle your way through when you have to. Sadly, validators are one of the sorely under-documented features of Angular and yet they can be an extremely powerful resource for creating and managing reactive forms with this highly reactive platform.


If you are a beginner or intermediate Angular developer, despite the "Advanced" nature of the validators that will be covered in this article, I strongly encourage you to STAY and keep reading! We progress through the process of starting with a simpler validator and eventually refactor it, step by step, into a more advanced, richly featured set of validators. This article is progressive, so you do not necessarily need to read it all on one sitting!

Recent Validator Excursions

Here at Briebug, we have had to delve into the farther reaches of validators in some of our recent projects. To solve our client's needs, in our most recent use case, we had to resort to using asynchronous validators that made http calls in concert with reactive forms to validate zip code information.


Initially, our validation focused solely on the zip code field itself and nothing more. As we learned more about the zip code validation API we were utilizing, we realized we cold perform more extensive validation; ensuring that not only was the zip code validated properly but that it was for the correct state and city.

Expanding the Solution

On further exploration, we learned that we could create additional validators for the state and city fields and tie all three of them together to ensure that the validation across all of them was fully in sync.


In order to optimize performance, and minimize unnecessary API hits, we also implemented an Http Interceptor to cache zip code validation results. The interceptor will be covered in a companion article here.


All of the validators and the interceptor were written in a completely reactive and functional manner; fully leveraging the deep integration of RxJs into Angular. No imperative code or control flow was used.

The Zip Code API

Our client wished to use a third party API to perform zip code validation. Conveniently, this particular API is named the Zip Code API!


This initially free API is actually rather powerful as it can not only determine if a zip code is properly formatted but it can also determine the most likely city and state the zip code covers. Further, it can also offer possible alternative candidate city/state combinations given that zip codes do not always follow city boundaries.

Async Reactive Validator

We originally started with a relatively basic asynchronous validator. Asynchronous due to the fact that we needed to make an API call over HTTP, which is an asynchronous operation by nature, in JavaScript. Reactive, as we intended to use the validator with an Angular Reactive form: And we like writing functionally reactive code!

Reactive Form Validator Basics

Our initial attempt at creating our new zip code validator was relatively focused: Were the zip codes formatted properly? We weren't concerned with much more than that in our initial pass.


Writing a validator for a Reactive Form is fundamentally straight forward: It's just a function that takes an AbstractControl to validate and returns a ValidationErrors | null result. In the case of an asynchronous validator, that same result must be wrapped in an observable stream: Observable<ValidationErrors | null>.

ValidationErrors

If there are no validation errors, a reactive validator should return null to indicate there are no errors. Otherwise, a reactive validator should return a ValidationErrors result. Note the plurality: Errors.


It may come as a surprise that a validator is allowed to identify and track more than one possible validation error for a given field at a time. When a field may end up invalid for more than one reason under different circumstances, this can be extremely useful.


Another small but useful note about ValidationErrors is they are a relatively flexible object. They are a basic key/value map; where the key is a unique validation error identifier and the value can actually be anything. In fact, the value doesn't even need to be truthy: Simple presence in the errors object is enough!


The vast majority of validator examples simply set the value of an error identifier key to true. However, you could also set them to a string describing the error in more detail. We'll go into the possibilities later on.

Initial Implementation

The initial implementation of our zip code validator, named knownValidZipCode, is relatively simple. That said, for those unfamiliar with purely functional code, some things may require explanation.


Our initial goal was to simply check whether the call to the zip code API returned a 400 response, indicating the zip code in the request was malformed. In this case, our validation errors included the zipCodeFormat error.


We designed the validator in such a manner that the dependencies could be provided separately from the actual validation function. Allowing the validation function itself to close around those dependencies. We'll explain why this design decision was made later on in the article:

A Function Returning a Function

You may have noticed that instead of simply being a function, as I originally described, our actual validator is a function that returns a function. Another name for this is a higher order function and, when each function accepts a single argument, the function can be said to be "curried". Currying is when you distribute the individual parameters of a basic function out into a series of nested higher order functions each accepting one of those parameters.


The outer function here is what allows us to provide the HttpClient to our validator. In order to call our zip code api, we need the http client. We will go into the mechanisms of how we provide this later on. Suffice it to say, this is what requires the validator be asynchronous!


Think of the outer function as a "configuration" function: configuring the inner function it returns. The inner function being returned is the actual reactive forms validation function, and in this specific case, AsyncValidatorFn.

Purely Reactive Code

Another key goal we had for our validator was to remain completely reactive and functional. One of the common challenges with Angular is the multi-paradigm codebases: A single Angular app can often have imperative, reactive, and functional code all jumbled together. We find that Angular apps become a lot cleaner when you stick to a particular paradigm. Namely reactive, which is inherently functional in nature as well.


It is very easy to turn (just about) any TypeScript code into reactive code using the of and from operators. These are observable or "stream" factory functions. They take a value (or values, in the case of from) such as an AbstractControl and wrap it in an Observable that may then be pipe()ed. Since Angular itself is deeply integrated with RxJs you can return observables pretty much anywhere that you can return actual values. This makes it very easy to switch from an imperative mindset, into a reactive mindset.


So, we start the process by wrapping the provided control to be validated in a stream using of and from there all the rest is relatively elementary. Extract the value from the control, combine that with the required config for the zip code API from our environment and switch to the http stream to make the call.

Completion!

We ran into an issue when first testing our validator: It didn't actually seem to work! We could see the validator running and we could see all the necessary errors added to the validation errors of the appropriate controls. Yet, it never seemed to actually apply in the end.


It took some fiddling and digging but we eventually discovered that a reactive validator's observable MUST COMPLETE in order for the Reactive Forms framework to know validation is complete. During debugging, we observed the validation status ended up getting stuck in a PENDING state. After an initial round of confusion, experimentally adding a first() operator to the end of the observable solved the problem as it forced the observable to complete.

Performing Validation

Our initial use case was very simple. If the API returned a 400 error, the zip code was malformed. So when the API call returned successfully, according to the rules that govern reactive form validators, we returned null. However, when the API returned an error we captured that with a catchError. And if the status code was 400, we returned our ValidationError of { zipCodeFormat: true }.


In the event that the API returned a 500 error, for the moment, we did not want to invalidate the form: the zip code may well be entirely valid. Further, some basic validation restrictions could be verified just with other local validators, like Validators.maxLength. So for any other error, besides 400, we returned null.


That was it for our initial pass!

Extending the Solution: Round 1

Once we had our initial implementation in place, we delved further into the documentation for the Zip Code API. We knew it was actually much more capable than simply being able to determine if a zip code is formatted properly and we wanted to leverage more of that power.


The first modification we made was to check an additional status code: 404 not found. If the zip code API returned 404 then the zip code, though properly formatted, was not a known zip code! So we extended our validator to handle this additional error case:

In this case, we used a different error identifier in the ValidationErrors object we were returning. This is where the flexibility of ValidationErrors starts to become apparent. First, the error identifier need not match the name of the validator (which is a very common practice; so common that it may seem like a requirement!). Second, a single validator can use different error identifiers to indicate, with greater specificity, exactly what is wrong with the field.


So our validator is handling two possible validation cases: improper formatting zipCodeFormat and is it a known zip code knownZipCode.

Extending the Solution: Round 2

We put our new validator into use with a couple of our app's forms. It quickly became apparent that, with the current design, we would very rapidly burn through the allotment of free zip code API calls! Every time the user typed, the validator would hit the API and check if the zip code was valid!


There are a few ways we could try and solve that issue. Debouncing could be one, however, it's not necessarily guaranteed to hit the API only when absolutely necessary. We decided, at the very least, a zip code of 5 digits was the minimum requirement before we would even bother hitting the API. So we extended our validator again:

Reactive Branching

Ok! Now what's going on here? What is this iif? This handy little RxJs observable factory function stands for inline if and is a nice purely functional way to implement a basic reactive branch. It accepts three parameters: a condition that evaluates a boolean expression, the true case and the false case.


It should be noted that the condition is a function that returns true or false, not simply a "bare" boolean expression. Being a function allows it to be evaluated (called) over and over again at the right moments in time, as values move through the observable stream. Further, this function can "close around" context from the parent scope.

Deferred Execution

Something else you may have noticed is that the true and false cases for the iif are not returning values: Instead, they are returning observables. As noted previously, iif is an observable factory function. It chooses streams rather than actual result values.


You may also have noticed that the true case is using another RxJS factory function, defer, while the false case is not! This is due to the nature of each case. In the false case, we need to return the same thing every time null. Due to the nature of RxJs operators, the of(null) will be called during pipeline "setup" (a subject for another article) and will often run earlier than expected and possibly only once during the execution of a program (depends on the implementation context).


The same thing would have happened to our http call, were it not for defer. This RxJS creator will delay execution of whatever code it encapsulates until the appropriate time at runtime. In the case of iif, the deferred observable will execute only when the true case is evaluated. This ensures that our http call can utilize the host, key and most importantly zipCode from the parent context for that particular instance of validation.


This is a nuance of reactive programming with RxJs that is often poorly understood. It can lead to unusual pitfalls that can be difficult to track down and the usual outcome is that a mix of reactive and imperative code ends up being employed (messy!).

Advanced Validation

During our previous exploration of the Zip Code API documentation, we learned that it can return detailed information about a validated zip code. If the zip code is properly formatted, and a known zip code, the status code is 200 and a handy result object is returned:

With this type, we could expand validation in several ways. Not only can we cross-check the city and state fields and make sure the zip code matches them: We could also write validators for those fields that cross-check the zip code. That would allow validation to occur on any of the fields and, if there is a mismatch between zip and city/state, we can notify the user of the mismatch.

Expanding the Solution: Round 3

With all of these goals in mind, we refactored the zip code validator again. This time around we wanted to check that the ZipCodeResponse city and state matched the city and state fields and, if not, check that one of the acceptableCityNames city/state pairs did.


First thing to note with the following code: We've added another function into the chain of functions. The outer function, which accepts the HttpClient, now returns another function that accepts the optional names of the city and state fields. These two "outer functions" can be thought of as "configuring" the innermost function. We'll get more into how these are used in the latter part of the article:

It is possible for neither the state nor the city to match the zip. In this case we want to be able to track the fact that both validation checks failed, independently of each other. This is where the flexibility of ValidationErrors really opens up! You can define many different possible validation error states for a validator and also even set more than one of them at a time!

Refactoring, Simplifying

Alright! Our validator is actually rather powerful now. It is capable of validating multiple possible error cases by leveraging data from more than one field of our form. It has grown in complexity a good deal, though, and following the code is a good deal harder now.


We decided to start extracting parts of the functionality of our validator into simple functions and RxJs operators to clean things up a bit. Looking at the code we have now, there are several things that can be encapsulated in functions. Some of them may even be reusable in more than one use case.

Checking Related Fields

The first unit of code we determined could be extracted was the code that checks other form fields if they match an expected value or possible values. A simple function can be used to check both the city and state fields:

With this simple little function in hand, we can update the first map of our http call pipe to the following:

This simple approach reduces the original structure of our ZipValidResponse object to only the list of possible matching values that the opposing field may have in order for the zip code to be valid. Slightly fewer lines of code but also much clearer as to what those lines are doing.

Updating This Field's Validation

Another opportunity to extract a function is in the final map that updates the zip code validation itself. We pass a function to the RxJs map operator, so this one is easy. Simply extract that function and give it an identifier:

So our final map becomes this:

All there is to it!

Extracting the Error Mapping

The last piece of our simplifying refactor was to pull out the catchError functionality that maps the status codes to specific validation errors. RxJs provides an extremely useful function, pipe(), that can be used to create custom RxJs operators usable within the observable pipeline.


The wonderful thing about this utility function is it makes creating custom operators utterly familiar: You just use everything you already know about RxJs pipes and other operators:

A relatively simple operator in design. Even simpler in use:

There are countless opportunities when writing reactive code with RxJs to pull out complex but repetitive parts of a pipe into custom operators. A library of custom operators can greatly simplify a reactive code base!


That about wrapped up our Zip Code Validator. Almost.

Client's Final Requests

Our client had a couple of final requests, as clients often do! They thought: since the zip code API returned a best possible match city/state for the given zip code, why couldn't we auto-populate the city and state? Well, of course we could! In fact, we could probably do a lot more than that. Including verifying that if the user then changes the city or state to something else that we validate those fields as well and verify that they match one of the possibilities for the current zip code.

Auto-Filling Fields

The first task was to set the counterpart fields when a zip code was entered. On our form, we moved the zip code field above the city and state to try and guide the user towards entering it first and thus gaining the potential benefit of having the other related fields automatically filled.


We leveraged a bit of TypeScript magic and knowledge of the raw flexibility of the ultimate JavaScript to work some magic here. We wanted to make sure we only auto-filled the field if it had not been manually set by the user. With Angular Forms, this is not actually as easy as it sounds. So we had to MacGyver up a solution using TypeScript intersection types:

The little bit of magic here is the FormControl & { autoSetValue?: any } type for the control parameter. This is an intersection of two types: FormControl and { autoSetValue?: any }. The latter is simply an optional copy of the value that was automatically set to the control and one which we dynamically "tack onto" the form control at point of use by way of type intersection. The beauty of intersection types in a case like this is they don't actually modify each other; only within context do they provide a new, unique blending of the two types. Thus, FormControl remains unchanged everywhere else: remaining compatible with Angular.


By tracking the auto-set value, we can determine if the previous auto-set value matches the current value. This is a simple yet effective way to determine if the user has manually changed the value to something else; allowing us to avoid overwriting a manual user change.


We added two taps to our zip code API call pipe to update the counterpart field values on a successful result:

Counter-Validation

We've finally wrapped up the zip code validator. However, we determined we could leverage the same zip code API to validate whether our city and state fields were also valid relative to the chosen zip code.


Given the last thing we did was to auto-set the city and state, you may be asking: Why even bother with validating the city and state? For that matter, why even bother letting the user edit them at all? If you can auto-set them, couldn't you just leave it at that? Turns out, postal codes don't always follow city boundaries and it is entirely possible for more than one city to fall within a given postal code.


While we may auto-fill the best-possible match for city and state that may, in fact, not be correct for the given user!

The matchesZip Validator

After some brainstorming, we decided we could write a single underlying validator that could be used to perform the necessary counter-validation for both the city and state fields. By exposing more options in the outer "configuration" functions, such as the acceptable property and the zip code field name, a simpler implementation would suffice:

As with the zip code validator, if some minimum basic validation rules were not initially met we simply did not bother applying this validator. In this case, the city or state needed at least one character and the zip code needed to be at least 5 digits.


Validity for this validator is simpler than the zip code field itself. We only need to know if the current city or state matches one of the acceptables returned by the zip code API. If not, it's invalid. Otherwise, this validator does not add any validation errors.


Thanks to the refactoring done on the original zip code validator, we were able to reuse some of that functionality. The fieldValueMatchesAcceptable came in quite handy for this new validator.

A Few More Refinements

Overall, at this point, our validators are in great shape. We can validate both zip codes as well as whether the city and state match the current zip code. When choosing a zip code, if the city and state fields haven't been filled in yet, we can auto-populate them. Great stuff! Client's loving it.

Stricter Length Requirements

There were a couple of things that we felt needed to be tweaked, however. First off, if the user ended up putting in a ZIP+4 code, any extra characters beyond 5 would spam the zip code API. That was no good. So we decided to extend the zip code length check to only call the API if the zip code was equal to 5 or equal to 10 (ZZZZ-XXXX), and not bother for any other length. This limited actual API calls to only fully formed ZIP5 or ZIP+4 codes.

Error Messaging

The next refinement we made was to replace all the true values for each of our possible validation errors with strings. Earlier in the article we mentioned that the ValidationErrors type was actually quite flexible and all that was really needed was for any error identifier to be present to invalidate a field. An error message string is just as useful as true:

The benefit with error message strings is we can easily just drop those into our UI to convert our error codes into human readable messages. In fact, there is even the potential to dynamically generate more rich messages from the information we have available to us within the validators.

Missing Piece

There is one final missing piece in all of our validators here: They need to integrate with each other and clear any counter-validation errors when normalization occurs. If the city or state start out invalid for a given zip code and then are updated to match one of the possible city matches the city/state AND zip code fields should both become valid! Same goes for the reverse: if the zip code is updated to match the city/state.


Our zip code validator already sets two possible "counter-field" validation errors: zipMatchesCity and zipMatchesState. Similarly, although dynamic, there are two possible validation errors for our new matches zip validator: cityMatchesZip and stateMatchesZip. These create correlated validation errors. If one clears on one "side" of the correlation then the other must update in order for the correlation itself to remain valid! Similarly, if one causes one "side" of the correlation to invalidate, the other side must invalidate.

Removing Control Errors

In order to facilitate removing errors from controls we put together another little utility function. This function's purpose is to identify if a control in the form has the specified validation error and, if so, remove it. The key here is we can have many validation errors applying concurrently to a single field. Validators are composable, so we have to be careful that we do not wipe out other validator's validation errors:

This function checks the specified FormControl: looking among any existing errors if they exist and removing the specified error from the ValidationErrors on that control. Otherwise, if the function determines that the counter control has no other errors, it clears them.


The reserialize function simply applies JSON.stringify() followed by JSON.parse() to eliminate undefined properties (remember, only "presence" of an error property matters. If the properties remain, even "set to undefined", the control remains invalid!).

Adding Control Errors

In order to facilitate adding errors to controls we put together a little utility function for that as well. This function's purpose is the opposite of the previous function. It identifies if a control is missing the specified error and adds it.

Toggling Errors

To simplify our actual validator even further, we can bundle these two functions together in a toggling function that will take a validity flag and run add or remove as appropriate.

Expanding the Solution: Round 4

Plugging this last missing piece of the puzzle into all of our validators, we finally come to a close in our advanced validators that correlate and integrate with each other to ensure all correlated controls remain valid, or become invalid, together under all the right circumstances.


The full code of our final validators, including all support types and utility functions and operators, ended up being the following:

One question that came to mind while working on these validators was: Can we break them up into more, smaller, simpler validators? We probably could: distributing some of the various possible errors out among more composable validators. One of the concerns there, however, is spamming the API with http requests. With fewer validators, you minimize the API hits. Fewer validators simplifies the cross-field coordination as well (imagine cross-correlating half a dozen validators!!).

Into the Form!

With our awesome new asynchronous zip code validators completed, we put them to use in our form. Integrating validators into an Angular reactive form is pretty straight forward:

Now, something to be cognizant of here is that our new validators are asynchronous validators. They cannot be bundled with any other synchronous validators: they are passed separately as a third config option for each field.

Pretty Packaging

From the get-go, we knew we were not really going to like the "user experience" with the bare validators. They aren't exactly sightly and are a little complicated given we have to provide the HttpClient to them as well as any possible form-specific configuration.


One of our final goals was to provide a "native-like" experience with these validators. Something along the lines of AsyncAddressValidators.cityMatchesZip('postalCode') would be familiar and clean enough. That also makes it clearer that these are async validators and address related validators.


We had to use an intriguing little provider pattern to make this work, however. First, we constructed a container that could hold all of our validators:

Not a whole lot to it but it may look a little odd at first glance. We are setting the validators to static properties! If you think about how the built-in Validators class works it's actually the same thing: all the validators are static properties of the class.


There is another oddity here, though. We are setting the static validators from within an instance constructor. Smells...fishy. In fact, it's just a little trick that allows us to provide an injectable dependency to statically accessible functionality. We simply need to provide and inject this class early in the app bootstrap cycle:

This ensures that our AsyncAddressValidators class is injected as soon as the app module is created, which will ensure that our validators are properly attached statically to the class, and that they have access to the HttpClient!

Pretty Forms

With the above static packaging of our validators into a container class we can now simplify our reactive form a bit and eliminate the need to manually inject and provide the HttpClient at every use case:

Ah, there we go! Much cleaner, much more professional and very clearly asynchronous!

The Next Piece

If you were particularly observant, you may have noticed something in our app module code above: An HTTP Interceptor!

Given the nature of our validators, each of which can call our zip code API and potentially call it many times, we found it important to find a way to minimize the number of hits that API received. To that end, we implemented an Http Interceptor that would cache the results of our API calls: ensuring that no zip code was validated more than once for a given run of the app.


This is a subject for another article, "Advanced Reactive Angular Interceptors." Head on over there to read more about how we implemented a fully functional, fully reactive HttpClient interceptor to cache the results of our zip code API and save us from burning through free validations.

Raw Validating Power

So you made it this far! Congrats! Oh, erm.... tl;dr ... chuckle


We hope that this article has given you deeper insight into how Angular reactive validators work, and how to leverage the raw power they have in your own Angular projects. We also hope the article clearly demonstrated how to leverage reactive and functional programming to implement robust asynchronous validators without the need to fall back on imperative instructions and also integrate validators with each other to ensure correlated validation results remain in sync across related validations.


We also hope that we have cleared up some common misconceptions and mistakes about validators. Particularly with regards to the ValidationErrors attached to each form control. Be cognizant of the fact that if you do not take care with the validation errors you may wipe out the results from other validators! Validators are composable and each validator may concurrently validate each form control.

Need Support With a Project?

Start experiencing the peace and security your team needs, and continue getting the recognition you deserve. Connect with us below. 

First Name* Required field!
Last Name* Required field!
Job Title* Required field!
Business Email* Required field!
Phone Required field!
Tell us about your project Required field!
View Details
- +
Sold Out