Templates und Slots

Februar 2024
Mit dem HTML-Element template kann HTML-Code in eine Seite für eine spätere Verwendung eingebettet werden. Ein Template wird im Browser nicht abgebildet, sein Inhalt kann aber mithilfe von JavaScript an anderere Stellen kopiert und damit sichtbar werden. Das kann in Abhängigkeit eines Ereignisses oder der Zeit passieren.
Siehe auch MDN: Using templates and slots.
In diesen Beitrag fließen auch die Kenntnisse zu Shadow DOM und Custom Elements ein.

Ein Template für ein Custom Element

Wir beginnen wieder mit einem Beispiel. Dazu bauen wir ein HTML-Dokument auf, nehmen das in HTML und CSS beschriebene Grunddokument als Vorlage und bringen folgenden Code ein.

in den HTML-Bereich

<h3>Template mit Accordion</h3>

<template id="myTemplate">
  <style>
  b { color: #0066cc; font-size: 120%; }
  </style>
  <details>
    <summary>
      <b class="name">Begriff</b>
    </summary>
      <i>Erklärung</i>
  </details>
</template>

<my-element></my-element>
Wenn wir das Dokument jetzt im Browser betrachten, würden wir die Überschrift sehen und sonst nichts.
Das leere my-element bietet ja auch nichts Sichtbares.
Das ändert sich aber sofort, wenn den Inhalt des Templates in das Element einbringen. Dazu platzieren wir folgenden Scriptcode in den Scriptbereich 2.
const myElement = document.getElementsByTagName('my-element')[0];
const templateInhalt = document.getElementById('myTemplate').content;

myElement.appendChild(templateInhalt.cloneNode(true));
Wichtig ist, die Methode cloneNode(true) am Templateinhalt zu verwenden, sodass wirklich eine Kopie des Inhalts und keine Referenz eingebracht wird.

Screenshot 1

Darstellung wie beschrieben
Jetzt ist das details-Element sichtbar, die Erklärung kann durch Anklicken des Markers ein- oder ausgeblendet werden.
So ein Element könnte vielleicht an anderen Stellen auf der Seite auch gebraucht werden, jedoch mit verschiedenen Inhalten.

Slots kommen ins Spiel

Eine Template-Kopie mit verschiedenen Inhalten kann über den Slot-Mechanismus realisiert werden. In unserem Beispiel müssten Begriff und Erklärung ausgetauscht werden. Dazu umhüllen wir die auszutauschenden Code-Teile mit slot-Elementen, die mit einem Namen versehen werden müssen.
<template id="myTemplate">
  <style>
  b { color: #0066cc; font-size: 120%; }
  </style>
  <details>
    <summary>
      <b><slot name="Slot1">Begriff</slot></b>
    </summary>
      <slot name="Slot2"><i>Erklärung</i></slot>
  </details>
</template>
Zuerst fügen wir zwei weitere Elemente des Typs my-element hinzu als Zielelemente des Templateinhalts.
In den Zielelementen bringen wir die konkreten Codeteile unter, die als Austausch dienen. Das oberste Element des Austausch-Codes referenziert mit dem Attribut slot und dem entsprechenden Slotnamen, für welchem Slot es bestimmt ist.
<my-element>
  <span slot="Slot1">Textfeld</span>
  <input slot="Slot2" type="text" size="15">
</my-element>

<my-element>
  <span slot="Slot1">Tabelle</span>
  <table slot="Slot2" border="1">
    <tr><th>A</th><th>B</th></tr>
    <tr><td>Feld 1</td><td>Feld 2</td></tr>
  </table>
</my-element>

<my-element></my-element>

So geht es jedoch nicht

Mit einer for-Schleife bringen wir den Templateinhalt in jedes der Zielelemente ein.
const list = document.getElementsByTagName('my-element');
const templateInhalt = document.getElementById('myTemplate').content;

for (let i = 0; i < list.length; i++)
{
  let custElem = list.item(i);
  // funktioniert nicht wie gewünscht
  custElem.appendChild(template.cloneNode(true));
}
Das passiert auch, aber der Slot-Mechanismus funktioniert so gar nicht.

ShadowRoot muss sein

Es funktioniert aber, wenn wir jedes Zielelement mit einem ShadowRoot initialisieren und den Templateinhalt über die Eigenschaft shadowRoot einbringen.
const list = document.getElementsByTagName('my-element');
const templateInhalt = document.getElementById('myTemplate').content;

for (let i = 0; i < list.length; i++)
{
  let custElem = list.item(i);
  custElem.attachShadow({ mode: "open" });
  custElem.shadowRoot.appendChild(templateInhalt.cloneNode(true));
}

Screenshot 2

Darstellung wie beschrieben
Jetzt haben wir das gewünschte Ergebnis. Über den Inspektor des Browsers sehen wir, dass statt der Slots die Austauschelemente eingefügt wurden. Lediglich im letzten Custom Element, das keine Austauschelemente enthielt, bleiben die Slots erhalten.
Wir sehen auch, dass das Austauschelement ganz verschiedenen Typs sein darf und keineswegs mit dem Inhalt des Slot übereinstimmen muss.

Effizienter mit registriertem Custom Element

Es ist nicht sonderlich effizient, vor jedem Einfügen eines Templateinhalts erst einen Shadow DOM aufzubauen. Auch auf die verwendete Schleife können wir verzichten, wenn wir aus unserem Customelement ein registriertes machen. Im folgenden Beispiel übernimmt die Definitionsklasse myAccordion sowohl das Anlegen des Shadow DOM als auch das Kopieren des Templateinhalts. Nur noch das CustomElement registrieren - und weiterer Scriptcode wird nicht benötigt.
class myAccordion extends HTMLElement {
  constructor() {
    super();
    const template = document.getElementById('myTemplate').content;
    this.attachShadow({mode: 'open'});
    this.shadowRoot.appendChild(template.cloneNode(true));
  }
}

customElements.define('my-element', myAccordion);
Ein weiterer Screenshot ist nicht nötig, denn das Ergebnis ist das gleiche wie bei Screenshot 2.
Das Beispiel hier zum Anschauen.
So erklärt sich auch, warum Beispiele zu Shadow DOM, Custom Element, Template und Slot in bekannten Beispielen meistens miteinander kombiniert sind. Es war vielleicht ein wenig hilfreich, diese Mechanismen in mehreren Beiträgen mal einzeln zu durchleuchten, bevor wir sie fusioniert haben.

Ausblick

Es wird über weitere Vereinfachungen nachgedacht. Das Zauberwort heißt Deklarativer Shadow DOM. So soll das Attribut shadowrootmode="open" an <template> bewirken, dass ein Shadow Root am Parent-Element des Templates eröffnet wird. Schaun wir mal, wie sich das weiterentwickelt und was die Zukunft bringt.