Wprowadzenie
W internecie brakuje wyrazu. Aby zobaczyć, o co chodzi, spójrz na „nowoczesne” aplikacje internetowe, takie jak Gmail:
Zupa <div>
nie jest nowoczesna. A jednak właśnie tak tworzymy aplikacje internetowe. To smutne.
Czy nie powinniśmy wymagać więcej od naszej platformy?
Bardzo dobre znaczniki. Zróbmy to
HTML to doskonałe narzędzie do strukturyzowania dokumentu, ale jego słownictwo jest ograniczone do elementów zdefiniowanych przez standard HTML.
Co, jeśli znaczniki w Gmailu nie były źle? Co, jeśli jest piękna:
<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>
To bardzo odświeżające. Ta aplikacja też ma sens. Jest znacząca, łatwo ją zrozumieć, a co najważniejsze, jest utrzymywalna. Przyszłe ja/Ty będziesz/będziesz wiedzieć dokładnie, co ono robi, po prostu analizując jego deklaratywną szkieletową część.
Pierwsze kroki
Elementy niestandardowe pozwalają twórcom stron internetowych definiować nowe typy elementów HTML. Specyfikacja jest jednym z kilku nowych interfejsów API, które mieszczą się w ramach Web Components, ale prawdopodobnie jest najważniejszym z nich. Komponenty internetowe nie istnieją bez funkcji udostępnionych przez elementy niestandardowe:
- Definiowanie nowych elementów HTML/DOM
- Tworzenie elementów rozszerzających inne elementy
- logicznie łączyć ze sobą funkcje niestandardowe w jeden tag;
- Rozszerzanie interfejsu API istniejących elementów DOM
Rejestrowanie nowych elementów
Elementy niestandardowe są tworzone 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, a <tabs>
i <foo_bar>
– nie. To ograniczenie pozwala parsownikowi odróżnić elementy niestandardowe od zwykłych, ale też zapewnia zgodność wsteczną po dodaniu nowych tagów do kodu HTML.
Drugi argument to (opcjonalnie) obiekt opisujący prototype
elementu.
Tutaj możesz dodawać do elementów niestandardowe funkcje (np. publiczne właściwości i metody).
Więcej informacji znajdziesz później.
Domyślnie elementy niestandardowe dziedziczą z poziomu HTMLElement
. W związku z tym poprzedni przykład jest równoważny:
var XFoo = document.registerElement('x-foo', {
prototype: Object.create(HTMLElement.prototype)
});
Wywołanie document.registerElement('x-foo')
informuje przeglądarkę o nowym elemencie i zwraca konstruktor, którego można użyć do tworzenia instancji <x-foo>
.
Jeśli nie chcesz używać konstruktora, możesz użyć innych technik tworzenia elementów.
Rozszerzanie elementów
Elementy niestandardowe umożliwiają rozszerzanie istniejących (natywnych) elementów HTML oraz innych elementów niestandardowych. Aby rozszerzyć element, musisz przekazać registerElement()
nazwę prototype
elementu, z którego chcesz odziedziczyć.
Rozszerzanie elementów natywnych
Załóżmy, że nie jesteś zadowolony z Regular Joe <button>
. Chcesz zwiększyć jego możliwości, aby stał się „Mega przyciskiem”. Aby rozszerzyć element <button>
, utwórz nowy element, który dziedziczy prototype
z elementu HTMLButtonElement
i extends
z nazwy elementu. W tym przypadku „button”:
var MegaButton = document.registerElement('mega-button', {
prototype: Object.create(HTMLButtonElement.prototype),
extends: 'button'
});
Elementy niestandardowe, które dziedziczą z elementów natywnych, nazywamy elementami niestandardowymi rozszerzenia typu.
Dziedziczą one z specjalnej wersji HTMLElement
, co oznacza, że „element X jest elementem Y”.
Przykład:
<button is="mega-button">
Rozszerzanie elementu niestandardowego
Aby utworzyć element <x-foo-extended>
, który rozszerza element niestandardowy <x-foo>
, wystarczy odziedziczyć jego prototyp i określić, z jakiego tagu chcesz odziedziczyć:
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.
Jak aktualizować elementy
Czy zastanawiałeś/się kiedyś, dlaczego parser HTML nie wyrzuca błędów w przypadku tagów niestandardowych?
Na przykład świetnie, jeśli na stronie zadeklarujemy <randomtag>
. Zgodnie ze specyfikacją HTML:
Przepraszam <randomtag>
Niestandardowy typ, który dziedziczy z HTMLUnknownElement
.
W przypadku elementów niestandardowych jest inaczej. Elementy z prawidłowymi nazwami elementów niestandardowych dziedziczą z elementu HTMLElement
. Aby to sprawdzić, otwórz konsolę: Ctrl + Shift + J
(lub Cmd + Opt + J
na Macu) i wklej te wiersze kodu, które zwracają 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
Nierozstrzygnięte elementy
Elementy niestandardowe są rejestrowane przez skrypt za pomocą document.registerElement()
, co oznacza, że można je zadeklarować lub utworzyć przed zarejestrowaniem ich definicji przez przeglądarkę. Możesz na przykład zadeklarować <x-tabs>
na stronie, ale wywołać document.registerElement('x-tabs')
dopiero później.
Zanim elementy zostaną zastąpione przez definicję, nazywane są nierozwiązanymi elementami. Są to elementy HTML, które mają prawidłową nazwę elementu niestandardowego, ale nie zostały zarejestrowane.
Ta tabela może Ci w tym pomóc:
Nazwa | Dziedziczy z | Przykłady |
---|---|---|
Nierozstrzygnięty element | HTMLElement |
<x-tabs> , <my-element> |
Nieznany element | HTMLUnknownElement |
<tabs> , <foo_bar> |
Tworzenie instancji elementów
Elementy niestandardowe nadal podlegają standardowym technikom tworzenia elementów. Podobnie jak w przypadku innych standardowych elementów, można je zadeklarować w HTML lub utworzyć w DOM za pomocą JavaScriptu.
Tworzenie wystąpienia tagów niestandardowych
Zadeklaruj:
<x-foo></x-foo>
Tworzenie 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 elementów rozszerzenia typu
Tworzenie elementów niestandardowych w stylu rozszerzeń typu jest bardzo podobne do tworzenia tagów niestandardowych.
Zadeklaruj:
<!-- <button> "is a" mega button -->
<button is="mega-button">
Tworzenie DOM w JS:
var megaButton = document.createElement('button', 'mega-button');
// megaButton instanceof MegaButton === true
Jak widać, teraz jest przeciążona wersja funkcji document.createElement()
, która przyjmuje jako drugi parametr atrybut is=""
.
Użyj operatora new
:
var megaButton = new MegaButton();
document.body.appendChild(megaButton);
Do tej pory dowiedzieliśmy się, jak za pomocą tagu document.registerElement()
poinformować przeglądarkę o nowym tagu, ale nie jest to zbyt skuteczne. Dodajmy właściwości i metody.
Dodawanie właściwości i metod JS
Elementy niestandardowe są przydatne, ponieważ możesz dodać do nich niestandardowe funkcje, definiując właściwości i metody w definicji elementu. Możesz to traktować jako sposób na utworzenie publicznego interfejsu API dla 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);
Oczywiście istnieje wiele tysięcy sposobów na stworzenie prototype
. Jeśli nie lubisz tworzyć prototypów w taki sposób, możesz skorzystać z bardziej skompresowanej wersji tego samego procesu:
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 korzystanie z ES5 Object.defineProperty
. Drugi umożliwia użycie metod get/set.
Metody wywołania zwrotnego w cyklu życia
Elementy mogą definiować specjalne metody wykorzystywania interesujących momentów ich istnienia. Te metody są odpowiednio nazywane wywołaniami cyklu życia. Każdy z nich ma określoną nazwę i przeznaczenie:
Nazwa połączenia zwrotnego | Wywoływany, gdy |
---|---|
createdCallback | tworzona jest instancja elementu. |
attachedCallback | w dokumencie została wstawiona instancja |
detachedCallback | wystąpienie zostało usunięte z dokumentu, |
attributeChangedCallback(attrName, oldVal, newVal) | atrybut został dodany, usunięty lub zaktualizowany; |
Przykład: definiowanie elementów createdCallback()
i attachedCallback()
w elemencie <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 metody obsługi zdarzeń cyklu życia są opcjonalne, ale zdefiniuj je, jeśli to ma sens.
Załóżmy na przykład, że element jest na tyle złożony, że otwiera połączenie z IndexedDB w createdCallback()
. Zanim usuniesz element z DOM-u, wykonaj niezbędne czynności czyszczące w detachedCallback()
. Uwaga: nie należy polegać na tym, na przykład gdy użytkownik zamyka kartę, ale można to traktować jako potencjalny element optymalizacji.
Innym przypadkiem użycia wywołań zwrotnych cyklu życia jest konfigurowanie domyślnych odbiorników zdarzeń na elemencie:
proto.createdCallback = function() {
this.addEventListener('click', function(e) {
alert('Thanks!');
});
};
Dodawanie znaczników
Utworzyliśmy <x-foo>
, dodaliśmy interfejs JavaScript API, ale jest on pusty. Czy ma on renderować kod HTML?
W tym przypadku przydają się wywołania zwrotne cyklu życia. W szczególności możemy użyć createdCallback()
, aby nadać elementowi 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});
Tworzenie wystąpienia tego tagu i przeglądanie go w narzędziach dla deweloperów (kliknij prawym przyciskiem myszy i wybierz Zbadaj element) powinno dać następujący wynik:
▾<x-foo-with-markup>
**I'm an x-foo-with-markup!**
</x-foo-with-markup>
Hermetyzacja wewnętrznych elementów w modelu Shadow DOM
Sam Shadow DOM jest potężnym narzędziem do otaczania treści. Używaj go w połączeniu z elementami niestandardowymi, a zyskasz niesamowite efekty.
Shadow DOM zapewnia elementom niestandardowym:
- sposób na ukrycie ich wnętrzności, aby chronić użytkowników przed drastycznymi szczegółami implementacji;
- Hermetyzacja stylów… bezpłatnie.
Tworzenie elementu w modelu Shadow DOM jest podobne do tworzenia elementu, który renderuje podstawowy znacznik. Różnica polega na tym, że 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 ustawiać element .innerHTML
, utworzyłem <x-foo-shadowdom>
w katalogu potomnym <x-foo-shadowdom>
i wypełniłem go znacznikami.
Gdy w Narzędziach deweloperskich włączysz opcję „Pokaż DOM cieni”, 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 jest katalog ROOT cienia.
Tworzenie elementów na podstawie szablonu
Szablony HTML to kolejny nowy element interfejsu API, który dobrze wpisuje się w świat elementów niestandardowych.
Przykład: rejestrowanie elementu utworzonego z poziomu <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>.
</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>.
</template>
<div class="demoarea">
<x-foo-from-template></x-foo-from-template>
</div>
Te kilka wierszy kodu jest bardzo skuteczne. Aby lepiej zrozumieć, co się dzieje:
- Zarejestrowaliśmy nowy element HTML:
<x-foo-from-template>
- DOM elementu został utworzony na podstawie
<template>
- Straszne szczegóły elementu są ukryte za pomocą Shadow DOM
- Model Shadow DOM zapewnia elementowi możliwość odizolowania stylów (np.
p {color: orange;}
nie zmienia koloru całej strony na pomarańczowy).
Świetnie!
Nadawanie stylu elementom niestandardowym
Podobnie jak w przypadku dowolnego tagu HTML, użytkownicy tagu niestandardowego mogą nadawać mu 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>
Nadawanie stylów elementom, które korzystają z modelu shadow DOM
Gdy dodasz do równania Shadow DOM, królicza nora stanie się znacznie głębsza. Elementy niestandardowe, które korzystają z modelu shadow DOM, dziedziczą jego zalety.
Shadow DOM pozwala na dodanie elementowi stylu. Style zdefiniowane w korzeniach zduplikowanych nie wydostają się poza hosta ani nie przenikają na stronę. W przypadku elementu niestandardowego hostem jest sam element. Właściwości zamykania w ramach stylu umożliwiają też elementom niestandardowym definiowanie własnych domyślnych stylów.
Stylizacja Shadow DOM to bardzo obszerny temat. Jeśli chcesz dowiedzieć się więcej na ten temat, zapoznaj się z tymi artykułami:
- „Przewodnik po stylizowaniu elementów” w dokumentacji Polymer.
- „Shadow DOM 201: CSS & Styling” tutaj.
Zapobieganie wyświetlaniu FOUC za pomocą parametru :unresolved
Aby ograniczyć FOUC, elementy niestandardowe zawierają nową pseudoklasę CSS :unresolved
. Używaj go do kierowania na nierozwiązane elementy aż do momentu, w którym przeglądarka wywołuje funkcję createdCallback()
(patrz metody cyklu życia).
Po tym etapie element nie jest już nierozwiązany. Proces uaktualnienia został zakończony, a element został przekształcony w swoją definicję.
Przykład: tagi „x-foo” są wyświetlane stopniowo po zarejestrowaniu:
<style>
x-foo {
opacity: 1;
transition: opacity 300ms;
}
x-foo:unresolved {
opacity: 0;
}
</style>
Pamiętaj, że :unresolved
dotyczy tylko nierozwiązanych elementów, a nie elementów dziedziczonych z HTMLUnknownElement
(patrz Jak elementy są ulepszane).
<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 cech
Wykrywanie funkcji polega na sprawdzeniu, czy istnieje document.registerElement()
:
function supportsCustomElements() {
return 'registerElement' in document;
}
if (supportsCustomElements()) {
// Good to go!
} else {
// Use other libraries to create components.
}
Obsługa przeglądarek
document.registerElement()
zaczęła być dostępna w Chrome 27 i Firefox 23. Od tego czasu specyfikacja znacznie się rozwinęła. Chrome 31 jest pierwszą wersją, która obsługuje w pełni zaktualizowaną specyfikację.
Do czasu, gdy przeglądarki będą obsługiwać tę funkcję, dostępna jest polyfilla, której używają Polymer Google i X-Tag Mozilli.
Co się stało z HTMLElementElement?
Ci, którzy śledzili prace nad standaryzacją, wiedzą, że kiedyś istniał <element>
.
To było coś. Możesz go używać do deklaratywnego rejestrowania nowych elementów:
<element name="my-element">
...
</element>
Niestety w procesie uaktualniania wystąpiło zbyt wiele problemów z dopasowaniem do czasu, rzadkich przypadków i sytuacji, które można porównać do zagłady. <element>
musiały zostać odłożone na półkę. W sierpniu 2013 r. Dimitri Glazkov opublikował na stronie public-webapps oświadczenie o tym, że usługa ta została usunięta (przynajmniej na razie).
Warto pamiętać, że Polymer implementuje deklaratywną formę rejestracji elementów za pomocą <polymer-element>
. Jak to zrobić? Używa ona document.registerElement('polymer-element')
oraz technik opisanych w artykule Tworzenie elementów na podstawie szablonu.
Podsumowanie
Elementy niestandardowe dają nam narzędzie do rozszerzania słownictwa HTML, nauczania go nowych sztuczek oraz przemieszczania się przez dziwne zakamarki platformy internetowej. Połącz je z innymi nowymi prymitywami platformy, takimi jak Shadow DOM i <template>
, a zaczniesz rozumieć obraz Web Components. Oznaczenia mogą znów być atrakcyjne!
Jeśli chcesz zacząć korzystać z komponentów internetowych, zapoznaj się z biblioteką Polymer. To więcej niż wystarczająco dużo, aby zacząć.