Shadow DOM v1: Componentes web independientes

Shadow DOM permite a los desarrolladores web crear DOM y CSS compartimentados para componentes web

Resumen

Shadow DOM elimina la fragilidad en la creación de aplicaciones web. La fragilidad proviene de la naturaleza global de HTML, CSS y JS. A lo largo de los años, hemos inventó un número exorbitante. de herramientas para eludir los problemas. Por ejemplo, cuando usas un nuevo ID o clase HTML, no se sabe si va a entrar en conflicto con un nombre existente que usa la página. Se producen insectos sutiles. La especificidad de CSS se convierte en un gran problema (¡!important todo lo relacionado!), el estilo los selectores se salen de control y el rendimiento puede verse afectado. La lista continúa.

Shadow DOM corrige CSS y DOM. Incorpora estilos específicos en la Web. plataforma. Sin herramientas ni convenciones de nombres, puedes agrupar CSS con el lenguaje de marcado, oculta los detalles de implementación y crea contenido componentes en JavaScript convencional.

Introducción

Shadow DOM es uno de los tres estándares de componentes web: Plantillas HTML, Shadow DOM y Elementos personalizados. Importaciones de HTML solían formar parte de la lista, pero ahora se consideran obsoletas.

No es necesario crear componentes web que usen shadow DOM. Pero cuando lo haces, aprovecha sus beneficios (alcance de CSS, encapsulamiento del DOM, composición) y compilar soluciones elementos personalizados que son resilientes, altamente configurables y extremadamente reutilizables. Si está personalizada son la forma de crear un nuevo HTML (con una API de JS), shadow DOM es la la manera en que proporcionas el código HTML y CSS. Las dos APIs se combinan para formar un componente con HTML, CSS y JavaScript independientes.

Shadow DOM está diseñado como una herramienta para crear apps basadas en componentes. Por lo tanto, aporta soluciones para problemas comunes del desarrollo web:

  • DOM aislado: El DOM de un componente es independiente (p.ej., document.querySelector() no mostrará nodos en el shadow DOM del componente).
  • CSS con alcance: El CSS definido dentro del shadow DOM está limitado a él. Reglas de estilo no se filtren y los estilos de la página no ingresen.
  • Composición: Diseña una API declarativa y basada en lenguaje de marcado para tu componente.
  • Simplifica el CSS: Un DOM con alcance implica que puedes usar selectores CSS simples y mucho más. nombres de id/clase genéricos, y no preocuparse por conflictos de nombres.
  • Productividad: piensa en las aplicaciones como fragmentos del DOM en lugar de uno grande. (global).

Demostración de fancy-tabs

En este artículo, haré referencia a un componente de demostración (<fancy-tabs>). y hacer referencia a sus fragmentos de código. Si tu navegador es compatible con las APIs, debería ver una demostración en vivo a continuación. De lo contrario, consulta la fuente completa en GitHub.

. Ver el código fuente en GitHub .

¿Qué es shadow DOM?

Información general del DOM

El lenguaje HTML se usa en la Web porque es fácil de usar. Si declaras algunas etiquetas, puedes crear una página en segundos que tenga presentación y estructura. Sin embargo, por sí solo, HTML no es nada útil. Es fácil para las personas comprender un texto lenguaje basado en la nube, pero las máquinas necesitan algo más. Ingresa el objeto Document Model, o DOM.

Cuando el navegador carga una página web, realiza muchas acciones interesantes. Uno de lo que hace es transformar el HTML del autor en un documento activo. Básicamente, para comprender la estructura de la página, el navegador analiza HTML (estático, cadenas de texto) en un modelo de datos (objetos/nodos). El navegador conserva el la jerarquía de HTML creando un árbol de estos nodos: el DOM. Lo genial del DOM es que es una representación activa de tu página. A diferencia del modelo El código HTML que creamos, los nodos producidos por el navegador contienen propiedades, métodos y prácticas pueden manipularse con programas. Por eso podemos crear DOM directamente usando JavaScript:

const header = document.createElement('header');
const h1 = document.createElement('h1');
h1.textContent = 'Hello DOM';
header.appendChild(h1);
document.body.appendChild(header);

produce el siguiente lenguaje de marcado HTML:

<body>
    <header>
    <h1>Hello DOM</h1>
    </header>
</body>

Todo eso está bien y es bueno. Después ¿qué diablos es shadow DOM?

DOM... en las sombras

