Reflecting server errors in Angular form validation

• 4 min read

Here's some sample code that shows how to display server validation errors sent by a django-restframework REST server.

<form [formGroup]='form' (ngSubmit)="doChangePassword()">
    <ion-item>
        <ion-label>{{ 'CURRENT_PASSWORD' | translate }}</ion-label>
        <ion-input formControlName="old_password" type="password"></ion-input>
    </ion-item>
    <ion-item class="ion-text-wrap ion-no-lines" *ngIf="form.controls.old_password.errors?.required && form.controls.old_password.dirty && form.controls.old_password.touched">
        <ion-label color='danger' class="ion-no-margin" stacked>{{ 'FIELD_IS_REQUIRED' | translate }}</ion-label>
    </ion-item>
    <ion-item class="ion-no-lines" *ngFor="let errorMsg of form.controls.old_password.errors?.serverErrors">
        <ion-label color='danger' class="ion-no-margin ion-text-wrap" stacked>
            <small>{{ errorMsg }}</small>
        </ion-label>
    </ion-item>
    <ion-item>
        <ion-label>{{ 'NEW_PASSWORD' | translate }}</ion-label>
        <ion-input formControlName="new_password1" type="password"></ion-input>
    </ion-item>
    <ion-item class="ion-text-wrap ion-no-lines" *ngIf="form.controls.new_password1.errors?.required && form.controls.new_password1.dirty && form.controls.new_password1.touched">
        <ion-label color='danger' class="ion-no-margin" stacked><small>{{ 'FIELD_IS_REQUIRED' | translate }}</small></ion-label>
    </ion-item>
    <ion-item class="ion-no-lines" *ngFor="let errorMsg of form.controls.new_password1.errors?.serverErrors">
        <ion-label color='danger' class="ion-no-margin ion-text-wrap" stacked>
            <small>{{ errorMsg }}</small>
        </ion-label>
    </ion-item>
    <ion-item>
        <ion-label>{{ 'CONFIRM_NEW_PASSWORD' | translate }}</ion-label>
        <ion-input formControlName="new_password2" type="password"></ion-input>
    </ion-item>
    <ion-item class="ion-text-wrap ion-no-lines" *ngIf="form.controls.new_password2.errors?.required && form.controls.new_password2.dirty && form.controls.new_password2.touched">
        <ion-label color='danger' class="ion-no-margin" stacked><small>{{ 'FIELD_IS_REQUIRED' | translate }}</small></ion-label>
    </ion-item>
    <ion-item class="ion-text-wrap ion-no-lines" *ngIf="form.controls.new_password2.errors?.matchError && form.controls.new_password2.touched && form.controls.new_password2.dirty">
        <ion-label color='danger' class="ion-no-margin" stacked>
            <small>{{ 'PASSWORDS_MISMATCH' | translate }}</small>
            </ion-label>
    </ion-item>
    <ion-item class="ion-no-lines" *ngFor="let errorMsg of form.controls.new_password2.errors?.serverErrors">
        <ion-label color='danger' class="ion-no-margin ion-text-wrap" stacked>
            <small>{{ errorMsg }}</small>
        </ion-label>
    </ion-item>
    <ion-button type="submit" expand="block" [disabled]="!form.valid">
        {{ 'CHANGE_PASSWORD' | translate }}
    </ion-button>
</form>

Specifically, note how ngIf & ngFor tags are used to conditionally detect and display error messages set on individual controls. The attribute serverErrors on a control is set by the page handler TypeScript code via a snippet like this:

  // Form definition
  this.form = formBuilder.group({
    old_password: ['', Validators.required],
    new_password1: ['', Validators.compose([
      Validators.required,
    ])],
    new_password2: ['', Validators.required],
  }, {

  // form submit handler
  async doFormSubmit() {
    try {
      await this.service.post(this.form.value);
      let toast = await this.toastCtrl.create({
        message: 'Server updated',
        duration: 3000,
        position: 'top'
      });
      toast.present();
      this.router.navigateByUrl('/home');
    } catch (err) {
      // 'err' contains server errors
      this.handleServerErrors(err.error);      
    }
  }

  // For fields with specific error messages, set the relevant field
  // control's error.serverError attribute to the server returned error
  // message. This then can be directly displayed in the template.
  handleServerErrors(err) {
    // Each key of the err object will correspond to the the field
    // name which has an error. Since the field names in the form match
    // the REST serializer fields, we can use this to attach the server
    // supplied error messages to corresponding UI input field.
    for (const controlName in err) {
      if (this.form.contains(controlName)) {
        this.form.controls[controlName].setErrors({serverErrors: err[controlName]});
      } else {
        // Need to handle generic form errors, that do not apply to a specific
        // field, but to the whole form.
      }
    }
  }