Angular4 - нет средства доступа к значению для управления формой

151

У меня есть собственный элемент:

<div formControlName="surveyType">
  <div *ngFor="let type of surveyTypes"
       (click)="onSelectType(type)"
       [class.selected]="type === selectedType">
    <md-icon>{{ type.icon }}</md-icon>
    <span>{{ type.description }}</span>
  </div>
</div>

Когда я пытаюсь добавить formControlName, я получаю сообщение об ошибке:

ОШИБКА Ошибка: нет средства доступа к значению для элемента управления формы с именем: 'SurveyType'

Пробовал добавить ngDefaultControlбезуспешно. Кажется, это потому, что нет ввода / выбора ... и я не знаю, что делать.

Я хотел бы привязать свой щелчок к этому formControl, чтобы, когда кто-то щелкает по всей карточке, мой «тип» вставлялся в formControl. Является ли это возможным?

jbtd
источник
Я не знаю, что я хочу сказать: formControl используется для управления формой в html, но div не является элементом управления формой. Я бы хотел связать свой опросный тип с type.id div моей карты
jbtd
Я знаю, что могу использовать старый способ angular и привязать к нему свой selectedType, но я пытался использовать и изучать реактивную форму из angular 4 и не знаю, как использовать formControl с этим типом случая.
jbtd
Хорошо, может быть, но этот случай не может быть обработан реактивной формой, поэтому. В любом случае
спасибо
Я дал ответ о том, как разбить огромные формы на подкомпоненты здесь stackoverflow.com/a/56375605/2398593, но это также очень хорошо применимо только с помощью специального средства доступа к значению элемента управления. Также
посетите
У меня была такая же проблема, и я решил ее в этом сообщении: stackoverflow.com/a/64617295/1190948
Юрий Синицон

Ответы:

268

Вы можете использовать formControlNameтолько те директивы, которые реализуют ControlValueAccessor.

Реализуйте интерфейс

Итак, чтобы делать то, что вы хотите, вам нужно создать компонент, который реализует ControlValueAccessor, что означает реализацию следующих трех функций :

  • writeValue (сообщает Angular, как записать значение из модели в представление)
  • registerOnChange (регистрирует функцию-обработчик, которая вызывается при изменении представления)
  • registerOnTouched (регистрирует обработчик, который будет вызываться, когда компонент получает событие касания, полезно для определения, был ли компонент сфокусирован).

Зарегистрируйте провайдера

Затем вы должны сообщить Angular, что эта директива является ControlValueAccessor(интерфейс не будет вырезать ее, поскольку она удаляется из кода, когда TypeScript компилируется в JavaScript). Вы делаете это, регистрируя провайдера .

Провайдер должен предоставить NG_VALUE_ACCESSORи использовать существующее значение . Вам также понадобится forwardRefздесь. Обратите внимание, что NG_VALUE_ACCESSORдолжен быть мульти-провайдер .

Например, если ваша настраиваемая директива называется MyControlComponent, вы должны добавить что-то в следующих строках внутри объекта, переданного в @Componentдекоратор:

providers: [
  { 
    provide: NG_VALUE_ACCESSOR,
    multi: true,
    useExisting: forwardRef(() => MyControlComponent),
  }
]

Применение

Ваш компонент готов к использованию. С формами на основе шаблонов ,ngModel связывание теперь будет работать должным образом.

С реактивными формами теперь вы можете правильно использоватьformControlName и элемент управления формы будет вести себя .

Ресурсы

Лазар Любенович
источник
также не забывайте ngDefaultControl на соответствующем вводе.
Надин
75

Я думаю, вам следует использовать formControlName="surveyType"на, inputа не наdiv

Вега
источник
Да, конечно, но я не знаю, как превратить div моей карты во что-то еще, что будет
элементом
6
Смысл CustomValueAccessor состоит в том, чтобы добавить элемент управления формой к ВСЕМУ, даже к div
SoEzPz
4
@SoEzPz Но это плохой шаблон. Вы имитируете функциональность ввода в компоненте-оболочке, повторно реализуя стандартные HTML-методы самостоятельно (таким образом, в основном заново изобретая колесо и делая ваш код подробным). но в 90% случаев вы можете выполнить все, что хотите, используя <ng-content>компонент-оболочку, и позволить родительскому компоненту, который определяет, formControlsпросто поместить <input> внутри <wrapper>
Фил
9