Shadow DOM es simplemente un DOM normal, con dos diferencias: 1) cómo se crea o usa, y 2) cómo se comporta en relación con el resto de la página. Normalmente, creas un DOM nodos y los adjuntan como secundarios de otro elemento. Con shadow DOM, puedes crear un árbol del DOM con ámbito que se adjunte al elemento, pero separado de su hijos. Este subárbol con alcance se denomina shadow tree (árbol en las sombras). El elemento al que se conecta es su shadow host. Todo lo que agregas en las sombras se convierte en local al elemento de hosting, incluido <style>. Así es como shadow DOM logra un alcance de estilo CSS.

Cómo crear un shadow DOM

Una shadow root (raíz en la sombra) es un fragmento de documento que se adjunta a un elemento "host". Al adjuntar una shadow root, el elemento obtiene su shadow DOM. Para Crea un shadow DOM para un elemento y llama a element.attachShadow():

const header = document.createElement('header');
const shadowRoot = header.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>'; // Could also use appendChild().

// header.shadowRoot === shadowRoot
// shadowRoot.host === header

Usaré .innerHTML para completar la shadow root, pero también podrías usar otro DOM APIs Esta es la Web. Tenemos opciones.

La especificación define una lista de elementos que no pueden alojar un shadow tree. Existen varias razones por las que un elemento puede ser en la lista:

  • El navegador ya aloja su propio shadow DOM interno para el elemento. (<textarea>, <input>).
  • No tiene sentido que el elemento aloje un shadow DOM (<img>).

Por ejemplo, esto no funciona:

    document.createElement('input').attachShadow({mode: 'open'});
    // Error. `<input>` cannot host shadow dom.

Cómo crear un shadow DOM para un elemento personalizado

Shadow DOM es particularmente útil para crear elementos personalizados. Usar shadow DOM para compartimentar el HTML, CSS y JS de un elemento, de modo que produciendo un “componente web”.

Ejemplo: un elemento personalizado adjunta un shadow DOM a sí mismo, encapsulando su DOM/CSS:

// Use custom elements API v1 to register a new HTML tag and define its JS behavior
// using an ES6 class. Every instance of <fancy-tab> will have this same prototype.
customElements.define('fancy-tabs', class extends HTMLElement {
    constructor() {
    super(); // always call super() first in the constructor.

    // Attach a shadow root to <fancy-tabs>.
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `
        <style>#tabs { ... }</style> <!-- styles are scoped to fancy-tabs! -->
        <div id="tabs">...</div>
        <div id="panels">...</div>
    `;
    }
    ...
});

Aquí ocurren varios eventos interesantes. La primera es que el el elemento personalizado crea su propio shadow DOM cuando se crea una instancia de <fancy-tabs>. cuando se cree. Eso se hace en constructor(). En segundo lugar, como estamos creando como shadow root, las reglas de CSS dentro de <style> se establecerán en el alcance de <fancy-tabs>.

Composición y ranuras

La composición es una de las funciones menos comprendidas de shadow DOM, pero es sin duda el más importante.

En nuestro mundo de desarrollo web, la composición es la forma en que construimos aplicaciones. a partir de HTML de forma declarativa. Diferentes componentes básicos (<div>, <header>, <form> y <input>) se unen para formar apps. Algunas de estas etiquetas incluso funcionan entre sí. La composición es el motivo por el que elementos nativos como <select>, <details>, <form> y <video> son muy flexibles. Cada una de esas etiquetas acepta ciertos HTML como secundarios y hace algo especial con ellos. Por ejemplo: <select> sabe cómo renderizar <option> y <optgroup> en el menú desplegable y de selección múltiple. El elemento <details> renderiza <summary> como una flecha expandible. Incluso <video> sabe cómo tratar con determinados elementos secundarios: Los elementos <source> no se renderizan, pero sí afectan el comportamiento del video. ¡Qué magia!

Terminología: Light DOM frente a shadow DOM

La composición de Shadow DOM introduce un conjunto de nuevos aspectos básicos en la Web en el desarrollo de software. Antes de entrar en los detalles, estandarizemos algunas la terminología, así que hablamos la misma jerga.

DOM claro

El lenguaje de marcado que escribe un usuario de tu componente. Este DOM se encuentra fuera del shadow DOM de este componente. Consta de los elementos secundarios reales.

<better-button>
    <!-- the image and span are better-button's light DOM -->
    <img src="gear.svg" slot="icon">
    <span>Settings</span>
</better-button>

Shadow DOM

Es el DOM que escribe el autor del componente. Shadow DOM es local del componente y Define su estructura interna, CSS con alcance y encapsula tu implementación más detalles. También puede definir cómo renderizar el lenguaje de marcado que creó el consumidor. de tu componente.

