Modern Angular Form validation

recently I tweaks on the Angular form validation and come up with a handy solution to simplify the tedious validation process on the form. A small library is created for this article.

The basic process to define a form in the Angular project.

1. Define your form in your component

export type ValidationMessages = { [key: string]: { [key: string]: string } };

@Component({
  selector: 'signup',
  templateUrl: './signup.page.html',
  styleUrls: ['./signup.page.scss'],
  providers: [FormValidationService], <-- we rely on this service to do the form validation.
})
export class SignupPageComponent {
  ...
  private destroyRef = inject(DestroyRef);
  private emailCheckValidator = inject(emailAvailableValidator); // this is a customized async validator
  
  // all the input elements in the form (signal var)
  formInputElements = viewChildren(FormControlName, {
    read: ElementRef,
  });
  
  // the generated form validation error message will be stoe here
  displayMessage = computed(() => this.formValidationService.displayMessage());
  
  registerForm = this.fb.group({
    email: [
      '',
      {
        validators: [
          Validators.required,
          Validators.pattern(regexConfig.emailRegex),
        ],
        asyncValidators: [
          this.emailCheckValidator.validate.bind(this.emailCheckValidator),
        ],
        updateOn: 'blur',
      },
    ],
    password: ['', [Validators.required, Validators.minLength(6)]],
    fullname: ['', Validators.required],
  });
  
  get email(): AbstractControl {
    return this.registerForm.get('email') as AbstractControl;
  }
  
  get password(): AbstractControl {
    return this.registerForm.get('password') as AbstractControl;
  }
  
  get fullname(): AbstractControl {
    return this.registerForm.get('fullname') as AbstractControl;
  }

private validationMessages: ValidationMessages = {
    email: {
      required: 'validate.email_required',
      pattern: 'validate.email_valid',
      email_taken: 'validate.email_is_taken',
    },
    password: {
      required: 'validate.password_required',
      minlength: 'validate.password_minlength',
    },
    fullname: {
      required: 'validate.fullname_required',
    },
};

3. register the validator for the form in constructor

constructor() {
  this.formValidationService
    .registerValidator(
      this.validationMessages,
      this.formInputElements(),
      this.registerForm,
    )
  .subscribe();
}

4 if we want to set the server side valdation message to the form validation, config in the catchError

this.auth
  .emailSignup(this.registerForm.value as UserSignupForm)
  .pipe(
    takeUntilDestroyed(this.destroyRef),
    catchError((res) =>
      throwError(() => {
        this.formValidationService.setServerValidationErrors(
          this.registerForm,
          res,
        );
        return res;
      }),
    ),
  )
  .subscribe();

5. now config the template

to render the error message in the template, we use a component to do that automatically

 <form [formGroup]="registerForm">
   <div>
      <memodir-error-message
        [control]="email"
        [validationMessages]="displayMessage()"
      />
      <input type="text" formControlName="email" />
    </div>
    
    <div>
      <memodir-error-message
        [control]="password"
        [validationMessages]="displayMessage()"
      />
      <input type="password" formControlName="password" />
    </div>
    
   <div>
      <memodir-error-message
        [control]="fullname"
        [validationMessages]="displayMessage()"
      />
      <input type="text" formControlName="fullname" />
    </div>
    
   <button (click)="signUp()" [disabled]="registerForm.invalid">
      Signup
    </button>
</form>