Ошибка означает, что Angular не знает, что делать, когда вы добавляете formControlфайл div. Чтобы исправить это, у вас есть два варианта.

  1. Вы устанавливаете formControlNameэлемент, который поддерживается Angular из коробки. Это: input, textareaи select.
  2. Вы реализуете ControlValueAccessorинтерфейс. Поступая так, вы сообщаете Angular, «как получить доступ к значению вашего элемента управления» (отсюда и название). Или, проще говоря: что делать, когда вы помещаете formControlNameэлемент, который, естественно, не имеет значения, связанного с ним.

Теперь реализация ControlValueAccessorинтерфейса поначалу может показаться немного сложной. Тем более, что там не так много хорошей документации, и вам нужно добавить много шаблонов в свой код. Итак, позвольте мне попытаться разбить это на несколько простых шагов.

Переместите элемент управления формы в отдельный компонент

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

Добавьте шаблон в свой код

Реализация ControlValueAccessor интерфейса довольно подробна, вот шаблон, который прилагается:

import {Component, OnInit, forwardRef} from '@angular/core';
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';


@Component({
  selector: 'app-custom-input',
  templateUrl: './custom-input.component.html',
  styleUrls: ['./custom-input.component.scss'],

  // a) copy paste this providers property (adjust the component name in the forward ref)
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomInputComponent),
      multi: true
    }
  ]
})
// b) Add "implements ControlValueAccessor"
export class CustomInputComponent implements ControlValueAccessor {

  // c) copy paste this code
  onChange: any = () => {}
  onTouch: any = () => {}
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }
  registerOnTouched(fn: any): void {
    this.onTouch = fn;
  }

  // d) copy paste this code
  writeValue(input: string) {
    // TODO
  }

Так что же делают отдельные части?

  • а) Сообщает Angular во время выполнения, что вы реализовали ControlValueAccessor интерфейс
  • б) Убедитесь, что вы реализуете ControlValueAccessor интерфейс
  • в) Это, наверное, самая запутанная часть. По сути, вы даете Angular возможность переопределить свойства / методы вашего класса onChangeи onTouchсобственную реализацию во время выполнения, чтобы затем вы могли вызывать эти функции. Поэтому важно понять этот момент: вам не нужно самостоятельно реализовывать onChange и onTouch (кроме исходной пустой реализации). Единственное, что вы делаете с (c), - это позволяете Angular прикреплять свои собственные функции к вашему классу. Зачем? Тогда вы можете позвонить вonChange и onTouchметодам , предоставляемых угловым в соответствующее время. Ниже мы увидим, как это работает.
  • г) Мы также увидим, как этот writeValueметод работает, в следующем разделе, когда мы его реализуем. Я поместил его здесь, поэтому все необходимые свойства ControlValueAccessorреализованы, а ваш код все еще компилируется.

Реализовать writeValue

Что writeValueделает, так это делать что-то внутри вашего пользовательского компонента, когда элемент управления формы изменяется снаружи . Так, например, если вы назвали свой компонент управления настраиваемой формой app-custom-inputи будете использовать его в родительском компоненте следующим образом:

<form [formGroup]="form">
  <app-custom-input formControlName="myFormControl"></app-custom-input>
</form>

затем writeValueсрабатывает всякий раз, когда родительский компонент каким-либо образом изменяет значение myFormControl. Это может быть, например, во время инициализации формы ( this.form = this.formBuilder.group({myFormControl: ""});) или при сбросе формы this.form.reset();.

Что вы обычно хотите сделать, если значение элемента управления формы изменяется снаружи, так это записать его в локальную переменную, которая представляет значение элемента управления формы. Например, если вы CustomInputComponentвращаетесь вокруг элемента управления текстовой формой, это может выглядеть так:

writeValue(input: string) {
  this.input = input;
}

и в html CustomInputComponent:

<input type="text"
       [ngModel]="input">

Вы также можете записать его непосредственно в элемент ввода, как описано в документации по Angular.

Теперь вы справились с тем, что происходит внутри вашего компонента, когда что-то меняется снаружи. Теперь посмотрим в другом направлении. Как вы информируете внешний мир, когда что-то меняется внутри вашего компонента?

Вызов onChange

Следующим шагом будет информирование родительского компонента об изменениях внутри вашего CustomInputComponent. Это где onChangeи onTouchфункция из (с) сверху вступает в игру. Вызывая эти функции, вы можете информировать внешний мир об изменениях внутри вашего компонента. Чтобы распространить изменения значения наружу, вам необходимо вызвать onChange с новым значением в качестве аргумента . Например, если пользователь вводит что-то в inputполе вашего настраиваемого компонента, вы вызываете onChangeс обновленным значением:

