Praca z elementami niestandardowymi

Wstęp

W internecie bardzo brakuje ekspresji. Aby zrozumieć, o co mi chodzi, rzuć okiem na „nowoczesną” aplikację internetową, taką jak Gmail:

Gmail

Zupa <div> nie ma nic nowoczesnego. A przecież w ten sposób tworzymy aplikacje internetowe. To smutne. Czy nie powinniśmy wymagać od naszej platformy więcej?

Seksowne oznaczenia. Zróbmy to razem

HTML to doskonałe narzędzie do tworzenia struktury dokumentu, ale jego słownictwo jest ograniczone do elementów zdefiniowanych w standardzie HTML.

Co zrobić, jeśli znaczniki Gmaila nie były okropne? A gdyby to było piękne:

<hangout-module>
    <hangout-chat from="Paul, Addy">
    <hangout-discussion>
        <hangout-message from="Paul" profile="profile.png"
            profile="118075919496626375791" datetime="2013-07-17T12:02">
        <p>Feelin' this Web Components thing.
        <p>Heard of it?
        </hangout-message>
    </hangout-discussion>
    </hangout-chat>
    <hangout-chat>...</hangout-chat>
</hangout-module>

Ale odprężające! Ta aplikacja też ma sens. Są sensowne, łatwe do zrozumienia, a co najważniejsze, łatwe w utrzymaniu. Dzięki analizie deklaratywnej szkieletu będziesz wiedzieć, jak działa.

Wprowadzenie

Elementy niestandardowe umożliwiają programistom stron internetowych definiowanie nowych typów elementów HTML. Specyfikacja jest jednym z kilku nowych podstawowych elementów interfejsu API dostępnych w ramach kategorii Komponenty sieciowe, ale prawdopodobnie najważniejszym. Komponenty sieciowe nie występują bez funkcji odblokowanych przez elementy niestandardowe:

  1. Zdefiniuj nowe elementy HTML/DOM
  2. Twórz elementy, które wykraczają poza inne elementy
  3. Połącz logicznie niestandardowe funkcje w jeden tag.
  4. Rozszerz interfejs API istniejących elementów DOM

Rejestrowanie nowych elementów

Elementy niestandardowe tworzy się za pomocą document.registerElement():

var XFoo = document.registerElement('x-foo');
document.body.appendChild(new XFoo());

Pierwszy argument funkcji document.registerElement() to nazwa tagu elementu. Nazwa musi zawierać łącznik (-). Na przykład <x-tags>, <my-element> i <my-awesome-app> to prawidłowe nazwy, ale <tabs> i <foo_bar> nie. To ograniczenie pozwala parserowi odróżniać elementy niestandardowe od elementów zwykłych, ale zapewnia też zgodność dalszą w przypadku dodawania do kodu HTML nowych tagów.

Drugi argument jest (opcjonalnym) obiektem opisującym prototype elementu. Tutaj możesz dodawać do elementów niestandardowe funkcje (np. właściwości publiczne i metody). Więcej na ten temat później.

Domyślnie elementy niestandardowe dziedziczą z elementu HTMLElement. Zatem poprzedni przykład jest odpowiednikiem:

var XFoo = document.registerElement('x-foo', {
    prototype: Object.create(HTMLElement.prototype)
});

Wywołanie document.registerElement('x-foo') przekazuje przeglądarce nowy element i zwraca konstruktor, którego możesz użyć do utworzenia wystąpień elementu <x-foo>. Jeśli nie chcesz używać konstruktora, możesz też zastosować inne metody tworzenia instancji elementów.

Elementy rozszerzające

Elementy niestandardowe pozwalają rozszerzyć istniejące (natywne) elementy HTML oraz inne elementy niestandardowe. Aby rozszerzyć element, musisz przekazać registerElement() nazwę i prototype elementu, który ma być dziedziczony.

Rozszerzanie elementów natywnych

Powiedzmy, że nie podoba Ci się zwykły Jan <button>. Chcesz wykorzystać wszystkie możliwości, aby zamienić je na „megaprzycisk”. Aby rozszerzyć element <button>, utwórz nowy element, który odziedziczy prototype właściwości HTMLButtonElement i extends nazwę elementu. W tym przypadku „przycisk”:

