Vue.js: Formulários e v-model

Última atualização: 2026-10-20

Os formulários são a interação central das aplicações web — login, cadastro, pesquisa e configurações dependem, todos, do processamento de formulários. O v-model do Vue é um “açúcar sintático” para o processamento de formulários, tornando o código de vinculação bidirecional extremamente conciso (1 linha contra 5 linhas).

O defineModel no Vue 3.4+ simplifica ainda mais a implementação do v-model. Formulários complexos também exigem recursos avançados, como validação, serialização e envio de arquivos. Esta lição aborda cinco cenários principais relacionados a formulários.

Diretivas relacionados:v-model

1. O que você vai aprender


2. O pesadelo das “5 seções padrão” em um formulário de login

(1) Desafio: 5 campos de formulário, 10 linhas de código v-model

O formulário de login da Alice tinha 5 campos, cada um com vinculação ao v-model:

5 campos = 10 linhas de texto padrão. 10 campos equivalem a 20 linhas. Quanto mais complexo for o formulário, pior fica a situação.

(2) Abordagem avançada ao v-model do Vue: Objetos reativos + defineModel

5 campos = 1 objeto. A programação reativa reduz a quantidade de código em 50%.

(3) Receita

Após a otimização do modelo V:


3. Como funciona o V-Model

(1) A essência do modelo V

VUE
<!-- v-model is props + emit syntax sugar -->
<input v-model="searchQuery">

<!-- equivalent to -->
<input 
  :value="searchQuery" 
  @input="searchQuery = $event.target.value"
>

(2) Modelo V em componentes

VUE
<!-- Using Parent Components v-model -->
<CustomInput v-model="searchQuery" />

<!-- equivalent to -->
<CustomInput 
  :modelValue="searchQuery" 
  @update:modelValue="searchQuery = $event"
/>

(3) defineModel (Simplificado para o Vue 3.4+)

▶ Exemplo: 5 Princípios básicos do modelo V

VUE
<!-- 1. text input -->
<input v-model="text">

<!-- 2. textarea -->
<textarea v-model="description"></textarea>

<!-- 3. checkbox -->
<input v-model="agreed" type="checkbox">

<!-- 4. radio -->
<input v-model="gender" type="radio" value="male">
<input v-model="gender" type="radio" value="female">

<!-- 5. select -->
<select v-model="selected">
  <option value="vue">Vue</option>
  <option value="react">React</option>
</select>
▶ Experimente

Saída:

TEXT
Digitar no input atualiza text linha a linha.
Marcar o checkbox → agreed = true; escolher female → gender = 'female'.
Selecionar Vue no select → selected = 'vue' (valor da option).

4. 5 tipos de modelo V personalizado

(1) Dados básicos

(2) mensagens de texto

(3) selecionar

(4) Caixa de seleção (seleções múltiplas)

(5) Modificador personalizado (trim)

VUE
<!-- Parent Component:v-model.trim -->
<CustomInput v-model.trim="username" />

<!-- Child component:Processing trim Modifiers -->
<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) }
})
const emit = defineEmits(['update:modelValue'])

function onInput(e) {
  let value = e.target.value
  if (props.modelModifiers.trim) {
    value = value.trim()
  }
  emit('update:modelValue', value)
}
</script>

▶ Exemplo: 5 Tipos de v-model personalizado

▶ Experimente

Saída:

TEXT
O pai escreve <CustomInput v-model="search" />.
Digitar no filho emite update:modelValue e search é atualizado no pai.
É o mesmo que :modelValue="search" mais @update:modelValue="search = $event".

5. Validação de formulários

(1) Validação personalizada

JS
// utils/validators.js
export function isEmail(value) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
}

export function isPhone(value) {
  return /^1[3-9]\d{9}$/.test(value)
}

export function minLength(value, min) {
  return value.length >= min
}
VUE
<template>
  <form @submit.prevent="handleSubmit">
    <input v-model="form.email" @blur="validateField('email')">
    <p v-if="errors.email" class="error">{{ errors.email }}</p>
    
    <input v-model="form.password" type="password">
    <p v-if="errors.password">{{ errors.password }}</p>
    
    <button :disabled="!isValid">Submit</button>
  </form>
