O que é um formulário no Ionic (e por que ele é diferente no mobile)
No Ionic, formulários são construídos com componentes visuais (como ion-input e ion-select) integrados ao sistema de formulários do Angular. A diferença no mobile é que o teclado, o tipo de input, o autocomplete e o feedback visual impactam diretamente a taxa de preenchimento e a experiência do usuário. Por isso, além de capturar dados, você precisa orientar o dispositivo a mostrar o teclado correto, reduzir digitação e validar com mensagens claras.
Componentes de formulário mais usados
ion-input
Campo de texto com suporte a tipos, teclado adequado e integração com validação. Use type, inputmode e autocomplete para melhorar UX.
- type: define comportamento e validação nativa (ex.:
email,password,tel,number). - inputmode: sugere teclado (ex.:
numeric,decimal,email), mesmo quandotypenão resolve. - autocomplete: permite preenchimento automático (ex.:
email,name,tel,new-password).
ion-select
Seleção de opções. Ideal para listas curtas e médias. No mobile, evita digitação e reduz erros.
ion-textarea
Texto longo (observações, descrição). Combine com autoGrow para crescer conforme o usuário digita.
ion-toggle
Alternância booleana (aceitar termos, receber notificações). Use rótulos claros e evite ambiguidade.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
ion-datetime
Seleção de data/hora com UI nativa do Ionic. Evita erros de formato e melhora a velocidade de preenchimento.
Passo a passo: criando um formulário completo com Reactive Forms
O exemplo abaixo cria um formulário de cadastro com validação síncrona, mensagens de erro, submit desabilitado e boas práticas de UX mobile.
1) Estrutura do formulário (TypeScript)
No arquivo da página (ex.: cadastro.page.ts), crie um FormGroup com validadores.
import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-cadastro',
templateUrl: './cadastro.page.html'
})
export class CadastroPage {
isSubmitting = false;
form = this.fb.group({
nome: ['', [Validators.required, Validators.minLength(2)]],
email: ['', [Validators.required, Validators.email]],
telefone: ['', [Validators.required, Validators.pattern(/^\+?\d{10,15}$/)]],
perfil: ['', [Validators.required]],
bio: ['', [Validators.maxLength(160)]],
aceitaTermos: [false, [Validators.requiredTrue]],
nascimento: ['', [Validators.required]]
});
constructor(private fb: FormBuilder) {}
get f() {
return this.form.controls;
}
markAllTouched() {
this.form.markAllAsTouched();
}
async onSubmit() {
this.markAllTouched();
if (this.form.invalid || this.isSubmitting) return;
this.isSubmitting = true;
try {
const payload = this.form.getRawValue();
// TODO: enviar para API
console.log('Enviando', payload);
} finally {
this.isSubmitting = false;
}
}
}2) Template com componentes do Ionic (HTML)
Use ion-list e ion-item para organizar campos. Para mensagens, ion-note funciona bem como texto auxiliar/erro. Repare no uso de inputmode, autocomplete e enterkeyhint.
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<ion-list lines="full">
<ion-item>
<ion-label position="stacked">Nome</ion-label>
<ion-input
formControlName="nome"
type="text"
inputmode="text"
autocomplete="name"
enterkeyhint="next"
placeholder="Seu nome completo">
</ion-input>
</ion-item>
<ion-note color="danger" *ngIf="f.nome.touched && f.nome.errors?.required">
Informe seu nome.
</ion-note>
<ion-note color="danger" *ngIf="f.nome.touched && f.nome.errors?.minlength">
Nome muito curto.
</ion-note>
<ion-item>
<ion-label position="stacked">E-mail</ion-label>
<ion-input
formControlName="email"
type="email"
inputmode="email"
autocomplete="email"
enterkeyhint="next"
placeholder="voce@exemplo.com">
</ion-input>
</ion-item>
<ion-note color="danger" *ngIf="f.email.touched && f.email.errors?.required">
Informe seu e-mail.
</ion-note>
<ion-note color="danger" *ngIf="f.email.touched && f.email.errors?.email">
E-mail inválido.
</ion-note>
<ion-item>
<ion-label position="stacked">Telefone</ion-label>
<ion-input
formControlName="telefone"
type="tel"
inputmode="tel"
autocomplete="tel"
enterkeyhint="next"
placeholder="Ex.: +5511999999999">
</ion-input>
</ion-item>
<ion-note color="danger" *ngIf="f.telefone.touched && f.telefone.errors?.required">
Informe seu telefone.
</ion-note>
<ion-note color="danger" *ngIf="f.telefone.touched && f.telefone.errors?.pattern">
Use apenas números (com DDI opcional). Ex.: +5511999999999
</ion-note>
<ion-item>
<ion-label position="stacked">Perfil</ion-label>
<ion-select formControlName="perfil" interface="popover" placeholder="Selecione">
<ion-select-option value="dev">Desenvolvedor(a)</ion-select-option>
<ion-select-option value="design">Designer</ion-select-option>
<ion-select-option value="pm">Produto</ion-select-option>
</ion-select>
</ion-item>
<ion-note color="danger" *ngIf="f.perfil.touched && f.perfil.errors?.required">
Selecione um perfil.
</ion-note>
<ion-item>
<ion-label position="stacked">Nascimento</ion-label>
<ion-datetime
formControlName="nascimento"
presentation="date"
preferWheel="false">
</ion-datetime>
</ion-item>
<ion-note color="danger" *ngIf="f.nascimento.touched && f.nascimento.errors?.required">
Informe sua data de nascimento.
</ion-note>
<ion-item>
<ion-label position="stacked">Bio (opcional)</ion-label>
<ion-textarea
formControlName="bio"
autoGrow="true"
inputmode="text"
autocomplete="off"
placeholder="Conte um pouco sobre você (até 160 caracteres)">
</ion-textarea>
</ion-item>
<ion-note color="danger" *ngIf="f.bio.touched && f.bio.errors?.maxlength">
Máximo de 160 caracteres.
</ion-note>
<ion-item>
<ion-label>Aceito os termos</ion-label>
<ion-toggle formControlName="aceitaTermos"></ion-toggle>
</ion-item>
<ion-note color="danger" *ngIf="f.aceitaTermos.touched && f.aceitaTermos.errors?.required">
Você precisa aceitar os termos.
</ion-note>
</ion-list>
<ion-button
type="submit"
expand="block"
[disabled]="form.invalid || isSubmitting">
Criar conta
</ion-button>
</form>Configuração de teclado, tipos de input e autocomplete (guia prático)
Em mobile, pequenos ajustes economizam tempo e evitam erros. A tabela abaixo resume escolhas comuns.
| Campo | type | inputmode | autocomplete | Dica |
|---|---|---|---|---|
| Nome | text | text | name | Use enterkeyhint="next" para avançar. |
| Teclado com “@” e “.” facilita. | ||||
| Telefone | tel | tel | tel | Evite máscara complexa; valide padrão e normalize no submit. |
| Senha | password | text | new-password | Considere botão “mostrar senha” (UX). |
| Número/quantidade | number | numeric | off | Para decimais, prefira inputmode="decimal". |
Exemplo: senha com “mostrar/ocultar”
<ion-item>
<ion-label position="stacked">Senha</ion-label>
<ion-input
[type]="showPassword ? 'text' : 'password'"
formControlName="senha"
autocomplete="new-password"
enterkeyhint="done">
</ion-input>
<ion-button fill="clear" slot="end" type="button" (click)="showPassword = !showPassword">
Alternar
</ion-button>
</ion-item>Validação síncrona com mensagens (padrão recomendado)
Uma abordagem prática é: mostrar erro apenas quando o usuário interagir (campo touched) e manter mensagens específicas por tipo de erro. Isso reduz “poluição” visual e evita frustração.
Função auxiliar para mensagens (opcional, mas limpa o template)
getErrorMessage(controlName: string): string | null {
const c = this.form.get(controlName);
if (!c || !c.touched || !c.errors) return null;
if (c.errors['required']) return 'Campo obrigatório.';
if (c.errors['email']) return 'E-mail inválido.';
if (c.errors['minlength']) return 'Muito curto.';
if (c.errors['maxlength']) return 'Muito longo.';
if (c.errors['pattern']) return 'Formato inválido.';
if (c.errors['requiredTrue']) return 'Você precisa aceitar.';
return 'Valor inválido.';
}No HTML, você pode usar:
<ion-note color="danger" *ngIf="getErrorMessage('email') as msg">{{ msg }}</ion-note>Validação assíncrona (ex.: e-mail já cadastrado)
Validações assíncronas são úteis quando dependem de API (disponibilidade de e-mail/usuário). A ideia é retornar um Observable ou Promise que resolve para null (válido) ou um objeto de erro (inválido). Combine com updateOn: 'blur' para não chamar a API a cada tecla.
1) Criando um AsyncValidator
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, map, switchMap, take } from 'rxjs/operators';
// Exemplo: serviço fictício
class UsuariosService {
emailDisponivel(email: string): Observable<boolean> {
// true = disponível, false = já existe
return of(email !== 'existe@exemplo.com');
}
}
export function emailDisponivelValidator(usuarios: UsuariosService): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
const value = (control.value || '').trim();
if (!value) return of(null);
return of(value).pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(email => usuarios.emailDisponivel(email)),
map(disponivel => (disponivel ? null : { emailIndisponivel: true })),
catchError(() => of(null)),
take(1)
);
};
}2) Aplicando no FormControl
// no construtor, injete UsuariosService e FormBuilder
form = this.fb.group({
email: this.fb.control('', {
validators: [Validators.required, Validators.email],
asyncValidators: [emailDisponivelValidator(this.usuariosService)],
updateOn: 'blur'
})
});3) Indicando estado “validando” no UI
Enquanto o async validator roda, o controle fica com status PENDING. Você pode mostrar um spinner discreto.
<ion-item>
<ion-label position="stacked">E-mail</ion-label>
<ion-input formControlName="email" type="email" inputmode="email" autocomplete="email"></ion-input>
<ion-spinner slot="end" *ngIf="f.email.pending" name="dots"></ion-spinner>
</ion-item>
<ion-note color="danger" *ngIf="f.email.touched && f.email.errors?.emailIndisponivel">
Este e-mail já está em uso.
</ion-note>UX mobile: recomendações aplicáveis imediatamente
Feedback imediato (sem ser agressivo)
- Mostre erros após interação: use
touchedoudirtypara evitar mensagens antes do usuário digitar. - Valide no blur para campos críticos: e-mail e usuário costumam funcionar melhor com
updateOn: 'blur'. - Use texto auxiliar (ex.: formato esperado) antes do erro, para prevenir falhas.
Estados de erro consistentes
- Mensagem específica: “E-mail inválido” é melhor que “Campo inválido”.
- Um erro por vez: priorize
requiredantes depatternpara não confundir. - Não dependa só de cor: combine cor com texto e, se possível, ícone.
Desabilitar submit (e evitar múltiplos envios)
- Desabilite o botão quando
form.invalidou durante envio (isSubmitting). - Ao clicar em submit com formulário inválido, chame
markAllAsTouched()para revelar erros relevantes.
Acessibilidade (essencial em formulários)
- Rótulos claros: prefira
ion-labelcomposition="stacked"para não depender apenas de placeholder. - Placeholder não substitui label: placeholder some ao digitar e prejudica compreensão.
- Ordem lógica: mantenha a sequência de campos coerente com o fluxo de preenchimento.
- Alvos de toque: use
ion-itempara aumentar área clicável em selects/toggles.
Checklist rápido para revisar seus formulários
- Todos os campos têm label visível e objetivo.
- Campos de e-mail/telefone/senha têm type, inputmode e autocomplete adequados.
- Erros aparecem apenas após interação (
touched/dirty). - Botão de submit fica desabilitado quando inválido e durante envio.
- Validações assíncronas mostram estado
PENDINGe não disparam a cada tecla (preferirupdateOn: 'blur').