Was ist ngDefaultControl in Angular?

99

Nein, dies ist keine doppelte Frage. Sie sehen, es gibt eine Menge Fragen und Probleme in SO und Github, die vorschreiben, dass ich diese Direktive einem Tag hinzufüge, das eine Direktive hat [(ngModel)]und nicht in einem Formular enthalten ist. Wenn ich es nicht hinzufüge, erhalte ich eine Fehlermeldung:

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

Ok, der Fehler verschwindet, wenn ich dieses Attribut dort platziere. Aber warte! Niemand weiß was es tut! Und Angulars Arzt erwähnt es überhaupt nicht. Warum brauche ich einen Value Accessor, wenn ich weiß, dass ich ihn nicht brauche? Wie ist dieses Attribut mit Value Accessors verbunden? Was macht diese Richtlinie? Was ist ein Value Acessor und wie verwende ich ihn?

Und warum machen alle immer wieder Dinge, die sie nicht alle verstehen? Fügen Sie einfach diese Codezeile hinzu und es funktioniert, danke, dies ist nicht der Weg, um gute Programme zu schreiben.

Und dann. Ich habe gleich zwei große Anleitungen zu Formularen in Angular und einen Abschnitt über Folgendes gelesen ngModel:

Und weisst du was? Keine einzige Erwähnung von Value Accessors oder ngDefaultControl. Wo ist es?

Gherman
quelle

Antworten:

177

[ngDefaultControl]

Steuerungen von Drittanbietern erfordern eine ControlValueAccessorFunktion mit eckigen Formen. Viele von ihnen, wie Polymers <paper-input>, verhalten sich wie das <input>native Element und können daher das verwenden DefaultValueAccessor. Durch Hinzufügen eines ngDefaultControlAttributs können sie diese Anweisung verwenden.

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

oder

<paper-input ngDefaultControl formControlName="name">

Dies ist der Hauptgrund, warum dieser Attribut eingeführt wurde.

In Alpha-Versionen von angle2 wurde es ng-default-controlAttribut genannt .

So ngDefaultControlist einer der Selektoren für die DefaultValueAccessor- Direktive:

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

Was heißt das?

Dies bedeutet, dass wir dieses Attribut auf Elemente (wie Polymerkomponenten) anwenden können, die keinen eigenen Wertzugriff haben. Dieses Element übernimmt also das Verhalten von DefaultValueAccessorund wir können dieses Element mit eckigen Formen verwenden.

Andernfalls müssen Sie Ihre eigene Implementierung von bereitstellen ControlValueAccessor

ControlValueAccessor

Angular Docs Zustände

Ein ControlValueAccessor fungiert als Brücke zwischen der Angular Forms-API und einem nativen Element im DOM.

Schreiben wir die folgende Vorlage in einer einfachen Angular2-Anwendung:

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

Um zu verstehen, wie sich unser inputoben genanntes Verhalten verhält, müssen wir wissen, welche Anweisungen auf dieses Element angewendet werden. Hier gibt Winkel einen Hinweis mit dem Fehler:

Nicht behandelte Ablehnung von Versprechungen: Fehler beim Analysieren von Vorlagen: Kann nicht an 'ngModel' gebunden werden, da dies keine bekannte Eigenschaft von 'input' ist.

Okay, wir können SO öffnen und die Antwort erhalten: Importieren FormsModulein Ihre @NgModule:

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

Wir haben es importiert und alles funktioniert wie vorgesehen. Aber was ist unter der Haube los?

FormsModule exportiert für uns folgende Richtlinien:

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

Geben Sie hier die Bildbeschreibung ein

Nach einigen Untersuchungen können wir feststellen, dass drei Richtlinien auf unsere angewendet werden 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 {

NgControlStatusRichtlinie nur manipuliert Klassen mögen ng-valid, ng-touched, ng-dirtyund wir können es hier weglassen.


DefaultValueAccesstorstellt NG_VALUE_ACCESSORToken im Provider-Array bereit :

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 {

NgModelDie Direktive fügt ein Konstruktortoken ein NG_VALUE_ACCESSOR, das auf demselben Hostelement deklariert wurde.

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

In unserem Fall NgModelwird injizieren DefaultValueAccessor. Und jetzt ruft die NgModel-Direktive die gemeinsame setUpControlFunktion auf:

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

Und hier ist die Brücke in Aktion:

Geben Sie hier die Bildbeschreibung ein

NgModelrichtet die Steuerung (1) ein und ruft die dir.valueAccessor !.registerOnChangeMethode auf. ControlValueAccessorspeichert den Rückruf in der Eigenschaft onChange(2) und löst diesen Rückruf aus, wenn ein inputEreignis eintritt (3) . Und schließlich wird die updateControlFunktion innerhalb des Rückrufs aufgerufen (4).

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

Wobei Winkelaufrufe API bilden control.setValue.

Das ist eine kurze Version davon, wie es funktioniert.

Yurzui
quelle
Ich habe gerade @Input() ngModelund @Output() ngModelChangefür bidirektionale Bindung gemacht und ich dachte, es sollte genug eine Brücke sein. Das sieht so aus, als würde man dasselbe auf eine ganz andere Art und Weise tun. Vielleicht sollte ich mein Feld nicht benennen ngModel?
Gherman
2
Wenn Sie diese Komponente nicht mit eckigen Formen verwenden, können Sie einfach Ihre eigene Zwei-Wege-Bindung erstellen @Input() value; @Output() valueChange: EventEmitter<any> = new EventEmitter();und dann einfach[(value)]="someProp"
yurzui
1
Genau das habe ich getan. Aber ich nannte meinen "Wert" als ngModelund Angular fing an, einen Fehler auf mich zu werfen und mit ControlValueAccessor danach zu fragen.
Gherman
Hat jemand, der was äquivalent zu ngDefaultControl in vue und React ist? Ich meine, ich habe eine benutzerdefinierte Eingabekomponente in Angular mit dem Steuerwert-Accessor erstellt und sie als Webkomponente in Angular-Elemente eingeschlossen. Im selben Projekt musste ich ngDefaultControl verwenden, damit das mit eckigen Formen funktioniert. Aber was soll ich tun, damit sie in Vue and React funktionieren? Auch in nativem JS?
Kavinda Jayakody
Ich verwende ngDefaultControl für meine benutzerdefinierte Komponente, habe jedoch Probleme mit einem Problem. Wenn ich den Standardwert für formControl in meiner formBuilder-Ansicht (benutzerdefinierte Eingabekomponente) festlege, wird nur das Modell aktualisiert. Was mache ich falsch?
Igor Janković