DEV Community

Cover image for Entendendo o ControlValueAccessor
Bruno Donatelli
Bruno Donatelli

Posted on

Entendendo o ControlValueAccessor

Introdução ao ControlValueAccessor

código do projeto
Formulários dentro do Angular podem ser tratados utilizando duas abordagens: Reactive forms ou Template Driven forms. Essas abordagens nos permitem capturar entrada de dados, validar campos que possam estar inválidos, e são baseados em um objeto que será enviado ao servidor.

Pensando, tanto em usar reactive forms ou template driven: quando componentizamos muitos elementos, podemos acabar sentindo uma dificuldade em: "como podemos criar um componente apartado de um formulário, e continuar tendo a referência do dado digitado pelo usuário, via bind, em nosso [(ngModel)] ou no formControlName"?

aka: como crio um control personalizado para o meu formulário

É aí que entra o PODEROSO ControlValueAccessor!

O que é o ControlValueAccessor

MAS O QUE É ESSE ControlValueAccessor???
Personagem link de the legend of zelda, caindo na água sofrendo dano, fazendo alusão a um sentimento de "talvez isso seja complexo"

O ControlValueAccessor é uma interface que atua como uma ponte entre elementos do DOM e a API de formulário do Angular. Implementando essa interface em um componente, podemos manipular valores através das propriedades dispostas pelo próprio FormsModule: ngModel; Ou no caso de um formulário reativo, o formControlName.

Gato fazendo um joia, imagem faz alusão a um sentimento de "entendi"

Os métodos obrigatórios a serem implementados dentro da classe são os seguintes:

interface ControlValueAccessor {
writeValue(obj: any): void
registerOnChange(fn: any): void
registerOnTouched(fn: any): void
setDisabledState(isDisabled: boolean)?: void
}
Enter fullscreen mode Exit fullscreen mode
  • O writeValue escreve um novo valor ao elemento
  • O registerOnChange registra uma função callback que é chamada quando o valor do nosso controle muda na tela.
  • O registerOnTouched serve para atualizar o estado do formulário para touched (ou blurred). Então, assim que o usuário interage com nosso elemento no controle personalizado, podemos chamar a função salva no callback, para informar ao Angular que o controle recebeu uma alteração.
  • O setDisabledState vai ser chamado para avisar a API de formulários, que o controle sofreu alteração no estado de desabilitado (true para false, ou false para true).

Uma pessoa surpresa

Agora que entendemos como funciona a interface ControlValueAccessor, e quais métodos ela implementa, vamos ao nosso código.

Formulário de "Contact us"

Vamos criar um formulário básico, sobre "Contact us". É um formulário simples, onde teremos dois inputs e um textarea:

  • Digite seu nome;
  • Digite seu e-mail;
  • Digite uma mensagem;

O formulário será feito com template-driven e com Reactive forms.

Template Driven

// template-driven-form HTML

<form (ngSubmit)="submit()">
  <fieldset class="">
    <legend>Fale conosco</legend>
    <div class="data">
      <label for="yourName">Digite seu nome</label>
      <input
        type="text"
        id="yourName"
        name="yourName"
        [(ngModel)]="contactUs.name"
        placeholder="DevTo da Silva"
      />
    </div>
    <div class="data">
      <label for="yourEmail">Digite seu email</label>
      <input
        type="email"
        id="yourEmail"
        name="yourEmail"
        [(ngModel)]="contactUs.email"
        placeholder="example@mail.com"
      />
    </div>

    <div class="data">
      <label for="message">Deixe sua mensagem</label>
      <textarea
        name="message"
        id="message"
        cols="30"
        rows="10"
        [(ngModel)]="contactUs.message"
      ></textarea>
    </div>
  </fieldset>

  <button
    type="submit"
    aria-label="Enviar o motivo do contato"
    aria-describedby="sendContactUs"
  >
    Enviar
  </button>
  <span [hidden]="true" id="sendContactUs"
    >Ao clicar no botão, você envia os dados que foram preenchidos. Em breve retornaremos o contato.</span>
</form>

<pre>{{contactUs | json}}</pre>
Enter fullscreen mode Exit fullscreen mode

e o nosso component .ts

// template-driven-form HTML

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { JsonPipe } from '@angular/common';

type ContactUsTemplateDriven = {
  name: string;
  email: string;
  message: string;
};

@Component({
  selector: 'app-template-driven-form-contactus',
  standalone: true,
  imports: [FormsModule, JsonPipe],
  templateUrl: './template-driven-form-contactus.component.html',
  styleUrl: './template-driven-form-contactus.component.scss',
})
export class TemplateDrivenFormContactusComponent {
  protected contactUs: ContactUsTemplateDriven = {
    name: '',
    email: '',
    message: '',
  };

