Capítulo 08: Encapsulamento de CSS e Content Projection com ng-content

Especialização em Front-End com Angular • Angular 18+ • Signals, Standalone Architecture, RxJS, NgRx e SSR


🗺️ Mapa Conceitual de Encapsulamento e Projeção de Conteúdo

flowchart TD
    ParentTemplate["Template do Componente Pai"] -->|"Passagem de nós HTML via Slots"| SlotProjection["Mecanismo de Content Projection"]

    subgraph PROJECTION["Projeção de Conteúdo (<ng-content>)"]
        S1["Single-Slot: <ng-content />"]
        S2["Multi-Slot: <ng-content select='[card-header]' />"]
        S3["Fallback Content: <ng-content>Conteúdo Padrão</ng-content>"]
    end

    SlotProjection --> PROJECTION

    subgraph ENCAPSULATION["Estratégias de ViewEncapsulation"]
        E1["Emulated (Padrão: _ngcontent-cXXX)"]
        E2["ShadowDom (Web Components Nativo)"]
        E3["None (Estilos Globais no Header)"]
    end

    PROJECTION --> OutputDOM["Árvore DOM Final Estilizada com Segurança"]
    ENCAPSULATION --> OutputDOM

    style ParentTemplate fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style PROJECTION fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style ENCAPSULATION fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style OutputDOM fill:#e8f5e9,stroke:#4caf50,stroke-width:2px

🏛️ 1. Encapsulamento de Estilos no Angular 18+

No ecossistema Angular, cada componente possui um escopo isolado de estilização CSS através do enum ViewEncapsulation. Esse mecanismo impede que classes com nomes comuns (como .btn, .card, .title) vazem e conflitem com outros componentes da aplicação.

1.1 As Três Estratégias de Encapsulamento

  1. ViewEncapsulation.Emulated (Padrão):
    • O Angular injeta atributos únicos no HTML compilado (ex: _nghost-ng-c123 e _ngcontent-ng-c123) e reescreve os seletores CSS (ex: .card[_ngcontent-ng-c123]).
    • Não usa Shadow DOM real, garantindo compatibilidade universal e alta performance.
  2. ViewEncapsulation.ShadowDom:
    • O Angular isola o componente dentro de uma Shadow Root nativa do navegador.
    • CSS global não penetra no componente e o CSS interno não vaza de forma absoluta.
  3. ViewEncapsulation.None:
    • O CSS declarado no componente é injetado diretamente na tag <head> como estilo global.
import { Component, ViewEncapsulation } from '@angular/core';

@Component({
  selector: 'app-painel-alerta',
  standalone: true,
  encapsulation: ViewEncapsulation.Emulated, // Estratégia padrão recomendada
  styles: [`
    :host {
      display: block;
      border: 1px solid #ccc;
      border-radius: 8px;
      padding: 1rem;
    }
    :host(.alerta-critico) {
      border-color: #f44336;
      background-color: #ffebee;
    }
    .titulo {
      font-weight: 700;
      color: #333;
    }
  `],
  template: `
    <div class="titulo">Alerta do Sistema</div>
    <ng-content />
  `
})
export class PainelAlertaComponent {}

📦 2. Projeção de Conteúdo com <ng-content>

A projeção de conteúdo permite construir componentes altamente reutilizáveis (como modais, cards, layouts de abas e painéis colapsáveis), onde o componente pai decide quais elementos internos serão exibidos.

2.1 Multi-Slot Content Projection (Slots Nomeados)

Você pode segmentar onde cada elemento do pai deve aterrissar utilizando o atributo select com seletores CSS (classes, tags ou atributos):

import { Component } from '@angular/core';

