Skip to content

Using Custom Async Validators in Angular Reactive Forms

What is a validator in Angular?

First, let's talk about Validators. What is a validator, and how we can use them to improve our forms.

Ocasionally, we want to validate some input in our fields before submission, which will be validated against an asynchronous source. For instance, you may want to check if a label or some data exists before submission. In Angular, you can do this using Async Validators.

Create a basic application

We are going to create a very minimalist form that does one thing: check if the username already exists. In this case, we are going to use a mix of async and sync validators to accomplish our task.

It's very common, in most registration forms, to require an unique username. Using an API call, we to capture if that username is already in use according to the database.

import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  constructor(private fb: FormBuilder) {}

  registrationForm = this.fb.group({
    name: [null, [Validators.minLength(3), Validators.required]],
    username: [null, [Validators.minLength(3), Validators.required]],
  });
}

Created a basic form that has a FormGroup called registrationForm, with 2 FormControl. Also, I added 2 sync validators for make the input required and also to check if the input has a minimum length, using Validators.required and Validators.minLength in each case.

<p>Using Custom Async Validators</p>

<div class="container" [formGroup]="registrationForm">
  <mat-form-field class="example-full-width">
    <mat-label>Name</mat-label>
    <input matInput placeholder="Ex. John Doe" formControlName="name" />
  </mat-form-field>

  <mat-form-field class="example-full-width">
    <mat-label>Username</mat-label>
    <input matInput placeholder="Ex. Batman" formControlName="username" />
  </mat-form-field>

  <div class="button-container">
    <button
      mat-raised-button
      color="primary"
      [disabled]="!registrationForm.valid"
    >
      Submit
    </button>
  </div>
</div>
async validator 1/3

I'm using @angular/material just to simplify our styles, and because it provides a clean way to display errors using the mat-error component. This component will be shown to the user if the required error is present. The button also checks if the FormGroup is valid, otherwise it will be disabled.

Async validators

Our current app works pretty well, and in most cases, will meet all of the requirements. But what if we want to make sure that the username is unique before allowing the user to submit their information?. Creating an Async Validator could be simple. Let's find how to create it, and add it to our current form.

import { Injectable } from '@angular/core';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class UserService {
  private existingUsernames = ['Batman', 'Superman', 'Joker', 'Luthor'];

  checkIfUsernameExists(value: string) {
    return of(this.existingUsernames.some((a) => a === value)).pipe(
      delay(1000)
    );
  }
}

Our method to check if the username already exists is called checkIfUsernameExists. It returns an observable with a 5 seconds delay to simulate a very slow API call. Now, we can create our Async Validator to check if the username exists against that method.

import {
  AbstractControl,
  AsyncValidatorFn,
  ValidationErrors,
} from '@angular/forms';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { UserService } from './user.service';

export class UsernameValidator {
  static createValidator(userService: UserService): AsyncValidatorFn {
    return (control: AbstractControl): Observable<ValidationErrors> => {
      return userService
        .checkIfUsernameExists(control.value)
        .pipe(
          map((result: boolean) =>
            result ? { usernameAlreadyExists: true } : null
          )
        );
    };
  }
}

Our UsernameValidator class takes our UserService as an argument. This method returns a AsyncValidatorFn which receives the FormControl that is placed on, providing us access to the current value.

An AsyncValidatorFn must return either a promise or an Observable of type ValidationErrors.

We use the RxJS map operator to check the value emitted, and either return null if the user doesn't exist, or return a ValidationError with an error type of usernameAlreadyExists.

import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { UserService } from './user.service';
import { UsernameValidator } from './username-validator';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  constructor(private fb: FormBuilder, private userService: UserService) {}

  registrationForm = this.fb.group({
    name: [null, [Validators.minLength(3), Validators.required]],
    username: [
      null,
      [Validators.minLength(3), Validators.required],
      [UsernameValidator.createValidator(this.userService)],
    ],
  });
}

We import our UsernameValidator and UserService into our component, and declare in the constructor component.