var MegaButton = document.registerElement('mega-button', {
    prototype: Object.create(HTMLButtonElement.prototype),
    extends: 'button'
});

Elementy niestandardowe, które dziedziczą z elementów natywnych, są nazywane elementami niestandardowymi rozszerzenia typu. Dziedziczą one ze specjalistycznej wersji funkcji HTMLElement, czyli na przykład „element X to Y”.

Przykład:

<button is="mega-button">

Rozszerzanie elementu niestandardowego

Aby utworzyć element <x-foo-extended>, który stanowi rozszerzenie elementu niestandardowego <x-foo>, odziedzicz po prostu jego prototyp i podaj z niego tag:

var XFooProto = Object.create(HTMLElement.prototype);
...

var XFooExtended = document.registerElement('x-foo-extended', {
    prototype: XFooProto,
    extends: 'x-foo'
});

Więcej informacji o tworzeniu prototypów elementów znajdziesz w sekcji Dodawanie właściwości i metod JS poniżej.

Uaktualnianie elementów

Zastanawiało Cię kiedyś, dlaczego parser HTML nie dopasowuje niestandardowych tagów? Na przykład świetnie będzie, gdy zadeklarujemy na stronie element <randomtag>. Zgodnie ze specyfikacją HTML:

<randomtag>, niestety Masz niestandardowy typ reklamy i jest on dziedziczony z domeny HTMLUnknownElement.

To samo dotyczy elementów niestandardowych. Elementy z prawidłowymi nazwami elementów niestandardowych dziedziczą atrybut HTMLElement. Możesz to sprawdzić, uruchamiając konsolę Ctrl + Shift + J (lub Cmd + Opt + J na komputerze Mac) i wklejając ten kod. Zwracają one wartość true:

// "tabs" is not a valid custom element name
document.createElement('tabs').__proto__ === HTMLUnknownElement.prototype

// "x-tabs" is a valid custom element name
document.createElement('x-tabs').__proto__ == HTMLElement.prototype

Nierozwiązane elementy

Elementy niestandardowe są rejestrowane przez skrypt za pomocą funkcji document.registerElement(), dlatego mogą zostać zadeklarowane lub utworzone przed zarejestrowaniem ich definicji przez przeglądarkę. Możesz na przykład zadeklarować na stronie właściwość <x-tabs>, ale znacznie później wywołasz właściwość document.registerElement('x-tabs').

Przed aktualizacją do określonej definicji elementy są nazywane elementami nierozstrzygniętymi. To elementy HTML, które mają prawidłową nazwę elementu niestandardowego, ale nie zostały zarejestrowane.

Poniższa tabela pomoże Ci utrzymać porządek:

Nazwa Dziedziczy wartość z Przykłady
Nierozwiązany element HTMLElement <x-tabs>, <my-element>
Nieznany element HTMLUnknownElement <tabs>, <foo_bar>

Tworzenie instancji elementów

Popularne techniki tworzenia elementów nadal mają zastosowanie do elementów niestandardowych. Podobnie jak w przypadku innych elementów standardowych, można je zadeklarować w HTML lub utworzyć w DOM za pomocą JavaScriptu.

Tworzenie instancji tagów niestandardowych

Zadeklaruj:

<x-foo></x-foo>

Utwórz DOM w JS:

var xFoo = document.createElement('x-foo');
xFoo.addEventListener('click', function(e) {
    alert('Thanks!');
});

Użyj operatora new:

var xFoo = new XFoo();
document.body.appendChild(xFoo);

Tworzenie wystąpienia elementów rozszerzenia typu

Możliwość tworzenia instancji niestandardowych elementów typu rozszerzenia jest wyraźnie podobna do tagów niestandardowych.

Zadeklaruj:

<!-- <button> "is a" mega button -->
<button is="mega-button">

Utwórz DOM w JS:

var megaButton = document.createElement('button', 'mega-button');
// megaButton instanceof MegaButton === true

Jak widać, istnieje teraz przeciążona wersja atrybutu document.createElement(), która przyjmuje jako drugi parametr is="".

Użyj operatora new:

var megaButton = new MegaButton();
document.body.appendChild(megaButton);

Na razie dowiedzieliśmy się, jak używać document.registerElement(), by poinformować przeglądarkę o nowym tagu, ale to nic nie da. Dodajmy właściwości i metody.