<input type="text"
       [ngModel]="input"
       (ngModelChange)="onChange($event)">

Если вы снова проверите реализацию (c) сверху, вы увидите, что происходит: Angular привязывает свою реализацию к onChangeсвойству класса. Эта реализация ожидает один аргумент - обновленное значение элемента управления. Сейчас вы вызываете этот метод и тем самым сообщаете Angular об изменении. Angular теперь продолжит работу и изменит значение формы снаружи. Это ключевая часть всего этого. Вы сказали Angular, когда он должен обновить элемент управления формы и с каким значением, вызвав егоonChange . Вы дали ему возможность «получить доступ к контрольному значению».

Кстати: Название onChangeвыбрано мной. Вы можете выбрать здесь что угодно, например propagateChangeили подобное. Как бы вы его ни называли, это будет та же функция, которая принимает один аргумент, который предоставляется Angular и который привязан к вашему классу с помощьюregisterOnChange методом во время выполнения.

Вызов onTouch

Так как элементы управления формы можно «коснуться», вы также должны дать Angular возможность понимать, когда происходит касание настраиваемого элемента управления формы. Вы догадались, что это можно сделать, вызвав onTouchфункцию. Итак, для нашего примера здесь, если вы хотите соответствовать тому, как Angular делает это для готовых элементов управления формой, вы должны вызвать, onTouchкогда поле ввода размыто:

<input type="text"
       [(ngModel)]="input"
       (ngModelChange)="onChange($event)"
       (blur)="onTouch()">

Еще раз, onTouch это имя выбрано мной, но реальная функция предоставляется Angular и не требует аргументов. Это имеет смысл, поскольку вы просто даете Angular знать, что элемент управления формы был затронут.

Собираем все вместе

Итак, как это выглядит, когда все вместе? Должно получиться так:

// custom-input.component.ts
import {Component, OnInit, forwardRef} from '@angular/core';
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';


@Component({
  selector: 'app-custom-input',
  templateUrl: './custom-input.component.html',
  styleUrls: ['./custom-input.component.scss'],

  // Step 1: copy paste this providers property
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomInputComponent),
      multi: true
    }
  ]
})
// Step 2: Add "implements ControlValueAccessor"
export class CustomInputComponent implements ControlValueAccessor {

  // Step 3: Copy paste this stuff here
  onChange: any = () => {}
  onTouch: any = () => {}
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }
  registerOnTouched(fn: any): void {
    this.onTouch = fn;
  }

  // Step 4: Define what should happen in this component, if something changes outside
  input: string;
  writeValue(input: string) {
    this.input = input;
  }

  // Step 5: Handle what should happen on the outside, if something changes on the inside
  // in this simple case, we've handled all of that in the .html
  // a) we've bound to the local variable with ngModel
  // b) we emit to the ouside by calling onChange on ngModelChange

}
// custom-input.component.html
<input type="text"
       [(ngModel)]="input"
       (ngModelChange)="onChange($event)"
       (blur)="onTouch()">
// parent.component.html
<app-custom-input [formControl]="inputTwo"></app-custom-input>

// OR

<form [formGroup]="form" >
  <app-custom-input formControlName="myFormControl"></app-custom-input>
</form>

Больше примеров

Вложенные формы

Обратите внимание, что аксессоры управляющих значений НЕ подходят для вложенных групп форм. Для вложенных групп форм вы можете просто использовать @Input() subformвместо. Аксессоры контрольного значения предназначены для обертывания controls, а не groups! Посмотрите этот пример, как использовать ввод для вложенной формы: https://stackblitz.com/edit/angular-nested-forms-input-2

Источники

Берслинг
источник
-1

Для меня это произошло из-за атрибута "множественный" в элементе управления выбранным вводом, поскольку Angular имеет другой ValueAccessor для этого типа элемента управления.

const countryControl = new FormControl();

И внутри шаблона используйте это

    <select multiple name="countries" [formControl]="countryControl">
      <option *ngFor="let country of countries" [ngValue]="country">
       {{ country.name }}
      </option>
    </select>

Подробнее см. Официальные документы

Судхир Сингх
источник
Что было из-за «множественности»? Я не понимаю, как ваш код что-то решает, и в чем заключалась исходная проблема. Ваш код показывает обычное базовое использование.
Лазар Любенович