The UsernameValidator is added to our FormControl as the third parameter, calling the createValidator method, and passing a reference to the UserService.

<mat-form-field class="example-full-width">
  <mat-label>Username</mat-label>
  <input matInput placeholder="Ex. Batman" formControlName="username" />
  <mat-error
    *ngIf="registrationForm.get('username').hasError('usernameAlreadyExists')"
  >
    Username already <strong>exists</strong>
  </mat-error>
</mat-form-field>

We update our template to check for an additional error called usernameAlreadyExists, so if our UserService finds that the username already exists, it will provide a

The validation status of the control. There are four possible validation status values:

VALID: This control has passed all validation checks.

INVALID: This control has failed at least one validation check.

PENDING: This control is in the midst of conducting a validation check.

DISABLED: This control is exempt from validation checks

async validator 3/3
async validator 2/3

Let's see StackBlitz in action.

Conclusion

Creating beautiful forms for our web app might seem simple, but it's typically more complicated than one would expect. Sometimes, we also need to verify the information, because we don't want to blindly send data to the backend, and then reject it. This can lead to a poor experience for users and result in low adoption rates.

Creating an AsyncValidator can help us improve the user-experience, and also avoid sending data to the backend, resulting in a rejection.

This Dot Labs is a development consultancy that is trusted by top industry companies, including Stripe, Xero, Wikimedia, Docusign, and Twilio. This Dot takes a hands-on approach by providing tailored development strategies to help you approach your most pressing challenges with clarity and confidence. Whether it's bridging the gap between business and technology or modernizing legacy systems, you’ll find a breadth of experience and knowledge you need. Check out how This Dot Labs can empower your tech journey.

You might also like

Angular 17: Continuing the Renaissance cover image

Angular 17: Continuing the Renaissance

