Podstawy DOM – Shadow

Dominic Cooney
Dominic Cooney

Wprowadzenie

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

  1. Umożliw tworzenie widżetów
  2. …które można niezawodnie ponownie wykorzystać
  3. …i nie spowoduje przerwania działania stron, jeśli następna wersja komponentu zmieni wewnętrzne szczegóły implementacji.

Czy to oznacza, że musisz decydować, kiedy używać HTML/JavaScript, a kiedy Web Components? Nie! HTML i JavaScript mogą służyć do tworzenia interaktywnych elementów wizualnych. Widżety to interaktywne elementy wizualne. Podczas tworzenia widżetu warto wykorzystać umiejętności związane z HTML-em i JavaScriptem. Standardy komponentów internetowych zostały opracowane, aby Ci w tym pomóc.

Istnieje jednak podstawowy problem, który utrudnia korzystanie z widżetów tworzonych w HTML i JavaScript: drzewo DOM w widżecie nie jest odseparowane od reszty strony. Ten brak enkapsulacji oznacza, że spersonalizowana szata graficzna dokumentu może zostać przypadkowo zastosowana do części wewnątrz widżetu, kod JavaScript może przypadkowo zmodyfikować części wewnątrz widżetu, identyfikatory mogą się pokrywać z identyfikatorami wewnątrz widżetu itd.

Web Components składa się z 3 części:

  1. Szablony
  2. Shadow DOM
  3. Elementy niestandardowe

Shadow DOM rozwiązuje problem hermetyzacji drzewa DOM. Cztery części Web Components są zaprojektowane tak, aby współpracowały ze sobą, ale możesz też wybrać, których części Web Components chcesz używać. Ten samouczek pokazuje, jak korzystać z Shadow DOM.

Cześć, Shadow World

Dzięki Shadow DOM elementy mogą mieć powiązany z nimi nowy rodzaj węzła. Ten nowy rodzaj węzła nazywamy korzeniami cieni. Element, który ma powiązany względny rdzeń, nazywa się względnym hostem. Treści hosta cienia nie są renderowane; zamiast tego renderowane są treści głównego hosta cienia.

Załóżmy na przykład, że masz taki znacznik:

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

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>

Twoja strona wygląda tak:

<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 kod JavaScript na stronie zapyta o wartość atrybutu textContent przycisku, nie otrzyma odpowiedzi „こんにちは、影の世界!”, tylko „Hello, world!”, ponieważ poddrzewie DOM pod węzłem głównym cienia jest zakapsułkowane.

Oddzielanie treści od prezentacji

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

<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 wpisujesz 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 otoczone, cała struktura tagu name jest widoczna w dokumencie. Jeśli inne elementy na stronie używają tych samych nazw klas do stylizacji lub skryptów, może to spowodować problemy.

Dzięki temu unikniesz przykrych doświadczeń.

Krok 1. Ukryj szczegóły prezentacji

Semantycznie ważne jest tylko to, że:

  • To wizytówka.
  • Nazwa to „Bob”.

Najpierw piszemy znaczniki, które są bliższe pożądanej semantyki:

<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 tylko „Bob” jest renderowany. Ponieważ elementy prezentacyjne interfejsu DOM zostały przeniesione do elementu <template>, nie są renderowane, ale można do nich uzyskać dostęp z poziomu kodu JavaScript. Robimy to teraz, aby wypełnić katalog skojarzony:

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

Po skonfigurowaniu katalogu potomnego cienia tag nazwy jest ponownie renderowany. Jeśli klikniesz prawym przyciskiem myszy plakietkę z nazwą i zbadasz element, zobaczysz, że jest to świetny znacznik semantyczny:

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

Pokazuje to, że dzięki użyciu modelu shadow DOM ukryliśmy szczegóły wyświetlania tagu name z dokumentu. Szczegóły prezentacji są hermetyzowane w modelu DOM cieni.

Krok 2. Oddziel treści od prezentacji

Tag nazwy teraz ukrywa szczegóły prezentacji na stronie, ale nie oddziela jej od treści, ponieważ chociaż treść (nazwa „Bob”) znajduje się na stronie, renderowana jest nazwa skopiowana do katalogu głównego cienia. Jeśli chcemy zmienić nazwę na plakietce, musimy to zrobić w 2 miejscach, co może spowodować utratę synchronizacji.

Elementy HTML są kompozycyjne – możesz na przykład umieścić przycisk w tabeli. Potrzebujemy tu kompozycji: plakietka z nazwiskiem musi składać się z czerwonego tła, tekstu „Cześć!” i treści, które się na niej znajdują.

Ty, autor komponentu, określasz, jak ma działać kompozycja w przypadku Twojego widżetu, za pomocą nowego elementu o nazwie <content>. Tworzy to punkt wstawiania w prezentacji widżetu, który wybiera treści z hosta szeptanego, aby je wyświetlić.

Jeśli zmienimy znaczniki w cieniach DOM na te:

<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 cienia jest wyświetlana w miejscu, 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 zaktualizować imię i nazwisko użytkownika, wystarczy napisać:

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

