Angular4 - nenhum acessador de valor para controle de formulário

146

Eu tenho um elemento personalizado:

<div formControlName="surveyType">
  <div *ngFor="let type of surveyTypes"
       (click)="onSelectType(type)"
       [class.selected]="type === selectedType">
    <md-icon>{{ type.icon }}</md-icon>
    <span>{{ type.description }}</span>
  </div>
</div>

Quando tento adicionar o formControlName, recebo uma mensagem de erro:

Erro de erro: nenhum acessador de valor para o controle de formulário com o nome: 'surveyType'

Eu tentei adicionar ngDefaultControlsem sucesso. Parece que é porque não há entrada / seleção ... e eu não sei o que fazer.

Gostaria de vincular meu clique a este formControl para que, quando alguém clicar em todo o cartão, forçar meu 'tipo' para o formControl. É possível?

jbtd
fonte
Não sei o que quero dizer: formControl vá para o controle de formulário em html, mas div não é um controle de formulário. Gostaria que você vinculasse o meu surveyType ao type.id do meu cartão div
jbtd
Eu sei que eu poderia usar o antigo caminho angular e ter meu selectedType vincular a ele, mas eu estava tentando usar e aprender a forma reativa do angular 4 e não sei como usar o formControl com esse tipo de caso.
Jbtd
Ok, talvez seja, mas esse caso não pode ser manipulado por uma forma reativa. Thx qualquer maneira :)
jbtd
Fiz uma resposta sobre como dividir formulários enormes em subcomponentes aqui stackoverflow.com/a/56375605/2398593, mas isso também se aplica muito bem com apenas um acessador de valor de controle personalizado. Também confira github.com/cloudnc/ngx-sub-form :)
maxime1992

Respostas:

250

Você pode usar formControlNameapenas as diretivas implementadas ControlValueAccessor.

Implementar a interface

Portanto, para fazer o que você deseja, é necessário criar um componente que implemente ControlValueAccessor, o que significa implementar as três funções a seguir :

  • writeValue (diz à Angular como gravar valor do modelo na visualização)
  • registerOnChange (registra uma função de manipulador chamada quando a exibição é alterada)
  • registerOnTouched (registra um manipulador a ser chamado quando o componente recebe um evento de toque, útil para saber se o componente foi focado).

Registrar um provedor

Então, você deve informar ao Angular que essa diretiva é uma ControlValueAccessor(a interface não será cortada, uma vez que é retirada do código quando o TypeScript é compilado no JavaScript). Você faz isso registrando um provedor .

O provedor deve fornecer NG_VALUE_ACCESSORe usar um valor existente . Você também precisará de um forwardRefaqui. Observe que NG_VALUE_ACCESSORdeve ser um provedor múltiplo .

Por exemplo, se sua diretiva personalizada se chama MyControlComponent, você deve adicionar algo ao longo das seguintes linhas dentro do objeto passado ao @Componentdecorador:

providers: [
  { 
    provide: NG_VALUE_ACCESSOR,
    multi: true,
    useExisting: forwardRef(() => MyControlComponent),
  }
]

Uso

Seu componente está pronto para ser usado. Com formulários controlados por modelo , a ngModelligação agora funcionará corretamente.

Com formulários reativos , agora você pode usar corretamente formControlNamee o controle de formulário se comportará conforme o esperado.

Recursos

Lazar Ljubenović
fonte
72

Eu acho que você deve usar formControlName="surveyType"em um inpute não em umdiv

Vega
fonte
Sim certeza, mas eu não sei como transformar minha div cartão em outra coisa que será um controle de formulário html
jbtd
5
O objetivo do CustomValueAccessor é adicionar o controle de formulário a QUALQUER COISA, até mesmo uma div
SoEzPz
4
@SoEzPz Esse é um padrão ruim. Você imita a funcionalidade de Entrada em um componente do invólucro, reimplementando os métodos HTML padrão você mesmo (basicamente reinventando a roda e tornando seu código detalhado). mas em 90% dos casos, você pode fazer tudo o que quiser usando <ng-content>em um componente envoltório e deixar o componente pai que define formControlssimplesmente colocar o <input> dentro do <invólucro>
Phil
3

O erro significa que o Angular não sabe o que fazer quando você coloca um formControla div. Para corrigir isso, você tem duas opções.

  1. Você coloca o formControlNameelemento em, que é suportado pelo Angular imediatamente. Essas são: input, textareae select.
  2. Você implementa a ControlValueAccessorinterface. Ao fazer isso, você está dizendo ao Angular "como acessar o valor do seu controle" (daí o nome). Ou em termos simples: o que fazer, quando você coloca um formControlNameelemento, que naturalmente não tem um valor associado a ele.

