Оригинальная ссылка:Never again be confused when implementing ControlValueAccessor in Angular forms
Если вы работаете над сложным проектом, вам неизбежно потребуется настроить управление формой, что в основном нужно реализоватьControlValueAccessor
Интерфейс (Примечание переводчика: метод определения интерфейса может относиться кОписание документации API, также см.Определение исходного кода Angular). В сети полно статей, описывающих, как реализовать этот интерфейс, но очень мало о том, какую роль он играет в архитектуре форм Angular.Если вы хотите знать не только как, но и почему, эта статья для вас.
Сначала позвольте мне объяснить, почемуControlValueAccessor
Интерфейс и как он используется в Angular. Затем я покажу, как инкапсулировать сторонние компоненты в виде компонентов Angular и как использовать механизм ввода и вывода для реализации взаимодействия компонентов (Примечание переводчика: механизм ввода и вывода связи компонентов Angular можно сослаться наОфициальная документация сайта), а в конце покажет, как использоватьControlValueAccessor
для достиженияДля угловых формНовый механизм передачи данных.
FormControl и ControlValueAccessor
Если вы раньше работали с формами Angular, возможно, вы знакомы сFormControl, официальная документация Angular описывает его как отслеживание одного элемента управления формойЦенность и действительностьобъект сущности. Важно понимать, что независимо от того, используете ли вы управляемые шаблоном или реактивные формы,FormControl
всегда создаются. Если вы используете адаптивные формы, вам необходимо явно создатьFormControl
объект и использоватьformControl
илиformControlName
директивы для привязки собственных элементов управления; если вы используете подход, основанный на шаблонах,FormControl
объект будетNgModel
Директивы создаются неявноэта линия):
@Directive({
selector: '[ngModel]...',
...
})
export class NgModel ... {
_control = new FormControl(); <---------------- here
Независимо от тогоformControl
Независимо от того, создано ли оно явно или неявно, его необходимо сравнивать с собственными элементами управления формы DOM, такими какinput,textarea
Для взаимодействия, и очень вероятно, что вам нужно настроить элемент управления формой как компонент Angular вместо использования собственного элемента управления формой, и обычно настраиваемый элемент управления формы будет инкапсулировать элемент управления, написанный на чистом JS, напримерjQuery UI's Slider
. В этой статье я буду использоватьСобственные элементы управления формойтермины для различения специфичных для AngularformControl
с тобойhtml
используемые элементы управления формы, но вам нужно знать, что любой пользовательский элемент управления формы можно использовать сformControl
Директивы для взаимодействия вместо собственных элементов управления формы, таких какinput
.
Количество собственных элементов управления формы ограничено, но количество настраиваемых элементов управления формы не ограничено, поэтому Angular нужен общий механизм длянаведение мостовсобственные/настраиваемые элементы управления формой иformControl
инструкция, что именноControlValueAccessor
Заниматься вещами.这个对象桥接原生表单控件和formControl
Инструкция и синхронизировать значения обоих. Официальная документация описывает это подобное (Примечание переводчика: для ясности описание не переводится):
A ControlValueAccessor acts as a bridge between the Angular forms API and a native element in the DOM.
Любой компонент или директива может быть реализованаControlValueAccessor
интерфейс и зарегистрирован какNG_VALUE_ACCESSOR
, который превращается вControlValueAccessor
тип объекта, мы увидим, как это сделать позже. Кроме того, этот интерфейс также определяет два важных метода —writeValue
а такжеregisterOnChange
(Примечание переводчика: Angular Просмотр исходного кодаэта линия):
interface ControlValueAccessor {
writeValue(obj: any): void
registerOnChange(fn: any): void
registerOnTouched(fn: any): void
...
}
formControl
Использование инструкцииwriteValue
метод для установки значения собственного элемента управления формы (Примечание переводчика: вы можете обратиться кL186а такжеL41);использоватьregisterOnChange
Метод для регистрации функции обратного вызова, запускаемой каждым собственным значением элемента управления формы (Примечание переводчика: вы можете обратиться к этим трем строкам,L186а такжеL43,так же какL85),Вам необходимо обновить значение, передаваемое в функцию обратного вызова, такое угловое значение для управления форм также обновляется(Примечание переводчика: это можно отнести к написанному Angular.DefaultValueAccessor
Способ написания заключается в том, как каждый раз передавать обновленное значение элемента управления вводом в функцию обратного вызова,L52а такжеL89);использоватьregisterOnTouched
метод для регистрации обратных вызовов, которые запускаются, когда пользователь взаимодействует с элементом управления (Примечание переводчика: вы можете обратиться кL95).
На картинке нижеAngular 表单控件
КакControlValueAccessor
Прийти и原生表单控件
Интерактивный (Примечание переводчика:formControl
а такжеТы пишешь или угловойCustomControlValueAccessor
Обе директивы должны быть привязаны к собственному элементу DOM, в то время какformControl
Инструкции требуют помощиCustomControlValueAccessor
Директива/компонент для обмена данными с собственным элементом DOM. ):
Опять же, независимо от того, созданы ли они явно с помощью реактивных форм или неявно с помощью форм, управляемых шаблонами,ControlValueAccessor
Всегда взаимодействуйте с элементами управления формы Angular.
Angular также создает все собственные элементы формы DOM.Angular
Элементы управления формой (Примечание переводчика: встроенный в Angular ControlValueAccessor):
Accessor | Form Element |
---|---|
DefaultValueAccessor | input,textarea |
CheckboxControlValueAccessor | input[type=checkbox] |
NumberValueAccessor | input[type=number] |
RadioControlValueAccessor | input[type=radio] |
RangeValueAccessor | input[type=range] |
SelectControlValueAccessor | select |
SelectMultipleControlValueAccessor | select[multiple] |
Как видно из приведенной выше таблицы, когда Angular встречает в шаблоне компонентаinput
илиtextarea
При использовании собственных элементов управления DOM он будет использоватьDefaultValueAccessor
инструкция:
@Component({
selector: 'my-app',
template: `
<input [formControl]="ctrl">
`
})
export class AppComponent {
ctrl = new FormControl(3);
}
Все директивы формы, в том числе в приведенном выше кодеformControl
команда, вызоветsetUpControlФункция для создания элементов управления формой иDefaultValueAccessor
Реализовать взаимодействиеformControl
директива, которая вызывается при создании экземпляраsetUpControl()
функция к той же привязанной кinput
изDefaultValueAccessor
Инструкции по выполнению монтажных работ, напримерL85,такformControl
инструкции могут бытьDefaultValueAccessor
прийти иinput
элементы обмениваются данными). Подробности можно посмотретьformControl
Код инструкции:
export class FormControlDirective ... {
...
ngOnChanges(changes: SimpleChanges): void {
if (this._isControlChanged(changes)) {
setUpControl(this.form, this);
а такжеsetUpControl
В исходном коде функции также указано, как встроенный элемент управления формы и элемент управления формы Angular синхронизируют данные.
export function setUpControl(control: FormControl, dir: NgControl) {
// initialize a form control
// 调用 writeValue() 初始化表单控件值
dir.valueAccessor.writeValue(control.value);
// setup a listener for changes on the native control
// and set this value to form control
// 设置原生控件值更新时监听器,每当原生控件值更新,Angular 表单控件值也更新
valueAccessor.registerOnChange((newValue: any) => {
control.setValue(newValue, {emitModelToViewChange: false});
});
// setup a listener for changes on the Angular formControl
// and set this value to the native control
// 设置 Angular 表单控件值更新监听器,每当 Angular 表单控件值更新,原生控件值也更新
control.registerOnChange((newValue: any, ...) => {
dir.valueAccessor.writeValue(newValue);
});
Пока мы понимаем внутренний механизм, мы можем реализовать собственный элемент управления формы Angular.
Компонентный инкапсулятор
Поскольку Angular по умолчанию для всех собственных элементов управления предоставляет средство доступа к значениям элементов управления, поэтому при использовании сторонних подключаемых модулей или компонентов пакета вам необходимо написать новое средство доступа к значениям элемента управления. Мы будем использовать упомянутую выше библиотеку пользовательского интерфейса jQuery.sliderПлагин для реализации пользовательского элемента управления формы.
простая обертка
Наиболее основные достигаются через простой пакет, чтобы он мог отображаться на экране, поэтому нам нуженNgxJquerySliderComponent
Компоненты и рендеринг в своих шаблонахslider
:
@Component({
selector: 'ngx-jquery-slider',
template: `
<div #location></div>
`,
styles: ['div {width: 100px}']
})
export class NgxJquerySliderComponent {
@ViewChild('location') location;
widget;
ngOnInit() {
this.widget = $(this.location.nativeElement).slider();
}
}
Здесь мы используем стандартjQuery
метод на родном элементе DOM для созданияslider
контролировать, а затем использоватьwidget
Свойство ссылается на этот элемент управления.
После простой упаковкиslider
компонент, мы можем использовать его в шаблоне родительского компонента:
@Component({
selector: 'my-app',
template: `
<h1>Hello {{name}}</h1>
<ngx-jquery-slider></ngx-jquery-slider>
`
})
export class AppComponent { ... }
Для запуска программы нам нужно добавитьjQuery
Связанные зависимости, для простоты, находятся вindex.html
Добавьте глобальные зависимости к:
<script src="https://code.jquery.com/jquery-3.2.1.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/smoothness/jquery-ui.css">
Вот зависимости установкиисходный код.
Интерактивные элементы управления формой
Приведенная выше реализация не позволяет нам настраиватьslider
Компоненты управления для взаимодействия с родителем, поэтому необходимо использовать привязку ввода/вывода для реализации обмена данными между компонентами:
export class NgxJquerySliderComponent {
@ViewChild('location') location;
@Input() value;
@Output() private valueChange = new EventEmitter();
widget;
ngOnInit() {
this.widget = $(this.location.nativeElement).slider();
this.widget.slider('value', this.value);
this.widget.on('slidestop', (event, ui) => {
this.valueChange.emit(ui.value);
});
}
ngOnChanges() {
if (this.widget && this.widget.slider('value') !== this.value) {
this.widget.slider('value', this.value);
}
}
}
однаждыslider
Создание компонента, вы можете подписатьсяslidestop
Событие получает измененное значение один разslidestop
Событие запускается, и можно использовать эмиттер выходных событий.valueChanges
Уведомить родительский компонент. Конечно, мы также можем использоватьngOnChanges
Крючки жизненного цикла для отслеживания входных свойствvalue
Значение изменяется, как только его значение изменяется, мы устанавливаем значениеslider
Значение контроля.
Тогда как использовать его в родительском компонентеslider
Реализация кода компонента:
<ngx-jquery-slider
[value]="sliderValue"
(valueChange)="onSliderValueChange($event)">
</ngx-jquery-slider>
исходный кодэто здесь.
Но мы хотим, чтобы это использоватьslider
Компоненты, которые действуют как часть формы и взаимодействуют со своими данными, использующими директивы директоров, управляемой шаблоном или адаптивной формой.ControlValueAccessor
интерфейс. Поскольку мы реализуем новый способ взаимодействия компонентов, нам не нужен стандартный способ привязки входных и выходных свойств, поэтому удалите соответствующий код. (Примечание переводчика: автор сначала реализует стандартный метод связи с привязкой входных и выходных атрибутов и удаляет его, главным образом, чтобы представитьНовое взаимодействие компонентов формы,Прямо сейчасControlValueAccessor
. )
Реализация пользовательских средств доступа к значениям элементов управления
Реализация пользовательских средств доступа к значениям элементов управления не сложна и требует всего два шага:
- регистр
NG_VALUE_ACCESSOR
провайдер - выполнить
ControlValueAccessor
интерфейс
NG_VALUE_ACCESSOR
Поставщик используется для указания того, что реализацияControlValueAccessor
Класс интерфейса и используется Angular для связи сformControl
Синхронизация обычно регистрируется с помощью классов компонентов или директив. Все директивы формы используютNG_VALUE_ACCESSOR
Identity для ввода средства доступа к управляющему значению, а затем выберите соответствующий метод доступа (Примечание переводчика: это предложение может относиться к этим двум строкам кода,L175а такжеL181). либо выбратьDefaultValueAccessor
Или встроенный метод доступа к данным, в противном случае Angular выберет пользовательский метод доступа к данным, и есть только один пользовательский метод доступа к данным (Примечание переводчика: эта ссылка на предложениеselectValueAccessor
реализация исходного кода).
Давайте сначала определим провайдера:
@Component({
selector: 'ngx-jquery-slider',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: NgxJquerySliderComponent,
multi: true
}]
...
})
class NgxJquerySliderComponent implements ControlValueAccessor {...}
Мы напрямую указываем имя класса в декораторе компонента, но реализация исходного кода Angular по умолчанию размещается вне декоратора класса:
export const DEFAULT_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DefaultValueAccessor),
multi: true
};
@Directive({
selector:'input',
providers: [DEFAULT_VALUE_ACCESSOR]
...
})
export class DefaultValueAccessor implements ControlValueAccessor {}
Его нужно использовать снаружиforwardRef
, вы можете обратиться к причинамWhat is forwardRef in Angular and why we need it. При реализации пользовательскихcontrolValueAccessor
, я предлагаю поместить его в декоратор класса.
Как только провайдер определен, давайте реализуемcontrolValueAccessor
интерфейс:
export class NgxJquerySliderComponent implements ControlValueAccessor {
@ViewChild('location') location;
widget;
onChange;
value;
ngOnInit() {
this.widget = $(this.location.nativeElement).slider(this.value);
this.widget.on('slidestop', (event, ui) => {
this.onChange(ui.value);
});
}
writeValue(value) {
this.value = value;
if (this.widget && value) {
this.widget.slider('value', value);
}
}
registerOnChange(fn) { this.onChange = fn; }
registerOnTouched(fn) { }
Поскольку нас не интересует, взаимодействует ли пользователь с компонентом, мы сначала ставимregisterOnTouched
Оставьте его пустым. существуетregisterOnChange
Здесь мы просто сохраняем функцию обратного вызоваfn
Ссылка на функцию обратного вызова задаетсяformControl
Входящая команда (Примечание переводчика: ссылкаL85), пока каждыйslider
组件值发生改变,就会触发这个回调函数。 существуетwriteValue
В методе мы передаем полученное значение вslider
компоненты.
Теперь превратим описанную выше функцию в интерактивный график:
Если поставить простую инкапсуляцию иcontrolValueAccessor
Сравнивая инкапсуляцию, вы обнаружите, что взаимодействие между родительским и дочерним компонентами отличается, хотя инкапсулированный компонент отличается отslider
Взаимодействие компонентов одинаковое. вы можете заметитьformControl
Директивы фактически упрощают взаимодействие с родительскими компонентами. Здесь мы используемwriteValue
для записи данных в дочерние компоненты и использования в простых методах-оболочкахngOnChanges
;передачаthis.onChange
метод выводит данные, а в простых методах-оболочках используетсяthis.valueChange.emit(ui.value)
.
Теперь понялControlValueAccessor
настройка интерфейсаslider
Полный код управления формы выглядит следующим образом:
@Component({
selector: 'my-app',
template: `
<h1>Hello {{name}}</h1>
<span>Current slider value: {{ctrl.value}}</span>
<ngx-jquery-slider [formControl]="ctrl"></ngx-jquery-slider>
<input [value]="ctrl.value" (change)="updateSlider($event)">
`
})
export class AppComponent {
ctrl = new FormControl(11);
updateSlider($event) {
this.ctrl.setValue($event.currentTarget.value, {emitModelToViewChange: true});
}
}
Вы можете ознакомиться с программойнаконец понял.
Github
ПроэктРепозиторий на гитхабе.