Podstawy DOM – Shadow

Dominic Cooney
Dominic Cooney

Wprowadzenie

Komponenty internetowe to zestaw najnowocześniejszych standardów, które:

  1. umożliwiać tworzenie widżetów,
  2. …które można wielokrotnie wykorzystywać
  3. …i nie spowoduje uszkodzenia stron, jeśli następna wersja komponentu zmieni wewnętrzne szczegóły implementacji.

Czy to oznacza, że musisz zdecydować, kiedy używać HTML/JavaScript, a kiedy komponentów internetowych? Nie! HTML i JavaScript pozwalają tworzyć interaktywne elementy wizualne. Widżety to interaktywne elementy wizualne. Podczas tworzenia widżetu warto wykorzystać swoje umiejętności związane z HTML i JavaScriptem. Standardy komponentów internetowych zostały zaprojektowane tak, aby Ci w tym pomóc.

Istnieje jednak podstawowy problem, który utrudnia korzystanie z widżetów utworzonych w językach HTML i JavaScript: drzewo DOM wewnątrz widżetu nie jest odizolowane od reszty strony. Brak enkapsulacji oznacza, że arkusz stylów dokumentu może zostać przypadkowo zastosowany do części widżetu, kod JavaScript może przypadkowo zmodyfikować części widżetu, identyfikatory mogą pokrywać się z identyfikatorami w widżecie itd.

Komponenty internetowe składają się z 3 części:

  1. Szablony
  2. Shadow DOM
  3. Elementy niestandardowe

Shadow DOM rozwiązuje problem enkapsulacji drzewa DOM. Cztery części komponentów internetowych są zaprojektowane tak, aby współpracowały ze sobą, ale możesz też wybrać, których części komponentów internetowych chcesz używać. Z tego samouczka dowiesz się, jak korzystać z Shadow DOM.

Witaj, Shadow World

Dzięki Shadow DOM elementy mogą mieć powiązany z nimi nowy rodzaj węzła. Ten nowy rodzaj węzła nazywa się shadow root. Element, z którym jest powiązany korzeń shadow, jest nazywany hostem shadow. Treść hosta shadow nie jest renderowana. Zamiast tego renderowana jest treść elementu shadow root.

Jeśli na przykład masz takie znaczniki:

<button>Hello, world!</button>
<script>
var host = document.querySelector('button');
var root = host.createShadowRoot();
root.textContent = 'こんにちは、影の世界!';
</script>

, to zamiast

<button id="ex1a">Hello, world!</button>
<script>
function remove(selector) {
  Array.prototype.forEach.call(
      document.querySelectorAll(selector),
      function (node) { node.parentNode.removeChild(node); });
}

if (!HTMLElement.prototype.createShadowRoot) {
  remove('#ex1a');
  document.write('<img src="SS1.png" alt="Screenshot of a button with \'Hello, world!\' on it.">');
}
</script>

wygląda Twoja strona

<button id="ex1b">Hello, world!</button>
<script>
(function () {
  if (!HTMLElement.prototype.createShadowRoot) {
    remove('#ex1b');
    document.write('<img src="SS2.png" alt="Screenshot of a button with \'Hello, shadow world!\' in Japanese on it.">');
    return;
  }
  var host = document.querySelector('#ex1b');
  var root = host.createShadowRoot();
  root.textContent = 'こんにちは、影の世界!';
})();
</script>

Co więcej, jeśli JavaScript na stronie zapyta, jaka jest wartość textContent przycisku, nie otrzyma „こんにちは、影の世界!”, ale „Hello, world!”, ponieważ poddrzewo DOM pod korzeniem shadow jest hermetyzowane.

Oddzielanie treści od prezentacji

Teraz przyjrzymy się, jak używać Shadow DOM do oddzielania treści od prezentacji. Załóżmy, że mamy taki tag:

<style>
.ex2a.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.ex2a .boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.ex2a .name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="ex2a outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

Oto znaczniki. To jest to, co napisałbyś dzisiaj. Nie używa Shadow DOM:

<style>
.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

Ponieważ drzewo DOM nie jest hermetyzowane, cała struktura tagu jest widoczna w dokumencie. Jeśli inne elementy na stronie przypadkowo używają tych samych nazw klas do stylizacji lub skryptów, będziemy mieć problem.

Możemy uniknąć nieprzyjemnych sytuacji.

Krok 1. Ukryj szczegóły prezentacji

Semantycznie prawdopodobnie interesuje nas tylko to, że:

  • To jest tag.
  • Imię to „Bob”.

Najpierw piszemy znaczniki, które są bliższe prawdziwej semantyce, którą chcemy uzyskać:

<div id="nameTag">Bob</div>

