Como a diretiva declarativa em GA no Winter ’27 permite renderizar Custom Elements padronizados (Lit, Stencil ou JavaScript puro) diretamente nos templates do LWC sob o isolamento do Lightning Web Security.

Durante anos, qualquer arquiteto ou desenvolvedor que precisasse embutir um componente visual da Web moderna dentro de uma tela Lightning caía no mesmo beco sem saída. Ou aceitava o isolamento pesado de um <iframe>, ou tentava a arriscada gambiarra de injetar nós no DOM via renderedCallback() e torcia para o motor de reconciliação do Virtual DOM não quebrar no deploy seguinte.

Se a sua equipe corporativa já mantinha um Design System maduro construído em Lit, Stencil, Shoelace ou web components nativos, a conversa com os líderes de projeto costumava ser frustrante. Para trazer qualquer gráfico interativo, planilha matricial ou widget de assinatura para o Salesforce, a resposta padrão era quase sempre a mesma: reescrever tudo do zero em LWC. Duplicação de esforço, débito técnico dobrado e manutenção em dose dupla.

No release Salesforce Winter ’27, essa barreira histórica caiu com a chegada em Disponibilidade Geral (GA) da diretiva lwc:external.


1. Como a diretiva lwc:external opera debaixo do capô

Por padrão, o compilador de templates do LWC é extremamente rigoroso. Se você tentar colocar uma tag HTML desconhecida que fuja das tags nativas (<div>, <span>, <input>) e não carregue o prefixo de namespace registrado (como <c-meu-card>), o build falha de imediato com erro de sintaxe.

A diretiva booleana lwc:external funciona como um passe livre para o compilador. Ela instrui o compilador a ignorar a checagem de namespace e delegar a criação daquele elemento diretamente ao registro global do navegador (window.customElements):

<template>
    <!-- O compilador do LWC delega a instanciação para o Custom Element nativo -->
    <kpi-sparkline 
        lwc:external 
        score={currentScore} 
        metric-label={currentLabel} 
        status-color={statusColor}
        onkpiclick={handleKpiClick}>
    </kpi-sparkline>
</template>

A mágica acontece em quatro tempos:

  1. Casca no Virtual DOM: O LWC cria o nó no Virtual DOM e reserva o espaço no layout.
  2. Resolução de Instância: O navegador consulta a definição registrada previamente via customElements.define('kpi-sparkline', ...) e instancia a classe.
  3. Reflexão Reativa: Todas as propriedades ligadas no template do LWC são repassadas como atributos do Custom Element. Quando o estado reativo do LWC muda no JavaScript, o Custom Element aciona seu ciclo de vida nativo (attributeChangedCallback).
  4. Captura Declarativa de Eventos: Eventos customizados emitidos internamente pelo web component são ouvidos diretamente pela sintaxe onnomedoevento do LWC, exatamente como se fosse um componente Lightning padrão.

2. A matriz obrigatória de segurança: Lightning Web Security (LWS)

Não tente rodar lwc:external sob o antigo Lightning Locker. Não vai funcionar.

O Locker legado operava criando proxies defensivos em volta de praticamente todos os objetos do DOM e interceptava agressivamente a chamada a customElements.define(), bloqueando tags que não tivessem sido geradas pelas fábricas proprietárias da Salesforce.

Já o Lightning Web Security (LWS) adota o padrão moderno da Web de isolamento por Sandboxes (compartimentos de JavaScript no navegador). Dentro do sandbox isolado do seu namespace, os objetos globais permanecem íntegros. A API customElements.define() funciona com liberdade e segurança, permitindo o registro de bibliotecas modernas sem atritos de runtime.

Importante: Certifique-se de que o LWS esteja ativado em:
Setup -> Session Settings -> Use Lightning Web Security for Lightning Web Components (and Aura).


3. As três regras de ouro para componentes de terceiros

Embora a diretiva facilite a marcação HTML, qualquer componente externo precisa respeitar três restrições fundamentais de arquitetura para conviver em paz com o ecossistema Salesforce.

Regra 1: Uso obrigatório de Closed Shadow DOM

No desenvolvimento Web tradicional fora do Salesforce, a maioria das bibliotecas cria a raiz do Shadow DOM no modo aberto (mode: 'open'). No ecossistema Lightning sob LWS, componentes de terceiros devem utilizar Closed Shadow DOM (mode: 'closed'):

// ❌ Quebra o encapsulamento esperado pelo LWS:
this.shadowRoot = this.attachShadow({ mode: 'open' });

// ✅ Isolamento completo e compatível:
this._shadow = this.attachShadow({ mode: 'closed' });
this._shadow.innerHTML = `<div class="card">Conteúdo Seguro</div>`;

Ao inspecionar o código-fonte de um componente de terceiros antes de empacotar, garanta que todas as chamadas internas a this.shadowRoot sejam substituídas por uma referência privada (como this._shadow).

Regra 2: Proibição estrita de CDNs e imports remotos

