1

Below is my Component :

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { HttpService } from './http.service';
import { ProjectidService } from './projectid.service';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
 projectDetailForm: FormGroup;
  public submitted = false;
  constructor(private fb: FormBuilder, private projectidvalidator: ProjectidService) { }

  ngOnInit() {
    this.projectDetailForm = this.fb.group({
      projectid: ['', [Validators.required], [this.projectidvalidator.validate.bind(this.projectidvalidator)]],
      projectname: ['name', Validators.required]
    })
  }
  get f() { return this.projectDetailForm.controls; }

  get validprojectid() { return this.projectDetailForm.get('projectid'); }

  onSubmit(form: FormGroup) {
    this.submitted = true;

    // stop here if form is invalid
    if (this.projectDetailForm.invalid) {
      return;
    }
    console.log('Valid?', this.projectDetailForm.valid); // true or false
    console.log('ID', this.projectDetailForm.value.projectid);
    console.log('Name', this.projectDetailForm.value.projectname);
  }

}

My Service :

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { delay, tap, debounceTime } from 'rxjs/operators';
@Injectable()
export class HttpService {

  constructor() { }

  checkProjectID(id): Observable<any> {
     // Here I will have valid HTTP service call to check the data

     return of(true)
  }
}

My Async validator :

import { HttpService } from './http.service';
import { Injectable } from '@angular/core';
import { AsyncValidator, AbstractControl, ValidationErrors } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { map, catchError, debounceTime, switchMap } from 'rxjs/operators';
@Injectable()
export class ProjectidService {

  constructor(private _httpService:HttpService) { }


    validate(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
        console.log(control.value);

        return control.valueChanges.pipe(
            debounceTime(500),
            switchMap(_ => this._httpService.checkProjectID(control.value).pipe(
                map(isTaken => {
                    console.log(isTaken);
                    if (isTaken) {
                        return { noproject: true }
                    } else {
                        return null
                    }

                })
            )),
            catchError(() => null)
        );


    }

}

and template :

<form [formGroup]="projectDetailForm" name="projectdetails" (ngSubmit)="onSubmit(projectDetailForm)">
    <div class="form-group">
        <label for="id">Project ID</label>
        <input type="text" class="form-control" id="id" [ngClass]="{ 'is-invalid': f.projectid.invalid && (f.projectid.dirty || f.projectid.touched) }" placeholder="Project ID" name="projectid" formControlName='projectid'>
        <button type="button">Validate</button>
        <div *ngIf="f.projectid.invalid && (f.projectid.dirty || f.projectid.touched)" class="invalid-feedback">
            <div *ngIf="f.projectid.errors.required">Project ID is required</div>
            <div *ngIf="f.projectid.errors?.noproject">
                Project id is not valid
            </div>
        </div>
        <div *ngIf="f.projectid.errors?.noproject">
            Project id is not valid
        </div>
        {{f.projectid.errors | json}}
    </div>
    <div class="form-group">
        <label for="name">Project Name</label>
        <input type="text" class="form-control" id="name" placeholder="Project Name" name="projectname" readonly formControlName='projectname'>
    </div>
    <div class="form-group d-flex justify-content-end">
        <div class="">
            <button type="button" class="btn btn-primary">Cancel</button>
            <button type="submit" class="btn btn-primary ml-1">Next</button>
        </div>
    </div>
</form>

Problem is my custom async validation error message is not getting displayed.

Here is stackblitz example

coder
  • 8,346
  • 16
  • 39
  • 53
user1608841
  • 2,455
  • 1
  • 27
  • 40

3 Answers3

1

You could do it as follows using rxjs/timer:

import { timer } from "rxjs";
....
    return timer(500).pipe(
      switchMap(() => {
        if (!control.value) {
          return of(null);
        }
        return this._httpService.checkProjectID(control.value).pipe(
          map(isTaken => {
            console.log(isTaken);
            if (isTaken) {
              return { noproject: true };
            } else {
              return null;
            }
          })
        );
      })
    );

Sample

shrys
  • 5,860
  • 2
  • 21
  • 36
  • shreyas why its not working with debouceTime and using `control.valueChange` ? any specific reason for the same ? – user1608841 Dec 11 '19 at 09:04
  • 1
    I think your approach coincides with [this](https://medium.com/grano/using-custom-async-validators-with-angular-b85a9fe9e298#d93f) explanation – shrys Dec 11 '19 at 09:15
  • 1
    also [hot v cold observables](https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339) – shrys Dec 11 '19 at 09:16
  • When the call takes longer then 500 miliseconds this code will not work as intended. – Hypenate Dec 11 '19 at 14:58
0

The real problem is and I have encountered this myself, you subscribe to the value change but you need to wait for the statuschange to return. It is "PENDING" while it is doing the call. The debounce/timer/... are just 'hacks' since you never know when the value is returned.

Declare a variable:

this.formValueAndStatusSubscription: Subscription;

In your

this.formValueAndStatusSubscription =
  combineLatest([this.form.valueChanges, this.form.statusChanges]).subscribe(
    () => this.formStatusBaseOnValueAndStatusChanges = this.form.status
  );

Don't forget to desstroy the subscription

Hypenate
  • 1,907
  • 3
  • 22
  • 38
0

The most important point in the async validation is as descriped in Angular Doc

The observable returned must be finite, meaning it must complete at some point. To convert an infinite observable into a finite one, pipe the observable through a filtering operator such as first, last, take, or takeUntil.

so basically you can use for example take(1) , it'll take the first emission then mark the Observable completed

 return control.valueChanges.pipe(
  debounceTime(500),
  take(1),
  switchMap(() =>
    this._httpService.checkProjectID(control.value).pipe(
      map(isTaken =>
        isTaken ? { noproject: true } : null
      )
    ))
)

demo

wessam yaacob
  • 909
  • 1
  • 6
  • 14