Angular

Crear controles de formulario personalizados usando ControlValueAccessor en Angular

Publicado

¿Cómo crear controles de formulario personalizados en Angular usando ControlValueAccessor? Podemos crear componentes de formulario personalizados y conectarlos a formularios basados en plantillas o formularios reactivos.

Entonces, cuando digo controles de formulario personalizados, me refiero a aquellos elementos que no son sus controles típicos, como inputs, radio buttons o checkboxes. Por ejemplo, un componente de clasificación con estrellas. Estos no están disponibles de fábrica.

También podemos crear secciones en un formulario como componentes secundarios que luego se pueden usar como controles de formulario personalizados. De esta manera, las formas más grandes se pueden dividir en partes manejables.

Elementos de formulario personalizados

Cuando escuchemos el término formulario, estaríamos pensando en inputs y tal vez en algunos checkboxes, etc. Pero cuando se trata de formularios realmente grandes en los que tenemos muchos botones, listas y selecciones personalizados, todo el formulario se volverá muy complejo y manejar esto sería un problema.

Cuando hay muchos elementos de formulario personalizados o cuando el formulario comienza a crecer, probablemente sea una buena idea dividirlo en secciones más pequeñas. Colocar todo en una sola plantilla lo haría realmente complicado.

Control de formulario personalizado en Angular

ControlValueAccessor es una interfaz incluida en Angular. Actúa como un puente entre los elementos DOM y la API de forma angular.

Entonces, si tiene un elemento personalizado que le gustaría conectar a su formulario, debe hacer uso de ControlValueAccessor para que el elemento sea compatible con la API de Angular Forms. Hacerlo permitirá que el elemento se conecte usando ngModel (Formularios controlados por plantillas) o formControl (Formularios reactivos).

Echemos un vistazo a cómo creamos un control de formulario personalizado

Cuando comencé con Angular, no sabía que existía algo como esto. Recuerdo cuando escribí componentes secundarios para formularios y usé @Input() y @Output() para recibir y enviar valores de formulario al componente de formulario principal. Solía escuchar los cambios en el componente hijo y luego emitir los valores al padre.

En el padre, los valores se tomarán y usarán para parchear el formulario. Esto fue hasta que me encontré con el ControlValueAccessor. No más inputs y outputs, todo simplemente funciona.

Paso 1: configuración del proyecto

Primero, cree un nuevo RatingInputComponent. Esto se puede lograr con @angular/cli:

ng generate component rating-input --inline-template --inline-style

Esto agregará el nuevo componente a las declarations de la aplicación y producirá un archivo rating-input.component.ts:

import { Component, OnInit } from '@angular/core';
 
@Component({
selector: 'app-rating-input',
template: ` <p>rating-input works!</p> `,
styles: [],
})
export class RatingInputComponent implements OnInit {
constructor() {}
 
ngOnInit(): void {}
}

Agregamos la plantilla, los estilos y la lógica:

import { Component } from '@angular/core';
 
@Component({
selector: 'app-rating-input',
template: `
<ng-container *ngFor="let starred of stars; let i = index">
<span (click)="rate(i + (starred ? (value > i + 1 ? 1 : 0) : 1))">
<ng-container *ngIf="starred; else noStar">⭐</ng-container>
<ng-template #noStar>·</ng-template>
</span>
</ng-container>
`,
styles: [
`
span {
display: inline-block;
width: 25px;
line-height: 25px;
text-align: center;
cursor: pointer;
}
`,
],
})
export class RatingInputComponent {
stars: boolean[] = Array(5).fill(false);
 
get value(): number {
return this.stars.reduce((total, starred) => {
return total + (starred ? 1 : 0);
}, 0);
}
 
rate(rating: number) {
this.stars = this.stars.map((_, i) => rating > i);
}
}

Podemos obtener el value del componente (0 a 5) y establecer el valor del componente llamando a la función de rate o haciendo clic en el número de estrellas deseado.

Puede agregar el componente a la aplicación: src/app/app.component.html

<app-rating-input></app-rating-input>

Y ejecute la aplicación:

ng serve

Esto es genial, pero no podemos simplemente agregar esta entrada a un formulario y esperar que todo funcione. Necesitamos convertirlo en un ControlValueAccessor.

Implemente la interface ControlValueAccessor

Implemente la interfaz en el componente personalizado. La interfaz nos pediría que agreguemos algunos métodos en nuestra clase.

interface ControlValueAccessor {
writeValue(obj: any): void
registerOnChange(fn: any): void
registerOnTouched(fn: any): void
setDisabledState(isDisabled: boolean)?: void
}

