2

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;
  }

}; }

4
  • I think you're complete missing the point here. There is no way an email address is the same format as a phone number as a name. Why not use a pattern validator to ensure that names don't have @ or numbers, and ensure that an email is an email, a phone is a phone. Commented Aug 11, 2020 at 16:25
  • I would suggest @rxweb/reactive-form-validators for such job. The same package offers no. of other useful validations. Commented Aug 11, 2020 at 16:27
  • 1
    If the "Add Fields" button adds a completely new set of the same fields into a FormArray, you would need to iterate over each FormGroup in the FormArray and ensure there are no duplicates across the FormArray. Commented Aug 11, 2020 at 16:35
  • Actually, according to the condition and feature, there should not be any validation to any of these individually except only one that is no two form fields cannot have the same values. Now I am just comparing the form values before submitting. If there is another solution for this without package then it would be helpful. Thanks for the comments. Commented Aug 12, 2020 at 16:10

2 Answers 2

1

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>
Sign up to request clarification or add additional context in comments.

Comments

0

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.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.