Qu'est-ce que ngDefaultControl dans Angular?

102

Non, ce n'est pas une question en double. Vous voyez, il y a une tonne de questions et de problèmes dans SO et Github qui prescrivent que j'ajoute cette directive à une balise qui a une [(ngModel)]directive 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

Ok, l'erreur disparaît si j'y mets cet attribut. Mais attendez! Personne ne sait ce que ça fait! Et le doc d'Angular ne le mentionne pas du tout. Pourquoi ai-je besoin d'un accesseur de valeur alors que je sais que je n'en ai pas besoin? Comment cet attribut est-il connecté aux accesseurs de valeur? Que fait cette directive? Qu'est-ce qu'un accessoire de valeur et comment l'utiliser?

Et pourquoi tout le monde continue-t-il à faire des choses qu'il ne comprend pas tout? Ajoutez simplement cette ligne de code et cela fonctionne, merci, ce n'est pas la façon d'écrire de bons programmes.

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

Et tu sais quoi? Pas une seule mention des accesseurs de valeur ou ngDefaultControl. Où est-ce?

Gherman
la source

Réponses:

180

[ngDefaultControl]

Les contrôles tiers nécessitent un ControlValueAccessorpour fonctionner avec des formes angulaires. Beaucoup d'entre eux, comme Polymer <paper-input>, se comportent comme l' <input>élément natif et peuvent donc utiliser le DefaultValueAccessor. L'ajout d'un ngDefaultControlattribut leur permettra d'utiliser cette directive.

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

ou

<paper-input ngDefaultControl formControlName="name">

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

Il a été appelé ng-default-controlattribut dans les versions alpha d'angular2 .

Il en ngDefaultControlva de même pour l'un des sélecteurs de la directive DefaultValueAccessor :

@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 ça veut dire?

Cela signifie que nous pouvons appliquer cet attribut à un élément (comme un composant polymère) qui n'a pas son propre accesseur de valeur. Donc, cet élément prendra le comportement deDefaultValueAccessor et nous pouvons utiliser cet élément avec des formes angulaires.

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

ControlValueAccessor

États de la documentation angulaire

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

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

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

Pour comprendre comment inputse comportera ce qui précède, nous devons savoir quelles directives sont appliquées à cet élément. Ici, angular donne un indice avec l'erreur:

Rejet de promesses non gérées: erreurs d'analyse de modèle: impossible de se lier à «ngModel» car ce n'est pas une propriété connue de «input».

D'accord, nous pouvons ouvrir SO et obtenir la réponse: importez FormsModuledans 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 {}

entrez la description de l'image ici

Après quelques recherches, 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) DEFAULT_VALUE_ACCESSOR

@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 {

NgControlStatusdirective seulement manipule des classes comme ng-valid, ng-touched, ng-dirtyet nous pouvons l' omettre ici.


DefaultValueAccesstorfournit un NG_VALUE_ACCESSORjeton 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 {

NgModeldirective injecte dans le NG_VALUE_ACCESSORjeton de constructeur 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 NgModelva injecter DefaultValueAccessor. Et maintenant, la directive NgModel appelle la setUpControlfonction partagée :

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:

entrez la description de l'image ici

NgModelconfigure le contrôle (1) et appelle la dir.valueAccessor !.registerOnChangeméthode. ControlValueAccessorstocke le rappel dans la propriété onChange(2) et déclenche ce rappel lorsque l' inputévénement se produit (3) . Et enfin la updateControlfonction 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ù les appels angulaires forment l'API control.setValue.

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

yurzui
la source
Je viens de faire @Input() ngModelet @Output() ngModelChangepour la liaison bidirectionnelle et j'ai pensé que cela devrait suffire à un pont. Cela ressemble à faire la même chose d'une manière complètement différente. Peut-être que je ne devrais pas nommer mon champ ngModel?
Gherman
2
Si vous n'utilisez pas ce composant avec des formes angulaires, vous pouvez simplement créer votre propre liaison bidirectionnelle @Input() value; @Output() valueChange: EventEmitter<any> = new EventEmitter();, puis utiliser simplement[(value)]="someProp"
yurzui
1
C'est exactement ce que je faisais. Mais j'ai nommé ma "valeur" comme ngModelet Angular a commencé à me lancer une erreur et à demander avec ControlValueAccessor.
Gherman
Quelqu'un qui est l'équivalent de ngDefaultControl en vue et React? Je veux dire, j'ai créé un composant d'entrée personnalisé en angulaire à l'aide de l'accesseur de valeur de contrôle et l'ai enveloppé en tant que composant Web dans des éléments angulaires. Dans le même projet, j'ai dû utiliser ngDefaultControl pour que cela fonctionne avec des formes angulaires. Mais que dois-je faire pour les faire fonctionner dans Vue et React? Aussi en JS natif?
Kavinda Jayakody le
J'utilise ngDefaultControl sur mon composant personnalisé mais je lutte avec un problème. Lorsque je définis la valeur par défaut pour formControl dans ma vue formBuilder (composant d'entrée personnalisé) n'est pas mise à jour, seul le modèle. Qu'est-ce que je fais mal?
Igor Janković