Usando plantillas y slots

Este artículo explica como puedes usar los elementos <template> y <slot> para crear una plantilla flexible que luego puede ser usada para rellenar el shadow DOM de un componente web.

La verdad acerca del elemento <template>

Cuando tienes que reutilizar las mismas estructuras de lenguaje de marcado repetidas veces en una p√°gina web, tiene sentido utilizar alg√ļn tipo de plantilla en lugar de repetir la misma estructura una y otra vez. Esto ya era posible hacerlo antes, pero ahora es mucho mas f√°cil con el elemento HTML <template> (que est√° bien soportado en los navegadores modernos). Este elemento y su contenido no son renderizados en el DOM, pero pueden ser referenciados v√≠a JavaScript.

Echemos un vistazo a un ejemplo sencillo:

<template id="my-paragraph">
  <p>Mi p√°rrafo</p>
</template>

Esto no aparecerá en tu página hasta que hagas una referencia a él con JavaScript y luego lo agregues al DOM, usando algo parecido a lo siguiente:

let template = document.getElementById('my-paragraph');
let templateContent = template.content;
document.body.appendChild(templateContent);

Aunque de una manera simple, ya puedes empezar a ver su utilidad.

Usando el elemento <template> con componentes web

Las plantillas son √ļtiles por si mismas, pero trabajan a√ļn mejor con componentes web. Definamos un componente web que use nuestra plantilla como el contenido de su shadow DOM. Lo nombraremos <my-paragraph>:

customElements.define('my-paragraph',
  class extends HTMLElement {
    constructor() {
      super();
      let template = document.getElementById('my-paragraph');
      let templateContent = template.content;

      const shadowRoot = this.attachShadow({mode: 'open'})
        .appendChild(templateContent.cloneNode(true));
  }
})

El punto clave a tener en cuenta aqu√≠ es que agregamos un clon del contenido de la plantilla al shadow root creado usando el m√©todo Node.cloneNode().

Y debido a que estamos agregando su contenido a un shadow DOM, podemos incluir cierta información de estilo dentro de la plantilla en un elemento <style>, que luego se encapsula dentro del elemento personalizado. Esto no funcionaría si nosotros solo lo agregásemos al DOM estandar.

Por ejemplo:

<template id="my-paragraph">
  <style>
    p {
      color: white;
      background-color: #666;
      padding: 5px;
    }
  </style>
  <p>Mi p√°rrafo</p>
</template>

Ahora podemos usarlo simplemente agreg√°ndolo a nuestro documento HTML:

<my-paragraph></my-paragraph>

Nota: Las plantillas est√°n bien soportadas en los navegadores: la API del Shadow DOM  es compatible por defecto con Firefox (version 63 en adelante), Chrome, Opera y Safari, Edge est√° trabajando en una implementaci√≥n.

A√Īadiendo flexibilidad con el elemento <slot>

Hasta aqu√≠ bien, pero el elemento no es muy flexible. Solo podemos mostrar una parte de texto dentro de √©l, lo que quiere decir que, hasta el momento, es menos √ļtil que un p√°rrafo normal. Podemos mostrar diferente texto en cada instancia de elemento de una forma declarativa agradable usando el elemento <slot>. Este tiene un soporte m√°s limitado que el elemento <template>, disponible desde Chrome 53, Opera 40, Safari 10, Firefox 59, pero a√ļn no disponible en Edge.

Los slots son identificados por su atributo name, y te permiten definir marcadores de posición en tu plantilla que pueden rellenarse con cualquier fragmento de marcado cuando el elemento es usado.

Así que, si queremos agregar un slot dentro de nuestro ejemplo sencillo, podemos actualizar el elemento de párrafo de la plantilla de la siguiente manera:

<p><slot name="my-text">Mi texto predeterminado</slot></p>

Si el contenido del slot no est√° definido cuando el elemento se agrega al marcado, o si el navegador no soporta el elemento slot, <my-paragraph> solo contiene el texto alternativo "Mi texto predeterminado".

Para definir el contenido de un slot, incluimos una estructura HTML dentro del elemento <my-paragraph> con un atributo slot cuyo valor es igual al nombre del slot que  queremos rellenar. Al igual que antes, esto puede ser cualquier cosa, por ejemplo:

<my-paragraph>
  <span slot="my-text">¬°Tengamos un texto diferente!</span>
</my-paragraph>

o

<my-paragraph>
  <ul slot="my-text">
    <li>¬°Tengamos un texto diferente!</li>
    <li>¬°En una lista!</li>
  </ul>
</my-paragraph>

Nota: Los elementos que pueden ser insertados en los slots son conocidos como Slotable; cuando un elemento ha sido insertado en un slot, se dice que fue eslotado por su término en inlgés slotted.

Nota: Un <slot> sin nombre se rellenar√° con todos los nodos secundarios de nivel superior del elemento personalizado que no tengan el atributo slot. Esto incluye nodos de texto.

Y eso es todo nuestro ejemplo sencillo. Si quieres jugar con él un poco más, puedes encontrarlo en GitHub (también puedes verlo en vivo).

Un ejemplo m√°s completo

