Search code examples
angularformsasynchronousangular-reactive-formscustomvalidator

Angular reactive form custom async validation from api response


I'm new on Angular (V6) and I'm kinda lost there. I was looking for an answer to my problem but after few days of research I wasn't able to find what I was looking for. Hope you guys will help me.

I developed an API where there is an endpoint for account registration. On a POST request, the server just checks if everything in the body request is fine, otherwise it sends a JSON with the field errors (e.g. username is already taken or too short, passwords mismatch, email address already taken, etc).

I wanted to use async validation in my reactive form when I submit it so I can get the server response and display the errors in the form. But I've not managed to do it. There is no really clear example in the Angular docs to me for this case.

sign-up.component.ts

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

import { ApiService } from './../../services/api.service';

@Component({
  selector: 'app-sign-up',
  templateUrl: './sign-up.component.html',
  styleUrls: ['./sign-up.component.scss']
})
export class SignUpComponent implements OnInit {

  signupForm: FormGroup;

  constructor(
    private formBuilder: FormBuilder,
    private apiService: ApiService
  ) { }

  ngOnInit() {
    this.initForm();
  }

  initForm() {
    this.signupForm = this.formBuilder.group({
      firstname: ['', Validators.required],
      lastname: ['', Validators.required],
      username: ['', Validators.required],
      email: ['', Validators.required],
      password: ['', Validators.required],
      confirmPassword: ['', Validators.required],
      birthdate: ['', Validators.required],
      gender: ['', Validators.required]
    }, {
      asyncValidator: this.myAsyncValidator() // Am I wrong?
    });
  }

  onSubmitForm() {
    console.log('submit');
    // On submit, I send the form to the server.
    // If the account has been created, I redirect the user (no problem for that)
    // but if there are some errors, I would like to display them into the form.
  }

  myAsyncValidator () {
    // I'm lost here
  }
}

api.service.ts

import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';

const httpOptions = {
  headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};

const apiURL = 'MY_SERVER_URL';

@Injectable({
  providedIn: 'root'
})
export class ApiService {

  constructor(private http: HttpClient) { }

  signUp(data: Object) {
    const body = JSON.stringify(data);
    return this.http.post(`${apiURL}/authentication/signup/`, body, httpOptions);
  }
}

sign-up.component.html

<div class="signup-container">
  <div class="mx-auto">
    <form [formGroup]="signupForm" (ngSubmit)="onSubmitForm()">
      <mat-form-field>
        <input matInput placeholder="Prénom" formControlName="firstname">
      </mat-form-field>

      <mat-form-field>
        <input matInput placeholder="Nom" formControlName="lastname">
      </mat-form-field>

      <mat-form-field>
        <input matInput placeholder="Pseudo" formControlName="username">
      </mat-form-field>

      <mat-form-field>
        <input matInput type="email" placeholder="Email" formControlName="email">
      </mat-form-field>

      <mat-form-field>
        <input matInput type="password" placeholder="Mot de passe" formControlName="password">
      </mat-form-field>

      <mat-form-field>
        <input matInput type="password" placeholder="Confirmer le mot de passe" formControlName="confirmPassword">
      </mat-form-field>

      <mat-form-field>
        <input matInput [matDatepicker]="picker" placeholder="Date de naissance" formControlName="birthdate" (focus)="picker.open()">
        <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
        <mat-datepicker #picker></mat-datepicker>
      </mat-form-field>

      <mat-radio-group formControlName="gender">
        <mat-radio-button value="Femme">Femme</mat-radio-button>
        <mat-radio-button value="Homme">Homme</mat-radio-button>
      </mat-radio-group>

      <button type="submit" mat-stroked-button class="save-button"><i class="material-icons save-icon">save</i>Inscription</button>
    </form>
  </div>
</div>

