2

I created an Injectable Service for a custom Form Validator:

    import { Injectable } from '@angular/core'
    import { FormControl } from '@angular/forms'
    import { Observable, of, timer } from 'rxjs'
    import { map, switchMap } from 'rxjs/operators'
    
    @Injectable({
      providedIn: 'root'
    })
    export class IsValidNicknameService {
    
      validate = (time: number = 500) => {
        return (input: FormControl) => {
          return timer(time).pipe(
            switchMap(() => this.isValidNickname(input.value)),
            map(isValid => {
              return isValid ? null : { shouldNotStartWithA: true }
            })
          )
        }
      }
    
      private isValidNickname(value: string): Observable<boolean> {
        return this.checkIfFirstLetterIsA(value)
      }
    
      private checkIfFirstLetterIsA(value: string): Observable<boolean> {
        const firstCharacter = value.toLowerCase().charAt(0)
        if (firstCharacter === 'a' || firstCharacter === 'à' || firstCharacter === 'ä' || firstCharacter === 'á' || firstCharacter === 'ã') {
          return of(false)
        } else {
          return of(true)
        }
      }
    
    }

Then I call it in my controller like that:

    import { IsValidNicknameService } from './src/Core/Services/isvalidnickname.service'
    createNicknamesGroup(): any {
      return new FormGroup({
        buildingRoom: new FormControl(this.mockBuildingRooms[0], Validators.required),
        nickname: new FormControl('', Validators.required, this.isValidNicknameService.validate())
      })
    }

It works, but I have the feeling it's not a good approach. Is there a better and more concise way to achieve that?

Kr1
  • 1,269
  • 2
  • 24
  • 56
  • 2
    That is almost the recommended way. I would just quit using a class and making it injectable. Instead simply use a function. You didn't inject the service anyway. You can read more about it here: https://angular.io/guide/form-validation#defining-custom-validators – Max K Nov 04 '20 at 12:00

1 Answers1

1

There is no correct or incorrect approaches here - do whatever feels comfortable for you.

From your code - you don't have any service dependencies so your service can be easily converted to a bunch of functions. An advantage of using plain functions is that they can be easily covered with unit tests.

Generally speaking, you'd want to use an @Injectable() class in case of some dependencies required to be injected in your constructor via Angular's DI. But this can also be avoided by passing the necessary dependencies to your validator higher order function:

# my-custom.validator.ts

export const setupRequiredValidator(minLength: number, someCheckerService: CheckerService): ValidatorFn => {
 return (control: AbstractControl) => {
   return control.value.length >= minLength && someCheckerService.check(control);
 }
}
# my.component.ts

constructor(private someCheckerService: CheckerService) {
}

createNicknamesGroup(): FormGroup {
  return new FormGroup({
      buildingRoom: new FormControl(this.mockBuildingRooms[0], Validators.required),
      nickname: new FormControl('', [Validators.required, setupRequiredValidator(5, this.someCheckerService)])
    }) // an example of synchronous validator
  }

Also you can create @Injectable() validator and don't call it a Service - no one will swear on you.

# my-custom.validator.ts

@Injectable({
 providedIn: 'root'
})
export class MyCustomValidator {
  constructor(private router: Router) {}

  validateName = (control: AbstractControl) => {
    // try to do only one validation per method if there are no dependencies between the validators. 
    // It's easier to combine and tests them afterwards
    return control.value ? null : {error: 'text'};
  }

  validateEmail = (control: AbstractControl) => {
    return control.value ? Validators.email(control) : null;
  }

  validateSurnameIfTheNameExists = (control: AbstractControl) => {
    return control.parent.get('name').valid ? null : this.router.navigate(['/error']);
  }
}
Yevhenii Dovhaniuk
  • 1,073
  • 5
  • 11