Simon245
Simon245

Reputation: 328

Angular forms using custom validation and a dynamic value

I am trying to create a custom reactive form validation that allows me to pass an array of data to check if a string already exists. I am able to do it one way, so that it would be a form level validation but I can't get it to work on an individual form control.

Form level validation

This will create the error on the whole form, not just that control

this.myForm = this.fb.group({
  name: ['', Validators.compose([
    Validators.required,
  ]),
  ],
}, {
  validator: (formGroup: FormGroup) => this.checkStringExists(
    formGroup.controls.name,
    this.arrayOfStrings,
  ),
});

Custom validation that allows me to take in the form control and check it against an array that is passed in.

checkStringExists(formInput: AbstractControl, names: string[]): { [s: string]: boolean } {
  if (names && names.length && formInput && formInput.value) {
    const isUnique = !names.find((name) => name === formInput);
    if (isUnique) {
      return { nameExists: true };
    }
  }
  return null;
}

Individual control validation (The way I would like to do this)

This will create the error only on the specific control

this.myForm = this.fb.group({
  name: ['', Validators.compose([
    Validators.required,
    this.checkStringExists(this.arrayOfStrings),
  ]),
  ],
});

Custom validation that allows me to take only the array as part of the Validators.compose[] Here

checkStringExists(names: string[]): ValidatorFn {
  return (formInput: AbstractControl): ValidationErrors | null => {
    if (names && names.length && formInput && formInput.value) {
      const isUnique = !names.find((name) => name === formInput);
      if (!isUnique) {
        return { nameExists: true };
      }
    }
    return null;
  };
}

This subscription sets the value for arrayOfStrings.

mySubscription.subscribe((value: string[]) => {
    this.arrayOfStrings = value;
  })

The problem I have is arrayOfStrings may update multiple times. if I use the validation the first way, the arrayOfStrings is up to date. If I use the validation the second way, arrayOfStrings is null/initial value.

I am trying to get this validation to work the second way, so that I can display validation based on an individual control, not if the whole form has this error. Does anyone know how I can pass this value and keep it up to date?

I also have the validation functions in a separate helper file for reusability across the app.

Here is an example

Upvotes: 1

Views: 4971

Answers (2)

Tiz
Tiz

Reputation: 707

Your requirements

From you post you seem to have the following wish-list:

  1. External validation function (for reusability across your app).
  2. Control level validation (and, by extension, error) rather than at the form level.
  3. A dynamic list of values to validate your field agains.

The solution

The following is copied from your question above:

checkStringExists(formInput: AbstractControl, names: string[]): { [s: string]: boolean } {
  if (names && names.length && formInput && formInput.value) {
    const isUnique = !names.find((name) => name === formInput);
    if (isUnique) {
      return { nameExists: true };
    }
  }
  return null;
}

Rather than using this in the validator section of the form you can use it at the field level, like so:

// Contents could change later!
arrayOfStrings: string[] = [];

...

this.individualControlForm = this.fb.group({
  name: ['', Validators.compose([
    Validators.required,
    (control: AbstractControl) => ValidationHelper.checkStringExists(
          control, this.arrayOfStrings
    )]),
  ],
});

Because we use an arrow function as our validation function we inherit this from the scope the function is defined in (paraphrase of the "call, apply and bind" section, here). This (heh) means we can use the array normally with the latest value being used each time the validation function is called.

If you want to reduce the size of this call you can use the .bind(this) function that Eliseo mentioned in his answer. It can make things harder to read, but certainly shortens the boilerplate of creating your form. Pick your poison.

side note

If you do ever need to use the whole form validation (e.g. if you need to consider multiple fields to decide if a single field is valid) but want the error to show up against the field you can use

formData.form.controls['email'].setErrors({'incorrect': true});

to manually set the error on that specific field (source).

Upvotes: 2

Eliseo
Eliseo

Reputation: 57919

Update

my bad! you can pass an array, an array is an inmutable value. so if you has

  export function findArray(array){
    return (control=>{
      return array.indexOf(control.value)<0?{error:'not match'}:null
    })
  }

  array=['one','two']
  control=new FormControl(null,findArray(this.array))

you can see a simple stackblitz

Really a validator can not has "dynamic" argument. so some like

foolValidator(name:string)
{
   return (control:AbstractControl)=>{
        return control.value!=name?{error:'it's not the name'}:null
   }
}
name="joe"
control=new FormControl(null,foolValidator(this.name))

Only take account 'joe', not the name of variable "name"

You can use bind(this), bind change the "scope", see e.g. this link

foolValidator() //see that you not pass the argument
{
   return (control:AbstractControl)=>{
        //see that you use "this.name"
        return control.value!=this.name?{error:'it's not the name'}:null
   }
}
name="joe"
control=new FormControl(null,foolValidator().bind(this))

Upvotes: 0

Related Questions