Capítulo 03: Sintaxe de Template, Diretivas (v-bind, v-on, v-model, v-if, v-for)

Especialização em Front-End com Vue • Vue 3 & Nuxt 3 • Composition API, Pinia, Vue Router, SSR e TypeScript


🗺️ Mapa Conceitual de Diretivas e Compilação de Template

flowchart TD
    A["HTML Declarativo no <template>"] --> B["Vue Template Compiler (@vue/compiler-core)"]
    B --> C["Diretivas de Vinculação (v-bind / :)"]
    B --> D["Diretivas de Eventos (v-on / @)"]
    B --> E["Diretivas de Estrutura (v-if / v-for)"]
    B --> F["Two-Way Binding (v-model)"]

    C --> G["AST Otimizada com Patch Flags"]
    D --> G
    E --> G
    F --> G

    G --> H["Função render() Nativa em JavaScript"]
    H --> I["Reconciliação Cirúrgica sem Reflow Desnecessário"]

    style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style G fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style I fill:#e8f5e9,stroke:#4caf50,stroke-width:2px

🏛️ 1. Compilação de Templates e o Poder das Diretivas

No Vue 3, os templates não são interpretados como strings no navegador; eles são compilados estaticamente durante a etapa de build (via Vite) em funções render() altamente eficientes. As diretivas são atributos especiais prefixados com v- que aplicam transformações reativas ao DOM subjacente.

1.1 v-bind (:) e Vinculação Dinâmica de Atributos

A diretiva v-bind (atalho :) liga atributos HTML ou propriedades de componentes a expressões JavaScript reativas:

  • Binding de Classes e Estilos: Suporta sintaxe de objeto (:class="{ 'bg-red-500': hasError, 'opacity-50': isDisabled }") e array (:class="[baseClass, variantClass]").
  • Binding Booleano: Atributos como disabled, checked e readonly são omitidos do nó do DOM se o valor for null, undefined ou false.

1.2 v-on (@) e Modificadores de Evento

A diretiva v-on (atalho @) registra ouvintes de eventos nativos do DOM ou eventos customizados de componentes. Os modificadores encadeados eliminam código imperativo repetitivo:

  • .prevent: Executa event.preventDefault().
  • .stop: Invoca event.stopPropagation(), impedindo o borbulhamento (bubbling).
  • .passive: Otimiza a performance de scroll em dispositivos touch ao informar ao navegador que o handler não cancelará o scroll.
  • Modificadores de teclado: @keyup.enter, @keydown.esc, @keydown.tab.

1.3 v-if vs v-show: Critérios de Arquitetura e Performance

  • v-if / v-else-if / v-else: Renderização condicional verdadeira. Os nós do DOM e seus componentes filhos são fisicamente destruídos e recriados. Ideal para seções pesadas que raramente alternam de visibilidade (ex: formulários complexos abertos sob demanda).
  • v-show: O nó permanece sempre presente no DOM, alternando unicamente a propriedade CSS display: none. Possui custo inicial de compilação maior, mas custo de alternância (toggle cost) próximo a zero. Ideal para modais frequentes, abas e tooltips.

1.4 v-for e a Regra Sagrada da Chave Única (:key)

Ao renderizar coleções, o Vue utiliza um algoritmo de reconciliação para comparar a árvore antiga com a nova. Nunca utilize o índice do array como :key se a lista sofrer ordenação, filtragem, inserção ou remoção intermediária. O uso de IDs estáveis (ex: product.id) garante que o estado interno de inputs e transições não seja associado a elementos incorretos.


⚖️ 2. Tabela Comparativa: v-if vs v-show

Dimensão Técnica v-if / v-else v-show
Presença no DOM O nó é completamente removido ou montado. O nó sempre existe; alterna display: none.
Custo de Inicialização Baixo (se a condição for falsa, nada é montado). Médio (o nó e seus filhos são instanciados no início).
Custo de Alternância (Toggle) Alto (dispara ciclo de vida onMounted / onUnmounted). Praticamente Zero (apenas alteração de regra CSS).
Compatibilidade com <template> Suportada diretamente (<template v-if="...">). Não suportada (a tag <template> não aceita CSS).
Suporte a v-else Suporte nativo completo encadeado. Não possui diretiva v-show-else equivalente.

⚠️ 3. Armadilhas Comuns em Produção (Gotchas & Anti-Patterns)

Anti-Pattern 1: Usar v-if e v-for no Mesmo Elemento

