💻 Exemplos: Capítulo 12: Teleport e Suspense para Componentes Assíncronos
Especialização em Front-End com Vue • Demonstrações Práticas Compiláveis
💻 Código de Exemplo 12 — Janelas Modais com <Teleport> e Orquestração Assíncrona com <Suspense>
Este exemplo apresenta a utilização combinada de <Teleport> para renderizar um diálogo modal diretamente no document.body (evitando problemas de z-index e overflow: hidden) e <Suspense> para gerenciar o estado de carregamento de um componente assíncrono com fallback visual em skeleton loader.
1. Componente Assíncrono com Top-Level Await: AsyncAnalyticsWidget.vue
<!-- src/components/AsyncAnalyticsWidget.vue -->
<script setup lang="ts">
// Top-Level Await nativo na Composition API do Vue 3
interface ClusterStats {
activeNodes: number;
totalMemoryGb: number;
iopsThroughput: number;
}
// Simulação de carregamento de dados pesados da infraestrutura
const fetchStats = async (): Promise<ClusterStats> => {
await new Promise((resolve) => setTimeout(resolve, 1500));
return {
activeNodes: 16,
totalMemoryGb: 256,
iopsThroughput: 48200
};
};
const stats = await fetchStats();
</script>
<template>
<div class="grid grid-cols-3 gap-4 p-4 bg-slate-50 rounded-xl border">
<div class="text-center p-3 bg-white rounded-lg border shadow-sm">
<div class="text-xs text-slate-500 font-semibold uppercase">Nós Ativos</div>
<div class="text-2xl font-black text-indigo-600 mt-1">{{ stats.activeNodes }}</div>
</div>
<div class="text-center p-3 bg-white rounded-lg border shadow-sm">
<div class="text-xs text-slate-500 font-semibold uppercase">Memória Total</div>
<div class="text-2xl font-black text-emerald-600 mt-1">{{ stats.totalMemoryGb }} GB</div>
</div>
<div class="text-center p-3 bg-white rounded-lg border shadow-sm">
<div class="text-xs text-slate-500 font-semibold uppercase">Throughput IOPS</div>
<div class="text-2xl font-black text-amber-600 mt-1">{{ stats.iopsThroughput.toLocaleString() }}</div>
</div>
</div>
</template>
2. Componente Principal com <Teleport> e <Suspense>: InfrastructureDashboard.vue
<!-- src/components/InfrastructureDashboard.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import AsyncAnalyticsWidget from './AsyncAnalyticsWidget.vue';
const isModalOpen = ref<boolean>(false);
const reloadKey = ref<number>(0);
function reloadWidget(): void {
reloadKey.value++;
}
</script>
<template>
<div class="max-w-2xl 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">
<div>
<h2 class="text-xl font-bold text-slate-800">📊 Painel de Infraestrutura</h2>
<p class="text-xs text-slate-500">Orquestração com Teleport e Suspense</p>
</div>
<div class="flex gap-2">
<button
@click="reloadWidget"
class="px-3 py-1.5 text-xs font-semibold bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-lg"
>
Recarregar Métricas
</button>
<button
@click="isModalOpen = true"
class="px-3 py-1.5 text-xs font-semibold bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg"
>
Abrir Modal de Detalhes
</button>
</div>
</div>
<!-- ORQUESTRAÇÃO DE COMPONENTE ASSÍNCRONO COM SUSPENSE -->
<Suspense>
<!-- Slot Padrão: Renderizado quando a Promise resolve -->
<template #default>
<AsyncAnalyticsWidget :key="reloadKey" />
</template>
<!-- Slot Fallback: Skeleton Loader exibido durante o carregamento -->
<template #fallback>
<div class="grid grid-cols-3 gap-4 p-4 bg-slate-50 rounded-xl border animate-pulse">
<div class="h-20 bg-slate-200 rounded-lg"></div>
<div class="h-20 bg-slate-200 rounded-lg"></div>
<div class="h-20 bg-slate-200 rounded-lg"></div>
</div>
</template>
</Suspense>
<!-- TELEPORT: Renderizado no final do <body> fora do fluxo do DOM deste card -->
<Teleport to="body">
<div
v-if="isModalOpen"
class="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/60 backdrop-blur-sm p-4"
@click.self="isModalOpen = false"
>
<div class="bg-white rounded-2xl shadow-2xl max-w-md w-full p-6 border space-y-4">
<div class="flex justify-between items-center border-b pb-3">
<h3 class="text-lg font-bold text-slate-900">Configurações Avançadas do Cluster</h3>
<button
@click="isModalOpen = false"
class="text-slate-400 hover:text-slate-600 font-bold text-sm"
>
✕
</button>
</div>
<p class="text-sm text-slate-600">
Este diálogo modal foi teletransportado para o nó <code class="font-mono bg-slate-100 px-1 py-0.5 rounded"><body></code>,
ficando imune a qualquer regra de <code class="font-mono bg-slate-100 px-1 py-0.5 rounded">overflow: hidden</code> ou stacking context restrito de containers pais.
</p>
<div class="flex justify-end gap-2 pt-2">
<button
@click="isModalOpen = false"
class="px-4 py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 font-medium text-xs rounded-lg"
>
Fechar
</button>
</div>
</div>
</div>
</Teleport>
</div>
</template>
🖥️ Saída / Comportamento Esperado
Renderização Inicial:
Durante os primeiros 1.500ms:
O slot #fallback do Suspense exibe 3 caixas de esqueleto cinzas com classe animate-pulse.
Após resolução da Promise (1500ms):
O componente AsyncAnalyticsWidget é montado exibindo:
- Nós Ativos: 16
- Memória Total: 256 GB
- Throughput IOPS: 48,200
Ao clicar em "Abrir Modal de Detalhes":
O Teleport cria o container do diálogo diretamente como filho de <body>.
Backdrop escurecido com efeito backdrop-blur-sm surge cobrindo a tela inteira.
Ao clicar fora do card ou no botão "✕", isModalOpen torna-se false e o nó é removido do <body>.
🔍 Destaques da Implementação
- Top-Level Await Transparente: Em componentes montados dentro de
<Suspense>, o Vue gerencia a espera assíncrona automaticamente. - Desacoplamento de Stacking Context: O
<Teleport to="body">resolve definitivamente problemas comuns de modais cortados por containers com scroll interno. - Skeleton Fallback Nativo: Elimina a necessidade de variáveis manuais
isLoadinge ternários no template.