Angular 17: A New Era November 8th marked a significant milestone in the world of Angular with the release of Angular 17. This wasn't just any ordinary update; it was a leap forward, signifying a new chapter for the popular framework. But what made this release truly stand out was the unveiling of Angular's revamped website, complete with a fresh brand identity and a new logo. This significant transformation represents the evolving nature of Angular, aligning with the modern demands of web development. To commemorate this launch, we also hosted a release afterparty, where we went deep into its new features with Minko Gechev from the Angular core team, and Google Developer Experts (GDEs) Brandon Roberts, Deborah Kurata, and Enea Jahollari. But what exactly are these notable new features in the latest version? Let's dive in and explore. The Angular Renaissance Angular has been undergoing a significant revival, often referred to as Angular's renaissance, a term coined by Sarah Drasner, the Director of Engineering at Google, earlier this year. This revival has been particularly evident in its recent versions. The Angular team has worked hard to introduce many new improvements, focusing on signal-based reactivity, hydration, server-side rendering, standalone components, and migrating to esbuild and Vite for a better and faster developer experience. This latest release, in particular, marks many of these features as production-ready. Standalone Components About a year ago, Angular began a journey toward modernity with the introduction of standalone components. This move significantly enhanced the developer experience, making Angular more contemporary and user-friendly. In Angular's context, a standalone component is a self-sufficient, reusable code unit that combines logic, data, and user interface elements. What sets these components apart is their independence from Angular's NgModule system, meaning they do not rely on it for configuration or dependencies. By setting a standalone: true` flag, you no longer need to embed your component in an NgModule and you can bootstrap directly off that component: `typescript // ./app/app.component.ts @Component({ selector: 'app', template: 'hello', standalone: true }) export class AppComponent {} // ./main.ts import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './app/app.component'; bootstrapApplication(AppComponent).catch(e => console.error(e)); ` Compared to the NgModules way of adding components, as shown below, you can immediately see how standalone components make things much simpler. `ts // ./app/app.component.ts import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], }) export class AppComponent { title = 'CodeSandbox'; } // ./app/app.module.ts import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } // .main.ts import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ` In this latest release, the Angular CLI now defaults to generating standalone components, directives, and pipes. This default setting underscores the shift towards a standalone-centric development approach in Angular. New Syntax for Enhanced Control Flow Angular 17 introduces a new syntax for control flow, replacing traditional structural directives like ngIf` or `ngFor`, which have been part of Angular since version 2. This new syntax is designed for fine-grained change detection and eventual zone-less operation when Angular completely migrates to signals. It's more streamlined and performance-efficient, making handling conditional or list content in templates easier. The @if` block replaces `*ngIf` for expressing conditional parts of the UI. `ts @if (a > b) { {{a}} is greater than {{b}} } @else if (b > a) { {{a}} is less than {{b}} } @else { {{a}} is equal to {{b}} } ` The @switch` block replaces `ngSwitch`, offering benefits such as not requiring a container element to hold the condition expression or each conditional template. It also supports template type-checking, including type narrowing within each branch. ```ts @switch (condition) { @case (caseA) { Case A. } @case (caseB) { Case B. } @default { Default case. } } ``` The @for` block replaces `*ngFor` for iteration and presents several differences compared to its structural directive predecessor, `ngFor`. For example, the tracking expression (calculating keys corresponding to object identities) is mandatory but offers better ergonomics. Additionally, it supports `@empty` blocks. `ts @for (item of items; track item.id) { {{ item.name }} } ` Defer Block for Lazy Loading Angular 17 introduces the @defer` block, a dramatically improving lazy loading of content within Angular applications. Within the `@defer` block framework, several sub-blocks are designed to elegantly manage different phases of the deferred loading process. The main content within the `@defer` block is the segment designated for lazy loading. Initially, this content is not rendered, becoming visible only when specific triggers are activated or conditions are met, and after the required dependencies have been loaded. By default, the trigger for a `@defer` block is the browser reaching an idle state. For instance, take the following block: it delays the loading of the calendar-imp` component until it comes into the viewport. Until that happens, a placeholder is shown. This placeholder displays a loading message when the `calendar-imp` component begins to load, and an error message if, for some reason, the component fails to load. `ts @defer (on viewport) { } @placeholder { Calendar placeholder } @loading { Loading calendar } @error { Error loading calendar } ` The on` keyword supports a wide a variety of other conditions, such as: - idle` (when the browser has reached an idle state) - interaction` (when the user interacts with a specified element) - hover` (when the mouse has hovered over a trigger area) - timer(x)` (triggers after a specified duration) - immediate` (triggers the deferred load immediately) The second option of configuring when deferring happens is by using the when` keyword. For example: `ts @defer (when isVisible) { } ` Server-Side Rendering (SSR) Angular 17 has made server-side rendering (SSR) much more straightforward. Now, a --ssr` option is included in the `ng new` command, removing the need for additional setup or configurations. When creating a new project with the `ng new` command, the CLI inquires if SSR should be enabled. As of version 17, the default response is set to 'No'. However, for version 18 and beyond, the plan is to enable SSR by default in newly generated applications. If you prefer to start with SSR right away, you can do so by initializing your project with the `--ssr` flag: `shell ng new --ssr ` For adding SSR to an already existing project, utilize the ng add` command of the Angular CLI: `shell ng add @angular/ssr ` Hydration In Angular 17, the process of hydration, which is essential for reviving a server-side rendered application on the client-side, has reached a stable, production-ready status. Hydration involves reusing the DOM structures rendered on the server, preserving the application's state, and transferring data retrieved from the server, among other crucial tasks. This functionality is automatically activated when server-side rendering (SSR) is used. It offers a more efficient approach than the previous method, where the server-rendered tree was completely replaced, often causing visible UI flickers. Such re-rendering can adversely affect Core Web Vitals, including Largest Contentful Paint (LCP), leading to layout shifts. By enabling hydration, Angular 17 allows for the reuse of the existing DOM, effectively preventing these flickers. Support for View Transitions The new View Transitions API, supported by some browsers, is now integrated into the Angular router. This feature, which must be activated using the withViewTransitions` function, allows for CSS-based animations during route transitions, adding a layer of visual appeal to applications. To use it, first you need to import withViewTransitions`: `ts import { provideRouter, withViewTransitions } from '@angular/router'; ` Then, you need to add it to the provideRouter` configuration: `ts bootstrapApplication(AppComponent, { providers: [ provideRouter(routes, withViewTransitions()) ] }) ` Other Notable Changes - Angular 17 has stabilized signals, initially introduced in Angular 16, providing a new method for state management in Angular apps. - Angular 17 no longer supports Node 16. The minimal Node version required is now 18.13. - TypeScript version 5.2 is the least supported version starting from this release of Angular. - The @Component` decorator now supports a `styleUrl` attribute. This allows for specifying a single stylesheet path as a string, simplifying the process of linking a component to a specific style sheet. Previously, even for a single stylesheet, an array was required under `styleUrls`. Conclusion With the launch of Angular 17, the Angular Renaissance is now in full swing. This release has garnered such positive feedback that developers are showing renewed interest in the framework and are looking forward to leveraging it in upcoming projects. However, it's important to note that it might take some time for IDEs to adapt to the new templating syntax fully. While this transition is underway, rest assured that you can still write perfectly valid code using the old templating syntax, as all the changes in Angular 17 are backward compatible. Looking ahead, the future of Angular appears brighter than ever, and we can't wait to see what the next release has in store!...

