Slots en WebComponents

Hay ocasiones en las que nos interesará que un cierto contenido del Light DOM se pueda utilizar en el Shadow DOM de nuestro componente de una forma sencilla y práctica, ofreciendo esa posibilidad al usuario que consume el componente y que debe crear el código desde el HTML. Justo esa es la finalidad de los slots.

Slots

En HTML5 aparece la etiqueta <slot> y el concepto de slots. Estos <slot> nos permiten poder enviar contenido (generalmente, marcado HTML) desde la parte de HTML y reutilizarlo en el componente. Es obligatorio tener un Shadow DOM en el componente para poder utilizar un <slot>.

WebComponents: Slots HTML

Pero veamos un fragmento de código de un componente, donde tenemos contenido en el custom element y una etiqueta <slot> dentro del componente:

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

<script>
  customElements.define("app-element", class extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: "open" });
    }

    connectedCallback() {
      this.shadowRoot.innerHTML = "<div>¡Hola, usuario <slot>desconocido</slot>!</div>";
    }
  });
</script>

Observemos el <slot> del Shadow DOM de nuestro componente, en el connectedCallback(). El navegador insertará el contenido del Light DOM que hemos colocado en el interior de <app-element> en el slot (reemplazando su contenido). En el navegador nos mostrará el texto «¡Hola, usuario Manz!», teniendo en cuenta que el marcado <span>Manz</span> se respetará.

En el caso de que tuvieramos el elemento <app-element> vacío, el texto que nos mostraría el navegador sería «¡Hola, usuario desconocido!», ya que utiliza el valor por defecto que le hemos propuesto a la etiqueta <slot> en el Shadow DOM.

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.

Veamos el siguiente ejemplo. Hemos ampliado el ejemplo anterior, en la que 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>
  customElements.define("app-element", class extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: "open" });
    }

    connectedCallback() {
      this.shadowRoot.innerHTML = `
        <div class="card">
          <slot name="name"></slot>
          <slot name="description"></slot>
          <slot name="role"></slot>
        </div>
      `;
    }
  });
</script>

Por otro lado, dentro del componente creamos la estructura del marcado HTML incluyendo varios elementos <slot> con el atributo name que coincidirá con el atributo slot del custom element. El navegador reemplazará el contenido de los slots por la información indicada.

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 estos últimos, se actualizarán en ambos lugares.

La pseudoclase ::slotted

Junto a los slots, aparece también la pseudoclase CSS ::slotted(), que nos permite dar estilo a etiquetas insertadas en un slot desde dentro del componente. En el interior de los paréntesis, podríamos indicar un selector que encaje con la etiqueta insertada.

Vamos a centrarnos en el connectedCallback() del ejemplo anterior. Utilizabamos un slot con nombre name donde insertabamos un elemento <h2>. Si quisieramos dar estilo a ese elemento desde el componente, se nos podrían ocurrir cualquiera de estas 3 opciones (comentadas):

    connectedCallback() {
      this.shadowRoot.innerHTML = `
        <style>
          h2 { color: red; }                  /* Opción 1 */
          slot[name=name] { color: green; }   /* Opción 2 */
          ::slotted(h2) { color: blue; }      /* Opción 3 */
        </style>
        <div class="card">
          <slot name="name"></slot>
          <slot name="description"></slot>
          <slot name="role"></slot>
        </div>
      `;
    }
  • La opción 1 es darle estilo al elemento <h2> directamente. Sin embargo, no es posible ya que el elemento no existe en el Shadow DOM donde le estamos dando estilo, sino que es una referencia a través de un slot a un elemento que está en el Light DOM. No funcionará ni aplicará estilo, por lo que no nos sirve.

  • La opción 2 trata de darle estilo al slot usando slot[name="name"]. Esta opción funcionaría siempre y cuando no exista un CSS global, que tendría preferencia aún incluso si especificamos !important en los estilos locales.

  • La opción 3 sería utilizar la pseudoclase ::slotted() seleccionando los slots que usen un <h2>. Esta opción, al igual que la anterior nos funcionaría, dándole también preferencia al estilo global. Sin embargo, la diferencia es que con un !important si podríamos cambiar la especificidad si lo desearamos, ya que la preferencia la tendrían los estilos globales.

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.

Manz
Publicado por Manz

Docente, divulgador informático y freelance. Autor de Emezeta.com, es profesor en la Universidad de La Laguna y dirige el curso de Programación web FullStack y Diseño web FrontEnd de EOI en Tenerife (Canarias). En sus ratos libres, busca GIF de gatos en Internet.