💻 Exemplos: Capítulo 08: Encapsulamento de CSS e Projeção de Conteúdo (ng-content)
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
- Multi-Slot Transclusion:
<ng-content select="[card-header]">distribui os elementos nos locais previstos de acordo com os seletores CSS declarados. - Pseudo-seletor
:host: Aplica estilos diretamente à tag do componente anfitrião (<app-collapsible-card>) sem poluir seletores globais. - Isolamento de Estilos
Emulated: Garante que as classes.card-shelle.card-bodynão colidam com outros componentes da aplicação.