Agora, implementar a ControlValueAccessorinterface pode ser um pouco assustador no começo. Especialmente porque não há muita documentação boa por aí e você precisa adicionar muita clichê no seu código. Então, deixe-me tentar detalhar isso em algumas etapas simples de seguir.

Mova seu controle de formulário para seu próprio componente

Para implementar o ControlValueAccessor, você precisa criar um novo componente (ou diretiva). Mova o código relacionado ao seu controle de formulário para lá. Assim, também será facilmente reutilizável. Ter um controle já dentro de um componente pode ser o motivo, em primeiro lugar, porque você precisa implementar a ControlValueAccessorinterface, caso contrário, não poderá usar seu componente personalizado junto com os formulários Angular.

Adicione o boilerplate ao seu código

A implementação da ControlValueAccessorinterface é bastante detalhada, aqui está o boilerplate que vem com ela:

import {Component, OnInit, forwardRef} from '@angular/core';
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';


@Component({
  selector: 'app-custom-input',
  templateUrl: './custom-input.component.html',
  styleUrls: ['./custom-input.component.scss'],

  // a) copy paste this providers property (adjust the component name in the forward ref)
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomInputComponent),
      multi: true
    }
  ]
})
// b) Add "implements ControlValueAccessor"
export class CustomInputComponent implements ControlValueAccessor {

  // c) copy paste this code
  onChange: any = () => {}
  onTouch: any = () => {}
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }
  registerOnTouched(fn: any): void {
    this.onTouch = fn;
  }

  // d) copy paste this code
  writeValue(input: string) {
    // TODO
  }

Então, o que as partes individuais estão fazendo?

  • a) Permite ao Angular saber durante o tempo de execução que você implementou a ControlValueAccessorinterface
  • b) Garante que você está implementando a ControlValueAccessorinterface
  • c) Esta é provavelmente a parte mais confusa. Basicamente, o que você está fazendo é dar ao Angular os meios para substituir suas propriedades / métodos de classe onChangee onTouchcom sua própria implementação durante o tempo de execução, para que você possa chamar essas funções. Portanto, é importante entender este ponto: você não precisa implementar onChange e onTouch por conta própria (exceto a implementação vazia inicial). A única coisa que você faz com (c) é deixar o Angular anexar suas próprias funções à sua classe. Por quê? Portanto, você pode chamar os métodos onChangee onTouchfornecidos pela Angular no momento apropriado. Vamos ver como isso funciona abaixo.
  • d) Veremos também como o writeValuemétodo funciona na próxima seção, quando o implementarmos. Coloquei aqui, para que todas as propriedades necessárias ControlValueAccessorsejam implementadas e seu código ainda seja compilado.

Implementar writeValue

O que writeValuefaz é fazer algo dentro do seu componente personalizado, quando o controle do formulário é alterado do lado de fora . Por exemplo, se você tiver nomeado seu componente de controle de formulário personalizado app-custom-inpute o estiver usando no componente pai, desta forma:

<form [formGroup]="form">
  <app-custom-input formControlName="myFormControl"></app-custom-input>
</form>

então writeValueé acionado sempre que o componente pai, de alguma forma, altera o valor de myFormControl. Pode ser, por exemplo, durante a inicialização do formulário ( this.form = this.formBuilder.group({myFormControl: ""});) ou em uma redefinição do formulário this.form.reset();.

O que você normalmente deseja fazer se o valor do controle de formulário for alterado externamente é gravá-lo em uma variável local que represente o valor do controle de formulário. Por exemplo, se você CustomInputComponentgira em torno de um controle de formulário baseado em texto, ele pode se parecer com o seguinte:

writeValue(input: string) {
  this.input = input;
}

e no html de CustomInputComponent:

<input type="text"
       [ngModel]="input">

Você também pode gravá-lo diretamente no elemento de entrada, conforme descrito nos documentos angulares.

Agora você lidou com o que acontece dentro do seu componente quando algo muda fora. Agora vamos olhar para a outra direção. Como você informa o mundo exterior quando algo muda dentro do seu componente?

Chamando onChange

O próximo passo é informar o componente pai sobre alterações dentro do seu CustomInputComponent. É aqui que as funções onChangee onTouchde (c) de cima entram em cena. Ao chamar essas funções, você pode informar o exterior sobre alterações dentro do seu componente. Para propagar alterações do valor para o exterior, você precisa chamar onChange com o novo valor como argumento . Por exemplo, se o usuário digitar algo no inputcampo em seu componente personalizado, você chamará onChangecom o valor atualizado:

<input type="text"
       [ngModel]="input"
       (ngModelChange)="onChange($event)">

Se você verificar a implementação (c) de cima novamente, verá o que está acontecendo: Angular vincula sua própria implementação à onChangepropriedade da classe. Essa implementação espera um argumento, que é o valor de controle atualizado. O que você está fazendo agora é chamar esse método e avisar a Angular sobre a mudança. Agora, o Angular vai em frente e altera o valor do formulário do lado de fora. Esta é a parte chave disso tudo. Você disse ao Angular quando deveria atualizar o controle de formulário e com qual valor chamandoonChange . Você forneceu os meios para "acessar o valor do controle".

A propósito: O nome onChangeé escolhido por mim. Você pode escolher qualquer coisa aqui, por exemplo propagateChangeou similar. No entanto, o nome que você escolher será a mesma função que recebe um argumento, que é fornecido pelo Angular e que é vinculado à sua classe pelo registerOnChangemétodo durante o tempo de execução.

Chamando onTouch

Como os controles de formulário podem ser "tocados", você também deve fornecer ao Angular os meios para entender quando seu controle de formulário personalizado é tocado. Você pode fazer isso, adivinhou, chamando a onTouchfunção Portanto, para o nosso exemplo aqui, se você quiser se manter em conformidade com o modo como o Angular está fazendo isso nos controles prontos para uso, chame onTouchquando o campo de entrada estiver embaçado:

<input type="text"
       [(ngModel)]="input"
       (ngModelChange)="onChange($event)"
       (blur)="onTouch()">

Novamente, onTouché um nome escolhido por mim, mas sua função real é fornecida pelo Angular e leva zero argumentos. O que faz sentido, já que você está apenas informando a Angular, que o controle de formulário foi tocado.

Juntando tudo

Então, como é que isso acontece quando tudo se junta? Deve ficar assim:

// custom-input.component.ts
import {Component, OnInit, forwardRef} from '@angular/core';
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';


@Component({
  selector: 'app-custom-input',
  templateUrl: './custom-input.component.html',
  styleUrls: ['./custom-input.component.scss'],

  // Step 1: copy paste this providers property
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomInputComponent),
      multi: true
    }
  ]
})
// Step 2: Add "implements ControlValueAccessor"
export class CustomInputComponent implements ControlValueAccessor {

  // Step 3: Copy paste this stuff here
  onChange: any = () => {}
  onTouch: any = () => {}
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }
  registerOnTouched(fn: any): void {
    this.onTouch = fn;
  }

  // Step 4: Define what should happen in this component, if something changes outside
  input: string;
  writeValue(input: string) {
    this.input = input;
  }

  // Step 5: Handle what should happen on the outside, if something changes on the inside
  // in this simple case, we've handled all of that in the .html
  // a) we've bound to the local variable with ngModel
  // b) we emit to the ouside by calling onChange on ngModelChange

}
// custom-input.component.html
<input type="text"
       [(ngModel)]="input"
       (ngModelChange)="onChange($event)"
       (blur)="onTouch()">
// parent.component.html
<app-custom-input [formControl]="inputTwo"></app-custom-input>

// OR

<form [formGroup]="form" >
  <app-custom-input formControlName="myFormControl"></app-custom-input>
</form>

Mais exemplos

Formulários aninhados

Observe que os Controladores de Valor de Controle NÃO são a ferramenta certa para grupos de formulários aninhados. Para grupos de formulários aninhados, você pode simplesmente usar um @Input() subform. Os Controladores de Valor de Controle são feitos para quebrar controls, não groups! Veja este exemplo como usar uma entrada para um formulário aninhado: https://stackblitz.com/edit/angular-nested-forms-input-2

Fontes

bersling
fonte
-1

Para mim, foi devido ao atributo "múltiplo" no controle de entrada selecionado, pois o Angular possui diferentes ValueAccessor para esse tipo de controle.

const countryControl = new FormControl();

E dentro do modelo use assim

    <select multiple name="countries" [formControl]="countryControl">
      <option *ngFor="let country of countries" [ngValue]="country">
       {{ country.name }}
      </option>
    </select>

Mais detalhes ref Documentos oficiais

Sudhir Singh
fonte
O que foi devido ao "múltiplo"? Não vejo como seu código resolve nada, ou qual era o problema original. Seu código mostra o uso básico usual.
Lazar Ljubenović