To wszystko. Renderowanie tagu nazwy jest automatycznie aktualizowane przez przeglądarkę, ponieważ projekujemy zawartość tagu nazwy w miejsce za pomocą <content>.

<div id="ex2b">

Udało nam się oddzielić treści od prezentacji. Treść jest w dokumencie, a prezentacja w shadow DOM. Gdy nadejdzie czas na renderowanie, przeglądarka automatycznie zsynchronizuje te pliki.

Krok 3. Zyski

Dzięki rozdzieleniu treści i prezentacji możemy uprościć kod, który manipuluje treścią. W przypadku tagu nazwy kod musi obsługiwać tylko prostą strukturę zawierającą 1 element <div> zamiast kilku.

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

Załóżmy, że chcemy zlokalizować nasz identyfikator. Nadal jest to tag nazwy, więc treść semantyczna w dokumencie się nie zmienia:

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

Kod konfiguracji korzenia cienia pozostaje bez zmian. Tylko to, co jest umieszczane w katalogu katalogu katalogu cieni:

<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>

Jest to znaczna poprawa w stosunku do obecnej sytuacji w internecie, ponieważ kod aktualizacji nazwy może zależeć od struktury elementu, która jest prosta i spójna. Twój kod aktualizacji nazwy nie musi znać struktury używanej do renderowania. Jeśli weźmiemy pod uwagę to, co jest renderowane, nazwa pojawia się po angielsku (po „Cześć! My name is”), ale najpierw w języku japońskim (przed „と申します”). To rozróżnienie nie ma znaczenia semantycznego z punktu widzenia aktualizacji wyświetlanej nazwy, więc kod aktualizacji nazwy nie musi znać tej informacji.

Dodatkowe informacje: zaawansowane wyświetlanie

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

Jeśli na przykład masz dokument zawierający te informacje:

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

oraz skrypt uruchamiający korzeń cienia, który używa selektorów CSS do wybierania konkretnych 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 do elementów <content select="div">, jak i <content select=".email">. Ile razy pojawia się adres e-mail Roberta i w jakich kolorach?

Odpowiedź brzmi: adres e-mail Boba pojawia się raz i jest podświetlony na żółto.

Jak wiedzą osoby, które zajmują się hackowaniem Shadow DOM, tworzenie drzewa tego, co jest faktycznie renderowane na ekranie, przypomina ogromną imprezę. Element treści to zaproszenie, które umożliwia umieszczenie treści z dokumentu w party renderowania Shadow DOM. Te zaproszenia są dostarczane w kolejności. Kto je otrzyma, zależy od tego, do kogo są one kierowane (czyli od atrybutu select). Treści, gdy zostaną zaproszone, zawsze akceptują zaproszenie (kto by tego nie zrobił?) i są gotowe do działania. Jeśli kolejne zaproszenie zostanie wysłane na ten sam adres, nikt nie będzie w domu, a zaproszenie nie dotrze na Twoją imprezę.

W powyższym przykładzie element <div class="email"> pasuje zarówno do selektora div, jak i do selektora .email, ale ponieważ element treści z selektorem div znajduje się wcześniej w dokumencie, element <div class="email"> jest przypisany do żółtej grupy, a żaden element nie jest dostępny dla niebieskiej grupy. (Może to wyjaśnia, dlaczego jest tak niebieski, chociaż nieszczęście lubi towarzystwo, więc nigdy nie wiadomo).

Jeśli coś jest zaproszone do żadnej grupy, nie jest w ogóle renderowane. Tak się stało z tekstem „Hello, world” w pierwszym przykładzie. Jest to przydatne, gdy chcesz uzyskać radykalnie inny sposób renderowania: napisz model semantyczny w dokumentie, 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 DOM cieni za pomocą JavaScriptu.

Na przykład HTML ma ładny selektor daty. Jeśli wpiszesz <input type="date">, zobaczysz wyskakujące okienko z kalendarza. Co jednak, jeśli chcesz umożliwić użytkownikowi wybranie zakresu dat na wakacje na wyspie (wiesz, z hamakami z czerwonych winogron)? Aby skonfigurować dokument:

<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 utwórz Shadow DOM, który używa tabeli do tworzenia wygodnej tabeli, która wyróżnia zakres dat itp. Gdy użytkownik klika dni w kalendarzu, komponent aktualizuje stan w polach wejściowych startDate i endDate. Gdy użytkownik prześle formularz, wartości z tych elementów wejściowych zostaną przesłane.

Dlaczego w dokumencie są etykiety, skoro nie będą renderowane? Jeśli użytkownik wyświetla formularz w przeglądarce, która nie obsługuje Shadow DOM, formularz nadal będzie działać, ale nie będzie tak ładnie wyglądać. Użytkownik widzi 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>

You Pass Shadow DOM 101

To są podstawy Shadow DOM – zaliczasz Shadow DOM 101. Dzięki Shadow DOM możesz np. użyć wielu klonów w jednym hostie klonów lub zagnieżdżonych klonów na potrzeby hermetyzacji albo zaprojektować stronę za pomocą widoków opartych na modelu (MDV) i Shadow DOM. Komponenty internetowe to coś więcej niż tylko model Shadow DOM.

Omówimy je w kolejnych wpisach.