Capítulo 07: Slots Padrão, Nomeados e Scoped Slots para UI Flexível
Capítulo 07: Slots Padrão, Nomeados e Scoped Slots para UI Flexível
Especialização em Front-End com Vue • Vue 3 & Nuxt 3 • Composition API, Pinia, Vue Router, SSR e TypeScript
🗺️ Mapa Conceitual do Sistema de Slots
flowchart TD
A["Componente Consumidor (Pai)"] -->|"Define Template com #nomeSlot"| B["Componente Container (Filho)"]
B --> C["<slot> (Padrão com Fallback)"]
B --> D["<slot name='header'> (Nomeado)"]
B --> E["<slot name='item' :row='dado'> (Scoped Slot)"]
E -->|"Expõe Dados Internos para o Template do Pai"| A
C & D & E --> F["Inversão de Controle & Design Systems"]
F --> G["Componentes Sem Apresentação (Renderless Components)"]
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style E fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style F fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
🏛️ 1. Distribuição de Conteúdo e Inversão de Controle com Slots
O sistema de Slots do Vue implementa o conceito de Content Distribution (inspirado na especificação de Web Components), permitindo que um componente componha seu layout aceitando blocos arbitrários de HTML, componentes ou outros templates injetados pelo componente pai.
1.1 Slots Padrão e Conteúdo Fallback
O slot mais simples é o elemento <slot />. Caso o componente pai não forneça nenhum filho entre as tags de abertura e fechamento, o Vue renderiza automaticamente o conteúdo de fallback especificado dentro da tag:
<!-- ModalBox.vue -->
<template>
<div class="modal">
<slot>
<!-- Conteúdo Fallback exibido se o pai não passar nada -->
<p class="text-slate-400">Nenhum conteúdo informado.</p>
</slot>
</div>
</template>
1.2 Slots Nomeados (#header, #footer)
Quando um componente requer múltiplos pontos de inserção (como cabeçalho, corpo principal e rodapé de ações), utilizam-se Slots Nomeados. No consumidor, utiliza-se a diretiva v-slot:nome ou a forma abreviada #nome:
<!-- No Consumidor: -->
<ModalBox>
<template #header>
<h2>Título do Diálogo</h2>
</template>
<template #default>
<p>Corpo da mensagem.</p>
</template>
<template #footer>
<button>Confirmar</button>
</template>
</ModalBox>
1.3 Scoped Slots: A Passagem Reversa de Dados
O maior diferencial arquitetural dos slots no Vue reside nos Scoped Slots. Eles permitem que o componente filho execute regras de negócio, paginação, filtros e requisições, enquanto expõe os dados calculados de volta para o template do pai através de atributos vinculados à tag <slot>:
<!-- Tabela.vue (Filho) -->
<slot name="row" :item="registro" :index="idx"></slot>
<!-- Consumidor.vue (Pai) -->
<template #row="{ item, index }">
<td class="font-bold">{{ index + 1 }} - {{ item.nome }}</td>
</template>
1.4 Padrão Renderless Component
Ao combinar Scoped Slots com a Composition API, podemos criar componentes que não renderizam nenhum elemento visual próprio; eles apenas fornecem lógica de estado (ex: gerenciamento de accordion, drag-and-drop, validação de campos) e deixam a renderização 100% livre para o pai.
⚖️ 2. Tabela Comparativa de Tipos de Slot
| Tipo de Slot | Sintaxe no Filho | Sintaxe no Consumidor (Pai) | Casos de Uso |
|---|---|---|---|
| Slot Padrão | <slot /> |
<Card>Conteúdo</Card> |
Containers simples, botões com ícones, cards de texto. |
| Slot Nomeado | <slot name="actions" /> |
<template #actions>...</template> |
Layouts de painéis, diálogos modais com barra de ação. |
| Scoped Slot | <slot name="cell" :data="row" /> |
<template #cell="{ data }">...</template> |
Tabelas genéricas, listas infinitas, seletores customizados. |
| Renderless Slot | <slot :isReady="state" /> |
<FeatureToggle v-slot="{ isReady }"> |
Lógica pura sem acoplamento a classes CSS ou HTML fixo. |
⚠️ 3. Armadilhas Comuns em Produção (Gotchas & Anti-Patterns)
Anti-Pattern 1: Confusão de Escopo de Variáveis
Erro Comum: Esperar que o template do slot tenha acesso direto a variáveis privadas do componente filho sem que elas tenham sido passadas como atributos do slot. Regra Arquitetural do Vue: Tudo no template do pai é compilado no escopo do pai; tudo no filho é compilado no escopo do filho. Para acessar dados do filho, use obrigatoriamente Scoped Slots.
Anti-Pattern 2: Não Tipar os Slots em Componentes Genéricos
Erro Comum: Usar Scoped Slots em TypeScript sem a macro defineSlots, resultando em perda de autocompletion e checagem de tipos estáticos no template do pai.
Abordagem Correta: Adote defineSlots<{ row(props: { item: T }): any }>() (Vue 3.3+).
💻 4. Código Conceitual Arquitetural Comentado
<!-- src/components/GenericAccordion.vue -->
<script setup lang="ts" generic="T extends { id: string; title: string }">
import { ref } from 'vue';
interface Props {
items: T[];
}
const props = defineProps<Props>();
const activeId = ref<string | null>(null);
function toggle(id: string): void {
activeId.value = activeId.value === id ? null : id;
}
// Declaração estrita dos slots do componente (Vue 3.3+)
defineSlots<{
header?: (props: { item: T; isOpen: boolean }) => any;
content: (props: { item: T }) => any;
}>();
</script>
<template>
<div class="border rounded-2xl divide-y bg-white overflow-hidden shadow-sm max-w-lg mx-auto">
<div v-for="item in props.items" :key="item.id">
<!-- Cabeçalho do Accordion (Customizável pelo Pai via Slot) -->
<button
type="button"
@click="toggle(item.id)"
class="w-full p-4 flex justify-between items-center text-left hover:bg-slate-50 transition-colors"
>
<slot name="header" :item="item" :is-open="activeId === item.id">
<!-- Fallback padrão caso o pai não forneça cabeçalho customizado -->
<span class="font-semibold text-slate-800 text-sm">{{ item.title }}</span>
<span class="text-xs text-slate-400">{{ activeId === item.id ? '▲' : '▼' }}</span>
</slot>
</button>
<!-- Conteúdo Interno com Scoped Slot Obrigatório -->
<div v-if="activeId === item.id" class="p-4 bg-slate-50 border-t text-sm text-slate-600">
<slot name="content" :item="item"></slot>
</div>
</div>
</div>
</template>
🛠️ 5. Cheat-Sheet de Sintaxe de Slots
<!-- Declaração de Slots no Componente Filho -->
<slot name="cabecalho" :dados="info"></slot>
<slot :total="quantidade"></slot> <!-- Default scoped -->
<!-- Consumo de Slots no Componente Pai -->
<MeuComponente>
<!-- Slot nomeado abreviado -->
<template #cabecalho="{ dados }">
<h2>{{ dados.titulo }}</h2>
</template>
<!-- Slot padrão desestruturado -->
<template #default="{ total }">
<p>Total de itens: {{ total }}</p>
</template>
</MeuComponente>