Signal Forms gerencia o estado de formulários usando signals do Angular para fornecer sincronização automática entre seu modelo de dados e a UI.
Este guia apresenta os conceitos principais para criar formulários com Signal Forms. Veja como funciona:
Criando seu primeiro formulário
1. Crie um modelo de formulário
Todo formulário começa criando um signal que armazena o modelo de dados do seu formulário:
interface LoginData { email: string; password: string;}const loginModel = signal<LoginData>({ email: '', password: '',});
2. Passe o modelo de formulário para form()
Em seguida, você passa seu modelo de formulário para a função form() para criar uma field tree - uma estrutura de objeto que espelha a forma do seu modelo, permitindo que você acesse campos com notação de ponto:
form(loginModel);// Access fields directly by property nameloginForm.emailloginForm.password
3. Vincule inputs com a directive [field]
Em seguida, você vincula seus inputs HTML ao formulário usando a directive [field], que cria um binding bidirecional entre eles:
<input type="email" [field]="loginForm.email" /><input type="password" [field]="loginForm.password" />
Como resultado, mudanças do usuário (como digitar no campo) atualizam automaticamente o formulário, e quaisquer mudanças programáticas também atualizam o valor exibido:
// Update the value programmaticallyloginForm.email().value.set('alice@wonderland.com');// The model signal is also updatedconsole.log(loginModel().email); // 'alice@wonderland.com'
NOTA: A directive [field] também sincroniza o estado do campo para atributos como required, disabled e readonly quando apropriado.
4. Leia valores de campos do formulário com value()
Você pode acessar o estado do campo chamando o campo como uma função. Isso retorna um objeto FieldState contendo signals reativos para o valor do campo, status de validação e estado de interação:
loginForm.email() // Returns FieldState with value(), valid(), touched(), etc.
Para ler o valor atual do campo, acesse o signal value():
<!-- Render form value that updates automatically as user types --><p>Email: {{ loginForm.email().value() }}</p>
// Get the current valueconst currentEmail = loginForm.email().value();
Aqui está um exemplo completo:
app.ts
import {Component, signal} from '@angular/core';import {form, Field} from '@angular/forms/signals';interface LoginData { email: string; password: string;}@Component({ selector: 'app-root', templateUrl: 'app.html', styleUrl: 'app.css', imports: [Field],})export class App { loginModel = signal<LoginData>({ email: '', password: '', }); loginForm = form(this.loginModel);}
app.html
<form> <label> Email: <input type="email" [field]="loginForm.email" /> </label> <label> Password: <input type="password" [field]="loginForm.password" /> </label> <p>Hello {{ loginForm.email().value() }}!</p> <p>Password length: {{ loginForm.password().value().length }}</p></form>
app.css
form { display: flex; flex-direction: column; gap: 1rem; max-width: 400px; padding: 1rem; font-family: Inter, system-ui, -apple-system, sans-serif;}label { display: flex; flex-direction: column; gap: 0.25rem;}input { padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; font-size: 1rem; font-family: inherit;}p { margin: 0.5rem 0; color: #666;}
Uso básico
A directive [field] funciona com todos os tipos de input HTML padrão. Aqui estão os padrões mais comuns:
Inputs de texto
Inputs de texto funcionam com vários atributos type e textareas:
<!-- Text and email --><input type="text" [field]="form.name" /><input type="email" [field]="form.email" />
Números
Inputs de número convertem automaticamente entre strings e números:
Data e hora
Inputs de data armazenam valores como strings YYYY-MM-DD, e inputs de hora usam o formato HH:mm:
<!-- Date and time - stores as ISO format strings --><input type="date" [field]="form.eventDate" /><input type="time" [field]="form.eventTime" />
Se você precisar converter strings de data para objetos Date, você pode fazer isso passando o valor do campo para Date():
const dateObject = new Date(form.eventDate().value());
Texto multilinha
Textareas funcionam da mesma forma que inputs de texto:
<!-- Textarea --><textarea [field]="form.message" rows="4"></textarea>
Checkboxes
Checkboxes vinculam a valores booleanos:
<!-- Single checkbox --><label> <input type="checkbox" [field]="form.agreeToTerms" /> I agree to the terms</label>
Múltiplas checkboxes
Para múltiplas opções, crie um field booleano separado para cada:
<label> <input type="checkbox" [field]="form.emailNotifications" /> Email notifications</label><label> <input type="checkbox" [field]="form.smsNotifications" /> SMS notifications</label>
Radio buttons
Radio buttons funcionam de forma similar a checkboxes. Desde que os radio buttons usem o mesmo valor [field], Signal Forms automaticamente vinculará o mesmo atributo name a todos eles:
<label> <input type="radio" value="free" [field]="form.plan" /> Free</label><label> <input type="radio" value="premium" [field]="form.plan" /> Premium</label>
Quando um usuário seleciona um radio button, o field do formulário armazena o valor do atributo value daquele radio button. Por exemplo, selecionar "Premium" define form.plan().value() como "premium".
Select dropdowns
Elementos select funcionam com opções estáticas e dinâmicas:
<!-- Static options --><select [field]="form.country"> <option value="">Select a country</option> <option value="us">United States</option> <option value="ca">Canada</option></select><!-- Dynamic options with @for --><select [field]="form.productId"> <option value="">Select a product</option> @for (product of products; track product.id) { <option [value]="product.id">{{ product.name }}</option> }</select>
NOTA: Select múltiplo (<select multiple>) não é suportado pela directive [field] neste momento.
Validação e estado
Signal Forms fornece validators integrados que você pode aplicar aos campos do seu formulário. Para adicionar validação, passe uma função schema como segundo argumento para form(). Esta função recebe um parâmetro FieldPath que permite referenciar os campos no modelo do seu formulário:
const loginForm = form(loginModel, (fieldPath) => { required(fieldPath.email); email(fieldPath.email);});
NOTA: FieldPath apenas espelha a forma dos seus dados e não permite acessar value ou qualquer outro estado.
Validators comuns incluem:
required()- Garante que o campo tenha um valoremail()- Valida o formato de emailmin()/max()- Valida intervalos de númerosminLength()/maxLength()- Valida o comprimento de string ou coleçãopattern()- Valida contra um padrão regex
Você também pode personalizar mensagens de erro passando um objeto de opções como segundo argumento para o validator:
required(p.email, { message: 'Email is required' });email(p.email, { message: 'Please enter a valid email address' });
Cada campo do formulário expõe seu estado de validação através de signals. Por exemplo, você pode verificar field().valid() para ver se a validação passa, field().touched() para ver se o usuário interagiu com ele, e field().errors() para obter a lista de erros de validação.
Aqui está um exemplo completo:
app.ts
import {Component, signal} from '@angular/core';import {form, Field, required, email, submit} from '@angular/forms/signals';interface LoginData { email: string; password: string;}@Component({ selector: 'app-root', templateUrl: 'app.html', styleUrl: 'app.css', imports: [Field],})export class App { loginModel = signal<LoginData>({ email: '', password: '', }); loginForm = form(this.loginModel, (p) => { required(p.email, {message: 'Email is required'}); email(p.email, {message: 'Enter a valid email address'}); required(p.password, {message: 'Password is required'}); }); onSubmit(event: Event) { event.preventDefault(); submit(this.loginForm, async () => { // Perform login logic here const credentials = this.loginModel(); console.log('Logging in with:', credentials); // e.g., await this.authService.login(credentials); }); }}
app.html
<form (submit)="onSubmit($event)"> <div> <label> Email: <input type="email" [field]="loginForm.email" /> </label> @if (loginForm.email().touched() && loginForm.email().invalid()) { <ul class="error-list"> @for (error of loginForm.email().errors(); track $index) { <li>{{ error.message }}</li> } </ul> } </div> <div> <label> Password: <input type="password" [field]="loginForm.password" /> </label> @if (loginForm.password().touched() && loginForm.password().invalid()) { <div class="error"> @for (error of loginForm.password().errors(); track $index) { <p>{{ error.message }}</p> } </div> } </div> <button type="submit">Log In</button></form>
app.css
form { display: flex; flex-direction: column; gap: 1rem; max-width: 400px; padding: 1rem; font-family: Inter, system-ui, -apple-system, sans-serif;}div { display: flex; flex-direction: column; gap: 0.25rem;}label { display: flex; flex-direction: column; gap: 0.25rem; font-weight: 500;}input { padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; font-size: 1rem; font-family: inherit;}input:focus { outline: none; border-color: #4285f4;}button { padding: 0.75rem 1.5rem; background-color: #4285f4; color: white; border: none; border-radius: 4px; font-size: 1rem; font-family: inherit; cursor: pointer; transition: background-color 0.2s;}button:hover { background-color: #357ae8;}button:active { background-color: #2a65c8;}.error-list { color: red; font-size: 0.875rem; margin: 0.25rem 0 0 0; padding-left: 0; list-style-position: inside;}.error-list p { margin: 0;}
Field State Signals
Cada field() fornece estes signals de estado:
| Estado | Descrição |
|---|---|
valid() |
Retorna true se o campo passa em todas as regras de validação |
touched() |
Retorna true se o usuário focou e desfocou o campo |
dirty() |
Retorna true se o usuário mudou o valor |
disabled() |
Retorna true se o campo está desabilitado |
pending() |
Retorna true se a validação assíncrona está em progresso |
errors() |
Retorna um array de erros de validação com propriedades kind e message |
DICA: Mostre erros apenas após field().touched() ser verdadeiro para evitar exibir mensagens de validação antes que o usuário tenha interagido com um campo.