Привет,
В последней статье мы создали загрузчик аватаров. На этот раз я хочу показать вам, ребята, как заставить этот (или любой другой) компонент вести себя как элемент управления формы, чтобы его можно было использовать вместе с реактивными формами 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]
})
}
После подачи:

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