web-dev-qa-db-fra.com

Puis-je accéder à formControl de mon ControlValueAccessor personnalisé dans Angular 2+?

Je voudrais créer un élément de formulaire personnalisé avec l'interface ControlValueAccessor dans Angular 2+. Cet élément serait un wrapper sur un <select>. Est-il possible de propager les propriétés formControl à l'élément encapsulé? Dans mon cas, l'état de validation n'est pas propagé à la sélection imbriquée comme vous pouvez le voir sur la capture d'écran ci-jointe.

enter image description here

Mon composant est disponible comme suit:

  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'
  })
  export class OptionsComponent implements ControlValueAccessor, OnInit {

  @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>>');
  }
  }

  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;
  }
  }

Voici mon modèle de composant:

<select 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>
18
SaWo

EXEMPLE DE PLONGEUR

Je vois deux options:

  1. Propager les erreurs du composant FormControl à <select>FormControl chaque fois que la valeur <select>FormControl change
  2. Propagez les validateurs du composant FormControl à <select>FormControl

Ci-dessous les variables suivantes sont disponibles:

  • selectModel est le NgModel du <select>
  • formControl est le FormControl du composant reçu en argument

Option 1: propager les erreurs

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

Option 2: propager les valideurs

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

La différence entre les deux est que la propagation des erreurs signifie avoir déjà les erreurs, tandis que l'option secondes implique d'exécuter les validateurs une deuxième fois. Certains d'entre eux, comme les validateurs asynchrones, peuvent être trop coûteux à réaliser.

Propager toutes les propriétés?

Il n'y a pas de solution générale pour propager toutes les propriétés. Différentes propriétés sont définies par diverses directives ou d'autres moyens, ayant ainsi un cycle de vie différent, ce qui signifie qu'elles nécessitent une manipulation particulière. La solution actuelle concerne la propagation des erreurs de validation et des validateurs. Il existe de nombreuses propriétés disponibles là-haut.

Notez que vous pouvez obtenir des changements de statut différents de l'instance FormControl en vous abonnant à FormControl.statusChanges() . De cette façon, vous pouvez savoir si le contrôle est VALID, INVALID, DISABLED ou PENDING (la validation asynchrone est toujours en cours d'exécution).

Comment fonctionne la validation sous le capot?

Sous le capot, les validateurs sont appliqués à l'aide de directives ( vérifier le code source ). Les directives ont providers: [REQUIRED_VALIDATOR] Ce qui signifie que leur propre injecteur hiérarchique est utilisé pour enregistrer cette instance de validateur. Ainsi, selon les attributs appliqués sur l'élément, les directives ajouteront des instances de validateur sur l'injecteur associé à l'élément cible.

Ensuite, ces validateurs sont récupérés par NgModel et FormControlDirective .

Les validateurs ainsi que les accesseurs de valeur sont récupérés comme:

  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)

et respectivement:

  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[])

Notez que @Self() est utilisé, donc propre injecteur (de l'élément auquel la directive est appliquée) est utilisé afin d'obtenir les dépendances.

NgModel et FormControlDirective ont une instance de FormControl qui met à jour la valeur et exécute les validateurs.

Par conséquent, le point principal avec lequel interagir est l'instance FormControl.

Tous les validateurs ou accesseurs de valeur sont également enregistrés dans l'injecteur de l'élément auquel ils sont appliqués. Cela signifie que le parent ne doit pas accéder à cet injecteur. Ce serait donc une mauvaise pratique d'accéder depuis le composant actuel à l'injecteur fourni par le <select>.

Exemple de code pour l'option 1 (facilement remplaçable par l'option 2)

L'exemple suivant a deux validateurs: un qui est requis et un autre qui est un modèle qui force l'option à correspondre à "l'option 3".

Le 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;
    }
  }
}

Utilisation

Définissez l'instance FormControl:

export class AppComponent implements OnInit {

  public control: FormControl;

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

Liez l'instance FormControl au composant:

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

Service de paramètres factices

/**
 * 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' },
    ];
  }
}
9
andreim