2
votes

Using Angular version 11.1.2

So, in my project there is a simple input field styled using angular material to enter name before redirecting to a new page, here are some validators that I've set in Form control:

  1. required
  2. minlength of 5
  3. maxlength of 25
  4. after all above validators are checked; through an api request at(/api/check/:name) it returns true (if name already exists) and false (if name doesnot not exists) -- (this one is not currently implemented in code and I think a custom async validators must be used)

I've also set autofocus on the input field; as soon as the component is loaded the focus goes directly to this input field. Also, for showing that username is valid or not there are some mat-icon as matSuffix ✔ for valid username and ❌ for invalid form input, also there is mat-progress-spinner for showing async validator is working.

Here is my component.html:

<mat-form-field appearance="outline" [ngStyle]="{ 'display': 'inline', 'font-size': '11pt' }">
  <mat-label class="inut-label">Enter your Username</mat-label>
  <input type="text" matInput placeholder="Eg: John123, Robo@123" [formControl]="username" autocomplete="off" autofocus required>

  <div class="form-condition" matSuffix>
    <mat-progress-spinner *ngIf="showSpinner" diameter="24" mode="indeterminate" color="accent"></mat-progress-spinner>

    <!-- form is incorrect icon -->
    <mat-icon *ngIf="username.invalid && !showSpinner" color="warn">clear</mat-icon>

    <!-- form is correct icon -->
    <mat-icon *ngIf="username.valid && !showSpinner" [ngStyle]="{ 'color': 'green' }">done</mat-icon>
  </div>

  <mat-error *ngIf="username.invalid">{{getErrorMessage()}}</mat-error>
</mat-form-field>

<button mat-raised-button color="primary" *ngIf="username.valid" class="form-login" (click)="navigateToUserWindow()">
   <mat-icon>login</mat-icon>
   <span>&nbsp;Login</span>
</button>

component.ts:

export class HomeComponent implements OnInit {
  username = new FormControl(null, [
    Validators.required,
    Validators.minLength(5),
    Validators.maxLength(25)
  ]);

  showSpinner = false;

  constructor(private route: Router) {}

  ngOnInit(): void {
    this.username.markAsPristine();
  }

  getErrorMessage(): string {
    if (this.username.hasError('required') && this.username.dirty) {
      return 'You cannot leave the username empty!';
    } else if (this.username.hasError('minlength') && this.username.dirty) {
      return 'Username must be at least 5 letters long!';
    } else if (this.username.hasError('maxlength') && this.username.dirty) {
      return 'Username must be atmost of 25 letters long!';
    }

    return '';
  }

  navigateToUserWindow(): void {
    this.route.navigate(['user']);
  }

}

here is the screenshot

enter image description here

Since, the login button will only appear if the form is valid. I want following things to work:

  1. On component init since autofocus works I want to hide ❌ icon.
  2. If validators are invoked when typing inside input show the error message as well as the respective icon representing for valid or invalid. Since, I've seen some examples and these validators show error only work when user remove focus outside of the input.
  3. I want to show one icon at a time so if async validator are busy show spinner when it's done then show whether form is valid or invalid.

Main problem arises when suppose on first input after autofocus user enters correct value for username and then validator are invoked for some reason this is only reflected by ❌ toggling but no appropriate message is displayed (this only happens when user removes the focus on error and then types again, then only error messages changes dynamically). Currently I'm bit confused about these properties like dirty, touched, untouched, pristine etc. and how can I use them properly according to this particular use case.

2

2 Answers

1
votes

To start here's an explanation of every properties:

  • valid: will be true if all validators are valid
  • invalid: will be true if one validator is invalid
  • dirty: will become true as soon as your FormControl value change
  • pristine: will be true as long as your FormControl value hasen't been changed
  • touched: will become true as soon as your element loose focus (similar to onBlur)
  • untouched: will be true as long as your element is not touched
  • pending: for asynchronous validation

Here's more information about asynchronous validation Form Validation - Creating asynchronous validators

When doing custom validation, I like to use invalid and touched. Here's what I'd use if I'd be you:

<mat-progress-spinner *ngIf="username.pending && username.touched" diameter="24" mode="indeterminate" color="accent"></mat-progress-spinner>
<mat-icon *ngIf="username.invalid && username.touched && !username.pending" color="warn">clear</mat-icon>
<mat-icon *ngIf="username.valid && username.touched && !username.pending" [ngStyle]="{ 'color': 'green' }">done</mat-icon>
1
votes

From @Leccho's reply, adding the following customErrorStateMatcher class to the component solved the problem.

// new class for changing the behavior when the error shoould be shown

class CustomErrorStateMatcher implements ErrorStateMatcher {
  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    return !!(control && control.invalid && (control.dirty || control.touched));
  }
}

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
  private mainAvatarPath: string;
  username = new FormControl(null, [
    Validators.required,
    Validators.minLength(5),
    Validators.pattern('^[a-zA-Z0-9_]*$')
  ]);

  matcher = new CustomErrorStateMatcher();

  constructor() {
  }

  ngOnInit(): void {
    // this.username.markAsTouched() ;
  }

  getErrorMessage(): string {
    if (this.username.hasError('required') && this.username.dirty) {
      return 'You cannot leave the username empty!';
    } else if (this.username.hasError('minlength') && this.username.dirty) {
      return 'Username must be at least 5 letters long!';
    } else if (this.username.hasError('pattern') && this.username.dirty) {
      return 'Only [a-z, A-Z and _] are allowed as valid username!';
    }

    return '';
  }

  navigateToUserWindow(): void {
    this.route.navigate(['user']);
  }

}

Then adding this matcher object of CustomErrorStateMatcher Class to the html as follows:

<mat-form-field appearance="outline" [ngStyle]="{ 'display': 'inline', 'font-size': '11pt' }">
  <mat-label class="inut-label">Enter your Username</mat-label>
  <input type="text" matInput placeholder="Eg: John123, Robo@123" [formControl]="username" [errorStateMatcher]="matcher" autocomplete="off" maxLength="25" autofocus>
  
  <!-- added above -->

  <div class="form-condition" matSuffix>
    <mat-progress-spinner *ngIf="username.pending" diameter="24" mode="indeterminate" color="accent"></mat-progress-spinner>

    <!-- form is incorrect icon -->
    <mat-icon *ngIf="username.invalid && (username.touched || username.dirty) && !username.pending" color="warn">clear</mat-icon>

    <!-- form is correct icon -->
    <mat-icon *ngIf="username.valid && (username.touched || username.dirty) && !username.pending" [ngStyle]="{ 'color': 'green' }">done</mat-icon>
  </div>

  <mat-error>{{getErrorMessage()}}</mat-error>
</mat-form-field>