#shadow-root
    <style>...</style>
    <slot name="icon"></slot>
    <span id="wrapper">
    <slot>Button</slot>
    </span>

Árbol del DOM plano

El resultado de que el navegador distribuya el Light DOM del usuario a tu sombra DOM, renderizando el producto final. El árbol aplanado es lo que finalmente se ve en las Herramientas para desarrolladores y lo que se renderiza en la página.

<better-button>
    #shadow-root
    <style>...</style>
    <slot name="icon">
        <img src="gear.svg" slot="icon">
    </slot>
    <span id="wrapper">
        <slot>
        <span>Settings</span>
        </slot>
    </span>
</better-button>

El <slot> elemento

El Shadow DOM compone distintos árboles DOM con el elemento <slot>. Los espacios son marcadores de posición dentro de tu componente que los usuarios pueden completar con sus propio lenguaje de marcado. Cuando defines uno o más espacios, permites que el lenguaje de marcado externo se renderice. en el shadow DOM de tu componente. Esencialmente, estás diciendo "Renderiza el nombre del usuario el lenguaje de marcado de aquí”.

Los elementos pueden "cruzarse" el límite de shadow DOM cuando un <slot> invita para permitirles la entrada. Estos elementos se denominan nodos distribuidos. Conceptualmente, los nodos distribuidos puede parecer un poco extraño. Los espacios no mueven físicamente el DOM; no representarlos en otra ubicación dentro del shadow DOM.

Un componente puede definir cero o más ranuras en su shadow DOM. Las ranuras pueden estar vacías o proporcionar contenido alternativo. Si el usuario no proporciona light DOM contenido, el espacio publicitario renderiza su contenido de resguardo.

<!-- Default slot. If there's more than one default slot, the first is used. -->
<slot></slot>

<slot>fallback content</slot> <!-- default slot with fallback content -->

<slot> <!-- default slot entire DOM tree as fallback -->
    <h2>Title</h2>
    <summary>Description text</summary>
</slot>

También puedes crear ranuras con nombre. Las ranuras con nombre son orificios específicos en tu shadow DOM al que los usuarios hacen referencia por su nombre.

Ejemplo: los espacios en el shadow DOM de <fancy-tabs>:

#shadow-root
    <div id="tabs">
    <slot id="tabsSlot" name="title"></slot> <!-- named slot -->
    </div>
    <div id="panels">
    <slot id="panelsSlot"></slot>
    </div>

Los usuarios de componentes declaran <fancy-tabs> de la siguiente manera:

<fancy-tabs>
    <button slot="title">Title</button>
    <button slot="title" selected>Title 2</button>
    <button slot="title">Title 3</button>
    <section>content panel 1</section>
    <section>content panel 2</section>
    <section>content panel 3</section>
</fancy-tabs>

<!-- Using <h2>'s and changing the ordering would also work! -->
<fancy-tabs>
    <h2 slot="title">Title</h2>
    <section>content panel 1</section>
    <h2 slot="title" selected>Title 2</h2>
    <section>content panel 2</section>
    <h2 slot="title">Title 3</h2>
    <section>content panel 3</section>
</fancy-tabs>

Y si te lo preguntan, el árbol aplanado luce así:

<fancy-tabs>
    #shadow-root
    <div id="tabs">
        <slot id="tabsSlot" name="title">
        <button slot="title">Title</button>
        <button slot="title" selected>Title 2</button>
        <button slot="title">Title 3</button>
        </slot>
    </div>
    <div id="panels">
        <slot id="panelsSlot">
        <section>content panel 1</section>
        <section>content panel 2</section>
        <section>content panel 3</section>
        </slot>
    </div>
</fancy-tabs>

Ten en cuenta que nuestro componente puede controlar diferentes configuraciones, pero el el árbol del DOM plano permanece igual. También podemos cambiar de <button> a <h2> Este componente se creó para controlar diferentes tipos de elementos secundarios... como lo hace <select>.

Diseño

Hay muchas opciones de diseño para los componentes web. Un componente que usa sombra Se puede aplicar estilo al DOM en la página principal, definir sus propios estilos o proporcionar hooks (en con el formato de propiedades personalizadas de CSS) para que los usuarios anulen los valores predeterminados.

Estilos definidos por el componente

Sin lugar a dudas, la función más útil de los shadow DOM es CSS con ámbito:

  • Los selectores CSS de la página externa no se aplican dentro de tu componente.
  • Los estilos definidos en el interior no se difuminan. Su alcance se define en relación con el elemento de host.

