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

  1. Tipagem com Generics no SFC: <script setup lang="ts" generic="T extends { id: string | number }"> oferece autocompletion estrito das propriedades no slot do template.
  2. 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.
  3. Macro defineSlots (Vue 3.3+): Checagem estática no compilador para garantir que os parâmetros expostos no slot satisfaçam o contrato.