  protected submit(): void {
    console.log('enviou', this.contactUs);
  }
}

Enter fullscreen mode Exit fullscreen mode

Formulário Reativo

// reactive-forms-contactus.component

<form (ngSubmit)="submit()" [formGroup]="contactUsForm">
  <fieldset class="">
    <legend>Fale conosco</legend>
    <div class="data">
      <label for="name">Digite seu nome</label>
      <input
        type="text"
        id="name"
        name="name"
        formControlName="name"
        placeholder="DevTo da Silva"
      />
    </div>
    <div class="data">
      <label for="email">Digite seu email</label>
      <input
        type="email"
        id="email"
        name="email"
        formControlName="email"
        placeholder="example@mail.com"
      />
    </div>

    <div class="data">
      <label for="message">Deixe sua mensagem</label>
      <textarea name="message" id="message" cols="30" rows="10" formControlName="message"></textarea>
    </div>
  </fieldset>

  <button
    type="submit"
    aria-label="Enviar o motivo do contato"
    aria-describedby="sendContactUs"
  >
    Enviar
  </button>
  <span [hidden]="true" id="sendContactUs">
    Ao clicar no botão, você envia os dados que foram preenchidos. E assim que
    pudermos, retornaremos o contato
  </span>
</form>

<pre>{{contactUsForm.getRawValue() | json}}</pre>
Enter fullscreen mode Exit fullscreen mode

// reactive-forms-contactus.component

type ContactUsReactiveForm = {
  name: FormControl<string>;
  email: FormControl<string>;
  message: FormControl<string>;
};

@Component({
  selector: 'app-reactive-form-contactus',
  standalone: true,
  imports: [FormsModule, ReactiveFormsModule, JsonPipe],
  templateUrl: './reactive-form-contactus.component.html',
  styleUrl: './reactive-form-contactus.component.scss',
})
export class ReactiveFormContactusComponent {
  private readonly formBuilder = inject(NonNullableFormBuilder);

  protected contactUsForm!: FormGroup<ContactUsReactiveForm>;

  constructor() {
    this.contactUsForm = this.formBuilder.group<ContactUsReactiveForm>({
      name: this.formBuilder.control({ value: '', disabled: false }),
      email: this.formBuilder.control(
        { value: '', disabled: false },
        { validators: [Validators.email] }
      ),
      message: this.formBuilder.control({ value: '', disabled: false }),
    });
  }

  protected submit() {
    console.log('enviou', this.contactUsForm.getRawValue());
  }
}
Enter fullscreen mode Exit fullscreen mode

Agora que finalizamos nossos formulários, vamos criar nossos controles personalizados.

Iremos criar três componentes, que serão os dois inputs e o textarea.

Criando controles personalizados com ControlValueAccessor

Vamos criar um novo componente, utilizando o comando do angular-cli:

$ ng g c components/contact-us-name-input
Enter fullscreen mode Exit fullscreen mode

contact-us-name-input, o componente que criamos mostrando três arquivos: .ts, .html e .scss

Dentro do nosso componente, iremos implementar a interface ControlValueAccessor e seus respectivos métodos. Porém, só implementar a interface ControlValueAccessor não é o suficiente.
Precisamos também, registrar dentro dos providers do nosso component, o token NG_VALUE_ACCESSOR. Esse token é responsável pela integração do component, com a API de formulários do Angular.

Junto do NG_VALUE_ACCESSOR, também colocaremos o forwardRef. O uso do forwardRef se faz necessário porque estamos fazendo referência ao componente que estamos criando (nesse caso, para o "ContactUsNameInputComponent". Porém, faremos para todos os nossos controles personalizados.)

@Component({
  selector: 'app-contact-us-name-input',
  standalone: true,
  imports: [],
  templateUrl: './contact-us-name-input.component.html',
  styleUrl: './contact-us-name-input.component.scss',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => ContactUsNameInputComponent),
      multi: true,
    },
  ],
})
export class ContactUsNameInputComponent implements ControlValueAccessor {
  @Input() public contactName = '';

  protected value = '';
  protected disabled = false;

  protected onChanged!: (value: string) => void;
  protected onTouched!: () => void;

  writeValue(value: string): void {
    this.value = value;
  }
  registerOnChange(fn: (value: string) => void): void {
    this.onChanged = fn;
  }
  registerOnTouched(fn: () => void): void {
    this.onTouched = fn;
  }

  setDisabledState?(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }
}

Enter fullscreen mode Exit fullscreen mode
<label for="name">Digite seu nome</label>
<input
  type="text"
  id="name"
  placeholder="DevTo da Silva"
  [value]="value"
  (input)="onChanged($any($event.target)?.value)"
