Reenvío de controles de formulario a componentes de control personalizados en Angular

Jan 25 2023
A veces queremos reenviar y usar un control de formulario existente en lugar de crear un contenedor de acceso de valor redundante. Un caso de uso común es cuando se crean, por ejemplo, componentes de entrada personalizados.

A veces queremos reenviar y usar un control de formulario existente en lugar de crear un contenedor de acceso de valor redundante. Un caso de uso común es cuando se crean, por ejemplo, inputcomponentes personalizados. La siguiente imagen describe nuestro objetivo:

Queremos usar controles de formulario pasados ​​a través de directivas formControl, formControlNamey ngModelen nuestro inputcomponente personalizado y reenviarlo a nuestro inputelemento interno. Veamos dos formas en que podemos hacerlo:

Configuración de la propiedad Control valueAccessor

La primera opción que podríamos tomar es usar NodeInjectory recuperar una referencia a nuestro control usando el NgControlproveedor:

@Component({
  selector: 'app-input',
  template: `<input />`,
  providers: [{ 
    provide: NG_VALUE_ACCESSOR, 
    multi: true, 
    useExisting: InputComponent 
  }]
})
export class InputComponent implements ControlValueAccessor {
   const ngControl = inject(NgControl, { self: true });

   ...
}

Es posible abordar esto eliminando el proveedor, inyectando el NgControly estableciendo explícitamente la valueAccessorpropiedad en un noopdescriptor de acceso de valor, ya que no nos importa el valor; solo queremos "satisfacer" a Angular.


class NoopValueAccessor implements ControlValueAccessor {
  writeValue() {}
  registerOnChange() {}
  registerOnTouched() {}
}

function injectNgControl() {
  const ngControl = inject(NgControl, { self: true, optional: true });

  if (!ngControl) throw new Error('...');

  if (
    ngControl instanceof FormControlDirective ||
    ngControl instanceof FormControlName ||
    ngControl instanceof NgModel
  ) {
    // 
    ngControl.valueAccessor = new NoopValueAccessor();

    return ngControl;
  }

  throw new Error(`...`);
}

@Component({
  selector: 'app-input',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `<input [formControl]="ngControl.control" /> `,
})
export class InputComponent {
  ngControl = injectNgControl();
}

La primera técnica funciona, pero siempre se siente "incómoda" ya que esencialmente estamos haciendo el trabajo de Angular al establecer la propiedad de acceso de valor. Ahora podemos aprovechar la función de directivas de host para obtener el mismo resultado. Primero, crearemos un NoopValueAccessorDirective:

@Directive({
  standalone: true,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      multi: true,
      useExisting: NoopValueAccessorDirective,
    },
  ],
})
export class NoopValueAccessorDirective implements ControlValueAccessor {
  writeValue(obj: any): void {}
  registerOnChange(fn: any): void {}
  registerOnTouched(fn: any): void {}
}

export function injectNgControl() {
  const ngControl = inject(NgControl, { self: true, optional: true });

  if (!ngControl) throw new Error('...');

  if (
    ngControl instanceof FormControlDirective ||
    ngControl instanceof FormControlName ||
    ngControl instanceof NgModel
  ) {
    return ngControl;
  }

  throw new Error('...');
}

@Component({
  selector: 'app-input',
  standalone: true,
  // 
  hostDirectives: [NoopValueAccessorDirective],
  imports: [ReactiveFormsModule],
  template: ` <input [formControl]="ngControl.control" /> `,
})
export class InputComponent {
  ngControl = injectNgControl();
}

<app-input [formControl]="control" />

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

    <ng-container formArrayName="skills">
       <app-input [formControlName]="index" 
            *ngFor="let c of skills.controls; index as index"></app-input>
    </ng-container>
</form>