Solution

  • I've found a solution to my problem. I've switched from reactive form to template driven form. I know this is not the best solution, I will try to find a better way to solve that. Hope this could help anyone!

    Here's what I've done:

    sign-up.component.ts

    import { Component, OnInit } from '@angular/core';
    import { Router } from '@angular/router';
    import { NgForm } from '@angular/forms';
    
    import { ApiService } from './../../services/api.service';
    
    @Component({
      selector: 'app-sign-up',
      templateUrl: './sign-up.component.html',
      styleUrls: ['./sign-up.component.scss']
    })
    export class SignUpComponent implements OnInit {
      isSending = false;
      submitText = 'Inscription';
    
      constructor(
        private apiService: ApiService,
        private router: Router
      ) { }
    
      ngOnInit() { }
    
      onSubmit(formData: NgForm) {
        this.isSending = true;
        this.submitText = 'Inscription en cours...';
        this.apiService.signUp(formData.value).subscribe(
          response => {
            this.router.navigate(['/signin']);
          },
          error => {
            const errors = error.error.message.errors; // Yeah... Mongoose validator...
            for (const fieldError in errors) {
              if (errors[fieldError]) {
                formData.form.controls[fieldError].setErrors({ invalid: errors[fieldError]['message'] });
              }
            }
            this.isSending = false;
            this.submitText = 'Inscription';
          }
        );
      }
    }
    

    sign-up.component.html

    <div class="signup-container">
      <div class="mx-auto">
        <form (ngSubmit)="onSubmit(f)" #f="ngForm">
          <mat-form-field>
            <input matInput placeholder="Prénom" ngModel name="firstname" #firstname="ngModel">
            <mat-error *ngIf="firstname.errors">{{firstname.errors['invalid']}}</mat-error>
          </mat-form-field>
    
          <mat-form-field>
            <input matInput placeholder="Nom" ngModel name="lastname" #lastname="ngModel">
            <mat-error *ngIf="lastname.errors">{{lastname.errors['invalid']}}</mat-error>
          </mat-form-field>
    
          <mat-form-field>
            <input matInput placeholder="Pseudo" ngModel name="username" #username="ngModel">
            <mat-error *ngIf="username.errors">{{username.errors['invalid']}}</mat-error>
          </mat-form-field>
    
          <mat-form-field>
            <input matInput type="email" placeholder="Email" ngModel name="email" #email="ngModel">
            <mat-error *ngIf="email.errors">{{email.errors['invalid']}}</mat-error>
          </mat-form-field>
    
          <mat-form-field>
            <input matInput type="password" placeholder="Mot de passe" ngModel name="password" #password="ngModel">
            <mat-error *ngIf="password.errors">{{password.errors['invalid']}}</mat-error>
          </mat-form-field>
    
          <mat-form-field>
            <input matInput type="password" placeholder="Confirmer le mot de passe" ngModel name="confirmPassword" #confirmPassword="ngModel">
            <mat-error *ngIf="confirmPassword.errors">{{confirmPassword.errors['invalid']}}</mat-error>
          </mat-form-field>
    
          <mat-form-field>
            <input matInput [matDatepicker]="picker" placeholder="Date de naissance" ngModel name="birthdate" #birthdate="ngModel" (focus)="picker.open()">
            <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
            <mat-datepicker #picker></mat-datepicker>
            <mat-error *ngIf="birthdate.errors">{{birthdate.errors['invalid']}}</mat-error>
          </mat-form-field>
    
          <mat-radio-group ngModel name="gender" #gender="ngModel">
            <mat-radio-button value="Femme">Femme</mat-radio-button>
            <mat-radio-button value="Homme">Homme</mat-radio-button>
            <mat-error *ngIf="gender.errors">{{gender.errors['invalid']}}</mat-error>
          </mat-radio-group>
    
          <button type="submit" mat-stroked-button class="save-button" [disabled]="isSending">
            <i class="material-icons save-icon">save</i>{{submitText}}
          </button>
        </form>
      </div>
    </div>