Veamos qué está haciendo cada uno de los métodos. Una vez que tengamos claro cómo están las cosas, podemos sumergirnos en la implementación.

  • writeValue() - la API de formularios llama a esta función para actualizar el valor del elemento. Cuando el valor de ngModel o formControl cambia, se llama a esta función y se pasa el último valor como argumento de la función. Podemos utilizar el último valor y realizar cambios en el componente. (referencia)
  • registerOnChange() - obtenemos acceso a una función en el argumento que se puede guardar en una variable local. Entonces esta función se puede llamar cuando hay algún cambio en el valor de nuestro control de formulario personalizado. (referencia)
  • registerOnTouched() - obtenemos acceso a otra función que se puede utilizar para actualizar el estado del formulario ha sido tocado (touched). Entonces, cuando el usuario interactúa con nuestro elemento de formulario personalizado, podemos llamar a la función guardada para que Angular sepa que se ha interactuado con el elemento. (referencia)
  • setDisabledState() - esta función será llamada por la API de formularios cuando se cambie el estado deshabilitado (disabled). Podemos obtener el estado actual y actualizar el estado del control de formulario personalizado. (referencia) Una vez que implementamos estas funciones, el siguiente paso es proporcionar el token NG_VALUE_ACCESSOR en la matriz de proveedores del componente así:
const CONTROL_VALUE_ACCESSOR: Provider = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RatingInputComponent),
multi: true,
};
 
@Component({
selector: 'app-rating-input',
template: ` <p>rating-input works!</p> `,
providers: [CONTROL_VALUE_ACCESSOR], // <-- agregar aquí
})
export class RatingInputComponent implements ControlValueAccessor {}

Entonces, ahora que sabemos lo que hace cada una de estas funciones, podemos comenzar a implementar nuestro elemento de formulario personalizado.

Volvamos a rating-input.component.ts en el editor de código y realicemos los siguientes cambios:

import { Component, forwardRef, HostBinding, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
 
const CONTROL_VALUE_ACCESSOR: Provider = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RatingInputComponent),
multi: true,
};
 
@Component({
selector: 'app-rating-input',
template: `
<ng-container *ngFor="let starred of stars; let i = index">
<span
(click)="onTouched(); rate(i + (starred ? (value > i + 1 ? 1 : 0) : 1))"
>
<ng-container *ngIf="starred; else noStar">⭐</ng-container>
<ng-template #noStar>·</ng-template>
</span>
</ng-container>
`,
styles: [
`
span {
display: inline-block;
width: 25px;
line-height: 25px;
text-align: center;
cursor: pointer;
}
`,
],
providers: [CONTROL_VALUE_ACCESSOR],
})
export class RatingInputComponent implements ControlValueAccessor {
stars: boolean[] = Array(5).fill(false);
 
// Permita que el input esté deshabilitado, y cuando lo esté, hágalo algo transparente.
@Input() disabled = false;
@HostBinding('style.opacity')
get opacity() {
return this.disabled ? 0.25 : 1;
}
 
// Función para llamar cuando cambia la calificación.
onChange = (rating: number) => {};
 
// Función para llamar cuando se toca el input (cuando se hace clic en una estrella).
onTouched = () => {};
 
get value(): number {
return this.stars.reduce((total, starred) => {
return total + (starred ? 1 : 0);
}, 0);
}
 
rate(rating: number) {
if (!this.disabled) {
this.writeValue(rating);
}
}
 
// Permite que Angular actualice el modelo (rating).
// Actualice el modelo y los cambios necesarios para la vista aquí.
writeValue(rating: number): void {
this.stars = this.stars.map((_, i) => rating > i);
this.onChange(this.value);
}
 
// Permite a Angular registrar una función para llamar cuando cambia el modelo (rating).
// Guarde la función como una propiedad para llamar más tarde aquí.
registerOnChange(fn: (rating: number) => void): void {
this.onChange = fn;
}
 
// Permite a Angular registrar una función para llamar cuando se ha tocado el input.
// Guarde la función como una propiedad para llamar más tarde aquí.
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
 
// Permite que Angular deshabilite el input.
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
}

Ahora puede desactivar los controles del input, agregar un [(ngModel)] o un formControlName:

<app-rating-input [disabled]="true"></app-rating-input>
 
<app-rating-input [(ngModel)]="rating"></app-rating-input>
 
<form [formGroup]="reactiveForm">
<app-rating-input formControlName="rating"></app-rating-input>
</form>

¡En este punto podemos decir que nuestro RatingInputComponent es un componente de formulario personalizado! Funcionará como cualquier otro input nativo (¡Angular proporciona ControlValueAccessors para esos!) En formas reactivas o basadas en plantillas.

Angular hace que sea realmente fácil implementar el control de formulario personalizado usando ControlValueAccessor. Al implementar algunos métodos, podemos conectar directamente nuestro componente a un formulario reactivo o Template Driven con facilidad.

Podemos escribir todo tipo de elementos de formas asombrosas y usarlos sin escribir lógica para manejar la comunicación entre padres e hijos. Deje que la API de formularios haga la magia por nosotros.

También podemos utilizar este enfoque para dividir secciones del formulario en su propio componente individual. De esta manera, si el formulario es grande/complejo, podemos dividirlo en componentes más pequeños que se pueden administrar fácilmente.