Slots en WebComponents

Contenido HTML desde el Light DOM al Shadow DOM

Si conocemos como funciona el Shadow DOM y las diferencias con el Light DOM, es muy probable que nos interese saber cuál es la finalidad del Light DOM en un componente con Shadow DOM.

¿Qué es un slot?

En HTML5 aparece un nuevo concepto denominado slot o ranura. Estos slots son «accesos directos» que nos permiten enviar contenido HTML desde el Light DOM para reutilizarlo en el Shadow DOM del componente. Si tenemos claro el concepto de Shadow DOM y Light DOM, esto se puede ver fácilmente como un hueco o ranura en el Shadow DOM que deja ver el contenido que está «debajo» en el Light DOM:

WebComponents: Slots HTML

Para comunicar el Light DOM con el Shadow DOM debemos aprender a utilizar las etiquetas <slot>, que explicaremos a continuación. Recuerda que es obligatorio tener un Shadow DOM en el componente para poder utilizar slots. De lo contrario, no funcionarán.

Slot por defecto

La etiqueta <slot> se puede utilizar en el Shadow DOM de un componente, para determinar donde aparecerá el contenido del custom element. Si indicamos un contenido en el interior de la etiqueta <slot> se utilizará como contenido por defecto, en el caso de que el componente no tenga definido ningún contenido.

Observa el siguiente ejemplo, donde escribimos dos componentes <app-element>:

  1. Al primero, no se le define ningún contenido
  2. Al segundo, se le pasa el contenido <span>Manz</span>

El fragmento de código completo sería el siguiente:

<app-element></app-element>
<app-element><span>Manz</span></app-element>

<script>
class AppElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}

connectedCallback() {
this.shadowRoot.innerHTML = /* html */`
<div class="container">
¡Hola, usuario <slot>desconocido</slot>!
</div>
`
;
}
};
customElements.define("app-element", AppElement);
</script>

Observemos ahora la etiqueta <slot> definida en el Shadow DOM de nuestro componente, concretamente, en el método especial connectedCallback(). El navegador tendrá un acceso directo e insertará el contenido del Light DOM que hemos colocado en el interior de <app-element> en el slot (reemplazando su contenido, si existiera).

Para cada componente, el navegador nos mostrará lo siguiente:

  1. En el primer componente, mostrará el texto ¡Hola, usuario desconocido!.
  2. En el segundo componente, mostrará el texto ¡Hola, usuario Manz!.

Slots nombrados

Pero es posible que, si nuestro componente es un poco más complejo, el slot se nos quede corto, ya que necesitaríamos varios slots por componente. Para ello tenemos los denominados slots nombrados o slots con nombre.

Si bien siguen la misma mecánica explicada anteriormente, veamos el siguiente ejemplo. Hemos ampliado el ejemplo, y el custom element ahora incluye 3 grupos de información: nombre, rol y descripción. Cada uno en su propia etiqueta HTML, con un atributo slot que le indicará el tipo de información que posee:

<app-element>
<h2 slot="name">Manz</h2>
<span slot="role">Developer</span>
<p slot="description">I hate Internet Explorer.</p>
</app-element>

<script>
class AppElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}

connectedCallback() {
this.shadowRoot.innerHTML = /* html */`
<div class="card">
<slot name="name"></slot>
<slot name="description"></slot>
<slot name="role"></slot>
</div>
`
;
}
};
customElements.define("app-element", AppElement);
</script>

Por otro lado, dentro del componente creamos la estructura del marcado HTML incluyendo varios elementos <slot> con un atributo name que coincidirá con el atributo slot del custom element. El navegador reemplazará el contenido de los slots por la información indicada, coincidiendo también con la etiqueta definida en el custom element, siendo así un <h2> el nombre o un <p> la descripción.

Esto nos permitirá crear múltiples slots en un mismo componente. Al igual que el ejemplo del apartado anterior, podemos utilizar contenido a modo de fallback, por si tenemos el slot vacío, tenga información por defecto.

Ten en cuenta que los slots especificados en el Shadow DOM, al ser referencias al Light DOM, si modificamos cualquiera de los dos, se actualizarán en ambos lugares.

Estilos CSS en slots

