Не путайте с ControlValueAccessor Angular Forms

внешний интерфейс исходный код Element Angular.js

Оригинальная ссылка:Never again be confused when implementing ControlValueAccessor in Angular forms

easy-control-value-accessor

Если вы работаете над сложным проектом, вам неизбежно потребуется настроить управление формой, что в основном нужно реализовать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. ):

angular_form_control-controlValueAccessor-native_form_control

Опять же, независимо от того, созданы ли они явно с помощью реактивных форм или неявно с помощью форм, управляемых шаблонами,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. )

Реализация пользовательских средств доступа к значениям элементов управления

Реализация пользовательских средств доступа к значениям элементов управления не сложна и требует всего два шага:

  1. регистрNG_VALUE_ACCESSORпровайдер
  2. выполнитьControlValueAccessorинтерфейс

NG_VALUE_ACCESSORПоставщик используется для указания того, что реализацияControlValueAccessorКласс интерфейса и используется Angular для связи сformControlСинхронизация обычно регистрируется с помощью классов компонентов или директив. Все директивы формы используютNG_VALUE_ACCESSORIdentity для ввода средства доступа к управляющему значению, а затем выберите соответствующий метод доступа (Примечание переводчика: это предложение может относиться к этим двум строкам кода,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компоненты.

Теперь превратим описанную выше функцию в интерактивный график:

jQuery_slider-slider_component-form_control

Если поставить простую инкапсуляцию и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

ПроэктРепозиторий на гитхабе.