Testing a Fastify app with the NodeJS test runner cover image

Testing a Fastify app with the NodeJS test runner

Introduction Node.js has shipped a built-in test runner for a couple of major versions. Since its release I haven’t heard much about it so I decided to try it out on a simple Fastify API server application that I was working on. It turns out, it’s pretty good! It’s also really nice to start testing a node application without dealing with the hassle of installing some additional dependencies and managing more configurations. Since it’s got my stamp of approval, why not write a post about it? In this post, we will hit the highlights of the testing API and write some basic but real-life tests for an API server. This server will be built with Fastify, a plugin-centric API framework. They have some good documentation on testing that should make this pretty easy. We’ll also add a SQL driver for the plugin we will test. Setup Let's set up our simple API server by creating a new project, adding our dependencies, and creating some files. Ensure you’re running node v20 or greater (Test runner is a stable API as of the 20 major releases) Overview `index.js` - node entry that initializes our Fastify app and listens for incoming http requests on port 3001 `app.js` - this file exports a function that creates and returns our Fastify application instance `sql-plugin.js` - a Fastify plugin that sets up and connects to a SQL driver and makes it available on our app instance Application Code A simple first test For our first test we will just test our servers index route. If you recall from the app.js` code above, our index route returns a 501 response for “not implemented”. In this test, we're using the createApp` function to create a new instance of our Fastify app, and then using the `inject` method from the Fastify API to make a request to the `/` route. We import our test utilities directly from the node. Notice we can pass async functions to our test to use async/await. Node’s assert API has been around for a long time, this is what we are using to make our test assertions. To run this test, we can use the following command: By default the Node.js test runner uses the TAP reporter. You can configure it using other reporters or even create your own custom reporters for it to use. Testing our SQL plugin Next, let's take a look at how to test our Fastify Postgres plugin. This one is a bit more involved and gives us an opportunity to use more of the test runner features. In this example, we are using a feature called Subtests. This simply means when nested tests inside of a top-level test. In our top-level test call, we get a test parameter t` that we call methods on in our nested test structure. In this example, we use `t.beforeEach` to create a new Fastify app instance for each test, and call the `test` method to register our nested tests. Along with `beforeEach` the other methods you might expect are also available: `afterEach`, `before`, `after`. Since we don’t want to connect to our Postgres database in our tests, we are using the available Mocking API to mock out the client. This was the API that I was most excited to see included in the Node Test Runner. After the basics, you almost always need to mock some functions, methods, or libraries in your tests. After trying this feature, it works easily and as expected, I was confident that I could get pretty far testing with the new Node.js core API’s. Since my plugin only uses the end method of the Postgres driver, it’s the only method I provide a mock function for. Our second test confirms that it gets called when our Fastify server is shutting down. Additional features A lot of other features that are common in other popular testing frameworks are also available. Test styles and methods Along with our basic test` based tests we used for our Fastify plugins - `test` also includes `skip`, `todo`, and `only` methods. They are for what you would expect based on the names, skipping or only running certain tests, and work-in-progress tests. If you prefer, you also have the option of using the describe` → `it` test syntax. They both come with the same methods as `test` and I think it really comes down to a matter of personal preference. Test coverage This might be the deal breaker for some since this feature is still experimental. As popular as test coverage reporting is, I expect this API to be finalized and become stable in an upcoming version. Since this isn’t something that’s being shipped for the end user though, I say go for it. What’s the worst that could happen really? Other CLI flags —watch` - https://nodejs.org/dist/latest-v20.x/docs/api/cli.html#--watch —test-name-pattern` - https://nodejs.org/dist/latest-v20.x/docs/api/cli.html#--test-name-pattern TypeScript support You can use a loader like you would for a regular node application to execute TypeScript files. Some popular examples are tsx` and `ts-node`. In practice, I found that this currently doesn’t work well since the test runner only looks for JS file types. After digging in I found that they added support to locate your test files via a glob string but it won’t be available until the next major version release. Conclusion The built-in test runner is a lot more comprehensive than I expected it to be. I was able to easily write some real-world tests for my application. If you don’t mind some of the features like coverage reporting being experimental, you can get pretty far without installing any additional dependencies. The biggest deal breaker on many projects at this point, in my opinion, is the lack of straightforward TypeScript support. This is the test command that I ended up with in my application: I’ll be honest, I stole this from a GitHub issue thread and I don’t know exactly how it works (but it does). If TypeScript is a requirement, maybe stick with Jest or Vitest for now 🙂...