/>
Enter fullscreen mode Exit fullscreen mode

Vamos analisar o nosso código:

  • Registramos dentro dos providers do nosso component, o NG_VALUE_ACCESSOR e fizemos referência ao nosso componente.

Nota: Sempre precisamos fazer referência, no forwardRef, à classe que estamos trabalhando.

  • Implementamos os métodos obrigatórios da interface ControlValueAccessor, tipamos os parâmetros dos métodos da interface, e atribuímos os valores aos atributos do componente.

  • No nosso template html, colocamos um evento onInput, chamado o callback onChanged para escutar as mudanças que ocorrem no nosso input, e informar ao nosso formulário que as mudanças ocorreram

Tendo criado nosso primeiro componente, vamos implementá-lo em nossos componentes de formulário da seguinte forma:

Substituiremos a parte do label e input de nome, pelo nosso componente:

<form (ngSubmit)="submit()">
  <fieldset class="">
    <legend>Fale conosco</legend>
    <div class="data">
// essa parte aqui
// pelo nosso novo componente
     <app-contact-us-name-input
        name="yourName"
        [contactName]="contactUs.name"
        [(ngModel)]="contactUs.name"
        [ngModelOptions]="{ standalone: true }"
      />
    </div>
... rest of html
Enter fullscreen mode Exit fullscreen mode

controle componentizado

Como estamos trabalhando com standalone components, precisamos informar à instância do NgModel que o nosso control também é standalone.

Faremos o mesmo trabalho dentro do input de email, e o textarea, registrando o NG_VALUE_ACCESSOR, e fazendo uso do forwardRef para referência dos nossos componentes. (para não ficar muito extenso e repetitivo, o código está no meu github, e você poder ver tudo que foi feito nesse link ao lado: ContactUs - Github.

Nosso formulário, agora com nossos controles personalizados integrados, ficará dessa forma:

<form (ngSubmit)="submit()">
  <fieldset class="">
    <legend>Fale conosco</legend>
    <div class="data">
      <app-contact-us-name-input
        name="yourName"
        [contactName]="contactUs.name"
        [(ngModel)]="contactUs.name"
        [ngModelOptions]="{ standalone: true }"
      />
    </div>
    <div class="data">
      <app-contact-us-email-input
        name="email"
        [contactEmail]="contactUs.email"
        [(ngModel)]="contactUs.email"
        [ngModelOptions]="{ standalone: true }"
      />
    </div>

    <div class="data">
      <app-contact-us-message-text
        [(ngModel)]="contactUs.message"
        [ngModelOptions]="{ standalone: true }"
        [contactText]="contactUs.message"
      />
    </div>
  </fieldset>

  <button
    type="submit"
    aria-label="Enviar o motivo do contato"
    aria-describedby="sendContactUs"
  >
    Enviar
  </button>
  <span [hidden]="true" id="sendContactUs"
    >Ao clicar no botão, você envia os dados que foram preenchidos. E assim que
    pudermos, retornaremos o contato</span
  >
</form>

<pre>{{ contactUs | json }}</pre>
Enter fullscreen mode Exit fullscreen mode

e nosso formulário reativo:

<form (ngSubmit)="submit()" [formGroup]="contactUsForm">
  <fieldset class="">
    <legend>Fale conosco</legend>
    <div class="data">
      <app-contact-us-name-input formControlName="name" />
    </div>
    <div class="data">
      <app-contact-us-email-input formControlName="email" />
    </div>
    <div class="data">
      <app-contact-us-message-text formControlName="message" />
    </div>
  </fieldset>

  <button
    type="submit"
    aria-label="Enviar o motivo do contato"
    aria-describedby="sendContactUs"
  >
    Enviar
  </button>
  <span [hidden]="true" id="sendContactUs">
    Ao clicar no botão, você envia os dados que foram preenchidos. E assim que
    pudermos, retornaremos o contato
  </span>
</form>

<pre>{{ contactUsForm.getRawValue() | json }}</pre>
Enter fullscreen mode Exit fullscreen mode

Conclusão

Conseguimos entender como criar controles customizados, para serem reutilizados dentro de nossos formulários, independente da abordagem que foi utilizada. Também foi mostrado como usamos a interface ControlValueAccessor, entendemos também o uso do token NG_VALUE_ACCESSOR, e como fazer a referência do nosso componente enquanto ele não está definido, utilizando forwardRef.

Top comments (3)

Collapse
 
strfelix profile image
Lucas Felix

gato jóia 🐱👍

Collapse
 
ortegavan profile image
Vanessa Ortega

Excelente texto, excelente didática, excelentes memes rs. o/

Collapse
 
samukacnp profile image
samuka

Você é foda! 👊🏽👏👏