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 comportant la directive [(ngModel)]
et ne figurant pas 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 je mets cet attribut là. Mais attendez! Personne ne sait ce que ça fait! Et le médecin d'Angular n'en parle pas du tout. Pourquoi ai-je besoin d'un accesseur de valeur quand 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 acesseur de valeur et comment l'utiliser?
Et pourquoi tout le monde continue-t-il à faire des choses qu’ils ne comprennent pas? Ajoutez simplement cette ligne de code et ça marche, merci, ce n’est pas la bonne façon d’écrire de bons programmes.
Puis. Je lis non pas un mais deux énormes guides sur les formes en Angular et une section sur ngModel
:
Et tu sais quoi? Pas une seule mention d'accesseurs de valeur ou de ngDefaultControl
. Où est-ce?
Les commandes tierces nécessitent un ControlValueAccessor
pour fonctionner avec les formes angular. Beaucoup d'entre eux, comme le <paper-input>
de Polymer, se comportent comme l'élément natif <input>
et peuvent donc utiliser le DefaultValueAccessor
. L'ajout d'un attribut 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 attrubute a été introduit.
Il s’appelait ng-default-control
attribut dans les versions alpha de angular2 .
Donc, ngDefaultControl
est 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 cela signifie?
Cela signifie que nous pouvons appliquer cet attribut à un élément (comme le composant polymer qui n'a pas son propre accesseur de valeur). Donc, cet élément prendra le comportement de DefaultValueAccessor
et nous pouvons utiliser cet élément avec les formes angular.
Sinon, vous devez fournir votre propre implémentation de ControlValueAccessor
Angular états de la documentation
Un ControlValueAccessor agit comme un pont entre l'API Angular forms et un élément natif du DOM.
Écrivons le modèle suivant dans une application angular2 simple:
<input type="text" [(ngModel)]="userName">
Pour comprendre le comportement de notre input
ci-dessus, nous devons savoir quelles directives sont appliquées à cet élément. Ici angular donne un indice avec l'erreur:
Rejet de la promesse non gérée: erreurs d'analyse du modèle: impossible de se lier à 'ngModel' car il ne s'agit pas d'une propriété connue de 'entrée'.
OK, nous pouvons ouvrir SO et obtenir la réponse suivante: importer FormsModule
sur 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 {}
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 {
La directive NgControlStatus
manipule simplement des classes telles que ng-valid
, ng-touched
, ng-dirty
et nous pouvons l'omettre ici.
DefaultValueAccesstor
fournit le jeton NG_VALUE_ACCESSOR
dans le tableau de 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 {
La directive NgModel
injecte dans le jeton du constructeur NG_VALUE_ACCESSOR
qui a été déclaré sur le même élément Host.
export NgModel extends NgControl implements OnChanges, OnDestroy {
constructor(...
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]) {
Dans notre cas, NgModel
injectera DefaultValueAccessor
. Et maintenant, la directive NgModel appelle shared setUpControl
function:
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:
NgModel
configure le contrôle (1) et appelle la méthode dir.valueAccessor !.registerOnChange
. ControlValueAccessor
enregistre le rappel dans la propriété onChange
(2) et déclenche ce rappel lorsque l'événement input
se produit (3) . Et enfin, la fonction updateControl
est appelée à l'intérieur du rappel (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 de formulaire control.setValue
.
C'est une version courte de la façon dont cela fonctionne.