Czym jest ngDefaultControl w Angular?

110

Nie, to nie jest powtórzone pytanie. Widzisz, jest mnóstwo pytań i problemów w SO i Githubie, które nakazują dodanie tej dyrektywy do tagu, który ma [(ngModel)]dyrektywę i nie jest zawarty w formularzu. Jeśli tego nie dodam, pojawia się błąd:

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

Ok, błąd znika, jeśli wstawię tam ten atrybut. Ale poczekaj! Nikt nie wie, co to robi! A doktor Angulara w ogóle o tym nie wspomina. Po co mi akcesor wartości, skoro wiem, że go nie potrzebuję? W jaki sposób ten atrybut jest powiązany z akcesoriami wartości? Co robi ta dyrektywa? Co to jest akcesor wartości i jak go używać?

I dlaczego wszyscy robią rzeczy, których w ogóle nie rozumieją? Po prostu dodaj tę linię kodu i działa, dziękuję, to nie jest sposób na pisanie dobrych programów.

I wtedy. Przeczytałem nie jeden, ale dwa obszerne przewodniki po formularzach w Angular oraz sekcję o ngModel:

I wiesz co? Ani jednej wzmianki o akcesorach wartości lub ngDefaultControl. Gdzie to jest?

Gherman
źródło
1
> „A dlaczego wszyscy robią rzeczy, których w ogóle nie rozumieją?” - tak! dokładnie! przydałoby się jednak więcej wykrzykników ;-)
Guss

Odpowiedzi:

191

[ngDefaultControl]

Kontrolki innych firm wymagają, ControlValueAccessoraby działały z formami kątowymi. Wiele z nich, podobnie jak Polymer <paper-input>, zachowuje się jak <input>element natywny i dlatego może używać rozszerzenia DefaultValueAccessor. Dodanie ngDefaultControlatrybutu umożliwi im korzystanie z tej dyrektywy.

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

lub

<paper-input ngDefaultControl formControlName="name">

To jest główny powód, dla którego wprowadzono ten atrubut.

Nazywało się to ng-default-controlatrybutem w wersjach alfa angular2 .

Tak ngDefaultControljest jednym z selektorów dyrektywy 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 {

Co to znaczy?

Oznacza to, że możemy zastosować ten atrybut do elementu (takiego jak komponent polimerowy), który nie ma własnego akcesorium wartości. Więc ten element będzie się zachowywał DefaultValueAccessori możemy go używać z formami kątowymi.

W przeciwnym razie musisz zapewnić własną implementację ControlValueAccessor

ControlValueAccessor

Angular Docs stwierdza

ControlValueAccessor działa jako pomost między interfejsem API Angular formularzy a natywnym elementem w DOM.

Napiszmy następujący szablon w prostej aplikacji angular2:

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

Aby zrozumieć, jak inputzachowa się powyższe, musimy wiedzieć, które dyrektywy są zastosowane do tego elementu. Tutaj angular daje wskazówkę z błędem:

Nieobsłużone odrzucenie obietnicy: Błędy analizy szablonu: nie można powiązać z „ngModel”, ponieważ nie jest to znana właściwość „input”.

Okay, możemy otworzyć SO i uzyskać odpowiedź: zaimportuj FormsModuledo @NgModule:

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

Sprowadziliśmy to i wszystko działa zgodnie z przeznaczeniem. Ale co się dzieje pod maską?

FormsModule eksportuje dla nas następujące dyrektywy:

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

wprowadź opis obrazu tutaj

Po pewnym zbadaniu możemy odkryć, że trzy dyrektywy zostaną zastosowane do naszego 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 {

NgControlStatusDyrektywa tylko manipuluje klas podoba ng-valid, ng-touched, ng-dirtya możemy go pominąć tutaj.


DefaultValueAccesstorudostępnia NG_VALUE_ACCESSORtoken w tablicy dostawców:

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 {

NgModeldyrektywa wprowadza do NG_VALUE_ACCESSORtokenu konstruktora, który został zadeklarowany w tym samym elemencie hosta.

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

W naszym przypadku NgModelwstrzyknie DefaultValueAccessor. A teraz dyrektywa NgModel wywołuje współdzieloną setUpControlfunkcję:

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

A oto most w akcji:

wprowadź opis obrazu tutaj

NgModelustawia control (1) i wywołuje dir.valueAccessor !.registerOnChangemetodę. ControlValueAccessorprzechowuje wywołanie zwrotne we właściwości onChange(2) i uruchamia to wywołanie zwrotne, gdy inputnastąpi zdarzenie (3) . I wreszcie updateControlfunkcja jest wywoływana wewnątrz callback (4)

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

gdzie wywołania kątowe tworzą API control.setValue.

To krótka wersja tego, jak to działa.

yurzui
źródło
Właśnie zrobiłem @Input() ngModeli @Output() ngModelChangedo wiązania dwukierunkowego i pomyślałem, że powinien wystarczyć mostek. To wygląda na zrobienie tego samego w zupełnie inny sposób. Może nie powinienem nazywać mojego pola ngModel?
Gherman
3
Jeśli nie używasz tego komponentu z formami kątowymi, możesz po prostu stworzyć własne dwukierunkowe wiązanie, @Input() value; @Output() valueChange: EventEmitter<any> = new EventEmitter();a następnie po prostu użyć[(value)]="someProp"
yurzui
2
Właśnie to robiłem. Ale nazwałem moją „wartość” jako ngModeli Angular zaczął rzucać we mnie błąd i prosić o to z ControlValueAccessor.
Gherman
Czy ktoś, kto jest odpowiednikiem ngDefaultControl w Vue i React? Chodzi mi o to, że utworzyłem niestandardowy komponent wejściowy w kątowym, używając akcesorium wartości kontrolnej i zawijam go jako komponent sieciowy w elementy Angular. W tym samym projekcie musiałem użyć ngDefaultControl, aby to działało z formami kątowymi. Ale co mam zrobić, aby działały w Vue i React? Również w natywnym JS?
Kavinda Jayakody
Używam ngDefaultControl na moim niestandardowym komponencie, ale mam jeden problem. Kiedy ustawiam wartość domyślną dla formControl w moim widoku formBuilder (niestandardowy składnik wejściowy) nie jest aktualizowany, tylko model. Co ja robię źle?
Igor Janković