</template>

<script setup>
import { reactive, ref } from 'vue'
import { isEmail, minLength } from '@/utils/validators'

const form = reactive({
  email: '',
  password: ''
})

const errors = reactive({})
const isValid = ref(false)

function validateField(field) {
  if (field === 'email') {
    if (!form.email) {
      errors.email = 'Email is required'
    } else if (!isEmail(form.email)) {
      errors.email = 'Invalid email format'
    } else {
      delete errors.email
    }
  }
  
  if (field === 'password') {
    if (!form.password) {
      errors.password = 'Password is required'
    } else if (!minLength(form.password, 8)) {
      errors.password = 'Password must be 8+ characters'
    } else {
      delete errors.password
    }
  }
  
  isValid.value = Object.keys(errors).length === 0
}

function handleSubmit() {
  Object.keys(form).forEach(validateField)
  if (isValid.value) {
    console.log('Submit:', form)
  }
}
</script>

(2) A biblioteca VeeValidate (recomendada)

BASH
npm install vee-validate @vee-validate/rules
VUE
<template>
  <Form @submit="handleSubmit" :validation-schema="schema">
    <Field name="email" type="email" v-model="form.email" />
    <ErrorMessage name="email" />
    
    <Field name="password" type="password" v-model="form.password" />
    <ErrorMessage name="password" />
    
    <button>Submit</button>
  </Form>
</template>

<script setup>
import { Form, Field, ErrorMessage } from 'vee-validate'
import * as yup from 'yup'

const schema = yup.object({
  email: yup.string().email().required(),
  password: yup.string().min(8).required()
})

const form = reactive({ email: '', password: '' })

function handleSubmit(values) {
  console.log('Valid:', values)
}
</script>

▶ Exemplo: 5 Cenários de validação

JS
// 1. Required
if (!form.email) errors.email = 'Required'

// 2. Email
if (!isEmail(form.email)) errors.email = 'Invalid email'

// 3. Cell Phone
if (!isPhone(form.phone)) errors.phone = 'Invalid phone'

// 4. Length
if (form.password.length < 8) errors.password = 'Too short'

// 5. Confirm Password
if (form.password !== form.confirm) errors.confirm = 'Mismatch'
▶ Experimente

Saída:

TEXT
Formulário vazio → errors recebe Required em email e as demais mensagens.
Email malformado mostra Invalid email; telefone inválido mostra Invalid phone.
Senha com menos de 8 caracteres → Too short; confirmação diferente → Mismatch.

6. Criação de formulários complexos

(1) Formulários aninhados

JS
const form = reactive({
  user: {
    name: '',
    email: '',
    age: 0
  },
  address: {
    city: '',
    country: ''
  }
})

(2) Campos dinâmicos (v-for)

JS
const form = reactive({
  items: [
    { name: '', quantity: 1, price: 0 }
  ]
})

function addItem() {
  form.items.push({ name: '', quantity: 1, price: 0 })
}

function removeItem(index) {
  form.items.splice(index, 1)
}

(3) Os 5 principais modificadores de forma

VUE
<!-- 1. .lazy:change Event Trigger(No input) -->
<input v-model.lazy="email">

<!-- 2. .number:Automatically Convert to Numbers -->
<input v-model.number="age" type="number">

<!-- 3. .trim:Automatically Remove Spaces -->
<input v-model.trim="username">

<!-- 4. Custom Modifiers(CustomInput) -->
<CustomInput v-model.trim="text" />

<!-- 5. .debounce(Custom):300ms Image Stabilization -->
<DebouncedInput v-model:debounce="500" />

7. Envio de arquivos

(1) Upload de um único arquivo

(2) Vários arquivos + arrastar e soltar


8. Exercício abrangente: modificadores de v-model

▶ Exemplo: 5 Principais modificadores de forma

VUE
<!-- 1. .lazy:change Event -->
<input v-model.lazy="email">

<!-- 2. .number:Automatically Convert to Numbers -->
<input v-model.number="age" type="number">

<!-- 3. .trim:Automatic trim -->
<input v-model.trim="username">

<!-- 4. Combinations of Multiple Modifiers -->
<input v-model.lazy.trim="email">