A política de segurança de conteúdo (Content Security Policy – CSP) da Salesforce bloqueia conexões e downloads dinâmicos de scripts hospedados em servidores externos ou CDNs públicas (https://cdn.jsdelivr.net/...).

Todo o componente externo precisa ser transpilado em um arquivo JavaScript único, autocontido, e carregado na organização como um Static Resource. Nada de imports dinâmicos em tempo de execução.

Regra 3: Disparo de eventos com composed e bubbles

Quando o componente externo dispara um evento que precisa ser ouvido pelo LWC pai, o evento precisa ser emitido explicitamente com bubbles: true e composed: true:

// Dentro do Custom Element externo:
this.dispatchEvent(new CustomEvent('kpiclick', {
    detail: { 
        scoreValue: score, 
        metricName: label,
        timestamp: new Date().toLocaleTimeString()
    },
    bubbles: true,
    composed: true // Obrigatório para atravessar a barreira do Shadow DOM!
}));

Sem a flag composed: true, o evento morre dentro do Shadow DOM do componente externo e o LWC jamais disparará o manipulador onkpiclick.


4. O laboratório prático: integrando o widget kpi-sparkline

Para comprovar a teoria em nossa org onlysalesforce_cgc_1, construímos um cenário real de negócios: um widget de KPI com barra de progresso, cores dinâmicas e emissão de eventos de clique.

O componente externo (KpiSparklineBundle.resource)

Empacotamos o script a seguir em um Static Resource chamado KpiSparklineBundle:

/**
 * Custom Element de Terceiros: kpi-sparkline
 * Conformidade estrita com LWS: Closed Shadow DOM e eventos composed.
 */
(function () {
    class KpiSparklineElement extends HTMLElement {
        constructor() {
            super();
            this._shadow = this.attachShadow({ mode: 'closed' });
        }

        static get observedAttributes() {
            return ['score', 'metric-label', 'status-color'];
        }

        connectedCallback() {
            this.render();
        }

        attributeChangedCallback(name, oldValue, newValue) {
            if (oldValue !== newValue) {
                this.render();
            }
        }

        render() {
            const score = this.getAttribute('score') || '0';
            const label = this.getAttribute('metric-label') || 'Indicador';
            const color = this.getAttribute('status-color') || '#0176D3';

            this._shadow.innerHTML = `
                <style>
                    :host {
                        display: inline-block;
                        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                        width: 100%;
                        max-width: 420px;
                    }
                    .kpi-card {
                        border: 1px solid #c9c7c5;
                        border-radius: 8px;
                        padding: 16px 20px;
                        background: #ffffff;
                        cursor: pointer;
                        box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
                        transition: all 0.25s ease-in-out;
                    }
                    .kpi-card:hover {
                        box-shadow: 0 6px 16px rgba(1, 118, 211, 0.25);
                        border-color: #0176D3;
                        transform: translateY(-2px);
                    }
                    .score {
                        font-size: 36px;
                        font-weight: 800;
                        color: ${color};
                    }
                    .progress-bar-bg {
                        width: 100%;
                        height: 8px;
                        background: #eef1f6;
                        border-radius: 4px;
                        margin-top: 14px;
                    }
                    .progress-bar-fill {
                        height: 100%;
                        width: ${Math.min(Math.max(parseInt(score, 10) || 0, 0), 100)}%;
                        background: ${color};
                        border-radius: 4px;
                    }
                </style>
                <div class="kpi-card" id="cardContainer">
                    <div class="label">${label}</div>
                    <div class="score">${score}%</div>
                    <div class="progress-bar-bg">
                        <div class="progress-bar-fill"></div>
                    </div>
                </div>
            `;

            const card = this._shadow.getElementById('cardContainer');
            if (card) {
                card.onclick = () => {
                    this.dispatchEvent(new CustomEvent('kpiclick', {
                        detail: { 
                            scoreValue: score, 
                            metricName: label,
                            timestamp: new Date().toLocaleTimeString()
                        },
                        bubbles: true,
                        composed: true
                    }));
                };
            }
        }
    }

    if (!customElements.get('kpi-sparkline')) {
        customElements.define('kpi-sparkline', KpiSparklineElement);
    }
})();

O controlador do LWC (kpiDashboardViewer.js)

No controlador, importamos o Static Resource através do loadScript de lightning/platformResourceLoader. O script só precisa ser executado uma vez para registrar a tag no catálogo do navegador:

import { LightningElement, track } from 'lwc';
import { loadScript } from 'lightning/platformResourceLoader';
import KPI_SPARKLINE_BUNDLE from '@salesforce/resourceUrl/KpiSparklineBundle';

export default class KpiDashboardViewer extends LightningElement {
    @track isLibraryLoaded = false;
    @track currentScore = 88;
    @track currentLabel = 'Aderência de SLA';
    @track statusColor = '#2e844a';
    @track lastEventReceived = '';

    renderedCallback() {
        if (this.isLibraryLoaded) {
            return;
        }

        loadScript(this, KPI_SPARKLINE_BUNDLE)
            .then(() => {
                this.isLibraryLoaded = true;
            })
            .catch((error) => {
                console.error('Erro ao carregar KpiSparklineBundle:', error);
            });
    }

    handleKpiClick(event) {
        const detail = event.detail;
        this.lastEventReceived = `Evento "kpiclick" capturado com sucesso! Indicador: "${detail.metricName}" | Score: ${detail.scoreValue}% | Horário: ${detail.timestamp}`;
    }

    handleRandomScore() {
        this.currentScore = Math.floor(Math.random() * 45) + 55;
        this.statusColor = this.currentScore >= 75 ? '#2e844a' : (this.currentScore >= 60 ? '#fe9339' : '#ea001e');
    }

    handleSetSales() {
        this.currentScore = 94;
        this.currentLabel = 'Volume de Vendas CGC';
        this.statusColor = '#2e844a';
    }

    handleSetStock() {
        this.currentScore = 42;
        this.currentLabel = 'Risco de Ruptura de Gôndola';
        this.statusColor = '#ea001e';
    }
}

O template declarativo (kpiDashboardViewer.html)

O HTML final utiliza a diretiva lwc:external e condiciona a renderização ao carregamento da biblioteca com lwc:if:

<template>
    <lightning-card title="Painel com Web Component de Terceiros (lwc:external)" icon-name="standard:dashboard">
        <div class="slds-p-around_medium">
            <template lwc:if={isLibraryLoaded}>
                <kpi-sparkline 
                    lwc:external 
                    score={currentScore} 
                    metric-label={currentLabel} 
                    status-color={statusColor}
                    onkpiclick={handleKpiClick}>
                </kpi-sparkline>

                <div class="slds-m-top_medium">
                    <lightning-button label="Simular Medição" variant="brand" onclick={handleRandomScore}></lightning-button>
                    <lightning-button label="Meta de Vendas (94%)" variant="neutral" onclick={handleSetSales}></lightning-button>
                    <lightning-button label="Ruptura de Estoque (42%)" variant="neutral" onclick={handleSetStock}></lightning-button>
                </div>

                <template lwc:if={lastEventReceived}>
                    <div class="slds-notify slds-notify_alert slds-theme_info slds-m-top_medium">
                        <h2><strong>{lastEventReceived}</strong></h2>
                    </div>
                </template>
            </template>
            
            <template lwc:else>
                <lightning-spinner alternative-text="Carregando..." size="medium"></lightning-spinner>
            </template>
        </div>
    </lightning-card>
</template>

5. Comparativo arquitetural: o fim das pontes improvisadas

A tabela a seguir resume as diferenças cruciais entre os métodos legados e a abordagem oficial no Winter ’27:

Aspecto de ArquiteturaAbordagem com <iframe>Abordagem com DOM ImperativoNova Abordagem com lwc:external
Integração com SLDSNula. Iframe bloqueia herança de estilos e tokens.Frágil. Estilos globais colidem com facilidade.Perfeita. Compartilha contexto e tokens do pai.
Passagem de PropriedadespostMessage assíncrono, lento e inseguro.Atribuição manual no nó do DOM via JS.Declarativa e reativa via marcação HTML.
Escuta de Eventoswindow.addEventListener com parsing manual de string.element.addEventListener procedural em nós soltos.Nativa via sintaxe padrão onnomedoevento.
Performance e MemóriaJanela separada do browser, alto custo de RAM.Risco contínuo de descompasso do Virtual DOM.Máxima. Compilado e gerenciado pelo Virtual DOM.
ManutenibilidadePobre. Múltiplos pontos de falha e delays de tela.Péssima. Propenso a quebras em upgrades da Salesforce.Excelente. Código limpo, moderno e sustentável.

6. Cuidados e armadilhas em produção

Antes de sair convertendo todos os componentes do seu repositório para lwc:external, atenção a três lições práticas que aprendemos na bancada de testes:

  1. Proteção contra registro duplicado no catálogo: O navegador lança uma exceção irrecuperável se você tentar executar customElements.define('minha-tag', ...) mais de uma vez na mesma sessão do browser. Sempre proteja seu script com:if (!customElements.get('kpi-sparkline')) { customElements.define('kpi-sparkline', KpiSparklineElement); }
  2. Empacotamento com Vite, Rollup ou Webpack: Se você estiver aproveitando componentes criados com Lit ou Stencil a partir de pacotes npm, realize o build gerando um bundle único (.iife.js ou bundle autocontido) sem dependências externas de node_modules antes de subir como Static Resource.
  3. Acessibilidade e navegação por teclado: Lembre-se de que o Shadow DOM fechado impede que scripts externos manipulem o foco interno de forma arbitrária. Garanta que botões e elementos interativos do Custom Element contenham atributos role, aria-label e suporte natural à tecla Tab.

Para levar para a sprint

A graduação para Disponibilidade Geral do lwc:external no Winter ’27 encerra uma das maiores queixas de arquitetura de frontend no ecossistema Salesforce.

A interoperabilidade entre o LWC e os padrões W3C de Custom Elements agora é uma realidade pronta para produção. Times que possuem bibliotecas de componentes construídas fora da Salesforce podem finalmente reaproveitar seu trabalho de design e desenvolvimento sem pagar o pedágio de desempenho dos iFrames ou o risco de estabilidade das montagens manuais no DOM.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *