El error significa que Angular no sabe qué hacer cuando le pones un formControl
a div
. Para solucionar esto, tienes dos opciones.
- Pones el
formControlName
en un elemento, que es compatible con Angular fuera de la caja. Estos son: input
, textarea
y select
.
- Implementas la
ControlValueAccessor
interfaz. 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 formControlName
un 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 ControlValueAccessor
interfaz 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
ControlValueAccessor
interfaz
- b) Se asegura de que esté implementando la
ControlValueAccessor
interfaz
- 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
onChange
y onTouch
con 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 onChange
yonTouch
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
writeValue
método en la siguiente sección, cuando lo implementemos. Lo puse aquí, para que ControlValueAccessor
se implementen todas las propiedades requeridas y su código aún se compile.
Implemente writeValue
Lo que writeValue
hace 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-input
y lo usaría en el componente principal de esta manera:
<form [formGroup]="form">
<app-custom-input formControlName="myFormControl"></app-custom-input>
</form>
luego writeValue
se 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 CustomInputComponent
gira 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 onChange
y onTouch
de (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 input
campo en su componente personalizado, llame onChange
con 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 onChange
propiedad 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 onChange
es elegido por mí. Puede elegir cualquier cosa aquí, por ejemplo propagateChange
o 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 registerOnChange
mé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 onTouch
funció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 onTouch
cuando el campo de entrada esté borroso:
<input type="text"
[(ngModel)]="input"
(ngModelChange)="onChange($event)"
(blur)="onTouch()">
Una vez más, onTouch
es 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() subform
en 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