<!-- 5. Custom Modifiers(Child component)-->
<CustomInput v-model.trim="text" />
▶ Experimente

Saída:

TEXT
.lazy só atualiza o dado no evento change (ao sair do campo).
.number converte o texto digitado em número; .trim remove os espaços das pontas.
Em componentes, os modificadores chegam ao filho em defineModel() ou modelModifiers.

(1) 📊 5. Referência rápida para 5 erros comuns

Erro Sintoma Solução
v-model.number ainda é uma string Erro de adição type="number" + .number
O v-model não é atualizado A referência fica inativa após a mudança para reativo Mude para .value ou toRefs
Falha no envio do arquivo usando FormData Erro entre domínios ou de método fetch + FormData
Momento incorreto da validação Exibir somente quando o foco for perdido Usar @blur
Muito código padrão de campo código prolixo usando objetos reativos

(2) 📊 6. 5 Principais comparações de desempenho

Padrão Simplicidade Desempenho Aplicabilidade
Várias referências ❌ Duplicado ⭐⭐⭐⭐ 1–2 campos
Objeto reativo ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ 5 ou mais campos
FormData (Manual) ❌ ⭐⭐⭐ Envio de arquivos
VeeValidate ⭐⭐⭐⭐ ⭐⭐⭐ Validação complexa
defineModel ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ Componente Vue 3.4+

Diretivas relacionados:v-model

❓ Perguntas Frequentes

P Como se usa o v-model em componentes personalizados?
R Use defineProps(['modelValue']) + defineEmits(['update:modelValue']) dentro do componente. Use v-model no componente pai. No Vue 3.4 ou superior, use defineModel para substituir 10 linhas por apenas 1.
P Por que o v-model.number não está funcionando?
R Ele deve ser usado com type="number". Como alternativa, use parseFloat() manualmente.
P Devo usar validação personalizada ou o VeeValidate para validar formulários?
R Use validação personalizada para formulários simples (< 5 campos); use o VeeValidate para formulários complexos (> 5 campos + campos aninhados + validação assíncrona).
P Como faço para enviar um arquivo e exibir uma pré-visualização?
R Use FileReader.readAsDataURL() para ler o arquivo como uma string Base64 e, em seguida, atribua-o a &lt;img src="..."&gt;. Use FormData para o envio.
P Qual é a diferença entre os modificadores .lazy e .debounce para o v-model?
R O .lazy é acionado pelo evento change (após perder o foco), o que reduz a sobrecarga da validação em tempo real. O .debounce é um modificador personalizado que deve ser implementado no componente filho; ele aciona o último evento dentro de 300 ms.
P Como faço para serializar os dados do formulário em JSON para envio?
R Basta usar JSON.stringify(form) (não é necessário usar FormData, a menos que você esteja enviando arquivos). O backend pode simplesmente receber o JSON.
P O defineModel substitui toda a sintaxe do v-model?
R Sim. defineModel('modelValue') is equivalent to the 10 lines of code in props.modelValue + emit('update:modelValue'). É totalmente compatível com versões anteriores.

📖 Resumo


📝 Exercícios

  1. Questões básicas (Dificuldade: ⭐)

    Implemente um formulário de login simples:

    • 5 campos (nome de usuário / senha / e-mail / telefone / captcha)
    • v-model + reativo
    • Imprimir todo o objeto do formulário após o envio
  2. Problemas avançados (Dificuldade: ⭐⭐)

    Implementação da validação de formulários:

    • 5 campos + validação personalizada (formato de e-mail, formato de telefone, comprimento da senha)
    • @blur aciona a validação
    • Mensagem de erro exibida
    • O botão “Enviar” fica ativado ou desativado de acordo com a propriedade isValid
  3. Problema desafiador (Dificuldade: ⭐⭐⭐)

    Implementar um sistema completo de formulários de “Cadastro de Usuário”:

    1. 10 campos (incluindo objetos address aninhados e uma matriz dinâmica tags)
    2. VeeValidate + esquema do Yup
    3. Envio de arquivo (foto de perfil)
    4. Cobertura total de 5 modificadores
    5. defineModel é usado para componentes filhos
    6. 5 Situações de erro
    7. Tipagem forte do TypeScript
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%