Integração com Formulários e Validadores
Os Web Components do GovBR-DS encapsulam a estrutura e a aparência dos inputs em um Shadow DOM, o que altera levemente a forma como as bibliotecas de validação interagem com eles.
Esta página concentra o contrato geral de validação, a matriz de componentes e as receitas de integração com React, Angular e Vue. Os detalhes de cada campo ficam na seção Validação da página do componente correspondente.
Para facilitar a integração, nossa documentação sugere algumas "receitas" para cenários práticos nos principais frameworks suportados.
A diferença fundamental
Os controles emitem input e change no host e mantêm o valor em propriedades como value, checked e files. Os wrappers oficiais traduzem esse contrato para ControlValueAccessor, props React e v-model. Consulte Eventos nativos e eventos customizados para a semântica e a migração dos aliases da série 2.x.
| Família de controles | Propriedade lida no host | Evento de usuário | Binding do wrapper |
|---|---|---|---|
br-input textual e br-textarea | value | input | string |
datas simples (br-datetime-picker, br-datetime-input, br-date-picker e br-time-picker) | value | input | Date | null |
br-input[type="number"] e br-slider simples | value | input | número no Angular; valor tipado pelo componente nos demais wrappers |
br-select e br-radio-group | value | change | seleção |
br-checkbox e br-switch | checked | change | booleano |
br-upload | files | change | FileList | null |
br-tag[interaction-select] | selected | change | booleano ou valor selecionado, conforme o modo |
| slider e datas em intervalo | rangeValue | input | objeto de intervalo tipado pelo componente |
O binding bidirecional é apenas um adaptador dessa tabela: a aplicação escreve na propriedade, e uma interação do usuário emite o evento que atualiza o modelo. Escritas programáticas, restauração de estado e form.reset() não fingem uma interação e, portanto, não emitem input ou change. Não use dispatchEvent() para sincronizar estado; atualize o modelo da aplicação explicitamente.
Datas no estado e no envio
Não confunda o valor tipado do componente com o valor enviado pelo formulário. Em seleção simples, mantenha Date | null no estado JavaScript, no FormControl, no state React ou no ref Vue. O componente converte esse valor para texto apenas na fronteira de um formulário nativo:
const picker = document.querySelector('br-datetime-picker[name="agendamento"]');
picker.value = new Date(2026, 5, 15, 10, 30);
picker.addEventListener('input', (event) => {
const selectedDate = event.currentTarget.value; // Date | null
atualizarEstado(selectedDate);
});
const form = picker.closest('form');
const serializedDate = new FormData(form).get('agendamento'); // string | null
O modo date envia YYYY-MM-DD, time envia HH:mm e datetime envia data, hora e deslocamento local. Um intervalo de datas usa YYYY-MM-DD/YYYY-MM-DD. Para atributos HTML e outras fronteiras textuais, use serializedValue; ao recuperar uma data persistida como JSON, converta a string novamente com new Date(valor) e valide o resultado antes de escrever em value.
🔵 Integração com React Hook Form
Com os wrappers nativos exportados do pacote @govbr-ds/webcomponents-react, componentes de entrada como br-input repassam a referência (ref) do HTMLElement e os eventos canônicos onInput e onChange. Para ligar ao ecossistema do React Hook Form sem fricção, a estratégia ideal é o uso do Controller.
Exemplo de uso com `Controller`
import { Controller, useForm } from 'react-hook-form';
import { BrButton, BrInput, BrMessage } from '@govbr-ds/webcomponents-react';
const initialValues = { username: '' };
export default function MyForm() {
const { control, handleSubmit, reset } = useForm({
defaultValues: initialValues,
mode: 'onBlur',
});
const onSubmit = (data: typeof initialValues) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)} onReset={() => reset(initialValues)} noValidate>
<Controller
name="username"
control={control}
rules={{ required: 'O nome de usuário é obrigatório' }}
render={({ field: { onChange, onBlur, value }, fieldState: { error } }) => (
<>
<BrInput
name="username"
label="Nome de usuário"
value={value}
required
onInput={(event) => onChange(event.currentTarget.value)}
onBlur={onBlur}
state={error ? 'danger' : undefined}
aria-invalid={Boolean(error)}
/>
{error && <BrMessage state="danger" isFeedback message={error.message} />}
</>
)}
/>
<BrButton type="reset" emphasis="secondary">Limpar</BrButton>
<BrButton type="submit">Enviar</BrButton>
</form>
);
}
valueChanges equivalente, refs e eventos customizados
No React, alterações externas devem passar por state/props. Eventos DOM nativos usam event.currentTarget.value;
eventos CustomEvent usam event.detail:
import { useEffect, useRef, useState } from 'react';
import { BrSelect } from '@govbr-ds/webcomponents-react';
export function SelectField() {
const selectRef = useRef<HTMLBrSelectElement>(null);
const [value, setValue] = useState('');
const [validationMessage, setValidationMessage] = useState('');
useEffect(() => {
const select = selectRef.current;
if (!select) return;
const onValidation = (event: CustomEvent<{ message: string | null }>) =>
setValidationMessage(event.detail.message ?? '');
select.addEventListener('brSelectValidationChange', onValidation);
return () => select.removeEventListener('brSelectValidationChange', onValidation);
}, []);
return (
<BrSelect
ref={selectRef}
value={value}
options={[{ label: 'Brasília', value: 'df' }]}
onChange={(event) => setValue(event.currentTarget.value as string)}
/>
);
}
🔴 Integração com Angular Reactive Forms
No pacote @govbr-ds/webcomponents-angular, nós providenciamos os Value Accessors nativos. Isso garante que as diretivas formControlName e [formControl] funcionem sem esforço extra, de forma similar ao que você já faz em formulários nativos.
Aplicação com NgModule
Na entrada tradicional, importe WebcomponentsAngularModule de @govbr-ds/webcomponents-angular. O método
forRoot() registra os Custom Elements e os value accessors do Angular.
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { WebcomponentsAngularModule } from '@govbr-ds/webcomponents-angular';
@NgModule({
imports: [ReactiveFormsModule, WebcomponentsAngularModule.forRoot()],
})
export class AppModule {}
Aplicação Angular standalone
Em uma aplicação standalone, importe os proxies e os validadores pela entrada
@govbr-ds/webcomponents-angular/standalone:
import { Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import {
BrButton,
BrInput,
FormValidityValidator,
TextValueAccessor,
} from '@govbr-ds/webcomponents-angular/standalone';
@Component({
selector: 'app-user-form',
standalone: true,
imports: [ReactiveFormsModule, BrInput, BrButton, TextValueAccessor, FormValidityValidator],
template: `
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<br-input label="E-mail" formControlName="email" type="email" required></br-input>
<br-button type="submit">Salvar</br-button>
</form>
`,
})
export class UserFormComponent {
readonly userForm = new FormGroup({
email: new FormControl('', { nonNullable: true }),
});
onSubmit() {
if (this.userForm.valid) console.log(this.userForm.getRawValue());
}
}
Não misture WebcomponentsAngularModule com os proxies standalone no mesmo componente.
Em standalone, importar o componente visual não importa automaticamente a diretiva que implementa ControlValueAccessor. Inclua os accessors usados pelo template:
| Campo | Accessor standalone |
|---|---|
| texto e textarea | TextValueAccessor |
datas simples (Date | null) | TextValueAccessor |
br-input[type="number"] e slider simples | NumericValueAccessor |
| select e radio group | SelectValueAccessor |
| radio isolado | RadioValueAccessor |
| checkbox e switch | BooleanValueAccessor |
| upload | FileValueAccessor |
| tag selecionável | SelectableTagValueAccessor |
| slider e datas em intervalo | RangeValueAccessor |
Sem o accessor correspondente, Angular emite NG01203. A entrada com NgModule já declara todos eles.
valueChanges, eventos customizados e zonas
O value accessor não chama onChange quando a alteração vem do código. Para observar alterações aceitas pelo
formulário use valueChanges; para observar a interação nativa use input/change no host. Callbacks fora da zona
devem voltar para o Angular com NgZone.run():
import { ChangeDetectorRef, ElementRef, NgZone, ViewChild, inject } from '@angular/core';
// No componente que possui o formulário:
@ViewChild('select', { read: ElementRef })
private readonly select!: ElementRef<HTMLBrSelectElement>;
private readonly zone = inject(NgZone);
private readonly cdr = inject(ChangeDetectorRef);
private validationMessage = '';
this.userForm.controls.email.valueChanges.subscribe((value) => {
console.log('valor aceito pelo formulário:', value);
});
this.select.nativeElement.addEventListener('brSelectValidationChange', (event: CustomEvent<{ message: string | null }>) => {
this.zone.run(() => {
this.validationMessage = event.detail.message ?? '';
this.cdr.markForCheck();
});
});
No template, associe a referência ao host: <br-select #select ...></br-select>. Em aplicações sem zone, atualize o estado explicitamente e chame o
ChangeDetectorRef após o evento.
🟢 Integração com Vue (v-model)
No ecossistema Vue (via pacote @govbr-ds/webcomponents-vue), nossos componentes são exportados para entender a diretiva v-model.
Exemplo usando Composition API
<script setup lang="ts">
import { BrButton, BrInput, BrMessage } from '@govbr-ds/webcomponents-vue';
import { ref } from 'vue';
const userEmail = ref('');
const errorMsg = ref('');
const resetData = () => {
userEmail.value = '';
errorMsg.value = '';
};
const submitData = () => {
if (!userEmail.value.includes('@')) {
errorMsg.value = 'E-mail inválido';
} else {
errorMsg.value = '';
console.log('Enviado:', userEmail.value);
}
};
</script>
<template>
<form @submit.prevent="submitData" @reset="resetData">
<BrInput
name="email"
label="Seu Email"
v-model="userEmail"
type="email"
required
:state="errorMsg ? 'danger' : undefined"
:aria-invalid="Boolean(errorMsg)"
/>
<BrMessage v-if="errorMsg" state="danger" is-feedback :message="errorMsg" />
<BrButton type="reset" emphasis="secondary">Limpar</BrButton>
<BrButton type="submit">Processar</BrButton>
</form>
</template>
Sincronização externa e eventos customizados
No Vue, v-model é a fonte reativa do valor. O wrapper padrão usa v-model também quando a propriedade real é checked, files ou selected; o mapeamento é feito pelos componentModels do pacote. Use BrSliderRange e BrDatetimePickerRange com v-model:range-value somente para intervalos. Eventos DOM devem ser tratados com @input/@change; eventos
customizados usam $event.detail:
<script setup lang="ts">
import { BrMessage, BrSelect, BrSelectOption } from '@govbr-ds/webcomponents-vue';
import { ref } from 'vue';
const city = ref('');
const validationMessage = ref('');
function onValidation(event: CustomEvent<{ message: string | null }>) {
validationMessage.value = event.detail.message ?? '';
}
</script>
<template>
<BrSelect
label="Cidade"
v-model="city"
@brSelectValidationChange="onValidation"
>
<BrSelectOption value="df" label="Brasília" />
</BrSelect>
<BrMessage v-if="validationMessage" state="danger" is-feedback :message="validationMessage" />
</template>
Contrato nativo de validação
Os controles associados a formulário participam de FormData, respeitam required, limites, padrões e comprimentos. Os métodos dos Web Components são assíncronos porque atravessam a API pública do Stencil; os métodos de HTMLFormElement continuam síncronos:
type FormValidationState = ValidityState & {
valid: boolean;
validationMessage: string;
willValidate: boolean;
};
const field = document.querySelector('br-input');
await field.setCustomValidity(cpfValido(field.value) ? '' : 'Informe um CPF válido.');
const state = await field.getValidationState();
if (!state.valid) await field.reportValidity();
checkValidity() apenas consulta e pode emitir invalid quando inválido. reportValidity() consulta e apresenta a mensagem. setCustomValidity('mensagem') torna o controle inválido; passe '' para limpar. O evento invalid é cancelável e não borbulha, como no HTML nativo.
Constraints por componente
| Componente | Constraints | Estado consultável |
|---|---|---|
| br-input | type, required, pattern, min, max, step, minlength, maxlength | value |
| br-textarea | required, readonly, minlength, maxlength | value |
| br-select | required, min-selections, max-selections | value |
| br-radio-group | required | value |
| br-checkbox, br-radio, br-switch | required | checked, value |
| br-slider | min, max, step; intervalo ordenado | value, rangeValue |
| br-datetime-picker | required, min, max; intervalo completo e ordenado | value, rangeValue |
| br-upload | required, min-files, max-files, accept, max-file-size | files |
br-tag com interaction-select | required, min-selections, max-selections | selected, value |
Constraints HTML ficam no componente. CPF, confirmação de senha, regras de negócio e dependências entre campos ficam na aplicação e devem ser sincronizadas com setCustomValidity.
Todos os campos de formulário possuem o slot feedback para a mensagem visual. Quando o slot não é preenchido e existe uma mensagem customizada, o componente renderiza um br-message padrão. A referência acessível usa aria-describedby e aria-errormessage; consulte a seção Validação de cada componente para os detalhes do seu valor e das suas constraints.
Validadores síncronos e assíncronos
br-input, br-textarea, br-select e br-radio-group também aceitam a propriedade validator para regras de domínio que não cabem nas constraints HTML. O contrato é executado no change ou quando validate() é chamado, e retorna null em caso de sucesso ou uma mensagem de erro:
const validator = async (value: string) => {
const disponivel = await verificarDisponibilidade(value);
return disponivel ? null : 'Esse valor já está em uso.';
};
const field = document.querySelector('br-input');
field.validator = validator;
if (!(await field.validate())) return;
No modo múltiplo, br-select recebe string[] em vez de string. Em React, Angular e Vue, passe a função como propriedade com validator={validator}, [validator]="validator" ou :validator="validator"; atributos HTML não transportam funções.
Durante a execução assíncrona, esses campos expõem aria-busy="true", o slot validation-loading e um evento específico de mudança de validação. Consulte a seção Validação do componente para detalhes, estados de concorrência, mensagens e exemplos.
Mensagem, acessibilidade e eventos
Mostre o erro em texto e associe-o ao campo com aria-describedby ou aria-errormessage. Ao apresentar o erro, mantenha aria-invalid="true"; não dependa somente de state="danger", cor ou ícone. O label visível deve fazer parte do nome acessível.
<br-input id="cpf" label="CPF" required aria-errormessage="cpf-erro"></br-input>
<br-message id="cpf-erro" state="danger" role="alert">
Informe um CPF válido.
</br-message>
input acompanha edição e change representa seleção ou commit. Leia o estado em event.target.value, checked, files, selected ou rangeValue; não espere payload em detail. Alterações externas, form.reset() e restauração de estado não emitem eventos de usuário.
form.addEventListener('submit', async (event) => {
event.preventDefault();
const cpf = form.elements.cpf;
await cpf.setCustomValidity(isCpfValid(cpf.value) ? '' : 'Informe um CPF válido.');
if (!form.reportValidity()) return;
salvar(new FormData(form));
});
No Angular, FormValidityValidator é declarado pelo módulo e também exportado pela entrada standalone; enquanto a Promise de validação está em andamento o controle fica PENDING. React usa eventos nativos e refs tipadas. Vue usa v-model padrão para valores simples e os wrappers de intervalo quando aplicável.
Envio e reset sem estado divergente
Todo controle que deve ser enviado precisa de name. new FormData(form) inclui os Web Components associados ao formulário; checkbox, radio e switch só contribuem quando marcados. O navegador não permite fabricar ou reatribuir livremente um FileList, então uploads devem ser lidos em files e limpos pelo reset nativo.
Em um formulário sem estado duplicado, form.reset() basta. Em React controlado, Angular Forms, Vue reativo ou bibliotecas externas, limpe as duas fontes na mesma ação:
- deixe o botão
type="reset"acionar o reset nativo; - trate o evento
resetdo formulário e restaure o modelo da biblioteca; - limpe erros, estado de
touchede mensagens globais; - não dispare eventos artificiais para simular a ação do usuário.
Contrato canônico dos quickstarts
Os quickstarts apresentam as abordagens na mesma ordem conceitual: estado explícito, integração nativa/manual e biblioteca do framework. Assim é possível comparar o mesmo fluxo sem confundir diferenças de API com diferenças de regra de negócio.
| Quickstart | Estado explícito | Nativo ou manual | Biblioteca/framework |
|---|---|---|---|
| JavaScript | formulario.html | formdata.html | APIs nativas do navegador |
| Angular | /formularios/completo (Reactive Forms) | /formularios/template-driven | /formularios/signals (Signals + Forms) |
| React | /formularios/completo | /formularios/nao-controlado | /formularios/react-hook-form |
| Vue | /formularios/completo | /formularios/manual | /formularios/veevalidate |
A tabela abaixo serve como referência para as regras dos campos. Os exemplos usam validadores nativos ou da biblioteca escolhida, e as mensagens podem variar. Quando uma regra de domínio também precisar bloquear o envio nativo, sincronize-a com setCustomValidity.
| Campo | Regra | Mensagem |
|---|---|---|
| nome | obrigatório, 5–100 caracteres | Nome deve ter no mínimo 5 caracteres. |
obrigatório, type=email, até 120 caracteres | Informe um e-mail válido. | |
| idade | obrigatório, inteiro entre 18 e 120 | A idade mínima é 18 anos. |
| CPF | obrigatório e dígitos verificadores válidos | Informe um CPF válido. |
| celular | obrigatório, 10 ou 11 dígitos | Informe um celular válido. |
| CEP | obrigatório, 8 dígitos | Informe um CEP válido. |
| cidade | seleção obrigatória | Selecione sua cidade. |
| descrição | obrigatória, 10–200 caracteres | O resumo deve ter entre 10 e 200 caracteres. |
| contato | uma opção obrigatória no grupo | Selecione como conheceu o projeto. |
| upload | ao menos um arquivo | Envie um documento probatório. |
| senha | obrigatória, 8–128 caracteres | A senha deve ter no mínimo 8 caracteres. |
| confirmação | igual à senha | As senhas devem ser iguais. |
| termos | checkbox obrigatório | Você deve aceitar os termos. |
| switch, slider, datetime e tags | obrigatoriedade conforme o exemplo; slider entre 0 e 10 | mensagem definida pelo exemplo |
Criação, edição, reset, listagem e persistência devem preservar esse contrato. Os testes Playwright de cada quickstart exercitam as rotas das abordagens, validação, datas tipadas, upload e reset em desenvolvimento. A publicação em base path exige validação separada dos artefatos publicados.