Привет,

В последней статье мы создали загрузчик аватаров. На этот раз я хочу показать вам, ребята, как заставить этот (или любой другой) компонент вести себя как элемент управления формы, чтобы его можно было использовать вместе с реактивными формами Angular или формами, управляемыми шаблонами. Он также может работать с валидаторами и так далее.

Превращение компонентов в элементы управления форм упрощает вашу жизнь, если вам нужно извлечь из них значение и использовать его для чего-то другого (например, для отправки запроса POST в REST API).

Вот что мы будем строить:

Исходный код здесь.

Средство доступа к управляющему значению

Первое, что нам нужно сделать, это сделать так, чтобы наш компонент реализовал класс ControlValueAccessor.

Я добавил ControlValueAcessor после OnInit и импортировал его:

import { ControlValueAccessor } from '@angular/forms';

Затем добавьте провайдера в аннотацию @ Component и импортируйте NG_VALUE_ACCESSOR.

@Component({
  selector: 'app-avatar',
  templateUrl: './avatar.component.html',
  styleUrls: ['./avatar.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      multi: true,
      useExisting: AvatarComponent
    }
  ]
})

Обратите внимание, что теперь AvatarComponent выдает ошибку:

Давайте будем хитрыми здесь, хорошо? Просто нажмите Быстрое исправление (или нажмите Ctrl + . ), затем нажмите Enter, среда IDE (надеюсь) создаст набор функций, как показано ниже.

writeValue(obj: any): void {
    throw new Error('Method not implemented.');
}
registerOnChange(fn: any): void {
    throw new Error('Method not implemented.');
}
registerOnTouched(fn: any): void {
    throw new Error('Method not implemented.');
}
setDisabledState?(isDisabled: boolean): void {
    throw new Error('Method not implemented.');
}

Если этот трюк не работает, вы можете написать их вручную.

Методы

Вам может быть интересно, что на самом деле делают эти 4 функции.

Итак… вот что они делают:

(Благодарности Университету Angular)

  • writeValue: этот метод вызывается модулем Forms для записи значения в элемент управления формы.
  • registerOnChange: когда значение формы изменяется из-за пользовательского ввода, мы должны сообщить об этом значении родительской форме. Это делается путем вызова обратного вызова, который изначально был зарегистрирован в элементе управления с помощью метода registerOnChange.
  • registerOnTouched: когда пользователь впервые взаимодействует с элементом управления формы, считается, что элемент управления имеет статус касания, что полезно для стиля. Для того, чтобы сообщить родительской форме о касании элемента управления, нам нужно использовать обратный вызов, зарегистрированный с помощью метода registerOnToched.
  • setDisabledState: элементы управления формой можно включать и отключать с помощью Forms API. Это состояние можно передать в элемент управления формы с помощью метода setDisabledState.

По сути:

Angular Form Api будет вызывать эти методы и отправлять значения в наш компонент через параметры.

writeValue(obj: any): void {}

Давайте проанализируем writeValue. Он получает «obj» в качестве параметра. Этот метод будет вызываться Angular, когда значение установлено для нашего элемента управления формой в хост-компоненте.

В приведенном ниже примере я инициализирую форму и устанавливаю значение (строку URL) для нашего аватара.

Затем будет выполнено writeValue, и URL-адрес будет параметром «obj».

Что нам нужно сделать?

  • В writeValue установите внутреннее значение нашего компонента на полученное значение из параметра.
  • Создайте внутренний метод, который будет получать метод из параметра registerOnChange.
  • Создайте внутренний метод, который будет получать метод из параметра registerOnTouched.
  • Создайте внутреннюю переменную с именем disabled и внутри setDisabledState установите значение параметра для внутренней переменной. (на самом деле это необязательно, если ваш пользовательский элемент управления формой не находится в отключенном состоянии).
  
onChange = (fileUrl: string) => {};

onTouched = () => {};

disabled: boolean = false;

writeValue(_file: string): void {
  this.file = _file;
}
registerOnChange(fn: any): void {
  this.onChange = fn;
}
registerOnTouched(fn: any): void {
  this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
  this.disabled = isDisabled;
}

Наконец, все, что вам нужно сделать, это каждый раз, когда значение вашего компонента изменяется, вызывать onChange(), чтобы уведомить Angular об изменении значения, например:

onFileChange(event: any) {
    const files = event.target.files as FileList;

    if (files.length > 0) {
      const _file = URL.createObjectURL(files[0]);
      this.resetInput();
      this.openAvatarEditor(_file)
      .subscribe(
        (result) => {
          if(result){
            this.file = result;
            this.onChange(this.file); // <= HERE
          }
        }
      )
    }
  }

Использование компонента внутри формы

Сначала вы создаете группу форм и инициализируете ее с помощью FormBuilder (вы также можете использовать форму, управляемую шаблоном). Функция отправки будет вызываться нашей кнопкой «Сохранить» и будет регистрировать значения формы. В реальном мире вы, вероятно, сделали бы запрос API для сохранения данных формы.

profile.component.ts

@Component({
  selector: 'app-profile',
  templateUrl: './profile.component.html',
  styleUrls: ['./profile.component.scss']
})
export class ProfileComponent implements OnInit {

  form!: FormGroup;

  constructor(private fb: FormBuilder) { }

  ngOnInit(): void {
    this.form = this.fb.group({
      name: '',
      lastName: '',
      email: '',
      avatar: ''
    })
  }

  submit(){
    console.log(this.form.value);
  }

}

Затем добавьте компонент внутрь формы и передайте ему formControlName в соответствии с объявлением формы в файле .ts.

profile.component.html

<form [formGroup]="form" (ngSubmit)="submit()">
        <app-avatar formControlName="avatar"></app-avatar> 

        //Now you can pass a formControlName to your component!

        <div class="row margin-top">
            <mat-form-field appearance="fill">
                <mat-label>First name</mat-label>
                <input type="text" matInput formControlName="name">
            </mat-form-field>
            <mat-form-field appearance="fill">
                <mat-label>Last name</mat-label>
                <input type="text" matInput formControlName="lastName">
            </mat-form-field>
        </div>
        <div class="row">
            <mat-form-field appearance="fill">
                <mat-label>Email</mat-label>
                <input type="email" matInput formControlName="email">
            </mat-form-field>
        </div>
        <div class="row">
            <button mat-flat-button color="primary" type="submit">Save</button>
        </div>
</form>

Пробовать это

Теперь вы можете легко получить доступ к значению вашего компонента и использовать его по своему усмотрению.

Вы также можете добавить к нему валидаторы.

Предположим, наш аватар требуется в форме.

  ngOnInit(): void {
    this.form = this.fb.group({
      name: '',
      lastName: '',
      email: '',
      avatar: ['', Validators.required]
    })
  }

После подачи:

Конец

Исходный код здесь.

Надеюсь, это было полезно, увидимся в следующий раз!