💻 Exemplos: Capítulo 03: Sintaxe de Template, Diretivas (v-bind, v-on, v-model, v-if, v-for)
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
- Modificadores Nativos:
v-model.trimremove espaços acidentais ev-model.numberrealiza parse automático para tipo numérico em tempo de digitação. - Estabilidade de
:key: O identificador exclusivoproduct.idgarante que o algoritmo de reconciliação de Virtual DOM do Vue reutilize nós existentes com segurança. - Escuta de Eventos Especializada:
@keyup.enterdispensa verificações manuais deevent.key === 'Enter'.