Manjeet Yadav
Manjeet Yadav

Reputation: 37

How to add custom unique validator for form values in angular reactive form?

I want to add a custom unique validator that will validate that all label fields values are unique. (I) When I change the form values, the value of this.form changes after it is passed in CustomValidator.uniqueValidator(this.form). How to fix this? (II) Is there any way of doing this without using any package?

Note: Forms have default values on load. Here is the screenshot.

enter image description here

this.form = this.fb.group({
      fields: this.fb.array([])
    });

private addFields(fieldControl?) {
return this.fb.group({
  label: [
    {value: fieldControl ? fieldControl.label : '', disabled: this.makeComponentReadOnly}, [
    Validators.maxLength(30), CustomValidator.uniqueValidator(this.form)
    ]],
  isRequired: [
    {value: fieldControl ? fieldControl.isRequired : false, disabled: this.makeComponentReadOnly}],
  type: [fieldControl ? fieldControl.type : 'text']
});

}

  static uniqueValidator(form: any): ValidatorFn | null {
return (control: AbstractControl): ValidationErrors | null => {
  console.log('control..: ', control);
  const name = control.value;

  if (form.value.fields.filter(v => v.label.toLowerCase() === control.value.toLowerCase()).length > 1) {
    return {
      notUnique: control.value
    };
  } else {
    return null;
  }

}; }

Upvotes: 2

Views: 11215

Answers (2)

Yilmaz
Yilmaz

Reputation: 49321

in real life, username or email properties are checked to be unique. This will be very long answer I hope you can follow along. I will show how to check uniqueness of username.

to check the database, you have to create a service to make a request. so this validator will be async validator and it will be written in class. this class will be communicate with the service via the dependency injection technique.

First thing you need to setup HttpClientModule. in app.module.ts

import { HttpClientModule } from '@angular/common/http';
@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, YourOthersModule , HttpClientModule],
  providers: [],
  bootstrap: [AppComponent],
})

then create a service

 ng g service Auth //named it Auth

in this auth.service.ts

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

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  constructor(private http: HttpClient) {}
  userNameAvailable(username: string) {
 // avoid type "any". check the response obj and put a clear type
    return this.http.post<any>('https://api.angular.com/username', {
      username:username,
    });
  }
}

now create a class ng g class UniqueUsername and in this class:

import { Injectable } from '@angular/core';
import { AsyncValidator, FormControl } from '@angular/forms';
import { map, catchError } from 'rxjs/operators';
import { of } from 'rxjs';
import { AuthService } from './auth.service';
// this class needs to use the dependency injection to reach the http client to make an api request
// we can only access to http client with dependecny injection system
// now we need to decorate this class with Injectable to access to AuthService
@Injectable({
  providedIn: 'root',
})
export class UniqueUsername implements AsyncValidator {
  constructor(private authService: AuthService) {}
  //this will be used by the usernamae FormControl
  //we use arrow function cause this function will be called by a 
  //different context, but we want it to have this class' context 
  //because this method needs to reach `this.authService`. in other context `this.authService` will be undefined.
  // if this validator would be used by the FormGroup, you could use 
  "FormGroup" type.
  //if you are not sure you can  use type "control: AbstractControl"
  //In this case you use it for a FormControl
  


   validate = (control: FormControl) => {
        const { value } = control;
        return this.authService.userNameAvailable(value).pipe(
        //errors skip the map(). if we return null, means we got 200 response code, our request will indicate that username is available
        //catchError will catch the error
          map(() => {
            return null;
          }),
          catchError((err) => {
            console.log(err);
         //you have to console the error to see what the error object is. so u can 
         // set up your logic based on properties of the error object.
       // i set as err.error.username as an  example. your api server might return an error object with different properties.
            if (err.error.username) {
       //catchError has to return a new Observable and "of" is a shortcut
       //if err.error.username exists, i will attach `{ nonUniqueUsername: true }` to the formControl's error object.
               return of({ nonUniqueUsername: true });
            }
            return of({ noConnection: true });
          })
        );
      };
    }