Dodawanie właściwości i metod JS

Ogromną zaletą elementów niestandardowych jest to, że możesz połączyć z elementem dostosowaną funkcjonalność, definiując właściwości i metody definicji elementu. Wyobraź sobie, że możesz utworzyć publiczny interfejs API dla swojego elementu.

Oto pełny przykład:

var XFooProto = Object.create(HTMLElement.prototype);

// 1. Give x-foo a foo() method.
XFooProto.foo = function() {
    alert('foo() called');
};

// 2. Define a property read-only "bar".
Object.defineProperty(XFooProto, "bar", {value: 5});

// 3. Register x-foo's definition.
var XFoo = document.registerElement('x-foo', {prototype: XFooProto});

// 4. Instantiate an x-foo.
var xfoo = document.createElement('x-foo');

// 5. Add it to the page.
document.body.appendChild(xfoo);

prototype można tworzyć oczywiście na tysiące sposobów. Jeśli nie przepadasz za tworzeniem prototypów takich jak ten, oto skrócona wersja tego samego materiału:

var XFoo = document.registerElement('x-foo', {
  prototype: Object.create(HTMLElement.prototype, {
    bar: {
      get: function () {
        return 5;
      }
    },
    foo: {
      value: function () {
        alert('foo() called');
      }
    }
  })
});

Pierwszy format umożliwia użycie ES5 Object.defineProperty. Drugi pozwala użyć polecenia get/set.

Metody wywołań zwrotnych cyklu życia

Elementy mogą definiować specjalne metody nawiązywania kontaktu z ciekawymi momentami z życia. Metody te mają odpowiednie nazwy wywołania zwrotnego cyklu życia. Każdy ma określoną nazwę i określone przeznaczenie:

Nazwa wywołania zwrotnego Wywołane, gdy
createdCallback tworzona jest instancja elementu
attachedCallback wystąpienie zostało wstawione do dokumentu
detachedCallback wystąpienie zostało usunięte z dokumentu
attributeChangedCallback(attrName, oldVal, newVal) dodany, usunięty lub zaktualizowany atrybut;

Przykład: definiowanie parametrów createdCallback() i attachedCallback() w domenie <x-foo>:

var proto = Object.create(HTMLElement.prototype);

proto.createdCallback = function() {...};
proto.attachedCallback = function() {...};

var XFoo = document.registerElement('x-foo', {prototype: proto});

Wszystkie wywołania zwrotne cyklu życia są opcjonalne, ale możesz je zdefiniować, jeśli ma to sens. Załóżmy na przykład, że Twój element jest wystarczająco złożony i w createdCallback() otwiera połączenie z IndexedDB. Zanim zostanie on usunięty z DOM, przeprowadź niezbędne operacje czyszczenia w detachedCallback(). Uwaga: nie korzystaj z tej możliwości (np. gdy użytkownik zamknie kartę), ale potraktuj ją jako potencjalny punkt optymalizacyjny.

Wywołania zwrotne cyklu życia są też używane do konfigurowania domyślnych detektorów zdarzeń w elemencie:

proto.createdCallback = function() {
  this.addEventListener('click', function(e) {
    alert('Thanks!');
  });
};

Dodawanie znaczników

Utworzyliśmy plik <x-foo>, nadaliśmy mu interfejs JavaScript API, ale jest on pusty. Czy powinniśmy udostępnić mu kod HTML do renderowania?

Przydatne mogą tu być wywołania zwrotne cyklu życia. Za pomocą createdCallback() możemy w szczególności dodać do elementu domyślny kod HTML:

var XFooProto = Object.create(HTMLElement.prototype);

XFooProto.createdCallback = function() {
    this.innerHTML = "**I'm an x-foo-with-markup!**";
};

var XFoo = document.registerElement('x-foo-with-markup', {prototype: XFooProto});

Utworzenie wystąpienia tego tagu i badanie go w Narzędziach deweloperskich (kliknij prawym przyciskiem myszy i wybierz Zbadaj element) powinno przynieść następujące wyniki:

▾<x-foo-with-markup>
  **I'm an x-foo-with-markup!**
</x-foo-with-markup>

Otoczenie zasobów wewnętrznych w modelu Shadow DOM