Los selectores CSS que se usan dentro de shadow DOM se aplican a tu componente de forma local. En práctica, esto significa que podemos usar nombres de ID/clase comunes de nuevo, sin preocuparnos sobre conflictos en otra parte de la página. Se recomienda usar selectores CSS más simples dentro de Shadow DOM. También son buenas para el rendimiento.

Ejemplo: Los estilos definidos en una shadow root son locales.

#shadow-root
    <style>
    #panels {
        box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
        background: white;
        ...
    }
    #tabs {
        display: inline-flex;
        ...
    }
    </style>
    <div id="tabs">
    ...
    </div>
    <div id="panels">
    ...
    </div>

Las hojas de estilo también se limitan al shadow tree:

#shadow-root
    <link rel="stylesheet" href="styles.css">
    <div id="tabs">
    ...
    </div>
    <div id="panels">
    ...
    </div>

¿Alguna vez te preguntaste cómo el elemento <select> renderiza un widget de selección múltiple (en lugar de un menú desplegable) cuando agregas el atributo multiple:

<select multiple>
  <option>Do</option>
  <option selected>Re</option>
  <option>Mi</option>
  <option>Fa</option>
  <option>So</option>
</select>

<select> puede usarse de manera diferente según los atributos que declarar en él. Los componentes web también pueden darse estilo a sí mismos si usas :host. selector.

Ejemplo: un componente que se define automáticamente

<style>
:host {
    display: block; /* by default, custom elements are display: inline */
    contain: content; /* CSS containment FTW. */
}
</style>

Un problema con :host es que las reglas de la página principal son más específicas. de :host reglas definidas en el elemento. Es decir, los estilos exteriores tienen prioridad. Esta permite a los usuarios anular tu estilo de nivel superior desde el exterior. Además, :host solo funciona en el contexto de una shadow root, por lo que no se puede usar fuera de shadow DOM.

El formato funcional de :host(<selector>) te permite orientar al host si coincide con un <selector>. Esta es una excelente manera de que tu componente encapsule comportamientos que reaccionan a la interacción del usuario, al estado o al diseño de nodos internos basados en en el host.

<style>
:host {
    opacity: 0.4;
    will-change: opacity;
    transition: opacity 300ms ease-in-out;
}
:host(:hover) {
    opacity: 1;
}
:host([disabled]) { /* style when host has disabled attribute. */
    background: grey;
    pointer-events: none;
    opacity: 0.4;
}
:host(.blue) {
    color: blue; /* color host when it has class="blue" */
}
:host(.pink) > #tabs {
    color: pink; /* color internal #tabs node when host has class="pink". */
}
</style>

Estilos basados en el contexto

:host-context(<selector>) coincide con el componente si este o cualquiera de sus principales. coincide con <selector>. Un uso común de esto es la aplicación de temas según la dirección de su entorno. Por ejemplo, muchas personas aplican temas aplicando una clase a <html> o <body>:

<body class="darktheme">
    <fancy-tabs>
    ...
    </fancy-tabs>
</body>

:host-context(.darktheme) le daría estilo a <fancy-tabs> cuando es un elemento subordinado. de .darktheme:

:host-context(.darktheme) {
    color: white;
    background: black;
}

:host-context() puede ser útil para la creación de temas, pero un enfoque aún mejor es Crea hooks de estilo con las propiedades personalizadas de CSS.

Cómo aplicar diseño a nodos distribuidos

::slotted(<compound-selector>) coincide con los nodos que se distribuyen en un <slot>

Supongamos que hemos creado un componente de identificación:

<name-badge>
    <h2>Eric Bidelman</h2>
    <span class="title">
    Digital Jedi, <span class="company">Google</span>
    </span>
</name-badge>

El shadow DOM del componente puede aplicar diseño a <h2> y .title del usuario:

<style>
::slotted(h2) {
    margin: 0;
    font-weight: 300;
    color: red;
}
::slotted(.title) {
    color: orange;
}
/* DOESN'T WORK (can only select top-level nodes).
::slotted(.company),
::slotted(.title .company) {
    text-transform: uppercase;
}
*/
</style>
<slot></slot>

Como vimos antes, los objetos <slot> no mueven el Light DOM del usuario. Cuándo los nodos se distribuyen en un <slot>, el <slot> renderiza su DOM, pero el queden físicamente en su lugar. Los estilos que se aplicaron antes de la distribución continúan a se aplican después de la distribución. Sin embargo, cuando el Light DOM se distribuye, puede estilos adicionales (definidos por el shadow DOM).

Otro ejemplo más detallado de <fancy-tabs>:

const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
    <style>
    #panels {
        box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
        background: white;
        border-radius: 3px;
        padding: 16px;
        height: 250px;
        overflow: auto;
    }
    #tabs {
        display: inline-flex;
        -webkit-user-select: none;
        user-select: none;
    }
    #tabsSlot::slotted(*) {
        font: 400 16px/22px 'Roboto';
        padding: 16px 8px;
        ...
    }
    #tabsSlot::slotted([aria-selected="true"]) {
        font-weight: 600;
        background: white;
        box-shadow: none;
    }
    #panelsSlot::slotted([aria-hidden="true"]) {
        display: none;
    }
    </style>
    <div id="tabs">
    <slot id="tabsSlot" name="title"></slot>
    </div>
    <div id="panels">
    <slot id="panelsSlot"></slot>
    </div>
`;

En este ejemplo, hay dos espacios: uno con nombre para los títulos de las pestañas y otro para el contenido del panel de pestañas. Cuando el usuario selecciona una pestaña, ponemos en negrita su selección. y revelar su panel. Para ello, debes seleccionar nodos distribuidos que tengan atributo selected. El JS del elemento personalizado (que no se muestra aquí) agrega que en el momento correcto.

Cómo aplicar diseño a un componente desde el exterior

Existen varias formas de aplicar diseño a un componente desde el exterior. La forma más fácil es usar el nombre de la etiqueta como selector:

fancy-tabs {
    width: 500px;
    color: red; /* Note: inheritable CSS properties pierce the shadow DOM boundary. */
}
fancy-tabs:hover {
    box-shadow: 0 3px 3px #ccc;
}

Los estilos externos siempre prevalecen sobre los estilos definidos en el shadow DOM. Por ejemplo: si el usuario escribe el selector fancy-tabs { width: 500px; }, prevalecerá la regla del componente: :host { width: 650px;}.

No será suficiente con definir el estilo propio del componente. Pero ¿qué sucede si quieres diseñar el interior de un componente? Para eso, necesitamos CSS propiedades.

Cómo crear hooks de estilo con las propiedades personalizadas de CSS

Los usuarios pueden ajustar los estilos internos si el autor del componente proporciona enlaces de estilo. con las propiedades personalizadas de CSS. Conceptualmente, la idea es similar a <slot> Creas "marcadores de posición de estilo" para que los usuarios los anulen.

Ejemplo: <fancy-tabs> permite que los usuarios anulen el color de fondo.

<!-- main page -->
<style>
    fancy-tabs {
    margin-bottom: 32px;
    --fancy-tabs-bg: black;
    }
</style>
<fancy-tabs background>...</fancy-tabs>

Dentro de su shadow DOM:

:host([background]) {
    background: var(--fancy-tabs-bg, #9E9E9E);
    border-radius: 10px;
    padding: 10px;
}

En este caso, el componente usará black como valor de fondo, ya que el elemento lo proporcionó el usuario. De lo contrario, se establecería de forma predeterminada como #9E9E9E.

Temas avanzados

Cómo crear shadow root cerradas (debe evitarse)

Existe otra clase de shadow DOM llamada "cerrado" . Cuando creas un Se cerró el shadow tree, fuera de JavaScript, no se podrá acceder al DOM interno de tu componente. Esto es similar al funcionamiento de los elementos nativos, como <video>. JavaScript no puede acceder al shadow DOM de <video> porque el navegador lo implementa con una shadow root de modo cerrado.

Ejemplo. Crea un shadow tree cerrado:

const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'closed'}); // close shadow tree
// div.shadowRoot === null
// shadowRoot.host === div

Otras APIs también se ven afectadas por el modo cerrado:

  • Element.assignedSlot / TextNode.assignedSlot devuelve null
  • Event.composedPath() para eventos asociados con elementos dentro de la sombra DOM, muestra []

Este es un resumen de por qué nunca deberías crear componentes web con {mode: 'closed'}:

  1. Sensación de seguridad artificial. No hay nada que detenga a un atacante usurpación de Element.prototype.attachShadow.

  2. El modo cerrado evita que el código del elemento personalizado acceda a su propio shadow DOM Un error total. En su lugar, deberás guardar una referencia para más adelante si quieres usar elementos como querySelector(). Esto, en especial, ¡Contrae el propósito original del modo cerrado!

        customElements.define('x-element', class extends HTMLElement {
        constructor() {
        super(); // always call super() first in the constructor.
        this._shadowRoot = this.attachShadow({mode: 'closed'});
        this._shadowRoot.innerHTML = '<div class="wrapper"></div>';
        }
        connectedCallback() {
        // When creating closed shadow trees, you'll need to stash the shadow root
        // for later if you want to use it again. Kinda pointless.
        const wrapper = this._shadowRoot.querySelector('.wrapper');
        }
        ...
    });
    
  3. El modo cerrado hace que el componente sea menos flexible para los usuarios finales. Como compilar componentes web, llegará un momento en que te olvidarás de agregar un . Una opción de configuración. Un caso de uso que el usuario desea. Un problema común ejemplo es olvidarse de incluir hooks de diseño adecuados para los nodos internos. Con el modo cerrado, no hay forma de que los usuarios anulen estilos. Poder acceder al interior del componente es muy útil. En última instancia, los usuarios bifurcarán tu componente, encontrarán otro o crearán su si no hace lo que quieren :(

Trabaja con ranuras en JS

La API de shadow DOM brinda utilidades para trabajar con ranuras y datos nodos. Son útiles cuando creas un elemento personalizado.

evento de cambio de espacio

El evento slotchange se activa cuando cambian los nodos distribuidos de un espacio. Para por ejemplo, si el usuario agrega elementos secundarios al Light DOM o los quita.

const slot = this.shadowRoot.querySelector('#slot');
slot.addEventListener('slotchange', e => {
    console.log('light dom children changed!');
});

Para supervisar otros tipos de cambios en Light DOM, puedes configurar un MutationObserver en el constructor de tu elemento.

¿Qué elementos se renderizan en una ranura?

A veces, es útil saber qué elementos están asociados con una ranura. Llamada slot.assignedNodes() para encontrar qué elementos está renderizando el espacio. El La opción {flatten: true} también mostrará el contenido de resguardo de un espacio (si no hay nodos). se están distribuyendo).

A modo de ejemplo, supongamos que tu shadow DOM tiene el siguiente aspecto:

<slot><b>fallback content</b></slot>
UsoLlamarResultado
<my-component>Texto del componente</my-component> slot.assignedNodes(); [component text]
&lt;my-component&gt;&lt;/my-component&gt; slot.assignedNodes(); []
&lt;my-component&gt;&lt;/my-component&gt; slot.assignedNodes({flatten: true}); [<b>fallback content</b>]

¿A qué ranura se asigna un elemento?

También puedes responder la pregunta inversa. element.assignedSlot lo dice a cuál de las ranuras del componente se asigna tu elemento.

El modelo de eventos de Shadow DOM

Cuando un evento surge del shadow DOM, su objetivo se ajusta para mantener el de encapsulamiento que brinda shadow DOM. Es decir, los eventos se vuelven a segmentar para que tengan como si vinieran del componente y no de los elementos internos de tu shadow DOM. Algunos eventos ni siquiera se propagan fuera del shadow DOM.

Estos son los eventos que cruzan el límite de la sombra:

  • Eventos de enfoque: blur, focus, focusin y focusout
  • Eventos del mouse: click, dblclick, mousedown, mouseenter, mousemove, etcétera.
  • Eventos de la rueda: wheel
  • Eventos de entrada: beforeinput y input
  • Eventos del teclado: keydown y keyup
  • Eventos de composición: compositionstart, compositionupdate y compositionend
  • DragEvent: dragstart, drag, dragend, drop, etcétera.

Sugerencias

Si el shadow tree está abierto, llamar a event.composedPath() mostrará un array de nodos por los que viajó el evento.

Utiliza eventos personalizados

Los eventos del DOM personalizados que se activan en nodos internos en un shadow tree fuera del límite de la sombra, a menos que el evento se cree usando el Marca composed: true:

// Inside <fancy-tab> custom element class definition:
selectTab() {
    const tabs = this.shadowRoot.querySelector('#tabs');
    tabs.dispatchEvent(new Event('tab-select', {bubbles: true, composed: true}));
}

Si es composed: false (predeterminado), los consumidores no podrán escuchar el evento. fuera de tu shadow root.

<fancy-tabs></fancy-tabs>
<script>
    const tabs = document.querySelector('fancy-tabs');
    tabs.addEventListener('tab-select', e => {
    // won't fire if `tab-select` wasn't created with `composed: true`.
    });
</script>

Cómo manejar el foco

Si recuerdas el modelo de eventos de shadow DOM, los eventos que se activan dentro de un shadow DOM se ajustan para que parezca que provienen del elemento de hosting. Por ejemplo, supongamos que haces clic en una <input> dentro de una shadow root:

<x-focus>
    #shadow-root
    <input type="text" placeholder="Input inside shadow dom">

El evento focus parecerá provenir de <x-focus>, no de <input>. Del mismo modo, document.activeElement será <x-focus>. Si la shadow root creado con mode:'open' (consulta modo cerrado), también se te acceso al nodo interno que se hizo foco:

document.activeElement.shadowRoot.activeElement // only works with open mode.

Si hay varios niveles de shadow DOM en juego (por ejemplo, un elemento personalizado dentro otro elemento personalizado), deberás desglosar de manera recursiva las shadow roots para encuentra el activeElement:

function deepActiveElement() {
    let a = document.activeElement;
    while (a && a.shadowRoot && a.shadowRoot.activeElement) {
    a = a.shadowRoot.activeElement;
    }
    return a;
}

Otra opción de enfoque es la opción delegatesFocus: true, que expande el Comportamiento de enfoque de los elementos dentro de un shadow tree:

  • Si haces clic en un nodo dentro del shadow DOM y el nodo no es un área enfocable, la primera área enfocable se enfoca.
  • Cuando un nodo dentro del shadow DOM obtiene el foco, :focus se aplica al host en además del elemento enfocado.

Ejemplo: Cómo cambia delegatesFocus: true el comportamiento del foco

<style>
    :focus {
    outline: 2px solid red;
    }
</style>

<x-focus></x-focus>

<script>
customElements.define('x-focus', class extends HTMLElement {
    constructor() {
    super(); // always call super() first in the constructor.

    const root = this.attachShadow({mode: 'open', delegatesFocus: true});
    root.innerHTML = `
        <style>
        :host {
            display: flex;
            border: 1px dotted black;
            padding: 16px;
        }
        :focus {
            outline: 2px solid blue;
        }
        </style>
        <div>Clickable Shadow DOM text</div>
        <input type="text" placeholder="Input inside shadow dom">`;

    // Know the focused element inside shadow DOM:
    this.addEventListener('focus', function(e) {
        console.log('Active element (inside shadow dom):',
                    this.shadowRoot.activeElement);
    });
    }
});
</script>

Resultado

delegatesFocus: comportamiento verdadero.

Arriba está el resultado cuando se enfoca <x-focus> (clic del usuario, con pestañas, focus(), etc.), "Texto de Shadow DOM en el que se puede hacer clic" se hace clic en él o en la dirección <input> está enfocado (incluido autofocus).

Si configuraras delegatesFocus: false, esto es lo que verías en su lugar:

delegatesFocus: falso y la entrada interna se enfoca.
delegatesFocus: false y el <input> interno están enfocados.
delegatesFocus: false y x-focus
    obtiene el foco (p.ej., tiene tabindex=&#39;0&#39;).
. delegatesFocus: false y <x-focus> obtiene el foco (p.ej., tiene tabindex="0").
delegatesFocus: falso y &quot;Texto del Shadow DOM en el que se puede hacer clic&quot; es
    (o se hace clic en otra área vacía dentro del shadow DOM del elemento).
. delegatesFocus: false y "Texto de Shadow DOM en el que se puede hacer clic" es (o se hace clic en otra área vacía dentro del shadow DOM del elemento).

Sugerencias

Con el paso de los años, he aprendido algunas cosas sobre la creación de componentes web. me algunas de estas sugerencias te resultarán útiles para crear componentes y depurar shadow DOM.

Usar la contención de CSS

Por lo general, el diseño, el estilo y la pintura de un componente web son bastante independientes. Usa Contención de CSS en :host para un rendimiento ganar:

<style>
:host {
    display: block;
    contain: content; /* Boom. CSS containment FTW. */
}
</style>

Cómo restablecer diseños heredables

Los estilos heredables (background, color, font, line-height, etc.) continúan heredarán en shadow DOM. Es decir, perforan el límite del shadow DOM de forma predeterminada. Si quieres comenzar desde cero, usa all: initial; para restablecer los estilos heredables a su valor inicial cuando cruzan el límite de la sombra.

<style>
    div {
    padding: 10px;
    background: red;
    font-size: 25px;
    text-transform: uppercase;
    color: white;
    }
</style>

<div>
    <p>I'm outside the element (big/white)</p>
    <my-element>Light DOM content is also affected.</my-element>
    <p>I'm outside the element (big/white)</p>
</div>

<script>
const el = document.querySelector('my-element');
el.attachShadow({mode: 'open'}).innerHTML = `
    <style>
    :host {
        all: initial; /* 1st rule so subsequent properties are reset. */
        display: block;
        background: white;
    }
    </style>
    <p>my-element: all CSS properties are reset to their
        initial value using <code>all: initial</code>.</p>
    <slot></slot>
`;
</script>

Encontrar todos los elementos personalizados que utiliza una página

A veces, resulta útil encontrar los elementos personalizados que se usan en la página. Para ello, necesitamos atravesar recursivamente el shadow DOM de todos los elementos utilizados en la página.

const allCustomElements = [];

function isCustomElement(el) {
    const isAttr = el.getAttribute('is');
    // Check for <super-button> and <button is="super-button">.
    return el.localName.includes('-') || isAttr && isAttr.includes('-');
}

function findAllCustomElements(nodes) {
    for (let i = 0, el; el = nodes[i]; ++i) {
    if (isCustomElement(el)) {
        allCustomElements.push(el);
    }
    // If the element has shadow DOM, dig deeper.
    if (el.shadowRoot) {
        findAllCustomElements(el.shadowRoot.querySelectorAll('*'));
    }
    }
}

findAllCustomElements(document.querySelectorAll('*'));

Cómo crear elementos a partir de una <template>

En lugar de propagar una shadow root con .innerHTML, podemos usar un elemento <template> Las plantillas son marcadores de posición ideales para declarar la estructura de un componente web.

Consulta el ejemplo en “Elementos personalizados: compilación de componentes web reutilizables”.

Historia y navegador compatible

Si has estado siguiendo los componentes web durante los últimos años, ten en cuenta que Chrome 35 y Opera han estado enviando una versión anterior de shadow DOM para algún tiempo. Blink seguirá admitiendo ambas versiones en paralelo para algunos usuarios tiempo. La especificación de v0 proporcionó un método diferente para crear una shadow root (element.createShadowRoot en lugar de element.attachShadow de la versión 1). Llamar al método anterior continúa creando una shadow root con semántica de v0, por lo que la v0 existente el código no se romperá.

Si estás interesado en la especificación anterior v0, echa un vistazo a html5rocks artículos: 1 2, 3 También hay una gran comparación de los diferencias entre shadow DOM v0 y v1

Navegadores compatibles

Shadow DOM v1 se envía en Chrome 53 (estado). Opera 40, Safari 10 y Firefox 63. Perimetrales comenzó a desarrollar.

Para detectar shadow DOM por medio de los componentes, verifica la existencia de attachShadow:

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

Polyfill

Hasta que la compatibilidad con navegadores esté ampliamente disponible, la shadydom y Los polyfills shadycss incluyen la versión 1. . Shady DOM imita el alcance del DOM de Shadow DOM y los polyfills shadycss. las propiedades personalizadas de CSS y el alcance de estilo que proporciona la API nativa.

Instala los polyfills:

bower install --save webcomponents/shadydom
bower install --save webcomponents/shadycss

Usa los polyfills:

function loadScript(src) {
    return new Promise(function(resolve, reject) {
    const script = document.createElement('script');
    script.async = true;
    script.src = src;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
    });
}

// Lazy load the polyfill if necessary.
if (!supportsShadowDOMV1) {
    loadScript('/bower_components/shadydom/shadydom.min.js')
    .then(e => loadScript('/bower_components/shadycss/shadycss.min.js'))
    .then(e => {
        // Polyfills loaded.
    });
} else {
    // Native shadow dom v1 support. Go to go!
}

Consulta https://github.com/webcomponents/shadycss#usage para obtener instrucciones sobre cómo corregir la compatibilidad de los estilos.

Conclusión

Por primera vez, tenemos una primitiva de API que define los permisos de CSS correctamente, alcance del DOM y tiene composición verdadera. En combinación con otras APIs de componentes web al igual que los elementos personalizados, el shadow DOM brinda una forma de crear contenido realmente encapsulado sin hackeos ni usar equipaje antiguo, como <iframe>.

No me malinterpretes. Shadow DOM es, sin duda, una bestia compleja. Pero es una bestia que vale la pena aprender. Dedícale tiempo. Aprende y haz preguntas.

Lecturas adicionales

Preguntas frecuentes

¿Puedo usar Shadow DOM v1 en la actualidad?

Sí, con un polyfill. Consulta Compatibilidad con navegadores.

¿Qué funciones de seguridad proporciona shadow DOM?

Shadow DOM no es una función de seguridad. Es una herramienta ligera para definir el alcance de CSS y ocultar árboles del DOM en el componente. Si quieres una verdadera barrera de seguridad, usa un <iframe>.

¿Un componente web debe usar shadow DOM?

De ninguna manera. No es necesario crear componentes web que utilicen shadow DOM. Sin embargo, La creación de elementos personalizados que usan Shadow DOM significa que puedes aprovechar funciones como el alcance de CSS, el encapsulamiento del DOM y la composición.

¿Cuál es la diferencia entre las shadow root abiertas y cerradas?

Consulta Raíces de sombras cerradas.