Formulários no Ionic: inputs, validação e UX mobile

Capítulo 9

Tempo estimado de leitura: 9 minutos

+ Exercício

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 quando type nã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.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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.

CampotypeinputmodeautocompleteDica
NometexttextnameUse enterkeyhint="next" para avançar.
E-mailemailemailemailTeclado com “@” e “.” facilita.
TelefonetelteltelEvite máscara complexa; valide padrão e normalize no submit.
Senhapasswordtextnew-passwordConsidere botão “mostrar senha” (UX).
Número/quantidadenumbernumericoffPara 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 touched ou dirty para 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 required antes de pattern para 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.invalid ou 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-label com position="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-item para 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 PENDING e não disparam a cada tecla (preferir updateOn: 'blur').

Agora responda o exercício sobre o conteúdo:

Ao implementar uma validação assíncrona de e-mail (ex.: verificar se já está cadastrado) em um formulário com Reactive Forms no Ionic, qual abordagem melhora a UX ao evitar chamadas à API a cada tecla digitada?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Usar updateOn: 'blur' evita disparar a validação a cada tecla, reduzindo chamadas à API. Enquanto valida, o controle pode ficar em PENDING, permitindo mostrar um indicador (ex.: spinner) e manter o feedback claro.

Próximo capitúlo

HTTP e consumo de APIs no Ionic: serviços, interceptors e tratamento de erros

Arrow Right Icon
Capa do Ebook gratuito Ionic para Iniciantes: aplicativos híbridos com HTML, CSS e TypeScript
43%

Ionic para Iniciantes: aplicativos híbridos com HTML, CSS e TypeScript

Novo curso

21 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.