Sharing Signals and Stores: Context API in SolidJS cover image

Sharing Signals and Stores: Context API in SolidJS

Introduction Welcome to our latest blog post on SolidJS, where we delve into the world of the Context API. Context API is a popular and versatile feature in SolidJS, allowing developers to pass data and functions through the component tree without the need for props drilling. In this post, we will discuss how SolidJS implements the Context API, including the creation of contexts, sharing Signals and stores, and utilizing them within the components. Whether you are a seasoned developer, or just starting out, this post will provide valuable insights into the use of Context API in SolidJS and what are the advantages in the Reactivity ecosystem. So sit back, grab a cup of coffee, and let's dive in! What is SolidJS? SolidJS is a unique and powerful JavaScript framework that is quickly gaining popularity among developers. One of the key features that sets SolidJS apart from other frameworks is its reactive nature. How about reactivity? Reactivity is a programming paradigm through which the application automatically updates and re-renders when the data changes. This means that developers do not have to manually update the view when the data changes, saving a lot of time and effort. SolidJS achieves this reactivity by using a virtual DOM which is a lightweight representation of the actual DOM. This virtual DOM is then used to update the actual DOM, making the process of updating the view much faster and more efficient. Performance Another advantage of SolidJS is its performance. SolidJS is designed to be fast and efficient thanks to its use of a virtual DOM and a functional programming approach. By using functional components and immutability, SolidJS is able to optimize the process of re-rendering, resulting in a smoother and more responsive user experience. In addition to its reactivity and performance, SolidJS also has a small footprint and a simple API. The framework is lightweight and easy to learn, making it a great choice for developers of all skill levels. Signals: SolidJS Signals are a powerful feature that allow for easy communication between different parts of your application. They are similar to events or hooks in other frameworks, but with a few key differences. One of the key advantages of Signals in SolidJS is that they are fully reactive, meaning that they automatically update and re-render when the data changes. This makes it easy to create responsive and dynamic applications without having to manually update the view. To use Signals in SolidJS, you need to first create a signal by using the createSignal function. `tsx const [count, setCount] = createSignal(0); ` Store In SolidJS, a store works similar to other frontend frameworks. To keep things light, SolidJS creates underlying Signals only for properties that are accessed under tracking scopes. All Signals in Stores are created lazily as requested. The createStore call takes the initial value, and returns a read/write tuple similar to Signals. The first element is the store proxy which is readonly, and the second is a setter function. What About context? SolidJS provides a context API to pass data around without relying on passing props; this is useful in sharing Signals and Stores as described above. According to SolidJS docs: “Using Context has the benefit of being created as part of the reactive system and managed by it.” Getting Started: First, let’s create our context: `tsx import { createContext } from 'solid-js'; const initialState = { count: 0, name: "John Doe", isLoading: false }; const MyContext = createContext(initialState); ` Then we can consume our recently Context created: `tsx export function MyProvider(props) { const [count, setCount] = createSignal(props.count || 0), counter = [ count, { increment() { setCount(c => c + 1); }, decrement() { setCount(c => c - 1); } } ]; const [name, setName] = createSignal(props.name || ''); const [isLoading, setIsLoading] = createSignal(props.isLoading || false); return ( {props.children} ); } export function useMyContext() { return useContext(MyContext); } ` To use context, we'll first wrap our App component in order to provide it globally: `tsx const App: Component = () => { return ( ); }; export default App; ` We can now consume the context and our components will look like this: `tsx export default function Counter() { const {counter: [count, { increment, decrement }], name, setName} = useMyContext(); return ( {count()} {name()} {setName('ThisDot')}}> Click Me + - ); }; ` `tsx export default function Loader() { const {isLoading, setIsLoading} = useMyContext(); return ( {String(isLoading())} {setIsLoading(!isLoading())}}> Click Me ); }; ` Conclusion In this article, we were able to use the contextAPI to share the props we want across the app, and benefit from the performance of Reactivity because of that. This will make our application more readable, but also brings some additional benefits to the app which are as follows: - avoid passing props down through multiple levels of the component tree. - reducing the number of re-renders that occur when props are passed down. - by isolating state and props to a specific context, you can make your code more organized and easier to understand. - components that use the Context API are more easily tested. Would you like to learn more about Signals and Stores, or SolidJS in general? You can see in detail how this was maximized in one of This Dot Labs' open source project starter.dev GitHub Showcases....

Software Team Leadership: Risk Taking & Decision Making with David Cramer, Co-Founder & CTO at Sentry cover image

Software Team Leadership: Risk Taking & Decision Making with David Cramer, Co-Founder & CTO at Sentry

In this episode of the engineering leadership series, Rob Ocel interviews David Cramer, co-founder and CTO of Sentry, delving into the importance of decision-making, risk-taking, and the challenges faced in the software engineering industry. David emphasizes the significance of having conviction and being willing to make decisions, even if they turn out to be wrong. He shares his experience of attending a CEO event, where he discovered that decision-making and conflict resolution are struggles even for successful individuals. David highlights the importance of making decisions quickly and accepting the associated risks, rather than attempting to pursue multiple options simultaneously. He believes that being decisive is crucial in the fast-paced software engineering industry. This approach allows for faster progress and adaptation, even if it means occasionally making mistakes along the way. The success of Sentry is attributed to a combination of factors, including market opportunity and the team's principles and conviction. David acknowledges that bold ideas often carry a higher risk of failure, but if they do succeed, the outcome can be incredibly significant. This mindset has contributed to Sentry’s achievements in the industry. The interview also touches on the challenges of developing and defending opinions in the software engineering field. David acknowledges that it can be difficult to navigate differing viewpoints and conflicting ideas. However, he emphasizes the importance of standing by one's convictions and being open to constructive criticism and feedback. Throughout the conversation, David emphasizes the need for engineering leaders to be decisive and take calculated risks. He encourages leaders to trust their instincts and make decisions promptly, even if they are uncertain about the outcome. This approach fosters a culture of innovation and progress within engineering teams. The episode provides valuable insights into the decision-making process and the challenges faced by engineering leaders. It highlights the importance of conviction, risk-taking, and the ability to make decisions quickly in the software engineering industry. David's experiences and perspectives offer valuable lessons for aspiring engineering leaders looking to navigate the complexities of the field....