Search code examples
angulartypescripttwitter-bootstrapangular-reactive-formsbootstrap-ui

Angular Reactive Form with Bootstrap - Custom validation is not working in animation


I created a Reactive Form in Angular 16, and added Bootstrap validation to it, normal built-in validators work fine, adding a custom validator also adds the error to the errors array but Bootstrap still shows the input element as valid.

bootstrap-form.component.html:

<div class="container">
    <div class="row">
        <div class="col">
            <h2 class="text-center fs-3 semibold">
                {{ loginForm.value | json }}
            </h2>
            <form class="needs-validation" [formGroup]="loginForm" novalidate>
                <div class="mt-4">
                    <label for="username-input" class="form-label fs-4">
                        username
                    </label>
                    <input
                        type="text"
                        id="username-input"
                        placeholder="username"
                        class="form-control mt-2"
                        formControlName="username"
                        required
                    />
                    <div
                        class="invalid-feedback"
                        *ngIf="
                            loginForm.controls['username'].hasError('required')
                        "
                    >
                        username cannot be empty
                    </div>
                </div>
                <div class="mt-4">
                    <label for="password-input" class="form-label fs-4">
                        password
                    </label>
                    <input
                        type="password"
                        id="password-input"
                        placeholder="password"
                        class="form-control mt-2"
                        formControlName="password"
                        required
                    />
                    <div
                        class="invalid-feedback"
                        *ngIf="
                            loginForm.controls['password'].hasError('required')
                        "
                    >
                        password cannot be empty
                    </div>
                    <div
                        class="invalid-feedback"
                        *ngIf="
                            loginForm.controls['password'].errors?.['passwordInvalid']
                        "
                    >
                        password cannot be less than 8 characters
                    </div>
                    <h3 class="fs-6">
                        {{ loginForm.controls["password"].errors | json }}
                    </h3>
                </div>
                <div class="mt-4">
                    <button type="submit" class="btn btn-primary col-12">
                        login
                    </button>
                </div>
            </form>
        </div>
    </div>
</div>

bootstrap-form.component.ts:

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

@Component({
    selector: 'app-bootstrap-form',
    templateUrl: './bootstrap-form.component.html',
    styleUrls: ['./bootstrap-form.component.css'],
})
export class BootstrapFormComponent implements OnInit {
    loginForm: FormGroup;

    constructor(private formBuilderService: FormBuilder) {
        this.loginForm = this.formBuilderService.group({
            username: ['', [Validators.required]],
            password: ['', [Validators.required, validatePassword]],
            phoneNumber: ['', [Validators.required]],
        });
    }

    ngOnInit(): void {
        let form = document.querySelector('form') as HTMLFormElement;
        form.addEventListener('submit', (submitEvent: SubmitEvent) => {
            if (!form.checkValidity()) {
                submitEvent.preventDefault();
                submitEvent.stopPropagation();
            }

            form.classList.add('was-validated');
        });
    }
}

export function validatePassword(
    formControl: AbstractControl
): { [key: string]: any } | null {
    if (formControl.value && formControl.value.length < 8) {
        return { passwordInvalid: true };
    }
    return null;
}

error-screenshot error-animation-screenshot

As you can see in the attached screenshots, the errors array has a nerror but Bootstrap still shows it in green.

I just tried out this code, and I can't understand what's wrong here so I don't know what to try out.


Solution

  • According to Form validation in Bootstrap docs,

    All modern browsers support the constraint validation API, a series of JavaScript methods for validating form controls.

    Although the Reactive form throws an error for the password field, it doesn't set the error in the constraint validation API.


    Approach 1: Use minLength attribute

    From the validatePassword function, you are validating the password minimum length, you can add the minLength="8" attribute to the <input> element.

    <input
        type="password"
        id="password-input"
        placeholder="password"
        class="form-control mt-2"
        formControlName="password"
        required
        minlength="8"
    />
    

    Note that, you can replace the validatePassword with Validators.minLength(8) for the form control validation

    password: ['', [Validators.required, Validators.minLength(8)]]
    

    Approach 2: Update the error message to Validation API

    If you are keen to use the Angular Reactive Form built-in/custom validation without the HTML attribute for the constraint validation API, you need to update the error message in the constraint validation API for each <input> element via setCustomValidity(error).

    <input
        #passwordInput
        type="password"
        id="password-input"
        placeholder="password"
        class="form-control mt-2"
        formControlName="password"
        required
        (input)="validatePasswordInput(passwordInput)"
    />
    
    validatePasswordInput(passwordField: HTMLInputElement) {
      if (this.loginForm.controls['password'].errors) {
        for (let error in this.loginForm.controls['password'].errors)
          passwordField.setCustomValidity(
            this.loginForm.controls['password'].errors[error]
          );
      } else {
        // No error
        passwordField.setCustomValidity('');
      }
    }
    

    Demo @ StackBlitz