Ahora hablemos de estilos. Si buscamos darle estilo a un elemento que se encuentra en un <slot> debemos pararnos a pensar donde está realmente el elemento para saber desde donde se le puede dar estilo. Recordemos que el componente, está aislado por un Shadow DOM y que los slots sólo son referencias a elementos que existen en el Light DOM.

Estilos desde el LightDOM

Como los elementos HTML realmente existen en el DOM global, es posible seleccionar los elementos por cualquiera de sus características: clases, id, atributos o por el propio elemento. En el siguiente ejemplo se ve como se le da estilo según el tipo de elemento <h2> y por su atributo slot="name":

<app-element>
<h2 slot="name">Manz</h2>
<span slot="role">Developer</span>
</app-element>

<style>
app-element h2 {
background: red;
}

app-element [slot="name"] {
background: blue;
}
</style>

Le hemos dado estilo al primer elemento del custom element, el elemento <h2>.

Estilos desde el Shadow DOM

Es posible que no queramos dar estilo a los elementos desde fuera del componente, sino desde dentro, desde el Shadow DOM. Así será mucho más fácil de localizar y mantener el código. En ese caso, tenemos varias opciones a nuestra disposición, que veremos a continuación:

class AppElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}

connectedCallback() {
this.shadowRoot.innerHTML = /* html */`
<style>
h2 {
background: red; /* No funciona */
}
[name="name"] {
display: block;
background: green;
}
</style>
<div class="card">
<slot name="name"></slot>
<slot name="role"></slot>
</div>
`
;
}
};

customElements.define("app-element", AppElement);

La primera de ellas sería darle estilo directamente a <h2>, sin embargo, ese elemento está realmente fuera del Shadow DOM, por lo que no podríamos aplicarle estilos. Sin embargo, si hacemos referencia al <slot> o sus atributos, si podremos hacerlo, porque dichas etiquetas si que están en el Shadow DOM. Eso sí, no olvides cambiarle el display.

Recuerda que los estilos que se encuentren dentro del Shadow DOM siempre tendrán menos prioridad que los estilos del documento HTML global. Si se aplican estilos en ambos lugares, siempre tendrá preferencia el documento HTML global.

El pseudoelemento ::slotted()

Existe una forma más específica de dar estilo a los elementos slots de un componente con Shadow DOM: mediante el pseudoelemento CSS ::slotted().

Este pseudoelemento nos permite dar estilo a etiquetas insertadas en un slot, pero desde dentro del componente. En el interior de los paréntesis, deberemos indicar un selector CSS que encaje con la etiqueta insertada.

Por ejemplo, en el fragmento de código anterior vimos que h2 desde el Shadow DOM no tendría efecto. En lugar de ello, utilizaremos ::slotted(h2):

  connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
h2 {
color: red; /* No funciona */
}
::slotted(h2) { /* Funciona */
color: green;
}
</style>
<div class="card">
<slot name="name"></slot>
<slot name="role"></slot>
</div>
`
;
}

La diferencia de utilizar ::slotted() es que, en este caso también se le da preferencia al estilo global, sin embargo en este caso con un !important si podríamos cambiar la especificidad si lo desearamos, ya que los estilos se están insertando en el DOM global a través del ::slotted().

Eventos de slots

Por último, los <slots> tienen un evento que puede resultar útil para detectar cuando hay ciertos cambios en slots. Por ejemplo, el evento slotchange se dispara cuando:

  • Un <slot> cambia su atributo name, y por lo tanto apunta a otro elemento.
  • Una etiqueta del Light DOM cambia su atributo slot, y por lo tanto, es referencia de otro slot.

En resumen, cuando una asociación de slot-elemento cambia, apuntando a un nuevo elemento o dejando de apuntar al que ya lo hacía.

Evento Descripción
slotchange Se dispara cuando detecta que una asociación slot-elemento ha cambiado.

Para utilizarlo, lo haremos como cualquier evento, utilizando .addEventListener() sobre el <slot> en cuestión para escucharlo y detectar cuando se dispara, ejecutando una función asociada:

const slot = document.querySelector("slot");
slot.addEventListener("slotchange", () => console.log("¡El slot ha cambiado!"));

De esta forma podremos detectar cambios y disparar cierta lógica necesaria en consecuencia.

Tabla de contenidos