Wprowadzenie
W internecie brakuje środków wyrazu. Aby zobaczyć, o co chodzi, spójrz na „nowoczesną” aplikację internetową, taką jak Gmail:
Zupa <div>
nie ma nic wspólnego z nowoczesnością. A my w ten sposób tworzymy aplikacje internetowe. To smutne.
Czy nie powinniśmy wymagać więcej od naszej platformy?
Znaczniki „seksualne” Zróbmy to
HTML to doskonałe narzędzie do tworzenia struktury dokumentu, ale jego słownictwo ogranicza się do elementów standardu HTML.
A co, jeśli znaczniki w Gmailu nie były obraźliwe? Co, jeśli jest 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>
Odświeżające! Ta aplikacja też ma sens. Jest zrozumiała i łatwa do zrozumienia, a co najważniejsze – nadaje się do obsługi. 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 podstawowych elementów interfejsu API dostępnych w grupie Komponenty sieciowe, ale prawdopodobnie jest najważniejszy. Komponenty internetowe nie istnieją bez funkcji udostępnionych przez elementy niestandardowe:
- Definiowanie nowych elementów HTML/DOM
- Tworzenie elementów rozszerzających inne elementy
- Połącz niestandardowe funkcje w jeden tag.
- Rozszerzanie interfejsu API dotychczasowych 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());
Pierwszym argumentem funkcji document.registerElement()
jest 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 parsującemu rozróżniać elementy niestandardowe od zwykłych, ale też zapewnia zgodność wsteczną po dodaniu nowych tagów do kodu HTML.
Drugi argument to (opcjonalny) obiekt opisujący prototype
elementu.
Tutaj możesz dodawać do elementów funkcje niestandardowe (np. publiczne właściwości i metody).
Więcej informacji później.
Domyślnie elementy niestandardowe dziedziczą z poziomu HTMLElement
. W związku z tym poprzedni przykład jest równoważny temu:
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 utworzenia instancji <x-foo>
.
Jeśli nie chcesz używać konstruktora, możesz też zastosować inne techniki tworzenia instancji elementów.
Rozciąganie elementów
Elementy niestandardowe umożliwiają rozszerzanie istniejących (natywnych) elementów HTML i 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 Joe <button>
. Chcesz jak najlepiej wykorzystać jego możliwości,
aby pełnić rolę „Mega Button”. 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, są nazywane elementami niestandardowymi rozszerzenia typu.
Dziedziczą one od wyspecjalizowanej wersji HTMLElement
, mówiąc w ten sposób: „element X to 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.
Uaktualnianie elementów
Zastanawiasz się czasem, dlaczego parser HTML nie obsługuje niestandardowych tagów?
Na przykład doskonale się sprawdza, jeśli na stronie zadeklarujemy <randomtag>
. Zgodnie ze specyfikacją HTML:
Przepraszam <randomtag>
Masz wersję niestandardową i dziedziczysz ją z domeny HTMLUnknownElement
.
To samo nie dotyczy elementów niestandardowych. Elementy z prawidłowymi nazwami elementów niestandardowych dziedziczą się z elementu HTMLElement
. Możesz to sprawdzić, uruchamiając Konsolę Ctrl + Shift + J
(lub Cmd + Opt + J
na Macu) i wklejając te wiersze kodu. Zwracają one 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
Elementy nierozstrzygnięte
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ę. Jeśli na przykład zadeklarujesz na stronie funkcję <x-tabs>
, wywołasz document.registerElement('x-tabs')
znacznie później.
Przed uaktualnieniem elementów do definicji są one nazywane elementami nierozwiązanymi. 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 |
---|---|---|
Nierozwiązany element | HTMLElement |
<x-tabs> , <my-element> |
Nieznany element | HTMLUnknownElement |
<tabs> , <foo_bar> |
Tworzenie instancji elementów
Powszechne 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ą JavaScript.
Tworzenie instancji tagów niestandardowych
Deklaruj je:
<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 instancji elementów rozszerzenia typu
Tworzenie instancji elementów niestandardowych w stylu rozszerzenia jest bardzo podobne do niestandardowych tagów.
Deklaruj je:
<!-- <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ć, dostępna jest teraz przeciążona wersja document.createElement()
, w której drugim parametrem jest atrybut is=""
.
Użyj operatora new
:
var megaButton = new MegaButton();
document.body.appendChild(megaButton);
Na razie dowiedzieliśmy się, jak za pomocą document.registerElement()
poinformować przeglądarkę o nowym tagu, ale niewiele to daje. Dodajmy właściwości i metody.
Dodawanie właściwości i metod JS
Najlepsze w elementach niestandardowych jest to, że możesz łączyć z nim dostosowane funkcje, definiując właściwości i metody w definicji elementu. Potraktuj to jako sposób na utworzenie publicznego interfejsu 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);
Oczywiście istnieje mnóstwo tysięcy sposobów stworzenia prototype
. Jeśli nie lubisz tworzyć prototypów w taki sposób, możesz użyć bardziej skompresowanej wersji:
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 na użycie get/set.
Metody wywołania zwrotnego cyklu życia
Elementy mogą definiować specjalne metody wykorzystywania interesujących momentów ich istnienia. Te metody są odpowiednio nazywane obsługami wywołania cyklu życia. Każdy z nich ma inną nazwę i przeznaczenie:
Nazwa wywołania zwrotnego | Wywoływany, gdy |
---|---|
createdCallback | tworzona jest instancja elementu. |
attachedCallback | instancja została wstawiona do dokumentu, |
detachedCallback | wystąpienie zostało usunięte z dokumentu, |
attributeChangedCallback(attrName, oldVal, newVal) | atrybut został dodany, usunięty lub zaktualizowany; |
Przykład: definiowanie funkcji 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 zwrotne cyklu życia są opcjonalne, ale musisz je zdefiniować, czy i kiedy ma to sens.
Załóżmy na przykład, że element jest wystarczająco złożony i otwiera połączenie z indeksem IndexedDB w usłudze createdCallback()
. Zanim element zostanie usunięty z DOM-u, wykonaj niezbędne czynności czyszczące w detachedCallback()
. Uwaga: nie należy polegać na tym, np. gdy użytkownik zamknie kartę, ale traktować to jako możliwy element przykuwający uwagę do optymalizacji.
Inne wywołania zwrotne cyklu życia przypadku użycia służą do skonfigurowania domyślnych detektorów zdarzeń elementu:
proto.createdCallback = function() {
this.addEventListener('click', function(e) {
alert('Thanks!');
});
};
Dodawanie znacznika
Utworzyliśmy <x-foo>
z użyciem JavaScript API, ale jest pusty. Dostarczymy kod HTML do renderowania?
Przydają się tu wywołania zwrotne cyklu życia. Korzystając z pola 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});
Po utworzeniu wystąpienia tego tagu i sprawdzeniu go w Narzędziach deweloperskich (kliknij prawym przyciskiem myszy i wybierz opcję Zbadaj element), zobaczysz:
▾<x-foo-with-markup>
**I'm an x-foo-with-markup!**
</x-foo-with-markup>
Umieszczanie komponentów wewnętrznych w modelu Shadow DOM
Sam model Shadow DOM jest zaawansowanym narzędziem do przenoszenia treści. Używaj go w połączeniu z elementami niestandardowymi, a zyskasz niesamowite efekty.
Shadow DOM udostępnia elementy niestandardowe:
- Sposób na ukrycie wnętrzności, dzięki któremu użytkownicy nie zobaczą drastycznego sposobu implementacji.
- Hermetyzacja stylu… bezpłatnie.
Tworzenie elementu w modelu Shadow DOM przypomina
tworzenie elementu renderującego podstawowe znaczniki. Różnica dotyczy 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 rdzeń <x-foo-shadowdom>
i wypełniłem go znacznikami.
Po włączeniu ustawienia „Pokaż Shadow DOM” w Narzędziach deweloperskich 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 korzeń 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 linijek kodu ma w sobie wiele możliwości. Przyjrzymy się temu, co się dzieje:
- Zarejestrowaliśmy nowy element w kodzie HTML:
<x-foo-from-template>
- DOM elementu został utworzony na podstawie
<template>
- Przerażające szczegóły elementu są ukryte za pomocą modelu Shadow DOM.
- Shadow DOM zapewnia elementowi możliwość stosowania stylu (np.
p {color: orange;}
nie zmienia koloru całej strony na pomarańczowy).
Świetnie!
Nadawanie stylu elementom niestandardowym
Podobnie jak w przypadku każdego 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ą swoje zalety.
Shadow DOM pozwala na hermetyzację stylu elementu. 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 hermetyzacji stylu pozwalają też elementom niestandardowym definiować style domyślne.
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 FOUC z użyciem wartości :unresolved
Aby ograniczyć FOUC, elementy niestandardowe powinny używać nowej pseudoklasy CSS :unresolved
. Użyj go do kierowania na nierozwiązane elementy aż do momentu, w którym przeglądarka wywołuje funkcję createdCallback()
(patrz metody cyklu życia).
Gdy to nastąpi, element przestaje być nierozwiązany. Uaktualnianie zostało zakończone, a element został przekształcony w swoją definicję.
Przykład: tagi „x-foo” pojawiają się 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()
na początku znalazła się za flagą w Chrome 27 i Firefoksie w wersji około 23. Od tego czasu specyfikacja znacznie się jednak zmieniła. Chrome 31 jako pierwsza ma faktyczną obsługę
zaktualizowanej specyfikacji.
Dopóki przeglądarki nie będą w pełni obsługiwane, będzie dostępny kod polyfill, który jest używany przez usługi Google Polymer i X-Tag firmy Mozilla.
Co się stało z HTMLElementElement?
Ci, którzy przygotowali się do pracy przy standaryzacji, wiedzą już, że było to <element>
.
Były to kolana pszczoły. 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 przypominających Armagedon, aby można było wszystko rozwiązać. <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 wycofana (przynajmniej na razie).
Warto zauważyć, że Polymer implementuje deklaratywną formę rejestracji elementów za pomocą <polymer-element>
. Jak to zrobić? Wykorzystuje document.registerElement('polymer-element')
oraz techniki opisane w sekcji Tworzenie elementów z 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 elementami platformy, takimi jak Shadow DOM i <template>
, a zaczniesz rozumieć koncepcję Web Components. Oznaczenia mogą znów być atrakcyjne!
Jeśli chcesz zacząć korzystać ze składników internetowych, zapoznaj się z biblioteką Polymer. To więcej niż wystarczająco dużo, aby zacząć.