Sam model Shadow DOM jest skutecznym narzędziem do uwzględniania treści. Możesz ją łączyć z elementami niestandardowymi, dzięki czemu nabiera magii.

Shadow DOM udostępnia elementy niestandardowe:

  1. Sposób ukrycia swojej odwagi, chroniący użytkowników przed drastycznymi szczegółami implementacji.
  2. Czapka stylu... bezpłatnie.

Tworzenie elementu za pomocą modelu Shadow DOM jest podobne do tworzenia elementu renderującego podstawowe znaczniki. Różnica jest w createdCallback():

var XFooProto = Object.create(HTMLElement.prototype);

XFooProto.createdCallback = function() {
    // 1. Attach a shadow root on the element.
    var shadow = this.createShadowRoot();

    // 2. Fill it with markup goodness.
    shadow.innerHTML = "**I'm in the element's Shadow DOM!**";
};

var XFoo = document.registerElement('x-foo-shadowdom', {prototype: XFooProto});

Zamiast ustawienia właściwości .innerHTML elementu utworzyliśmy dla elementu <x-foo-shadowdom> cień korzenie, a potem wypełniłem go znacznikami. Gdy w Narzędziach deweloperskich włączysz ustawienie „Pokaż DOM Shadow”, zobaczysz #shadow-root, który można rozwinąć:

▾<x-foo-shadowdom>
  ▾#shadow-root
    **I'm in the element's Shadow DOM!**
</x-foo-shadowdom>

To właśnie Korzenie Cienia!

Tworzenie elementów na podstawie szablonu

Szablony HTML to kolejny nowy element interfejsu API, który ładnie wpasowuje się w świat elementów niestandardowych.

Przykład: rejestrowanie elementu utworzonego na podstawie modelu <template> i modelu Shadow DOM:

<template id="sdtemplate">
  <style>
    p { color: orange; }
  </style>
  <p>I'm in Shadow DOM. My markup was stamped from a <template&gt;.
</template>

<script>
  var proto = Object.create(HTMLElement.prototype, {
    createdCallback: {
      value: function() {
        var t = document.querySelector('#sdtemplate');
        var clone = document.importNode(t.content, true);
        this.createShadowRoot().appendChild(clone);
      }
    }
  });
  document.registerElement('x-foo-from-template', {prototype: proto});
</script>

<template id="sdtemplate">
  <style>:host p { color: orange; }</style>
  <p>I'm in Shadow DOM. My markup was stamped from a <template&gt;.
</template>

<div class="demoarea">
  <x-foo-from-template></x-foo-from-template>
</div>

Kilka linijek kodu daje mnóstwo możliwości. Spróbujmy zrozumieć, co się dzieje:

  1. Zarejestrowaliśmy nowy element w kodzie HTML: <x-foo-from-template>
  2. DOM elementu został utworzony na podstawie <template>
  3. Przerażające szczegóły elementu ukryte za pomocą interfejsu Shadow DOM.
  4. Model cienia DOM nadaje estetyce stylu elementu (np. p {color: orange;} nie zmienia całej strony na pomarańczowy)

Po prostu super!

Styl elementów niestandardowych

Podobnie jak w przypadku każdego tagu HTML użytkownicy tagu niestandardowego mogą zmieniać jego styl za pomocą selektorów:

<style>
  app-panel {
    display: flex;
  }
  [is="x-item"] {
    transition: opacity 400ms ease-in-out;
    opacity: 0.3;
    flex: 1;
    text-align: center;
    border-radius: 50%;
  }
  [is="x-item"]:hover {
    opacity: 1.0;
    background: rgb(255, 0, 255);
    color: white;
  }
  app-panel > [is="x-item"] {
    padding: 5px;
    list-style: none;
    margin: 0 7px;
  }
</style>

<app-panel>
    <li is="x-item">Do</li>
    <li is="x-item">Re</li>
    <li is="x-item">Mi</li>
</app-panel>

Styl elementów, które korzystają z modelu Shadow DOM

Królicza dziura wchodzi o wiele znacznie głębiej, jeśli zastosujesz model Shadow DOM. Elementy niestandardowe korzystające z modelu Shadow DOM mają swoje zalety.

