Tips for properly executing directives in sequential order while using async in Angular 13

I created a standard registration form using Angular and implemented an async directive (UserExistsDirective) to validate if the email is already taken. To manage error messages, I also utilized a second directive (ValidityStyleDirective) which is also used on other input fields.

The challenge I'm facing is ensuring that ValidityStyleDirective waits for UserExistsDirective to complete before executing. Otherwise, the error message may be out of sync. I've included the code I have so far below and would appreciate any guidance on how to ensure that UserExistsDirective completes before triggering ValidityStyleDirective in Angular.

Thank you,

HTML :

...
<div class="mb-4 inputcontainer">
                    <label class="form-label" for="email">Email</label>
                    <input appUserExists validityStyle formControlName="email" class="form-control" id="email" type="email" name="email" >
                    <div *ngIf="registerForm.get('email')?.pending" class="icon-container">
                        <i class="loader"></i>
                    </div>
                    <div class="valid-feedback">Looks good!</div>
                    <div class="invalid-feedback">
                        <span *ngIf="registerForm.get('email')?.hasError('required')">Please provide an email.</span>
                        <span *ngIf="registerForm.get('email')?.errors?.['pattern']">Please provide a valid email.</span>
                        <span *ngIf="registerForm.get('email')?.errors?.['userExists']">This email address is already registered. Please use another one.</span>
                    </div>
                </div>
...

UserExistsDirective : it add a the key userExists to validator if an email is found.

@Directive({
  selector: '[appUserExists]',
  providers: [{    
    provide: NG_ASYNC_VALIDATORS,    
    useExisting: UserExistsDirective,    
    multi: true  
  }]
})
export class UserExistsDirective implements AsyncValidator {
  constructor() { 

  }
  validate(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
    return this.emailExists(control.value).pipe(
      take(1),
      map(isTaken => (isTaken ? { userExists: true } : null)),
      catchError(() => of(null))
    );
  }

  emailExists(email: string): Observable<boolean> {
    return of(email).pipe(
      delay(500),
      map((email) => {
        const emails = ['example@example.com', 'test@test.com'];
        return emails.includes(email);
      })
    );
  }
}

Then ValidityStyleDirective adds the necessary error styling when needed

@Directive({
    selector: '[validityStyle]'
})
export class ValidityStyleDirective {

    constructor(@Self() private ngControl: NgControl,
        private formGroup: FormGroupDirective,
        private renderer: Renderer2,
        private el: ElementRef) { }
    
    @HostListener('blur')
    onBlur($event: any) {
        if (this.ngControl.errors == null) {
            this.formGroup.control.get('email')?.setErrors(null);
        }

        if (this.ngControl.value === '') {
            this.renderer.removeClass(this.el.nativeElement, 'is-invalid');
            return false;
        }

        if (!this.ngControl.valid) {
            this.renderer.addClass(this.el.nativeElement, 'is-invalid');
            return true;
        }

        this.renderer.removeClass(this.el.nativeElement, 'is-invalid');
        return false;
    }
}

Answer №1

I discovered the solution to my issue and decided to share it in case others may benefit from it.

To achieve the desired outcome, I made changes by adding/removing the IS-INVALID class from the appUserExists directive

@Directive({
  selector: '[appUserExists]',
  providers: [{
    provide: NG_ASYNC_VALIDATORS,
    useExisting: UserExistsDirective,
    multi: true
  }]
})
export class UserExistsDirective implements AsyncValidator {

  constructor(private renderer: Renderer2,
    private el: ElementRef) {

  }

  validate(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
    return this.emailExists(control.value).pipe(
      first(),
      map(isTaken => (isTaken ? { userExists: true } : null)),
      catchError(() => of(null))
    );
  }

  emailExists(email: string): Observable<boolean> {
    return of(email).pipe(
      delay(500),
      map((email) => {
        const emails = ['<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="7a4b48493a15... 
        const isTaken = emails.includes(email);

        isTaken
          ? this.renderer.addClass(this.el.nativeElement, 'is-invalid')
          : this.renderer.removeClass(this.el.nativeElement, 'is-invalid');

        return isTaken;
      })
    );
  }
}

Additionally, I resolved the out-of-sync error message by utilizing the STATUS property instead of VALID

@Directive({
    selector: '[validityStyle]'
})
export class ValidityStyleDirective {
    constructor(@Self() private ngControl: NgControl
    , private formGroup: FormGroupDirective) { }
  
    @HostBinding('class.is-invalid')
    get setInvalid() {
        return  (this.formGroup as FormGroupDirective)?.submitted && this.ngControl.status == 'INVALID' 
        || (this.ngControl.dirty && this.ngControl.status == 'INVALID' && this.ngControl.value != '');
    }
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Determine the category of a container based on the enclosed function

The goal is to determine the type of a wrapper based on the wrapped function, meaning to infer the return type from the parameter type. I encountered difficulties trying to achieve this using infer: function wrap<T extends ((...args: any[]) => any) ...

Modifying the user interface (UI) through the storage of data in a class variable has proven to be

If I need to update my UI, I can directly pass the data like this: Using HTML Template <li *ngFor="let post of posts; let i = index;"> {{i+1}}) {{post.name}} <button (click)="editCategory(post)" class="btn btn-danger btn-sm">Edit</butto ...

In an Angular component, attempt to retrieve the data type of a class property

Discover how to retrieve the type of properties from an object by following this Typescript tutorial link. However, it seems to be behaving like Javascript and returning the value of the property instead of the type: const x = { foo: 10, bar: 'hello! ...

With TypeScript, you have the flexibility to specify any data type in the generic types when using the axios.get method

axios.get('/api') When working with TypeScript as shown above, it is important to designate types for better clarity. This allows us to reference the type definition of axios, like so: (method) AxiosInstance.get<any, AxiosResponse<any> ...

What is the error message "Cannot assign type 'IArguments' to argument"?

Currently employing a workaround that is unfortunately necessary. I have to suppress specific console errors that are essentially harmless. export const removeConsoleErrors = () => { const cloneConsoleError = console.error; const suppressedWarnings ...

What is the best way to refresh a Load on Demand feature in Nativescript?

In my RadListView, the data displayed changes depending on the day selected by the user in a calendar. Currently, I am using loadOnDemandMode = "Manual" and it functions smoothly until all the available data is loaded. At that point, I trigger listView.no ...

How can I only accept one of two specific function signatures in Typescript and reject any others?

Looking for an answer from the community on Typescript, require either of two function signatures In my code, I am handling a callback where I need to either pass an error or leave the first argument as undefined and pass data as the second argument. To ...

FormArray in reactive forms with draggable formGroups

Within the angular drag-drop module, there is documentation available for the moveItemInArray() function which allows dragging content within an array. However, how can we shuffle (formGroups/formControls) in formArray? I attempted to use the moveItemInFo ...

What events can cause all store states to be loaded when the page is altered?

I am currently utilizing ngrx/store, ngrx/effects, and ngrx/router. The implementation of my effects is structured as follows: @Effect() loadOneProduct$ = this.updates$ .whenAction(LOAD_ONE_PRODUCT) .switchMap(() => this.productService.loadOn ...

The 'eventKey' argument does not match the 'string' parameter. This is because 'null' cannot be assigned to type 'string'

Encountering an issue while invoking a TypeScript function within a React Project. const handleLanguageChange = React.useCallback((eventKey: eventKey) => { setLanguage(eventKey); if(eventKey == "DE") setCurre ...

What is the proper way to add an SSL cert to an Angular HTTP API request?

Is there a need to utilize a certificate when making an API call to retrieve an access token? For example, if the method is POST at getAccess.com/getCode, how should we handle this in Postman with a certificate attached? I am currently working on an Angula ...

Exploring the functionality of CanDeactiveGuard and ModalDialogService through unit testing

In my application, the CanDeactiveGuard is functioning properly. During unit testing, I encountered an issue with one test where I intended to use callThrough to invoke the openConfirmDialog() method within the Guard. This method triggers the Modal Dialog ...

What is the proper way to apply a background color to the entire body using CSS?

I'm facing an issue with getting the background color to cover the entire body of my webpage. Currently, it only captures a certain size and when there is scrolling on the window, two shades of colors appear. I want the background color to span the en ...

What is the process for creating a new type from a nested part of an existing type?

Currently, my website is being developed with a focus on utilizing code generation to ensure type safety when handling GraphQl queries. Certain components within the application receive a portion of an object as a prop. The specific type structure is outli ...

Resolving Cross-Origin Resource Sharing problem in Google authentication and authorization with React, Node.js, and Passport

I am currently experiencing the same issue as described in this Stack Overflow post. Unfortunately, I have been unable to implement @Yazmin's suggested solution successfully. My goal is to develop a stack using React, Express/Node.js, and MongoDB wit ...

Utilizing React Typescript to dynamically render a duo of components

On a single page, I want to display two components simultaneously. There is a bottom navbar that, when clicked on, for example the profile icon, should render the profile page. However, I would like to change the color of the icon based on which component ...

The parent component can successfully call the setState function, but for some reason, the

In my code structure, I have the following setup (simplified): Here is the parent component: //code... const {handleClick} = useClick; <ul> {actions.map((action: string) => ( <li onClick={() => handleClick()} key={uuidv4()}> ...

Display a loading indicator in Angular during app initialization with APP_INITIALIZER

Is there a way to display a waiting indicator while the Angular app is running the app_initializer code? Currently, I can display a waiting indicator until the app is fully loaded. However, once the page loads, it remains empty until the app_initializer c ...

Angular Algolia InstantSearch displaying a type error

Whenever I attempt to navigate using Angular navigation to a component, it immediately redirects back to the previous page and throws this error in the console log core.js:14597 ERROR Error: Uncaught (in promise): TypeError: Cannot read property 'add ...

Checking for GitHub API connectivity issues with GitHub can be done by verifying whether the GitHub API is

Is there a way to check from the GitHub API if it is unable to connect to GitHub or if the internet is not connected? When initializing the API like this: GitHubApi = require("github"); github = new GitHubApi({ version: "3.0.0" ...