So far we handled the service and async class validator, now we implement this on the form. I ll have only username field.

    import { Component, OnInit } from '@angular/core';
    import { FormGroup, FormControl, Validators } from '@angular/forms';
    import { UniqueUsername } from '../validators/unique-username';
    
    @Component({
      selector: 'app-signup',
      templateUrl: './signup.component.html',
      styleUrls: ['./signup.component.css'],
    })
    export class SignupComponent implements OnInit {
      authForm = new FormGroup(
        {
          // async validators are the third arg
          username: new FormControl(
            '',
            [
              Validators.required,
              Validators.minLength(3),
              Validators.maxLength(20),
              Validators.pattern(/^[a-z0-9]+$/),
            ],
         // async validators are gonna run after all sync validators 
         successfully completed running because async operations are 
         expensive.
            this.uniqueUsername.validate
          ),
        },
        { validators: [this.matchPassword.validate] }
      );
      constructor(
        private uniqueUsername: UniqueUsername
      ) {}
    
  
    //this is used inside the template file. you will see down below
    showErrors() {
        const { dirty, touched, errors } = this.control;
        return dirty && touched && errors;
      }
      ngOnInit(): void {}
    }

Final step is to show the error to the user: in the form component's template file:

<div class="field">
  <input  formControl="username"  />
  <!-- this is where you show the error to the client -->
  <!-- showErrors() is a method inside the class -->
  
  <div *ngIf="showErrors()" class="ui pointing red basic label">
    <!-- authForm.get('username') you access to the "username" formControl -->
    <p *ngIf="authForm.get('username').errors.required">Value is required</p>
    <p *ngIf="authForm.get('username').errors.minlength">
      Value must be longer
      {{ authForm.get('username').errors.minlength.actualLength }} characters
    </p>
    <p *ngIf="authForm.get('username').errors.maxlength">
      Value must be less than {{ authForm.get('username').errors.maxlength.requiredLength }}
    </p>
    <p *ngIf="authForm.get('username').errors.nonUniqueUsername">Username is taken</p>
    <p *ngIf="authForm.get('username').errors.noConnection">Can't tell if username is taken</p>
  </div>
</div>

Upvotes: 1

H3AR7B3A7
H3AR7B3A7

Reputation: 5261

You could create a validator directive that goes on the parent element (an ngModelGroup or the form itself):

import { Directive } from '@angular/core';
import { FormGroup, ValidationErrors, Validator, NG_VALIDATORS } from '@angular/forms';

@Directive({
  selector: '[validate-uniqueness]',
  providers: [{ provide: NG_VALIDATORS, useExisting: UniquenessValidator, multi: true }]
})
export class UniquenessValidator implements Validator {

  validate(formGroup: FormGroup): ValidationErrors | null {
    let firstControl = formGroup.controls['first']
    let secondControl = formgroup.controls['second']
    // If you need to reach outside current group use this syntax:
    let thirdControl =  (<FormGroup>formGroup.root).controls['third']

    // Then validate whatever you want to validate
    // To check if they are present and unique:
    if ((firstControl && firstControl.value) &&
        (secondControl && secondControl.value) &&
        (thirdContreol && thirdControl.value) &&
        (firstControl.value != secondControl.value) &&
        (secondControl.value != thirdControl.value) &&
        (thirdControl.value != firstControl.value)) {
      return null;
    }

    return { validateUniqueness: false }
  }

}

You can probably simplify that check, but I think you get the point. I didn't test this code, but I recently did something similar with just 2 fields in this project if you want to take a look:

https://github.com/H3AR7B3A7/EarlyAngularProjects/blob/master/modelForms/src/app/advanced-form/validate-about-or-image.directive.ts

Needless to say, custom validators like this are fairly business specific and hard to make reusable in most cases. Change to the form might need change to the directive. There is other ways to do this, but this does work and it is a fairly simple option.

Upvotes: 0

Related Questions