angular
March 7

Добавляем валидацию в кастомные контролы через  ControlValueAcсessor

Пример реализуем на примере компонента капчи.
Поскольку мы решили, чтобы наш компонент Captcha будет интегрирован с формами Angular, поэтому для начала реализуем интерфейс ControlValueAccessor:

@Component({
  selector: 'app-captcha',
  templateUrl: './captcha.component.html',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: CaptchaComponent,
      multi: true
    },
  ]
})
export class CaptchaComponent implements ControlValueAccessor {
  onChange;
  onTouched;
  
  writeValue(value) { }

  registerOnChange(fn) { this.onChange = fn; }
  registerOnTouched(fn) { this.onTouched = fn; }
}

В примере реализовано три метода, которые требует интерфейс ControlValueAccessor.

Далее создадим шаблон компонента. Чтобы отобразить вопрос, используем элемент canvas:это позволит избежать его нахождения в DOM и, следовательно, предотвратит перехват роботами.

<div>
  <canvas #canvas width="130" height="50"></canvas>

  <input type="number" (input)="change(input.value)" #input>
</div>

Далее получим ссылку на элемент canvas из компонента и нарисуем на нем вопрос:

@Component({...})
export class CaptchaComponent implements OnInit, ControlValueAccessor {
  @ViewChild('canvas', { static: true }) canvas: ElementRef<HTMLCanvasElement>;
  answer: number;

  ngOnInit() { this.createCaptcha(); }

  ...
    
  createCaptcha() {
    const ctx = this.canvas.nativeElement.getContext("2d");
    const [numOne, numTwo] = [random(), random()];
    this.answer = numOne + numTwo;

    ctx.font = "30px Arial";
    ctx.fillText(`${numOne} + ${numTwo} = `, 10, 35);
  }

  change(value: string) {
    this.onChange(value);
    this.onTouched();
  }

}

function random() {
  return Math.floor(Math.random() * 10) + 1;
}

Метод createCaptcha()прост в реализации. Он генерирует два случайных числа и рисует их с помощью метода fillText элемента canvas. Мы также сохраняем ответ на вопрос, чтобы затем проверить его с пользовательским вводом.

Давайте изучим как реализовать часть с валидацией. Существует много способов добавить пользовательскую валидацию для контрола формы, но в нашем случае мы хотим, чтобы валидация была встроена в компонент.

Используем NG_VALIDATORS

Зарегистрируем компонент с помощью провайдера NG_VALIDATORSи имплементируем метод validate():

@Component({
  providers: [
    {
      provide: NG_VALIDATORS,
      useExisting: CaptchaComponent,
      multi: true
    }
  ]
})
export class CaptchaComponent implements OnInit, ControlValueAccessor {
  ...

  validate({ value }: FormControl) {
    const isNotValid = this.answer !== Number(value);
    return isNotValid && {
      invalid: true
    }
  }

}

Метод validate() будет вызываться с экземпляром FormControl при изменении значения управляющего элемента. При вызове мы проверяем текущее значение управляющего элемента по ответу на вопрос. Если значения не совпадают, мы возвращаем объект ошибки, который помечает текущий контроль как невалидный.

export class AppComponent {
  group = new FormGroup({
    email: new FormControl(),
    password: new FormControl(),
    captcha: new FormControl(),
  })
}
<form [formGroup]="group" #form="ngForm">
  <input type="email" formControlName="email">
  <input type="password" formControlName="password">

  <app-captcha formControlName="captcha"></app-captcha>
  <div *ngIf="group.get('captcha').hasError('invalid') && form.submitted">
    Wrong answer...
  </div>

  <button type="sumbit">Sumbit</button>
</form>

Мы можем экспортировать директиву ngForm в локальную переменную и использовать ее свойство submitted в качестве индикатора того, была ли запущена отправка формы. Если да, и captcha недопустима, мы отображаем ошибку.

Регистрация контрола

Мы можем получить ссылку на директиву управления формой через DI и установить свойство доступа к значению вручную:

@Component({
  selector: 'app-captcha',
  templateUrl: './captcha.component.html'
})
export class CaptchaComponent implements OnInit, ControlValueAccessor {
  ...

  constructor(@Self() private controlDirective: NgControl) {
    controlDirective.valueAccessor = this;
  }

  ngOnInit() {
    this.controlDirective.control.setValidators([this.validate.bind(this)]);
    this.controlDirective.control.updateValueAndValidity();
    ...
  }

  validate({ value }: FormControl) {
    const isNotValid = this.answer !== Number(value);
    return isNotValid && {
      invalid: true
    }
  }
}

У нас есть ссылка на базовый контрол, и мы регистрируем функцию валидации, используя метод setValidators.

Обратите внимание, что при использовании этого варианта мы не можем зарегистрировать наш компонент с провайдером NG_VALUE_ACCESSOR; Если мы сделаем это, возникнет круговая зависимость, и будет сгенерирована ошибка.

Исходный код примера доступен по ссылке.

Это вольный перевод оригинального материала