Especialização em Front-End com Vue • Demonstrações Práticas Compiláveis


💻 Código de Exemplo 03 — Diretivas Estruturais, Eventos e Two-Way Data Binding

O exemplo a seguir demonstra o uso corporativo das diretivas fundamentais do Vue 3: manipulação condicional com v-if/v-else, renderização de coleções eficientes com v-for e :key, vinculação de formulário com modificadores de v-model, e listeners de evento com atalhos de teclado.

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

interface ProductItem {
  id: string;
  name: string;
  category: 'hardware' | 'software' | 'cloud';
  price: number;
  inStock: boolean;
}

const filterCategory = ref<string>('all');
const searchQuery = ref<string>('');
const showOnlyInStock = ref<boolean>(false);
const newProductName = ref<string>('');
const newProductPrice = ref<number>(0);

const products = ref<ProductItem[]>([
  { id: 'p1', name: 'Servidor Rack 1U Xeon', category: 'hardware', price: 4200.0, inStock: true },
  { id: 'p2', name: 'Licença Enterprise Linux', category: 'software', price: 850.0, inStock: true },
  { id: 'p3', name: 'Cluster Kubernetes Dedicado', category: 'cloud', price: 1200.0, inStock: false },
  { id: 'p4', name: 'Switch Gerenciável 24p PoE', category: 'hardware', price: 1890.0, inStock: true }
]);

// Lista computada com múltiplos critérios de filtro
const filteredProducts = computed<ProductItem[]>(() => {
  return products.value.filter(item => {
    const matchesCategory = filterCategory.value === 'all' || item.category === filterCategory.value;
    const matchesSearch = item.name.toLowerCase().includes(searchQuery.value.toLowerCase().trim());
    const matchesStock = !showOnlyInStock.value || item.inStock;
    return matchesCategory && matchesSearch && matchesStock;
  });
});

function addProduct(): void {
  if (!newProductName.value.trim() || newProductPrice.value <= 0) return;

  products.value.push({
    id: `p-${Date.now()}`,
    name: newProductName.value.trim(),
    category: 'hardware',
    price: newProductPrice.value,
    inStock: true
  });

  // Limpeza de formulário
  newProductName.value = '';
  newProductPrice.value = 0;
}

function removeProduct(id: string): void {
  products.value = products.value.filter(p => p.id !== id);
}

function toggleStock(product: ProductItem): void {
  product.inStock = !product.inStock;
}
</script>

<template>
  <div class="max-w-3xl mx-auto p-6 bg-white border rounded-2xl shadow-sm space-y-6">
    <div class="flex items-center justify-between border-b pb-4">
      <h2 class="text-xl font-bold text-slate-800">📦 Catálogo de Produtos e Diretivas Vue</h2>
      <span class="text-xs bg-indigo-100 text-indigo-800 font-semibold px-2.5 py-1 rounded-full">
        Total: {{ products.length }} | Filtrados: {{ filteredProducts.length }}
      </span>
    </div>

    <!-- Barra de Filtros com v-model e modificadores -->
    <div class="grid grid-cols-1 md:grid-cols-3 gap-3">
      <input
        type="text"
        v-model.trim="searchQuery"
        placeholder="Buscar por nome..."
        class="border px-3 py-2 rounded-lg text-sm focus:ring-2 focus:ring-indigo-500 outline-none"
      />

      <select
        v-model="filterCategory"
        class="border px-3 py-2 rounded-lg text-sm focus:ring-2 focus:ring-indigo-500 outline-none"
      >
        <option value="all">Todas as Categorias</option>
        <option value="hardware">Hardware</option>
        <option value="software">Software</option>
        <option value="cloud">Cloud</option>
      </select>

      <label class="flex items-center gap-2 text-sm text-slate-700 cursor-pointer">
        <input type="checkbox" v-model="showOnlyInStock" class="rounded text-indigo-600" />
        Apenas em Estoque
      </label>
    </div>

    <!-- Formulário de Criação Rápida com v-on.enter -->
    <div class="flex gap-2 p-3 bg-slate-50 border rounded-xl">
      <input
        type="text"
        v-model="newProductName"
        @keyup.enter="addProduct"
        placeholder="Novo Produto (Enter para salvar)"
        class="border px-3 py-1.5 rounded-md text-sm flex-1"
      />
      <input
        type="number"
        v-model.number="newProductPrice"
        @keyup.enter="addProduct"
        placeholder="Preço (R$)"
        class="border px-3 py-1.5 rounded-md text-sm w-28"
      />
      <button
        @click="addProduct"
        class="bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-semibold px-4 py-2 rounded-md"
      >
        Adicionar
      </button>
    </div>

    <!-- Lista de Itens com v-if / v-else e v-for com chave única -->
    <div v-if="filteredProducts.length > 0" class="divide-y border rounded-xl overflow-hidden">
      <div
        v-for="product in filteredProducts"
        :key="product.id"
        class="p-4 flex items-center justify-between hover:bg-slate-50 transition-colors"
      >
        <div>
          <div class="flex items-center gap-2">
            <span class="font-medium text-slate-800">{{ product.name }}</span>
            <span
              :class="product.inStock ? 'text-emerald-700 bg-emerald-50' : 'text-red-700 bg-red-50'"
              class="text-xs px-2 py-0.5 rounded-full font-medium"
            >
              {{ product.inStock ? 'Disponível' : 'Esgotado' }}
            </span>
          </div>
          <span class="text-xs text-slate-400 capitalize">Categoria: {{ product.category }}</span>
        </div>

        <div class="flex items-center gap-4">
          <span class="font-bold text-slate-900">R$ {{ product.price.toFixed(2) }}</span>
          <button
            @click="toggleStock(product)"
            class="text-xs text-slate-500 hover:text-indigo-600 underline"
          >
            Alternar Status
          </button>
          <button
            @click="removeProduct(product.id)"
            class="text-xs text-red-500 hover:text-red-700"
            title="Excluir Item"
          >
            ✕
          </button>
        </div>
      </div>
    </div>

    <!-- Estado Vazio (Empty State) -->
    <div v-else class="text-center py-10 bg-slate-50 rounded-xl border border-dashed text-slate-500 text-sm">
      Nenhum produto atende aos critérios de pesquisa selecionados.
    </div>
  </div>
</template>

🖥️ Saída / Comportamento Esperado

Renderização Inicial:
Total: 4 | Filtrados: 4
Exibe os 4 produtos da lista inicial.

Interação 1 — Filtro de Busca:
Digitar "Rack" no input de busca:
Filtrados atualiza reativamente para 1.
Apenas "Servidor Rack 1U Xeon" permanece visível.

Interação 2 — Modificador de Teclado @keyup.enter:
Digitar "Storage NAS 8TB" no nome e "3400" no preço.
Pressionar [Enter]:
O produto é instantaneamente adicionado à lista e os inputs são zerados.

Interação 3 — v-if vs v-else:
Digitar "Não existente" no campo de busca:
O container da lista é removido do DOM pelo v-if.
O bloco v-else é renderizado com mensagem de Empty State.

🔍 Destaques da Implementação

  1. Modificadores Nativos: v-model.trim remove espaços acidentais e v-model.number realiza parse automático para tipo numérico em tempo de digitação.
  2. Estabilidade de :key: O identificador exclusivo product.id garante que o algoritmo de reconciliação de Virtual DOM do Vue reutilize nós existentes com segurança.
  3. Escuta de Eventos Especializada: @keyup.enter dispensa verificações manuais de event.key === 'Enter'.