Создание custom form field control (ControlValueAccessor)
- ControlValueAccessor - связующее звено между Angular forms API и нативным DOM элементом
- Чтобы Angular знал об новом field control необходимо зарегистрировать
NG_VALUE_ACCESSOR
провайдер.
Интерфейс ControlValueAccessor
В компоненте должны реализовываться 3 обязательных метода и 1 один необязательный:
export interface ControlValueAccessor {
/**
* Записать значение в компонент (из ts в html), основная функция
*/
writeValue(obj: any): void;
/**
* Обработать значение из компонента (из html в ts), основная функция fn
*/
registerOnChange(fn: any): void;
/**
* Обработать, когда потрогали поле (потеря фокуса)
*/
registerOnTouched(fn: any): void;
/**
* Обработка недоступности
*/
setDisabledState?(isDisabled: boolean): void;
}
Регистрация NG_VALUE_ACCESSOR
провайдера
Используется мультипровайдер с токеном NG_VALUE_ACCESSOR
.
const NG_VALUE_ACCESSOR: InjectionToken<ControlValueAccessor>;
Функция forwardRef
необходима потому, что в провайдере идёт обращение к классу, который еще не определен (объявлен ниже). Иначе ошибка (ERROR in : Cannot instantiate cyclic dependency! NgControl
)
import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
...
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TyapkComponent),
multi: true
}]
}) export class TyapkComponent implements ControlValueAccessor {}
Custom control
Миниальный
import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'app-quantity-input',
template: `
<button
(click)="updateValue(+value - 10)"
[disabled]="disabled">--</button>
<button
(click)="updateValue(+value - 1)"
[disabled]="disabled">-</button>
<input
[ngModel]="value"
(ngModelChange)="updateValue($event)"
[disabled]="disabled"
type="number"
/>
<button
(click)="updateValue(+value + 1)"
[disabled]="disabled">+</button>
<button
(click)="updateValue(+value + 10)"
[disabled]="disabled">++</button>
`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => QuantityInputComponent),
multi: true,
}],
})
export class QuantityInputComponent implements ControlValueAccessor {
value = 0;
disabled = false;
private onChange = (value: any) => {};
private onTouched = () => {};
registerOnChange(fn: any) {
this.onChange = fn;
}
registerOnTouched(fn: () => {}): void {
this.onTouched = fn;
}
writeValue(outsideValue: number) {
// получить из Forms API
this.value = outsideValue;
}
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
}
updateValue(insideValue: number) {
this.value = insideValue; // html
this.onChange(insideValue); // уведомить Forms API
this.onTouched();
}
}
Супер простой компонент переключателя
@Component({
selector: 'toggle',
template: `{{ value }}`,
styles: [`
:host {
display: block;
height: 20px;
width: 285px;
cursor: pointer;
border: 1px solid #ccc;
border-radius: 4px;
padding: 5px;
text-align: center;
}
:host(.disabled) {
opacity: 0.35;
pointer-events: none;
}
`],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ToggleComponent),
multi: true
}]
})
export class ToggleComponent implements ControlValueAccessor {
value: boolean;
disabled: boolean;
onChange = (value: boolean) => {};
onTouched = () => {};
registerOnChange(fn) {
this.onChange = fn;
}
registerOnTouched(fn) {
this.onTouched = fn;
}
@HostBinding('class.disabled')
get isDisabled() {
return this.disabled;
}
@HostListener('click')
click() {
this.writeValue(!this.value);
}
writeValue(value: boolean) {
this.value = value;
this.onChange(this.value);
}
setDisabledState(disabled: boolean) {
this.disabled = disabled;
}
}
Классика
Рассказ автора Angular форм об имплементации ControlValueAccessor:
- ControlValueAccessor - 12:25
- Nested Forms - 25:23