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">&lt;body&gt;</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

  1. Top-Level Await Transparente: Em componentes montados dentro de <Suspense>, o Vue gerencia a espera assíncrona automaticamente.
  2. Desacoplamento de Stacking Context: O <Teleport to="body"> resolve definitivamente problemas comuns de modais cortados por containers com scroll interno.
  3. Skeleton Fallback Nativo: Elimina a necessidade de variáveis manuais isLoading e ternários no template.