O que é ngDefaultControl no Angular?

102

Não, esta não é uma pergunta duplicada. Veja, há uma tonelada de questões e problemas no SO e no Github que prescrevem que eu adicione essa diretiva a uma tag que tem [(ngModel)]diretiva e não está contida em um formulário. Se eu não adicionar, recebo um erro:

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

Ok, o erro desaparece se eu colocar esse atributo lá. Mas espere! Ninguém sabe o que isso faz! E o médico de Angular não menciona nada. Por que preciso de um acessador de valor quando sei que não preciso dele? Como esse atributo está conectado aos acessadores de valor? O que esta diretiva faz? O que é um acessor de valor e como o utilizo?

E por que todo mundo continua fazendo coisas que eles não entendem tudo? Basta adicionar esta linha de código e funciona, obrigado, esta não é a maneira de escrever bons programas.

E depois. Eu li não um, mas dois guias enormes sobre formulários no Angular e uma seção sobre ngModel:

E sabe de uma coisa? Nem uma única menção aos acessores de valor ou ngDefaultControl. Cadê?

Gherman
fonte

Respostas:

180

[ngDefaultControl]

Os controles de terceiros requerem um ControlValueAccessorpara funcionar com formas angulares. Muitos deles, como o do Polymer <paper-input>, se comportam como o <input>elemento nativo e, portanto, podem usar o DefaultValueAccessor. Adicionar um ngDefaultControlatributo permitirá que eles usem essa diretiva.

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

ou

<paper-input ngDefaultControl formControlName="name">

Portanto, esta é a principal razão pela qual este atributo foi introduzido.

Era chamado de ng-default-controlatributo nas versões alfa do angular2 .

Então, ngDefaultControlé um dos seletores para a diretiva 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 {

O que isso significa?

Isso significa que podemos aplicar este atributo a um elemento (como um componente de polímero) que não tem seu próprio acessador de valor. Portanto, este elemento assumirá o comportamento de DefaultValueAccessore podemos usar este elemento com formas angulares.

Caso contrário, você deve fornecer sua própria implementação de ControlValueAccessor

ControlValueAccessor

Angular docs states

Um ControlValueAccessor atua como uma ponte entre a API de formulários Angular e um elemento nativo no DOM.

Vamos escrever o seguinte modelo em um aplicativo angular2 simples:

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

Para entender como nosso inputcódigo acima se comportará, precisamos saber quais diretivas são aplicadas a este elemento. Aqui o angular dá alguma dica com o erro:

Rejeição de promessa não tratada: Erros de análise de modelo: Não é possível vincular a 'ngModel', pois não é uma propriedade conhecida de 'input'.

Ok, podemos abrir o SO e obter a resposta: importar FormsModulepara o seu @NgModule:

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

Nós importamos e tudo funciona como pretendido. Mas o que está acontecendo sob o capô?

O FormsModule exporta para nós as seguintes diretivas:

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

insira a descrição da imagem aqui

Após alguma investigação, podemos descobrir que três diretivas serão aplicadas ao nosso 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 {

NgControlStatusdirectiva aulas apenas manipula como ng-valid, ng-touched, ng-dirtye podemos omiti-lo aqui.


DefaultValueAccesstorfornece NG_VALUE_ACCESSORtoken na matriz de provedores:

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 {

NgModeldiretiva injeta um NG_VALUE_ACCESSORtoken de construtor que foi declarado no mesmo elemento de host.

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

No nosso caso NgModelvai injetar DefaultValueAccessor. E agora a diretiva NgModel chama a setUpControlfunção compartilhada :

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

E aqui está a ponte em ação:

insira a descrição da imagem aqui

NgModelconfigura o controle (1) e chama o dir.valueAccessor !.registerOnChangemétodo. ControlValueAccessorarmazena o retorno de chamada na propriedade onChange(2) e dispara esse retorno de chamada quando o inputevento acontece (3) . E, finalmente, a updateControlfunção é chamada dentro de callback (4)

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

onde angular chama o forms API control.setValue.

Essa é uma versão resumida de como funciona.

Yurzui
fonte
Acabei de fazer @Input() ngModele @Output() ngModelChangepara ligação bidirecional e achei que deveria ser uma ponte suficiente. Isso parece fazer a mesma coisa de uma maneira completamente diferente. Talvez eu não deva nomear meu campo ngModel?
Gherman
2
Se você não usar este componente com formas angulares, você pode simplesmente criar sua própria encadernação bidirecional como @Input() value; @Output() valueChange: EventEmitter<any> = new EventEmitter();e, em seguida, usar[(value)]="someProp"
yurzui
1
Isso é exatamente o que eu estava fazendo. Mas eu nomeei meu "valor" como ngModele o Angular começou a lançar um erro para mim e a perguntar com ControlValueAccessor.
Gherman
Alguém que é equivalente a ngDefaultControl in vue e React? Quer dizer, eu fiz um componente de entrada personalizado no angular usando o acessador de valor de controle e envolvi-o como um componente da web em elementos angulares. No mesmo projeto eu tive que usar o ngDefaultControl para fazer isso funcionar com formas angulares. Mas o que devo fazer para que funcionem no Vue and React? Também em JS nativo?
Kavinda Jayakody
Estou usando o ngDefaultControl em meu componente personalizado, mas tenho um problema. Quando eu defino o valor padrão para formControl dentro da minha visão formBuilder (componente de entrada personalizado) não é atualizado, apenas o modelo. O que estou fazendo de errado?
Igor Janković