Adrián UB Adrián UB

Formularios reactivos en Angular con ControlValueAccessor

Sep 15, 2021

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:

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

// rating-input.component.ts
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.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.

Una vez que implementamos estas funciones, el siguiente paso es proporcionar el token NG_VALUE_ACCESSOR en la matriz de proveedores del componente así:

// rating-input.component.ts
const CONTROL_VALUE_ACCESSOR: Provider = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => RatingInputComponent),
  multi: true,
};

@Component({
  selector: "app-rating-input",
  template: `...`,
  providers: [CONTROL_VALUE_ACCESSOR], // <-- agregar aquí
})
export class RatingInputComponent {}

Aquí creé una constante de proveedor y luego la pasé a los providers. Es necesario porque nos referimos a la clase RatingInputComponent que no está definida antes de su referencia. 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:

// rating-input.component.ts
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.component.html -->
<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.


> cd ..
CC BY-NC-SA 4.0 2021-PRESENT © Adrián UB
Hecho con Astro Vitesse