Especialização em Front-End com Angular • Demonstrações Práticas Compiláveis


💻 Código de Exemplo 08 — Projeção Multi-Slot com <ng-content> e Estilização com :host

Este exemplo apresenta a criação de um componente corporativo de cartão expansível (app-collapsible-card) com isolamento de estilos (ViewEncapsulation.Emulated), seletor :host e Projeção de Conteúdo Multi-Slot via seletores semânticos do <ng-content>.

1. Componente de UI com Content Projection: src/app/components/collapsible-card.component.ts

// src/app/components/collapsible-card.component.ts
import { Component, signal, ViewEncapsulation } from '@angular/core';

@Component({
  selector: 'app-collapsible-card',
  standalone: true,
  encapsulation: ViewEncapsulation.Emulated, // Padrão do Angular com atributos _ngcontent-*
  styles: [`
    :host {
      display: block;
      margin-bottom: 1.25rem;
    }
    :host(.is-collapsed) .card-body {
      display: none;
    }
    .card-shell {
      border: 1px solid #e2e8f0;
      border-radius: 1rem;
      background: #ffffff;
      overflow: hidden;
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
      transition: border-color 0.2s;
    }
    .card-shell:hover {
      border-color: #cbd5e1;
    }
  `],
  template: `
    <div class="card-shell">
      <!-- 1. Header com Projeção Nomeada (select="[card-header]") -->
      <header class="p-4 bg-slate-50 border-b flex items-center justify-between">
        <div class="flex items-center gap-3">
          <ng-content select="[card-header]"></ng-content>
        </div>

        <div class="flex items-center gap-2">
          <!-- Projeção de Ações Extras no Cabeçalho -->
          <ng-content select="[card-actions]"></ng-content>

          <button
            type="button"
            (click)="toggleCollapse()"
            class="text-xs px-2.5 py-1 text-slate-500 hover:text-slate-900 bg-white border rounded-md"
          >
            {{ isOpen() ? '▲ Recolher' : '▼ Expandir' }}
          </button>
        </div>
      </header>

      <!-- 2. Corpo do Cartão com Projeção Padrão (Default ng-content) -->
      @if (isOpen()) {
        <div class="card-body p-5 text-sm text-slate-700 space-y-3">
          <ng-content></ng-content>
        </div>
      }

      <!-- 3. Rodapé com Projeção Opcional (select="[card-footer]") -->
      <footer class="p-3 bg-slate-50 border-t text-xs text-slate-500 flex justify-between items-center">
        <ng-content select="[card-footer]">
          <!-- Fallback padrão caso o pai não projete rodapé -->
          <span>Status: Sincronizado</span>
          <span>Atualizado recentemente</span>
        </ng-content>
      </footer>
    </div>
  `,
  host: {
    '[class.is-collapsed]': '!isOpen()'
  }
})
export class CollapsibleCardComponent {
  isOpen = signal<boolean>(true);

  toggleCollapse(): void {
    this.isOpen.update((v) => !v);
  }
}

2. Componente Consumidor Pai: src/app/components/cluster-fleet-view.component.ts

// src/app/components/cluster-fleet-view.component.ts
import { Component } from '@angular/core';
import { CollapsibleCardComponent } from './collapsible-card.component';

@Component({
  selector: 'app-cluster-fleet-view',
  standalone: true,
  imports: [CollapsibleCardComponent],
  template: `
    <div class="max-w-xl mx-auto p-6 space-y-6">
      <h2 class="text-xl font-bold text-slate-800">🖥️ Orquestração de Nós com ng-content</h2>

      <app-collapsible-card>
        <!-- Slot de Cabeçalho Projetado -->
        <div card-header class="flex items-center gap-2">
          <span class="w-2.5 h-2.5 rounded-full bg-emerald-500"></span>
          <h3 class="font-bold text-slate-900 text-sm">Cluster Prod-Alpha-01</h3>
          <span class="text-[10px] px-2 py-0.5 rounded bg-emerald-100 text-emerald-800 font-bold">ONLINE</span>
        </div>

        <!-- Slot de Ações do Cabeçalho -->
        <div card-actions>
          <button class="text-[11px] font-semibold text-indigo-600 hover:underline">Reiniciar Pods</button>
        </div>

        <!-- Conteúdo Principal do Corpo (Default Slot) -->
        <p>
          Este nó executa 18 microsserviços críticos de pagamento com latência de resposta de 24 ms.
        </p>
        <div class="p-3 bg-slate-100 rounded-lg text-xs font-mono">
          CPU: 42% | RAM: 14.2 GB / 32 GB | Rede: 1.2 Gbps
        </div>

        <!-- Slot de Rodapé Customizado -->
        <div card-footer class="flex justify-between w-full">
          <span class="text-slate-500">Região: us-east-1 (N. Virginia)</span>
          <span class="font-mono text-emerald-600 font-bold">SLA: 99.99%</span>
        </div>
      </app-collapsible-card>
    </div>
  `
})
export class ClusterFleetViewComponent {}

🖥️ Saída / Comportamento Esperado

Renderização do Cartão Projetado:
Cabeçalho:
Ponto verde | "Cluster Prod-Alpha-01" | [ONLINE] verde | Link "Reiniciar Pods" | Botão "▲ Recolher"

Corpo:
"Este nó executa 18 microsserviços críticos de pagamento com latência de resposta de 24 ms."
Bloco cinza: "CPU: 42% | RAM: 14.2 GB / 32 GB | Rede: 1.2 Gbps"

Rodapé:
"Região: us-east-1 (N. Virginia)" | "SLA: 99.99%"

Ao clicar em "▲ Recolher":
O signal isOpen() muta para false.
O corpo é descarregado do DOM via @if (isOpen()).
O botão muda para "▼ Expandir".
A classe host .is-collapsed é aplicada ao elemento <app-collapsible-card>.

🔍 Destaques da Implementação

  1. Multi-Slot Transclusion: <ng-content select="[card-header]"> distribui os elementos nos locais previstos de acordo com os seletores CSS declarados.
  2. Pseudo-seletor :host: Aplica estilos diretamente à tag do componente anfitrião (<app-collapsible-card>) sem poluir seletores globais.
  3. Isolamento de Estilos Emulated: Garante que as classes .card-shell e .card-body não colidam com outros componentes da aplicação.