Angular4: sin valor de acceso para el control de formulario

146

Tengo un elemento personalizado:

<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>

Cuando intento agregar el formControlName, recibo un mensaje de error:

Error de ERROR: sin valor de acceso para el control de formulario con el nombre: 'surveyType'

Traté de agregar ngDefaultControlsin éxito. Parece que es porque no hay entrada / selección ... y no sé qué hacer.

Me gustaría vincular mi clic a este formularioControl para que cuando alguien haga clic en toda la tarjeta que empuje mi 'tipo' en el formularioControl. ¿Es posible?

jbtd
fuente
No sé mi punto es que: formControl va para el control de formularios en html pero div no es un control de formularios. Me gustaría vincular mi surveyType con el type.id de mi tarjeta div
jbtd
Sé que podría usar la antigua forma angular y hacer que mi selectType se vincule, pero estaba tratando de usar y aprender la forma reactiva de angular 4 y no sé cómo usar formControl con este tipo de casos.
jbtd
Ok, es posible que ese caso no pueda ser manejado por una forma reactiva. Gracias de todos modos :)
jbtd
He respondido cómo dividir los formularios enormes en subcomponentes aquí stackoverflow.com/a/56375605/2398593 pero esto también se aplica muy bien con solo un valor de control personalizado. Consulte también github.com/cloudnc/ngx-sub-form :)
maxime1992

Respuestas:

251

Puede usar formControlNamesolo en directivas que implementan ControlValueAccessor.

Implementar la interfaz

Entonces, para hacer lo que quiere, debe crear un componente que implemente ControlValueAccessor, lo que significa implementar las siguientes tres funciones :

  • writeValue (le dice a Angular cómo escribir el valor del modelo a la vista)
  • registerOnChange (registra una función de controlador que se llama cuando cambia la vista)
  • registerOnTouched (registra un controlador que se llamará cuando el componente recibe un evento táctil, útil para saber si el componente se ha enfocado).

Registrar un proveedor

Luego, debe decirle a Angular que esta directiva es una ControlValueAccessor(la interfaz no la cortará, ya que se elimina del código cuando TypeScript se compila en JavaScript). Para ello, debe registrar un proveedor .

El proveedor debe proporcionar NG_VALUE_ACCESSORy usar un valor existente . También necesitarás un forwardRefaquí. Tenga en cuenta que NG_VALUE_ACCESSORdebe ser un proveedor múltiple .

Por ejemplo, si su directiva personalizada se llama MyControlComponent, debe agregar algo en las siguientes líneas dentro del objeto pasado al @Componentdecorador:

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

Uso

Su componente está listo para ser utilizado. Con los formularios basados ​​en plantillas , el ngModelenlace ahora funcionará correctamente.

Con los formularios reactivos , ahora puede usarlos correctamente formControlNamey el control del formulario se comportará como se esperaba.

Recursos

Lazar Ljubenović
fuente
72

Creo que deberías usar formControlName="surveyType" en un inputy no en undiv

Vega
fuente
Sí, claro, pero no sé cómo convertir mi tarjeta div en otra cosa que será un control de formulario html
jbtd
55
El punto de CustomValueAccessor es agregar control de formulario a CUALQUIER COSA, incluso un div
SoEzPz
44
@SoEzPz Sin embargo, este es un mal patrón. Imita la funcionalidad de entrada en un componente contenedor, re-implementando métodos HTML estándar usted mismo (básicamente reinventando la rueda y haciendo que su código sea detallado). pero en el 90% de los casos, puede lograr todo lo que quiera mediante el uso <ng-content>de un componente contenedor y dejar que el componente principal que define formControlssimplemente ponga el <input> dentro del <wrapper>
Phil
3

El error significa que Angular no sabe qué hacer cuando le pones un formControla div. Para solucionar esto, tienes dos opciones.

  1. Pones el formControlNameen un elemento, que es compatible con Angular fuera de la caja. Estos son: input, textareay select.
  2. Implementas la ControlValueAccessorinterfaz. Al hacerlo, le está diciendo a Angular "cómo acceder al valor de su control" (de ahí el nombre). O en términos simples: qué hacer, cuando coloca formControlNameun elemento, que naturalmente no tiene un valor asociado.

Ahora, implementando el ControlValueAccessor interfaz puede ser un poco desalentador al principio. Especialmente porque no hay mucha buena documentación de esto y necesita agregar una gran cantidad de repeticiones a su código. Así que déjame intentar desglosar esto en algunos pasos fáciles de seguir.

Mueva su control de formulario a su propio componente

Para implementar el ControlValueAccessor, debe crear un nuevo componente (o directiva). Mueva el código relacionado con su control de formulario allí. De esta manera, también será fácilmente reutilizable. En primer lugar, tener un control dentro de un componente podría ser la razón, por qué necesita implementar elControlValueAccessor interfaz, porque de lo contrario no podrá usar su componente personalizado junto con formas angulares.

Agregue el repetitivo a su código

La implementación de la ControlValueAccessorinterfaz es bastante detallada, aquí está la plantilla que viene con ella:

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
  }

Entonces, ¿qué están haciendo las partes individuales?

  • a) Permite a Angular saber durante el tiempo de ejecución que implementó la ControlValueAccessorinterfaz
  • b) Se asegura de que esté implementando la ControlValueAccessorinterfaz
  • c) Esta es probablemente la parte más confusa. Básicamente, lo que está haciendo es darle a Angular los medios para anular las propiedades / métodos de su clase onChangey onTouchcon su propia implementación durante el tiempo de ejecución, de modo que luego pueda llamar a esas funciones. Por lo tanto, es importante entender este punto: no necesita implementar onChange y onTouch usted mismo (aparte de la implementación vacía inicial). Lo único que estás haciendo con (c) es dejar que Angular adjunte sus propias funciones a tu clase. ¿Por qué? Entonces puedes llamar al onChangeyonTouch métodos proporcionados por Angular en el momento apropiado. Veremos cómo funciona esto a continuación.
  • d) También veremos cómo funciona el writeValuemétodo en la siguiente sección, cuando lo implementemos. Lo puse aquí, para que ControlValueAccessorse implementen todas las propiedades requeridas y su código aún se compile.

Implemente writeValue

Lo que writeValuehace es hacer algo dentro de su componente personalizado, cuando el control de formulario se cambia por fuera . Entonces, por ejemplo, si ha nombrado su componente de control de formulario personalizado app-custom-inputy lo usaría en el componente principal de esta manera:

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

luego writeValuese activa cada vez que el componente padre cambia de alguna manera el valor de myFormControl. Esto podría ser, por ejemplo, durante la inicialización del formulario ( this.form = this.formBuilder.group({myFormControl: ""});) o en un reinicio del formulario this.form.reset();.

Lo que normalmente querrá hacer si el valor del control del formulario cambia en el exterior, es escribirlo en una variable local que represente el valor del control del formulario. Por ejemplo, si CustomInputComponentgira alrededor de un control de formulario basado en texto, podría verse así:

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

y en el html de CustomInputComponent:

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

También puede escribirlo directamente en el elemento de entrada como se describe en los documentos angulares.

Ahora ha manejado lo que sucede dentro de su componente cuando algo cambia afuera. Ahora veamos la otra dirección. ¿Cómo se informa al mundo exterior cuando algo cambia dentro de su componente?

Llamando aCambiar

El siguiente paso es informar al componente padre sobre los cambios dentro de su CustomInputComponent. Aquí es donde entran en juego las funciones onChangey onTouchde (c) desde arriba. Al llamar a esas funciones, puede informar al exterior sobre los cambios dentro de su componente. Para propagar los cambios del valor al exterior, debe llamar a onChange con el nuevo valor como argumento . Por ejemplo, si el usuario escribe algo en el inputcampo en su componente personalizado, llame onChangecon el valor actualizado:

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

Si vuelve a comprobar la implementación (c) desde arriba, verá lo que sucede: Angular limita su propia implementación a la onChangepropiedad de clase. Esa implementación espera un argumento, que es el valor de control actualizado. Lo que está haciendo ahora es llamar a ese método y, por lo tanto, informar a Angular sobre el cambio. Angular ahora continuará y cambiará el valor del formulario en el exterior. Esta es la parte clave de todo esto. Le dijo a Angular cuándo debería actualizar el control de formulario y con qué valor llamandoonChange . Le ha dado los medios para "acceder al valor de control".

Por cierto: el nombre onChangees elegido por mí. Puede elegir cualquier cosa aquí, por ejemplo propagateChangeo similar. Sin embargo, sin importar cómo lo nombre, será la misma función que toma un argumento, que es proporcionada por Angular y que está vinculada a su clase por el registerOnChangemétodo durante el tiempo de ejecución.

Llamando a Touch

Dado que los controles de formulario se pueden "tocar", también debe darle a Angular los medios para comprender cuándo se toca su control de formulario personalizado. Puede hacerlo, lo adivinó, llamando a la onTouchfunción. Entonces, para nuestro ejemplo aquí, si desea seguir cumpliendo con cómo Angular lo está haciendo para los controles de formulario listos para usar, debe llamar onTouchcuando el campo de entrada esté borroso:

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

Una vez más, onTouches un nombre elegido por mí, pero Angular proporciona su función real y no requiere argumentos. Lo que tiene sentido, ya que solo le está haciendo saber a Angular, que se ha tocado el control de formulario.

Poniendolo todo junto

Entonces, ¿cómo se ve eso cuando se trata todo junto? Debe tener un aspecto como este:

// 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>

Más ejemplos

Formas anidadas

Tenga en cuenta que los Access Value Accessors NO son la herramienta adecuada para los grupos de formularios anidados. Para grupos de formularios anidados, simplemente puede usar un @Input() subformen su lugar. ¡Los Access Value Controlors están diseñados para envolver controls, no groups! Vea en este ejemplo cómo usar una entrada para un formulario anidado: https://stackblitz.com/edit/angular-nested-forms-input-2

Fuentes

bersling
fuente
-1

Para mí, se debió al atributo "múltiple" en el control de entrada seleccionado, ya que Angular tiene un ValueAccessor diferente para este tipo de control.

const countryControl = new FormControl();

Y dentro de la plantilla use así

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

Más detalles ref Documentos oficiales

Sudhir Singh
fuente
¿Qué se debió a "múltiple"? No veo cómo su código resuelve nada, o cuál fue el problema original. Su código muestra el uso básico habitual.
Lazar Ljubenović