Erro Comum: Escrever <li v-for="user in users" v-if="user.isActive" :key="user.id">. No Vue 3, o v-if possui precedência de avaliação superior ao v-for. Isso resulta em erro de compilação, pois a variável de escopo user ainda não existe no momento em que a condição é avaliada. Abordagem Correta: Pré-filtre os dados utilizando uma propriedade computada (const activeUsers = computed(() => users.value.filter(u => u.isActive))) e itere sobre a lista limpa.

Anti-Pattern 2: Usar Índice como :key em Listas Editáveis

Erro Comum: <tr v-for="(item, index) in list" :key="index">. Ao excluir o primeiro item, os inputs de texto associados mantêm os valores visualmente no primeiro índice físico, corrompendo a fidelidade dos dados visuais. Abordagem Correta: Use sempre uma chave imutável: :key="item.uuid".


💻 4. Código Conceitual Arquitetural Comentado

<!-- src/components/DynamicFilterList.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue';

interface DatabaseInstance {
  id: string;
  name: string;
  engine: 'postgres' | 'mysql' | 'redis';
  isReplica: boolean;
}

const instances = ref<DatabaseInstance[]>([
  { id: 'db-01', name: 'Primary DB Prod', engine: 'postgres', isReplica: false },
  { id: 'db-02', name: 'Read Replica Alpha', engine: 'postgres', isReplica: true },
  { id: 'db-03', name: 'Session Cache Pod', engine: 'redis', isReplica: false }
]);

const filterText = ref<string>('');
const showOnlyReplicas = ref<boolean>(false);

// Filtragem computada pura (resolve o anti-pattern do v-if com v-for)
const filteredInstances = computed<DatabaseInstance[]>(() => {
  return instances.value.filter((inst) => {
    const matchesSearch = inst.name.toLowerCase().includes(filterText.value.toLowerCase().trim());
    const matchesReplica = !showOnlyReplicas.value || inst.isReplica;
    return matchesSearch && matchesReplica;
  });
});
</script>

<template>
  <div class="max-w-md mx-auto p-5 bg-white border rounded-2xl shadow-sm space-y-4">
    <div class="space-y-2">
      <!-- v-model com modificador trim -->
      <input
        type="text"
        v-model.trim="filterText"
        placeholder="Buscar banco de dados..."
        class="w-full border px-3 py-2 rounded-lg text-sm outline-none focus:ring-2 focus:ring-indigo-500"
      />

      <label class="flex items-center gap-2 text-xs text-slate-600 cursor-pointer">
        <input type="checkbox" v-model="showOnlyReplicas" class="rounded text-indigo-600" />
        Mostrar apenas réplicas de leitura
      </label>
    </div>

    <!-- Lista condicional limpa com chave única estável -->
    <ul v-if="filteredInstances.length > 0" class="divide-y border rounded-xl overflow-hidden">
      <li
        v-for="db in filteredInstances"
        :key="db.id"
        class="p-3 flex justify-between items-center text-sm"
      >
        <div>
          <span class="font-medium text-slate-800">{{ db.name }}</span>
          <p class="text-xs text-slate-400 uppercase font-mono">{{ db.engine }}</p>
        </div>
        <span
          class="text-xs font-semibold px-2 py-0.5 rounded-full"
          :class="db.isReplica ? 'bg-amber-100 text-amber-800' : 'bg-emerald-100 text-emerald-800'"
        >
          {{ db.isReplica ? 'Réplica' : 'Primário' }}
        </span>
      </li>
    </ul>

    <!-- Empty state ativado se filteredInstances estiver vazia -->
    <div v-else class="p-6 text-center text-xs text-slate-400 bg-slate-50 border border-dashed rounded-xl">
      Nenhuma instância corresponde aos filtros informados.
    </div>
  </div>
</template>

🛠️ 5. Cheat-Sheet de Diretivas e Modificadores

Diretiva Atalho Modificadores Frequentes Exemplo de Aplicação
v-bind : .prop, .camel :class="activeClass", :disabled="isPending"
v-on @ .prevent, .stop, .once, .enter @submit.prevent="save", @keyup.esc="closeModal"
v-model - .trim, .number, .lazy v-model.trim="email", v-model.number="price"
v-if / v-else - - <div v-if="auth">...</div><div v-else>...</div>
v-show - - <div v-show="isDropdownOpen">...</div>
v-for - - <li v-for="item in items" :key="item.id">