Shadow DOM wplata element z enkapsulacją stylu. Style zdefiniowane w katalogu głównym cieni nie wyciekają z hosta ani nie wyleją ze strony. W przypadku elementu niestandardowego hostem jest sam element. Właściwości hermetyzacji stylu pozwalają też elementom niestandardowym samodzielnie definiować style domyślne.

Stosowanie stylów cieni DOM to bardzo duży temat. Jeśli chcesz dowiedzieć się więcej na ten temat, polecam kilka innych artykułów:

Zapobieganie FOUC za pomocą ciągu :unresolved

Aby ograniczyć ryzyko FOUC, specyfikacja elementów niestandardowych wymaga nowej pseudoklasy CSS, :unresolved. Służy on do kierowania na nierozwiązane elementy aż do momentu, w którym przeglądarka wywoła metodę createdCallback() (patrz metody cyklu życia). Gdy to nastąpi, element przestanie być nierozpoznany. Proces uaktualniania został zakończony, a element stał się swoją definicją.

Przykład: zanikanie w tagach „x-foo” po ich zarejestrowaniu:

<style>
  x-foo {
    opacity: 1;
    transition: opacity 300ms;
  }
  x-foo:unresolved {
    opacity: 0;
  }
</style>

Pamiętaj, że właściwość :unresolved ma zastosowanie tylko do nierozwiązanych elementów, a nie elementów, które dziedziczą z elementu HTMLUnknownElement (zobacz sposób uaktualniania elementów).

<style>
  /* apply a dashed border to all unresolved elements */
  :unresolved {
    border: 1px dashed red;
    display: inline-block;
  }
  /* x-panel's that are unresolved are red */
  x-panel:unresolved {
    color: red;
  }
  /* once the definition of x-panel is registered, it becomes green */
  x-panel {
    color: green;
    display: block;
    padding: 5px;
    display: block;
  }
</style>

<panel>
    I'm black because :unresolved doesn't apply to "panel".
    It's not a valid custom element name.
</panel>

<x-panel>I'm red because I match x-panel:unresolved.</x-panel>

Obsługa historii i przeglądarki

Wykrywanie funkcji

Wykrywanie cech polega na sprawdzaniu, czy obiekt document.registerElement() istnieje:

function supportsCustomElements() {
    return 'registerElement' in document;
}

if (supportsCustomElements()) {
    // Good to go!
} else {
    // Use other libraries to create components.
}

Obsługiwane przeglądarki

W Chrome od wersji 27 i Firefoksie (ok. 23) aplikacja document.registerElement() najpierw znalazła się za flagą. Od tego czasu jednak specyfikacja nieco się zmieniła. Chrome 31 jest pierwszą, która obsługuje zaktualizowane specyfikacje.

Dopóki przeglądarki nie będą obsługiwane, będziemy stosować kod polyfill, z którego korzystają moduły Google Polymer i X-Tag Mozilla.

Co się stało z elementem HTMLElementElement?

Dla firm, które przeszły proces standaryzacji, wiadomo, że było już <element>. Chodziło o kolany pszczół. Możesz go użyć do deklaratywnego zarejestrowania nowych elementów:

<element name="my-element">
    ...
</element>

Niestety wystąpiło zbyt wiele problemów z czasem związanych z procesem uaktualniania, przypadkami narożnymi i scenariuszami podobnymi do Armageddon. Element <element> trzeba było umieścić na półce. W sierpniu 2013 roku Dimitri Glazkov opublikował wpis w public-webapps o informacjach o usunięciu aplikacji (przynajmniej na razie).

Warto zauważyć, że Polymer implementuje deklaratywną formę rejestracji elementów za pomocą funkcji <polymer-element>. Jak to zrobić? Wykorzystuje document.registerElement('polymer-element') i techniki opisane w sekcji Tworzenie elementów z szablonu.

Podsumowanie

Dzięki elementom niestandardowym możemy poszerzyć słownictwo HTML, nauczyć go nowych sztuczek i przeskoczyć przez tunele internetowe. Po połączeniu ich z innymi elementami podstawowymi platformy, takimi jak Shadow DOM i <template>, zaczynamy uświadamiać sobie obraz komponentów sieciowych. Znaczniki znów mogą być seksowne!

Jeśli chcesz zacząć korzystać z komponentów sieciowych, polecamy program Polymer. Znajdziesz w nim więcej, niż to konieczne.