angularangular-observableangular-custom-validators

Can't get through with async validators


So, I have a blog, and I'm trying to setup a check, on the creation of a new text, to lock the form if the title already exists.

I got a TextService

 export class TextService {

  private _url = "http://localhost:8080";
  private _textsUrl = "http://localhost:8080/texts";

  findAll(): Observable<Text[]> {
    return this._hc.get<Text[]>(this._textsUrl);
  }

  checkIfTitleExists(testedTitle: string) {
    var existing_titles: String[] = [];
    this.findAll().subscribe(texts => existing_titles = texts.map(t => t.title));
    return of(existing_titles.includes(testedTitle));
  }

I got a TextAddComponent

export class TextAddComponent implements OnInit {

  text: Text = new Text();
  form: any;

  constructor(
    private _ts: TextService,
    private fb: FormBuilder
  ) {}

  ngOnInit(): void {
    this.form = this.fb.group({
      title: ["", this.alreadyExistingTitle(this._ts)],
      subtitle: [""],
      content: [""] ,
    });
  }

  alreadyExistingTitle(ts: TextService): AsyncValidatorFn {
    return (control: AbstractControl): Observable<ValidationErrors | null> => {
      return ts.checkIfTitleExists(control.value).pipe(
        map((result: boolean) =>
          result ? { titleAlreadyExists: true } : null
        )
      )
      }
  }

  onSubmit() {
    this.text= Object.assign(this.text, this.form.value);
    this._ts.save(this.text).subscribe();
  }

}

And I got a template

<form [formGroup]="form" (ngSubmit)="onSubmit()">
    <div>
        <label>
            Title
        </label>
    </div>
    <div>
        <input type="text" formControlName="title">
        <div *ngIf="form.controls.title.errors?.alreadyExistingTitle">
            Title already exists
        </div>
    </div>

    <div>
        <label>
            Subtitle
        </label>
    </div>
    <div>
        <input type="text" formControlName="subtitle">
    </div>

    <div>
        <label>
            Content
        </label>
    </div>
    <div>
        <input type="text" formControlName="content">
    </div>

    <p>
        <button type="submit" [disabled]="!form.valid">Sauvegarder le texte</button>
    </p>
</form>

As you can see, I declare an async validator in the component, and this async validator uses a method from the service

I got two issues here:


Solution

    1. From the below line:
    this.findAll().subscribe(texts => existing_titles = texts.map(t => t.title));
    

    It is asynchronous. Thus the next line:

    return of(existing_titles.includes(testedTitle));
    

    will be executed without waiting for the Observable to be returned.

    Fix: Migrate the checking logic into Observable.

    export class TextService {
      ...
    
      checkIfTitleExists(testedTitle: string): Observable<boolean> {
        return this.findAll().pipe(
          map((texts) => texts.findIndex((t) => t.title == testedTitle) > -1)
        );
      }
    }
    
    1. Fix: As your custom validator is AsyncValidatorFn, pass it into the asyncValidators parameter for the constructor.
    this.form = this.fb.group({
      title: ['', { asyncValidators: this.alreadyExistingTitle(this._ts) }],
      ...
    });
    
    1. The custom validator return ValidatorError with the titleAlreadyExists property.
    map((result: boolean) =>
      result ? { titleAlreadyExists: true } : null
    )
    

    Fix: The error should be "titleAlreadyExists" but not "alreadyExistingTitle".

    <div *ngIf="form.controls.title.errors?.titleAlreadyExists">
        Title already exists
    </div>
    

    Demo @ StackBlitz