@Component({
  selector: 'app-modal-dialog',
  standalone: true,
  styles: [`
    .modal-box { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
    .modal-header { border-bottom: 1px solid #eee; padding-bottom: 10px; font-size: 1.25rem; font-weight: bold; }
    .modal-body { padding: 15px 0; }
    .modal-footer { border-top: 1px solid #eee; padding-top: 10px; display: flex; justify-content: flex-end; gap: 8px; }
  `],
  template: `
    <div class="modal-box">
      <header class="modal-header">
        <ng-content select="[slot-header]" />
      </header>

      <section class="modal-body">
        <ng-content select="[slot-body]" />
      </section>

      <footer class="modal-footer">
        <!-- Angular 18: Conteúdo Fallback caso o pai não forneça ações -->
        <ng-content select="[slot-footer]">
          <button type="button" class="btn-padrao">Fechar Padrão</button>
        </ng-content>
      </footer>
    </div>
  `
})
export class ModalDialogComponent {}

2.2 Consumo no Componente Pai

<app-modal-dialog>
  <h3 slot-header>Confirmar Exclusão de Registro</h3>
  
  <div slot-body>
    <p>Tem certeza de que deseja excluir permanentemente o item selecionado?</p>
    <small>Esta ação não poderá ser desfeita.</small>
  </div>

  <div slot-footer>
    <button class="btn-cancelar">Cancelar</button>
    <button class="btn-perigo">Confirmar Exclusão</button>
  </div>
</app-modal-dialog>

🎯 3. O Fim do ::ng-deep e Alternativas Modernas

Historicamente, desenvolvedores usavam ::ng-deep para forçar estilos em componentes filhos projetados. Hoje o ::ng-deep está oficialmente descontinuado (deprecated) e deve ser evitado.

As Duas Abordagens Recomendadas:

  1. CSS Custom Properties (Variáveis CSS): O componente filho expõe variáveis CSS que penetram qualquer encapsulamento:
    /* Componente Filho */
    :host {
      background-color: var(--card-bg, #ffffff);
      color: var(--card-color, #333333);
    }
    

    O componente pai customiza diretamente:

    /* Componente Pai */
    app-card-filho {
      --card-bg: #1e1e2f;
      --card-color: #f1f1f1;
    }
    
  2. Pseudo-classe :host-context(): Permite aplicar estilos condicionais dependendo de classes presentes em ancestrais do componente (ex: modo noturno na tag body):
    :host-context(.dark-mode) .modal-box {
      background-color: #121212;
      color: #ffffff;
    }
    

⚖️ 4. Matriz Comparativa: ViewEncapsulation

Critério Emulated ShadowDom None
Isolamento de Estilos Escopo simulado via atributos HTML Isolamento de hardware nativo do browser Nenhum (escopo global no <head>)
Penetração de Estilos Globais Estilos globais afetam seletores internos Estilos globais NÃO entram Estilos se misturam
Uso de Variáveis CSS Suportado Suportado Suportado
Compatibilidade SSR 100% Nativo e Leve Requer suporte do navegador Total

⚠️ 5. Armadilhas Comuns e Anti-Patterns

  1. Estilizar Conteúdo Projetado dentro do CSS do Filho:
    • Erro: O CSS do componente filho NÃO afeta os nós projetados via <ng-content>. Quem estila os elementos projetados é o componente Pai, pois a origem dos nós é dele.
    • Correção: Se o filho precisa forçar estilos nos slots, use o pseudo-elemento :host ::slotted(seletor) ou variáveis CSS.
  2. Múltiplos <ng-content> sem seletor (select):
    • Se você colocar dois <ng-content /> sem atributo select, o Angular projetará todos os elementos no primeiro slot e deixará o segundo vazio.

🛡️ 6. Boas Práticas na Construção de Design Systems

  • Crie Componentes Agósticos de Layout: Use :host { display: block; } por padrão, pois custom elements são inline por especificação do HTML.
  • Fallbacks Nativos: Aproveite o recurso do Angular 18 para definir fallbacks dentro de <ng-content>, reduzindo a necessidade de lógica condicional com @if para exibir estados vazios.

📋 7. Cheat-Sheet: Síntese de Seletores Especiais

:host                    /* Estiliza o próprio custom element (<app-card>) */
:host(.ativo)            /* Aplica se o host tiver a classe 'ativo' */
:host-context(.dark)     /* Aplica se algum elemento ancestral tiver '.dark' */
::slotted(button)        /* Estiliza elementos projetados (no ShadowDom) */