💻 Exemplos: Capítulo 07: Slots Padrão, Nomeados e Scoped Slots para UI Flexível
Especialização em Front-End com Vue • Demonstrações Práticas Compiláveis
💻 Código de Exemplo 07 — Composição de Componentes com Slots Nomeados e Scoped Slots
Este exemplo demonstra a criação de um componente corporativo genérico de tabela de dados (DataTable.vue) que delega a renderização customizada de células e cabeçalhos ao componente pai através de Scoped Slots tipados.
1. Componente de UI Flexível: DataTable.vue
<!-- src/components/DataTable.vue -->
<script setup lang="ts" generic="T extends { id: string | number }">
interface ColumnDef {
key: string;
label: string;
align?: 'left' | 'center' | 'right';
}
interface Props {
data: T[];
columns: ColumnDef[];
captionTitle?: string;
}
const props = defineProps<Props>();
// Declaração de tipos para os slots do componente
defineSlots<{
caption?: (props: { title: string; count: number }) => any;
headerCell?: (props: { column: ColumnDef }) => any;
rowItem?: (props: { item: T; index: number }) => any;
emptyState?: () => any;
}>();
</script>
<template>
<div class="border rounded-2xl overflow-hidden shadow-sm bg-white">
<!-- Slot Nomeado de Título / Caption -->
<div class="p-4 bg-slate-50 border-b flex justify-between items-center">
<slot name="caption" :title="props.captionTitle ?? 'Listagem Geral'" :count="props.data.length">
<!-- Conteúdo Padrão (Fallback) -->
<h3 class="font-bold text-slate-800">{{ props.captionTitle ?? 'Dados' }}</h3>
<span class="text-xs text-slate-500">{{ props.data.length }} registros</span>
</slot>
</div>
<table class="w-full text-left text-sm">
<thead class="bg-slate-100/75 text-slate-700 text-xs uppercase font-semibold">
<tr>
<th
v-for="col in props.columns"
:key="col.key"
class="px-4 py-3"
:class="col.align === 'right' ? 'text-right' : col.align === 'center' ? 'text-center' : 'text-left'"
>
<!-- Slot Nomeado de Célula de Cabeçalho -->
<slot name="headerCell" :column="col">
{{ col.label }}
</slot>
</th>
</tr>
</thead>
<tbody v-if="props.data.length > 0" class="divide-y text-slate-600">
<tr v-for="(item, index) in props.data" :key="item.id" class="hover:bg-slate-50/80 transition-colors">
<!-- Scoped Slot de Linha Inteira com Dados Expostos -->
<slot name="rowItem" :item="item" :index="index">
<!-- Fallback simples caso o pai não forneça template customizado -->
<td v-for="col in props.columns" :key="col.key" class="px-4 py-3">
{{ (item as any)[col.key] }}
</td>
</slot>
</tr>
</tbody>
</table>
<!-- Slot de Estado Vazio -->
<div v-if="props.data.length === 0" class="p-8 text-center text-slate-400 text-sm">
<slot name="emptyState">
Nenhum registro encontrado para exibição.
</slot>
</div>
</div>
</template>
2. Componente Consumidor Pai: ServerFleetView.vue
<!-- src/views/ServerFleetView.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import DataTable from '../components/DataTable.vue';
interface ServerNode {
id: string;
hostname: string;
region: string;
status: 'ONLINE' | 'DEGRADED' | 'OFFLINE';
cpuLoad: number;
}
const columns = [
{ key: 'hostname', label: 'Nome do Servidor Host' },
{ key: 'region', label: 'Região AWS / GCP' },
{ key: 'status', label: 'Status Operacional', align: 'center' as const },
{ key: 'cpuLoad', label: 'Carga de CPU', align: 'right' as const }
];
const servers = ref<ServerNode[]>([
{ id: 'srv-01', hostname: 'prod-api-cluster-01', region: 'us-east-1', status: 'ONLINE', cpuLoad: 34.2 },
{ id: 'srv-02', hostname: 'prod-worker-queue-02', region: 'sa-east-1', status: 'DEGRADED', cpuLoad: 88.5 },
{ id: 'srv-03', hostname: 'stage-db-replica-01', region: 'eu-central-1', status: 'ONLINE', cpuLoad: 18.0 }
]);
</script>
<template>
<div class="max-w-4xl mx-auto p-6 space-y-6">
<h2 class="text-2xl font-bold text-slate-800">🖥️ Orquestrador de Servidores</h2>
<DataTable :data="servers" :columns="columns" caption-title="Instâncias Produtivas Ativas">
<!-- 1. Customização do Slot 'caption' com Scoped Props -->
<template #caption="{ title, count }">
<div class="flex items-center gap-3">
<span class="w-2.5 h-2.5 rounded-full bg-indigo-600"></span>
<h3 class="font-bold text-slate-900">{{ title }}</h3>
</div>
<span class="text-xs font-semibold px-2 py-1 bg-indigo-50 text-indigo-700 rounded-full">
{{ count }} nós conectados
</span>
</template>
<!-- 2. Customização do Slot 'rowItem' consumindo o objeto da iteração -->
<template #rowItem="{ item, index }">
<td class="px-4 py-3 font-mono font-medium text-slate-900">
<span class="text-xs text-slate-400 mr-2">#{{ index + 1 }}</span>
{{ item.hostname }}
</td>
<td class="px-4 py-3 text-slate-600 text-xs">{{ item.region }}</td>
<td class="px-4 py-3 text-center">
<span
class="px-2 py-0.5 rounded-full text-xs font-bold"
:class="{
'bg-emerald-100 text-emerald-800': item.status === 'ONLINE',
'bg-amber-100 text-amber-800': item.status === 'DEGRADED',
'bg-red-100 text-red-800': item.status === 'OFFLINE'
}"
>
{{ item.status }}
</span>
</td>
<td class="px-4 py-3 text-right font-mono font-bold" :class="item.cpuLoad > 80 ? 'text-red-600' : 'text-slate-700'">
{{ item.cpuLoad.toFixed(1) }}%
</td>
</template>
</DataTable>
</div>
</template>
🖥️ Saída / Comportamento Esperado
Renderização da Tabela:
Cabeçalho superior (slot #caption):
- Ponto azul indigo com "Instâncias Produtivas Ativas"
- Badge: "3 nós conectados"
Linhas customizadas (slot #rowItem):
Linha 1: #1 prod-api-cluster-01 | us-east-1 | [ONLINE] verde | 34.2%
Linha 2: #2 prod-worker-queue-02 | sa-east-1 | [DEGRADED] âmbar | 88.5% vermelho
Linha 3: #3 stage-db-replica-01 | eu-central-1 | [ONLINE] verde | 18.0%
🔍 Destaques da Implementação
- Tipagem com Generics no SFC:
<script setup lang="ts" generic="T extends { id: string | number }">oferece autocompletion estrito das propriedades no slot do template. - Inversão de Controle com Scoped Slots: O filho administra paginação e disposição da grade enquanto o pai dita a apresentação visual de badges e cores.
- Macro
defineSlots(Vue 3.3+): Checagem estática no compilador para garantir que os parâmetros expostos no slot satisfaçam o contrato.