Następnie umieszczamy wszystkie style i elementy div używane do prezentacji w elemencie <template>:

<div id="nameTag">Bob</div>
<template id="nameTagTemplate">
<span class="unchanged"><style>
.outer {
  border: 2px solid brown;

  … same as above …

</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div></span>
</template>

W tym momencie renderowany jest tylko „Bob”. Ponieważ przenieśliśmy elementy DOM prezentacji do elementu <template>, nie są one renderowane, ale można uzyskać do nich dostęp z poziomu JavaScriptu. Zróbmy to teraz, aby wypełnić shadow root:

<script>
var shadow = document.querySelector('#nameTag').createShadowRoot();
var template = document.querySelector('#nameTagTemplate');
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);

Po skonfigurowaniu korzenia shadow DOM tag z nazwą jest renderowany ponownie. Jeśli klikniesz prawym przyciskiem myszy tag nazwy i zbadasz element, zobaczysz, że jest to świetny znacznik semantyczny:

<div id="nameTag">Bob</div>

Pokazuje to, że dzięki Shadow DOM ukryliśmy w dokumencie szczegóły prezentacji tagu. Szczegóły prezentacji są zawarte w Shadow DOM.

Krok 2. Oddziel treść od prezentacji

Nasz tag name ukrywa teraz szczegóły prezentacji na stronie, ale nie oddziela prezentacji od treści, ponieważ chociaż treść (imię „Bob”) znajduje się na stronie, renderowane imię to to, które skopiowaliśmy do korzenia shadow. Jeśli chcemy zmienić nazwę na tagu, musimy to zrobić w 2 miejscach, które mogą się rozsynchronizować.

Elementy HTML są kompozycyjne – możesz na przykład umieścić przycisk w tabeli. W tym przypadku potrzebujemy kompozycji: tag nazwy musi być kompozycją czerwonego tła, tekstu „Cześć!” i treści, która znajduje się na tagu nazwy.

Ty, jako autor komponentu, określasz, jak kompozycja działa z widżetem, używając nowego elementu o nazwie <content>. Spowoduje to utworzenie punktu wstawiania w prezentacji widżetu, a punkt wstawiania wybierze treść z hosta shadow, która ma zostać wyświetlona w tym punkcie.

Jeśli zmienimy znaczniki w Shadow DOM na takie:

<span class="unchanged"><template id="nameTagTemplate">
<style>
  …
</style></span>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    <content></content>
  </div>
</div>
<span class="unchanged"></template></span>

Gdy tag nazwy jest renderowany, zawartość hosta shadow jest rzutowana na miejsce, w którym pojawia się element <content>.

Teraz struktura dokumentu jest prostsza, ponieważ nazwa znajduje się tylko w jednym miejscu – w dokumencie. Jeśli na stronie trzeba będzie zaktualizować nazwę użytkownika, wystarczy wpisać:

document.querySelector('#nameTag').textContent = 'Shellie';

i to wszystko. Wyświetlanie tagu jest automatycznie aktualizowane przez przeglądarkę, ponieważ rzutujemy zawartość tagu na miejsce za pomocą elementu <content>.

<div id="ex2b">

Teraz udało nam się oddzielić treść od prezentacji. Treść znajduje się w dokumencie, a prezentacja w Shadow DOM. Są one automatycznie synchronizowane przez przeglądarkę, gdy nadchodzi czas na renderowanie.

Krok 3. Zysk

Dzięki oddzieleniu treści od prezentacji możemy uprościć kod, który manipuluje treścią – w przykładzie z tagiem kod musi obsługiwać tylko prostą strukturę zawierającą jeden element <div> zamiast kilku.

Teraz, jeśli zmienimy prezentację, nie musimy zmieniać kodu.

Załóżmy na przykład, że chcemy zlokalizować nasz tag. Nadal jest to tag nazwy, więc zawartość semantyczna dokumentu nie ulega zmianie:

<div id="nameTag">Bob</div>

Kod konfiguracji elementu shadow root pozostaje bez zmian. Zmienia się tylko to, co jest umieszczane w shadow root:

<template id="nameTagTemplate">
<style>
.outer {
  border: 2px solid pink;
  border-radius: 1em;
  background: url(sakura.jpg);
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
  font-family: sans-serif;
  font-weight: bold;
}
.name {
  font-size: 45pt;
  font-weight: normal;
  margin-top: 0.8em;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="name">
    <content></content>
  </div>
  と申します。
</div>
</template>

To duża poprawa w porównaniu z obecną sytuacją w internecie, ponieważ kod aktualizacji nazwy może zależeć od struktury komponentu, która jest prosta i spójna. Kod aktualizujący nazwę nie musi znać struktury używanej do renderowania. Jeśli weźmiemy pod uwagę to, co jest renderowane, imię pojawia się na drugim miejscu w języku angielskim (po „Hi! My name is”), but first in Japanese (before “と申します”). That distinction is semantically meaningless from the point of view of updating the name that is being displayed, so the name update code doesn’t have to know about that detail.

Dodatkowe środki: zaawansowana projekcja

W powyższym przykładzie element <content> wybiera wszystkie treści z hosta shadow. Za pomocą atrybutu select możesz kontrolować, co wyświetla element treści. Możesz też użyć wielu elementów treści.

Jeśli na przykład masz dokument, który zawiera ten tekst:

<div id="nameTag">
  <div class="first">Bob</div>
  <div>B. Love</div>
  <div class="email">bob@</div>
</div>

oraz element shadow root, który używa selektorów CSS do wybierania określonych treści:

<div style="background: purple; padding: 1em;">
  <div style="color: red;">
    <content **select=".first"**></content>
  </div>
  <div style="color: yellow;">
    <content **select="div"**></content>
  </div>
  <div style="color: blue;">
    <content **select=".email">**</content>
  </div>
</div>

Element <div class="email"> jest dopasowywany zarówno przez element <content select="div">, jak i <content select=".email">. Ile razy pojawia się adres e-mail Boba i w jakich kolorach?

Odpowiedź brzmi: adres e-mail Boba pojawia się raz i jest żółty.

Powodem jest to, że – jak wiedzą osoby, które pracują nad Shadow DOM – tworzenie drzewa tego, co jest faktycznie renderowane na ekranie, jest jak wielka impreza. Element content to zaproszenie, które umożliwia treściom z dokumentu wejście na zakulisową imprezę renderowania Shadow DOM. Zaproszenia są dostarczane w kolejności. To, kto otrzyma zaproszenie, zależy od tego, do kogo jest ono adresowane (czyli od atrybutu select). Treści, gdy tylko zostaną zaproszone, zawsze akceptują zaproszenie (kto by tego nie zrobił?!) i ruszają w drogę. Jeśli na ten adres zostanie wysłane kolejne zaproszenie, nikt go nie odbierze i nie dotrze ono na Twoją imprezę.

W powyższym przykładzie element <div class="email"> pasuje zarówno do selektora div, jak i .email, ale ponieważ element treści z selektorem div pojawia się wcześniej w dokumencie, <div class="email"> trafia do żółtej grupy i nikt nie może dołączyć do niebieskiej. (Może dlatego jest taki niebieski, chociaż w towarzystwie zawsze raźniej, więc nigdy nie wiadomo).

Jeśli element nie jest zaproszony na żadne przyjęcie, nie jest w ogóle renderowany. Tak właśnie stało się z tekstem „Hello, world” w pierwszym przykładzie. Jest to przydatne, gdy chcesz uzyskać radykalnie inne renderowanie: zapisz model semantyczny w dokumencie, który jest dostępny dla skryptów na stronie, ale ukryj go na potrzeby renderowania i połącz z zupełnie innym modelem renderowania w Shadow DOM za pomocą JavaScriptu.

Na przykład HTML ma ładny selektor daty. Jeśli wpiszesz <input type="date">, pojawi się wyskakujące okienko z kalendarzem. Co jednak, jeśli chcesz pozwolić użytkownikowi wybrać zakres dat na wakacje na deserowej wyspie (takiej z hamakami z żelek)? Dokument został skonfigurowany w ten sposób:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

ale tworzy Shadow DOM, który używa tabeli do utworzenia eleganckiego kalendarza, który wyróżnia zakres dat itd. Gdy użytkownik kliknie dni w kalendarzu, komponent aktualizuje stan w polach startDate i endDate. Gdy użytkownik przesyła formularz, przesyłane są wartości z tych elementów wejściowych.

Dlaczego w dokumencie zostały uwzględnione etykiety, skoro nie będą renderowane? Dzięki temu, jeśli użytkownik wyświetli formularz w przeglądarce, która nie obsługuje Shadow DOM, formularz nadal będzie działać, ale nie będzie wyglądać tak dobrze. Użytkownik zobaczy coś takiego:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

Zaliczyłeś(-aś) Shadow DOM 101

To podstawy działania Shadow DOM – znasz już najważniejsze informacje. Możesz zrobić więcej z Shadow DOM, na przykład użyć wielu shadow na jednym shadow host, lub zagnieżdżonych shadow do enkapsulacji, albo zaprojektować stronę przy użyciu Model-Driven Views (MDV) i Shadow DOM. Komponenty internetowe to nie tylko Shadow DOM.

Wyjaśnimy je w kolejnych postach.