197 votes

Qu'est-ce que ngDefaultControl en Angular?

Non, ce n'est pas une question en double. En fait, il y a plein de questions et de problèmes sur SO et Github qui prescrivent d'ajouter cette directive à une balise qui contient la directive [(ngModel)] et qui n'est pas contenue dans un formulaire. Si je ne l'ajoute pas, j'obtiens une erreur :

ERROR Error: No value accessor for form control with unspecified name attribute

D'accord, l'erreur disparaît si j'ajoute cet attribut. MAIS, attends ! Personne ne sait ce qu'il fait ! Et la documentation d'Angular n'en parle pas du tout. Pourquoi ai-je besoin d'un value accessor alors que je sais que je n'en ai pas besoin ? Comment cet attribut est-il lié aux value accessors ? Que fait cette directive ? Qu'est-ce qu'un value accessor et comment l'utiliser ?

Et pourquoi tout le monde continue de faire des choses qu'ils ne comprennent pas du tout ? Il suffit d'ajouter cette ligne de code et ça fonctionne, merci, ce n'est pas la bonne manière d'écrire des programmes de qualité.

Et ensuite. J'ai lu non pas un mais deux guides énormes sur les formulaires en Angular et une section sur ngModel:

Et vous savez quoi ? Pas un seul mot sur les value accessors ou ngDefaultControl. Où est-ce que c'est ?

5 votes

> "Et pourquoi tout le monde continue-t-il à faire des choses qu'ils ne comprennent pas du tout?" - Oui! exactement! cela pourrait utiliser quelques points d'exclamation de plus, cependant ;-)

316voto

yurzui Points 85802

[ngDefaultControl]

Les contrôles effectués par des tiers nécessitent un ControlValueAccessor pour fonctionner avec des formes angulaires. Beaucoup d'entre elles, comme celle de Polymer <paper-input> se comportent comme les <input> et peut donc utiliser l'élément DefaultValueAccessor . Ajout d'un ngDefaultControl leur permettra d'utiliser cette directive.

<paper-input ngDefaultControl [(ngModel)]="value>

ou

<paper-input ngDefaultControl formControlName="name">

C'est donc la raison principale pour laquelle cet attribut a été introduit.

Il s'appelait ng-default-control attribut dans les versions alpha d'angular2 .

Alors ngDefaultControl est l'un des sélecteurs pour DefaultValueAccessor directive :

@Directive({
  selector:
      'input:not([type=checkbox])[formControlName],
       textarea[formControlName],
       input:not([type=checkbox])[formControl],
       textarea[formControl],
       input:not([type=checkbox])[ngModel],
       textarea[ngModel],
       [ngDefaultControl]', <------------------------------- this selector
  ...
})
export class DefaultValueAccessor implements ControlValueAccessor {

Qu'est-ce que cela signifie ?

Cela signifie que nous pouvons appliquer cet attribut à un élément (comme un composant polymère) qui ne possède pas son propre accesseur de valeur. Ainsi, cet élément prendra le comportement de DefaultValueAccessor et nous pouvons utiliser cet élément avec les formulaires angulaires.

Sinon, vous devez fournir votre propre implémentation de ControlValueAccessor

ControlValueAccessor

Angulaire états docs

Un ControlValueAccessor agit comme un pont entre l'API des formulaires Angular et un élément natif dans le DOM.

Écrivons le modèle suivant dans une simple application angular2 :

<input type="text" [(ngModel)]="userName">

Pour comprendre comment notre input ci-dessus se comportera, nous devons savoir quelles directives sont appliquées à cet élément. Ici, angular donne un indice avec l'erreur :

Rejet d'une promesse non gérée : Erreurs d'analyse du modèle : Impossible de se lier à ngModel' car ce n'est pas une propriété connue de 'input'.

Ok, nous pouvons ouvrir SO et obtenir la réponse : import FormsModule à votre @NgModule :

@NgModule({
  imports: [
    ...,
    FormsModule
  ]
})
export AppModule {}

Nous l'avons importé et tout fonctionne comme prévu. Mais que se passe-t-il sous le capot ?

FormsModule exporte pour nous les directives suivantes :

@NgModule({
 ...
  exports: [InternalFormsSharedModule, TEMPLATE_DRIVEN_DIRECTIVES]
})
export class FormsModule {}

enter image description here

Après quelques investigations, nous pouvons découvrir que trois directives seront appliquées à notre input

  1. NgControlStatus

    @Directive({ selector : '[formControlName],[ngModel],[formControl]', ... }) export class NgControlStatus extends AbstractControlStatus { ... }

  2. NgModel

    @Directive({ selector : '[ngModel]:not([formControlName]):not([formControl])', providers : [formControlBinding], exportAs : 'ngModel'. }) export class NgModel extends NgControl implements OnChanges,

  3. ACCESSEUR DE VALEUR PAR DÉFAUT

    @Directive({ selector : `input:not([type=checkbox]) [formControlName], textarea[formControlName], input:not([type=checkbox])formControl], textarea[formControl], input:not([type=checkbox]) [ngModel], textarea[ngModel], [ngDefaultControl]', ,,, }) export class DefaultValueAccessor implements ControlValueAccessor {

NgControlStatus ne fait que manipuler des classes comme ng-valid , ng-touched , ng-dirty et nous pouvons l'omettre ici.


DefaultValueAccesstor fournit NG_VALUE_ACCESSOR dans le tableau des fournisseurs :

export const DEFAULT_VALUE_ACCESSOR: any = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => DefaultValueAccessor),
  multi: true
};
...
@Directive({
  ...
  providers: [DEFAULT_VALUE_ACCESSOR]
})
export class DefaultValueAccessor implements ControlValueAccessor {

NgModel injecte dans le constructeur NG_VALUE_ACCESSOR qui a été déclaré sur le même élément hôte.

export NgModel extends NgControl implements OnChanges, OnDestroy {
 constructor(...
  @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]) {

Dans notre cas NgModel injectera DefaultValueAccessor . Et maintenant les appels de la directive NgModel partagée setUpControl fonction :

export function setUpControl(control: FormControl, dir: NgControl): void {
  if (!control) _throwError(dir, 'Cannot find control with');
  if (!dir.valueAccessor) _throwError(dir, 'No value accessor for form control with');

  control.validator = Validators.compose([control.validator !, dir.validator]);
  control.asyncValidator = Validators.composeAsync([control.asyncValidator !, dir.asyncValidator]);
  dir.valueAccessor !.writeValue(control.value);

  setUpViewChangePipeline(control, dir);
  setUpModelChangePipeline(control, dir);

  ...
}

function setUpViewChangePipeline(control: FormControl, dir: NgControl): void 
{
  dir.valueAccessor !.registerOnChange((newValue: any) => {
    control._pendingValue = newValue;
    control._pendingDirty = true;

    if (control.updateOn === 'change') updateControl(control, dir);
  });
}

function setUpModelChangePipeline(control: FormControl, dir: NgControl): void {
  control.registerOnChange((newValue: any, emitModelEvent: boolean) => {
    // control -> view
    dir.valueAccessor !.writeValue(newValue);

    // control -> ngModel
    if (emitModelEvent) dir.viewToModelUpdate(newValue);
  });
}

Et voici le pont en action :

enter image description here

NgModel met en place un contrôle (1) et appelle dir.valueAccessor !.registerOnChange méthode. ControlValueAccessor enregistre le rappel dans onChange (2) et déclenche ce rappel lorsque input l'événement se produit (3) . Et enfin updateControl est appelée à l'intérieur du callback (4)

function updateControl(control: FormControl, dir: NgControl): void {
  dir.viewToModelUpdate(control._pendingValue);
  if (control._pendingDirty) control.markAsDirty();
  control.setValue(control._pendingValue, {emitModelToViewChange: false});
}

où angular appelle l'API des formulaires control.setValue .

C'est une version courte de la façon dont cela fonctionne.

1 votes

Je viens de créer @Input() ngModel et @Output() ngModelChange pour une liaison bidirectionnelle et je pensais que cela devrait être suffisant comme pont. Cela semble faire la même chose d'une manière complètement différente. Peut-être que je ne devrais pas nommer mon champ ngModel?

6 votes

Si vous n'utilisez pas ce composant avec les formulaires Angular, vous pouvez simplement créer votre propre liaison bidirectionnelle comme @Input() value; @Output() valueChange: EventEmitter = new EventEmitter(); et ensuite il suffit d'utiliser [(value)]="someProp"

2 votes

C'est exactement ce que je faisais. Mais j'ai nommé ma "valeur" comme ngModel et Angular a commencé à me renvoyer une erreur et à demander avec ControlValueAccessor.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X