Para finalizar el artículo, veamos algo menos trivial.

El siguiente conjunto de fragmentos de c√≥digo muestra c√≥mo usar <slot> junto con  <template> y algo de JavaScript para:

Observa que es técnicamente posible usar el elemento <slot> sin un elemento <template>, por ejemplo, dentro de un elemento <div> normal, y aun así tomar ventaja de los indicadores de posición de el elemento <slot> para el contenido del Shadow DOM, y al hacerlo puedes evitar el problema de tener que acceder primero a la propiedad content del elemento de la plantilla y clonarlo. Sin embargo, por lo general, es más práctico agregar slots dentro de un elemento <template>, ya que es poco probable que necesites definir un patrón basado en un elemento ya renderizado.

Adem√°s, incluso si no est√° renderizado, el prop√≥sito del contenedor como plantilla deber√≠a ser sem√°nticamente m√°s claro cuando se usa el elemento <template>. Adem√°s, el elemento <template> puede tener elementos agregados directamente a √©l, como <td>, que desaparecer√≠an al a√Īadirse a un <div>.

Nota: Puedes encontrar el ejemplo completo en element-details (también lo puedes ver en vivo)

Creando una plantilla con algunos elementos <slot>

En primer lugar, usamos el elemento <slot> dentro de un elemento <template> para crear un nuevo fragmento de documento de tipo "element-details-template" que contiene algunos slots con el atributo name:

<template id="element-details-template">
  <style>
  details {font-family: "Open Sans Light",Helvetica,Arial}
  .name {font-weight: bold; color: #217ac0; font-size: 120%}
  h4 { margin: 10px 0 -8px 0; }
  h4 span { background: #217ac0; padding: 2px 6px 2px 6px }
  h4 span { border: 1px solid #cee9f9; border-radius: 4px }
  h4 span { color: white }
  .attributes { margin-left: 22px; font-size: 90% }
  .attributes p { margin-left: 16px; font-style: italic }
  </style>
  <details>
    <summary>
      <span>
        <code class="name">&lt;<slot name="element-name">NECESITA NOMBRE</slot>&gt;</code>
        <i class="desc"><slot name="description">NECESITA DESCRIPCI√ďN</slot></i>
      </span>
    </summary>
    <div class="attributes">
      <h4><span>Atributos</span></h4>
      <slot name="attributes"><p>Ninguno</p></slot>
    </div>
  </details>
  <hr>
</template>

Ese elemento <template> tiene varias características.

Crear un nuevo elemento <element-details> desde el elemento <template>

A continuaci√≥n, crearemos un nuevo elemento personalizado llamado <element-details> y usaremos Element.attachShadow para anclarlo, como su shadow root, a ese fragmento de documento que creamos anteriormente con nuestro elemento <template>. Usa exactamente el mismo patr√≥n que vimos antes en el ejemplo sencillo.

customElements.define('element-details',
  class extends HTMLElement {
    constructor() {
      super();
      var template = document
        .getElementById('element-details-template')
        .content;
      const shadowRoot = this.attachShadow({mode: 'open'})
        .appendChild(template.cloneNode(true));
  }
})

Usando el elemento <element-details> con slots con el atributo name

Ahora tomaremos el elemento <element-details> para usarlo en nuestro documento.

<element-details>
  <span slot="element-name">slot</span>
  <span slot="description">Un marcador de posición dentro de un
     componente web que los usuarios pueden rellenar con su propio marcado,
     con el efecto de componer diferentes √°rboles DOM
     juntos.</span>
  <dl slot="attributes">
    <dt>name</dt>
    <dd>El atributo name del slot.</dd>
  </dl>
</element-details>

<element-details>
  <span slot="element-name">template</span>
  <span slot="description">Un mecanismo para guardar contenido
     en el lado cliente que no se renderiza cuando la p√°gina se
     carga sino que posteriormente se puede instanciar en
     tiempo de ejecución usando JavaScript.</span>
</element-details>

Observa estos puntos sobre el fragmento anterior:

  • El fragento tiene dos instancias de elementos <element-details> que usan el atributo slot para referenciar los slots con atributo name "element-name" y "description" que colocamos en el shadow root del <element-details>
  • Solo el primero de esos dos elementos <element-details> hace referencia a los "attributes" de slot con atributo name. El segundo elemento <element-details> carece de cualquier referencia a "attributes" de slot con atributo name.
  • El primer elemento <element-details> est√° referenciando los "attributes"  de slot con atributo name usando un elemento <dl> con <dt> y <dd> como hijos.

A√Īadamos algunos estilos

Como toque final, a√Īadiremos algunos estilos CSS a los ellementos <dl>, <dt>, y <dd> en el documento:

  dl { margin-left: 6px; }
  dt { font-weight: bold; color: #217ac0; font-size: 110% }
  dt { font-family: Consolas, "Liberation Mono", Courier }
  dd { margin-left: 16px }

Resultado

Finalmente, juntemos todos los fragmentos y veamos cómo se ve el resultado renderizado.

ScreenshotLive sample

Observa los siguientes puntos del resultado renderizado: