Saltar al contenido

¿Puedo acceder a formControl de mi ControlValueAccessor personalizado en Angular 2+?

Solución:

PLUNKER DE MUESTRA

Veo dos opciones:

  1. Propagar los errores del componente FormControl para <select> FormControl siempre que el <select> FormControl cambios de valor
  2. Propagar los validadores del componente FormControl para <select> FormControl

A continuación se encuentran disponibles las siguientes variables:

  • selectModel es el NgModel de El <select>
  • formControl es el FormControl del componente recibido como argumento

Opción 1: propagar errores

  ngAfterViewInit(): void {
    this.selectModel.control.valueChanges.subscribe(() => {
      this.selectModel.control.setErrors(this.formControl.errors);
    });
  }

Opción 2: propagar validadores

  ngAfterViewInit(): void {
    this.selectModel.control.setValidators(this.formControl.validator);
    this.selectModel.control.setAsyncValidators(this.formControl.asyncValidator);
  }

La diferencia entre los dos es que propagar los errores significa tener ya los errores, mientras que la opción de segundos implica ejecutar los validadores por segunda vez. Algunos de ellos, como los validadores asíncronos, pueden ser demasiado costosos de realizar.

¿Propagando todas las propiedades?

No existe una solución general para propagar todas las propiedades. Varias propiedades se establecen mediante varias directivas u otros medios, por lo que tienen un ciclo de vida diferente, lo que significa que requieren un manejo particular. La solución actual se refiere a la propagación de errores de validación y validadores. Hay muchas propiedades disponibles allí.

Tenga en cuenta que es posible que obtenga cambios de estado diferentes FormControl instancia suscribiéndose a FormControl.statusChanges(). De esta forma puede saber si el control es VALID, INVALID, DISABLED o PENDING (la validación asíncrona todavía se está ejecutando).

¿Cómo funciona la validación bajo el capó?

Bajo el capó, los validadores se aplican mediante directivas (consulte el código fuente). Las directivas tienen providers: [REQUIRED_VALIDATOR] lo que significa que se utiliza un inyector jerárquico propio para registrar esa instancia del validador. Entonces, dependiendo de los atributos aplicados en el elemento, las directivas agregarán instancias de validación en el inyector asociado al elemento de destino.

A continuación, estos validadores son recuperados por NgModel y FormControlDirective.

Los validadores y los descriptores de acceso a valores se recuperan como:

  constructor(@Optional() @Host() parent: ControlContainer,
              @Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>,
              @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<AsyncValidator|AsyncValidatorFn>,
              @Optional() @Self() @Inject(NG_VALUE_ACCESSOR)

y respectivamente:

  constructor(@Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>,
              @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<AsyncValidator|AsyncValidatorFn>,
              @Optional() @Self() @Inject(NG_VALUE_ACCESSOR)
              valueAccessors: ControlValueAccessor[])

Tenga en cuenta que @Self() se utiliza, por lo tanto, se utiliza el propio inyector (del elemento al que se está aplicando la directiva) para obtener las dependencias.

NgModel y FormControlDirective tener una instancia de FormControl que actualizan el valor y ejecutan los validadores.

Por lo tanto, el punto principal con el que interactuar es el FormControl ejemplo.

Asimismo, todos los validadores o descriptores de acceso a valores se registran en el inyector del elemento al que se aplican. Esto significa que el padre no debe acceder a ese inyector. Por lo tanto, sería una mala práctica acceder desde el componente actual al inyector proporcionado por el <select>.

Código de muestra para la opción 1 (fácilmente reemplazable por la Opción 2)

La siguiente muestra tiene dos validadores: uno que es obligatorio y otro que es un patrón que obliga a la opción a coincidir con la “opción 3”.

El PLUNKER

options.component.ts

import {AfterViewInit, Component, forwardRef, Input, OnInit, ViewChild} from '@angular/core';
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, NgModel} from '@angular/forms';
import {SettingsService} from '../settings.service';

const OPTIONS_VALUE_ACCESSOR: any = {
  multi: true,
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => OptionsComponent)
};

@Component({
  providers: [OPTIONS_VALUE_ACCESSOR],
  selector: 'inf-select[name]',
  templateUrl: './options.component.html',
  styleUrls: ['./options.component.scss']
})
export class OptionsComponent implements ControlValueAccessor, OnInit, AfterViewInit {

  @ViewChild('selectModel') selectModel: NgModel;
  @Input() formControl: FormControl;

  @Input() name: string;
  @Input() disabled = false;

  private propagateChange: Function;
  private onTouched: Function;

  private settingsService: SettingsService;

  selectedValue: any;

  constructor(settingsService: SettingsService) {
    this.settingsService = settingsService;
  }

  ngOnInit(): void {
    if (!this.name) {
      throw new Error('Option name is required. eg.: <options [name]="myOption"></options>>');
    }
  }

  ngAfterViewInit(): void {
    this.selectModel.control.valueChanges.subscribe(() => {
      this.selectModel.control.setErrors(this.formControl.errors);
    });
  }

  writeValue(obj: any): void {
    this.selectedValue = obj;
  }

  registerOnChange(fn: any): void {
    this.propagateChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }
}

options.component.html

<select #selectModel="ngModel"
        class="form-control"
        [disabled]="disabled"
        [(ngModel)]="selectedValue"
        (ngModelChange)="propagateChange($event)">
  <option value="">Select an option</option>
  <option *ngFor="let option of settingsService.getOption(name)" [value]="option.description">
    {{option.description}}
  </option>
</select>

options.component.scss

:host {
  display: inline-block;
  border: 5px solid transparent;

  &.ng-invalid {
    border-color: purple;
  }

  select {
    border: 5px solid transparent;

    &.ng-invalid {
      border-color: red;
    }
  }
}

Uso

Definir el FormControl ejemplo:

export class AppComponent implements OnInit {

  public control: FormControl;

  constructor() {
    this.control = new FormControl('', Validators.compose([Validators.pattern(/^option 3$/), Validators.required]));
  }
...

Unir el FormControl instancia al componente:

<inf-select name="myName" [formControl]="control"></inf-select>

Servicio de configuración ficticia

/**
 * TODO remove this class, added just to make injection work
 */
export class SettingsService {

  public getOption(name: string): [{ description: string }] {
    return [
      { description: 'option 1' },
      { description: 'option 2' },
      { description: 'option 3' },
      { description: 'option 4' },
      { description: 'option 5' },
    ];
  }
}

Aquí está lo que, en mi opinión, es la solución más limpia para acceder a FormControl en un ControlValueAccessor componente basado. La solución se basó en lo que se menciona aquí en la documentación de Angular Material.

// parent component template
<my-text-input formControlName="name"></my-text-input>
@Component({
  selector: 'my-text-input',
  template: '<input
    type="text"
    [value]="value"
  />',
})
export class MyComponent implements AfterViewInit, ControlValueAccessor  {

  // Here is missing standard stuff to implement ControlValueAccessor interface

  constructor(@Optional() @Self() public ngControl: NgControl) {
    if (ngControl != null) {
      // Setting the value accessor directly (instead of using
      // the providers) to avoid running into a circular import.
      ngControl.valueAccessor = this;
    }
  }

    ngAfterContentInit(): void {
       const control = this.ngControl && this.ngControl.control;
       if (control) {
          // FormControl should be available here
       }
    }
}
¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *