Bekerja dengan Elemen Khusus

Pengantar

Web sangat kekurangan ekspresi. Untuk melihat maksud saya, lihat aplikasi web "modern" seperti Gmail:

Gmail

Tidak ada yang modern dari sup <div>. Namun, inilah cara kita mem-build aplikasi web. Sedih sekali. Bukankah kita harus menuntut lebih banyak dari platform kita?

Markup yang menarik. Mari kita buat

HTML memberi kita alat yang sangat baik untuk menyusun dokumen, tetapi kosakatanya terbatas pada elemen yang ditentukan oleh standar HTML.

Bagaimana jika markup untuk Gmail tidak mengerikan? Bagaimana jika desainnya bagus:

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

Sungguh menyegarkan! Aplikasi ini juga sangat masuk akal. Model ini bermakna, mudah dipahami, dan yang terbaik, dapat dikelola. Saya/Anda di masa mendatang akan mengetahui dengan tepat apa yang dilakukannya hanya dengan memeriksa backbone deklaratifnya.

Memulai

Elemen Kustom memungkinkan developer web menentukan jenis elemen HTML baru. Spesifikasi ini adalah salah satu dari beberapa primitif API baru yang berada di bawah payung Web Components, tetapi mungkin ini yang paling penting. Komponen Web tidak ada tanpa fitur yang dibuka oleh elemen kustom:

  1. Menentukan elemen HTML/DOM baru
  2. Membuat elemen yang diperluas dari elemen lain
  3. Menggabungkan fungsi kustom secara logis ke dalam satu tag
  4. Memperluas API elemen DOM yang ada

Mendaftarkan elemen baru

Elemen kustom dibuat menggunakan document.registerElement():

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

Argumen pertama ke document.registerElement() adalah nama tag elemen. Nama harus berisi tanda hubung (-). Jadi, misalnya, <x-tags>, <my-element>, dan <my-awesome-app> semuanya adalah nama yang valid, sedangkan <tabs> dan <foo_bar> tidak valid. Batasan ini memungkinkan parser membedakan elemen kustom dari elemen reguler, tetapi juga memastikan kompatibilitas mendatang saat tag baru ditambahkan ke HTML.

Argumen kedua adalah objek (opsional) yang menjelaskan prototype elemen. Ini adalah tempat untuk menambahkan fungsi kustom (misalnya, properti dan metode publik) ke elemen Anda. Selengkapnya akan dibahas nanti.

Secara default, elemen kustom mewarisi dari HTMLElement. Jadi, contoh sebelumnya setara dengan:

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

Panggilan ke document.registerElement('x-foo') akan memberi tahu browser tentang elemen baru, dan menampilkan konstruktor yang dapat Anda gunakan untuk membuat instance <x-foo>. Atau, Anda dapat menggunakan teknik pembuatan instance elemen lainnya jika tidak ingin menggunakan konstruktor.

Memperluas elemen

Elemen kustom memungkinkan Anda memperluas elemen HTML (native) yang ada serta elemen kustom lainnya. Untuk memperluas elemen, Anda harus meneruskan registerElement() nama dan prototype elemen yang akan diwarisi.

Memperluas elemen native

Misalnya, Anda tidak puas dengan Joe Biasa <button>. Anda ingin meningkatkan kemampuannya menjadi "Tombol Mega". Untuk memperluas elemen <button>, buat elemen baru yang mewarisi prototype dari HTMLButtonElement dan extends nama elemen. Dalam hal ini, "button":

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

Elemen kustom yang diwarisi dari elemen native disebut elemen kustom ekstensi jenis. Elemen ini mewarisi dari versi khusus HTMLElement sebagai cara untuk mengatakan, "elemen X adalah Y".

Contoh:

<button is="mega-button">

Memperluas elemen kustom

Untuk membuat elemen <x-foo-extended> yang memperluas elemen kustom <x-foo>, cukup warisi prototipenya dan sebutkan tag yang Anda warisi:

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

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

Lihat Menambahkan properti dan metode JS di bawah untuk mengetahui informasi selengkapnya tentang cara membuat prototipe elemen.

Cara mengupgrade elemen

Pernahkah Anda bertanya-tanya mengapa parser HTML tidak mengalami masalah pada tag non-standar? Misalnya, tidak masalah jika kita mendeklarasikan <randomtag> di halaman. Menurut spesifikasi HTML:

Maaf, <randomtag>. Anda non-standar dan mewarisi dari HTMLUnknownElement.

Hal yang sama tidak berlaku untuk elemen kustom. Elemen dengan nama elemen kustom yang valid mewarisi dari HTMLElement. Anda dapat memverifikasi fakta ini dengan mengaktifkan Konsol: Ctrl + Shift + J (atau Cmd + Opt + J di Mac), dan menempelkan baris kode berikut; baris kode tersebut akan menampilkan 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

Elemen yang belum terselesaikan

Karena elemen kustom didaftarkan oleh skrip menggunakan document.registerElement(), elemen tersebut dapat dideklarasikan atau dibuat sebelum definisinya didaftarkan oleh browser. Misalnya, Anda dapat mendeklarasikan <x-tabs> di halaman, tetapi akhirnya memanggil document.registerElement('x-tabs') jauh di kemudian hari.

Sebelum diupgrade ke definisinya, elemen disebut elemen yang belum terselesaikan. Ini adalah elemen HTML yang memiliki nama elemen kustom yang valid, tetapi belum terdaftar.

Tabel ini mungkin dapat membantu Anda memahaminya:

Nama Diwarisi dari Contoh
Elemen yang belum terselesaikan HTMLElement <x-tabs>, <my-element>
Elemen tidak diketahui HTMLUnknownElement <tabs>, <foo_bar>

Membuat instance elemen

Teknik umum pembuatan elemen masih berlaku untuk elemen kustom. Seperti elemen standar lainnya, elemen ini dapat dideklarasikan dalam HTML atau dibuat di DOM menggunakan JavaScript.

Membuat instance tag kustom

Deklarasikan:

<x-foo></x-foo>

Buat DOM di JS:

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

Gunakan operator new:

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

Membuat instance elemen ekstensi jenis

Membuat instance elemen kustom bergaya ekstensi jenis sangat mirip dengan tag kustom.

Deklarasikan:

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

Buat DOM di JS:

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

Seperti yang dapat Anda lihat, kini ada versi document.createElement() yang kelebihan muatan yang menggunakan atribut is="" sebagai parameter keduanya.

Gunakan operator new:

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

Sejauh ini, kita telah mempelajari cara menggunakan document.registerElement() untuk memberi tahu browser tentang tag baru…tetapi hal ini tidak banyak berguna. Mari kita tambahkan properti dan metode.

Menambahkan properti dan metode JS

Keunggulan elemen kustom adalah Anda dapat memaketkan fungsi yang disesuaikan dengan elemen dengan menentukan properti dan metode pada definisi elemen. Anggap ini sebagai cara untuk membuat API publik untuk elemen Anda.

Berikut adalah contoh lengkapnya:

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

Tentu saja ada ribuan cara untuk membuat prototype. Jika Anda tidak suka membuat prototipe seperti ini, berikut adalah versi yang lebih ringkas dari hal yang sama:

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

Format pertama memungkinkan penggunaan Object.defineProperty ES5. Yang kedua memungkinkan penggunaan get/set.

Metode callback siklus proses

Elemen dapat menentukan metode khusus untuk memanfaatkan waktu keberadaannya yang menarik. Metode ini diberi nama callback siklus proses yang sesuai. Masing-masing memiliki nama dan tujuan tertentu:

Nama callback Dipanggil saat
createdCallback instance elemen dibuat
attachedCallback instance disisipkan ke dalam dokumen
detachedCallback instance dihapus dari dokumen
attributeChangedCallback(attrName, oldVal, newVal) atribut ditambahkan, dihapus, atau diperbarui

Contoh: menentukan createdCallback() dan attachedCallback() di <x-foo>:

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

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

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

Semua callback siklus proses bersifat opsional, tetapi tentukan jika/saat diperlukan. Misalnya, elemen Anda cukup kompleks dan membuka koneksi ke IndexedDB di createdCallback(). Sebelum dihapus dari DOM, lakukan pekerjaan pembersihan yang diperlukan di detachedCallback(). Catatan: Anda tidak boleh mengandalkannya, misalnya, jika pengguna menutup tab, tetapi anggaplah ini sebagai kemungkinan hook pengoptimalan.

Callback siklus proses kasus penggunaan lainnya adalah untuk menyiapkan pemroses peristiwa default di elemen:

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

Menambahkan markup

Kita telah membuat <x-foo>, memberinya JavaScript API, tetapi kosong. Mari kita berikan beberapa HTML untuk dirender.

Callback siklus proses sangat berguna di sini. Secara khusus, kita dapat menggunakan createdCallback() untuk memberi elemen beberapa HTML default:

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});

Membuat instance tag ini dan memeriksanya di DevTools (klik kanan, pilih Inspect Element) akan menampilkan:

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

Meng-enkapsulasi internal di Shadow DOM

Shadow DOM sendiri adalah alat yang canggih untuk mengaitkan konten. Gunakan bersama elemen kustom dan Anda akan mendapatkan hasil yang luar biasa.

Shadow DOM memberi elemen kustom:

  1. Cara untuk menyembunyikan isinya, sehingga melindungi pengguna dari detail implementasi yang mengerikan.
  2. Enkapsulasi gaya…gratis.

Membuat elemen dari Shadow DOM sama seperti membuat elemen yang merender markup dasar. Perbedaannya ada di 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});

Daripada menetapkan .innerHTML elemen, saya telah membuat Root Bayangan untuk <x-foo-shadowdom>, lalu mengisinya dengan markup. Dengan setelan "Tampilkan Shadow DOM" diaktifkan di DevTools, Anda akan melihat #shadow-root yang dapat diluaskan:

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

Itu adalah Root Bayangan.

Membuat elemen dari template

Template HTML adalah primitif API baru lainnya yang sangat cocok dengan dunia elemen kustom.

Contoh: mendaftarkan elemen yang dibuat dari <template> dan 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>

Beberapa baris kode ini memiliki banyak manfaat. Mari kita pahami semua yang terjadi:

  1. Kami telah mendaftarkan elemen baru di HTML: <x-foo-from-template>
  2. DOM elemen dibuat dari <template>
  3. Detail menakutkan elemen disembunyikan menggunakan Shadow DOM
  4. Shadow DOM memberikan enkapsulasi gaya elemen (misalnya, p {color: orange;} tidak mengubah seluruh halaman menjadi oranye)

Bagus!

Menata gaya elemen kustom

Seperti tag HTML lainnya, pengguna tag kustom Anda dapat menata gayanya dengan pemilih:

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

Menata gaya elemen yang menggunakan Shadow DOM

Anda akan semakin sangat terperosok ke dalam lubang kelinci saat memasukkan Shadow DOM ke dalam campuran. Elemen kustom yang menggunakan Shadow DOM mewarisi manfaatnya yang luar biasa.

Shadow DOM memasukkan elemen dengan enkapsulasi gaya. Gaya yang ditentukan di Root Bayangan tidak bocor dari host dan tidak bocor dari halaman. Dalam kasus elemen kustom, elemen itu sendiri adalah host. Properti enkapsulasi gaya juga memungkinkan elemen kustom untuk menentukan gaya defaultnya sendiri.

Gaya Shadow DOM adalah topik yang sangat besar. Jika Anda ingin mempelajarinya lebih lanjut, sebaiknya baca beberapa artikel saya yang lain:

Pencegahan FOUC menggunakan :unresolved

Untuk mengurangi FOUC, elemen kustom menentukan class semu CSS baru, :unresolved. Gunakan untuk menargetkan elemen yang belum terselesaikan, hingga browser memanggil createdCallback() Anda (lihat metode siklus proses). Setelah itu, elemen tersebut tidak lagi menjadi elemen yang belum terselesaikan. Proses upgrade telah selesai dan elemen telah diubah menjadi definisinya.

Contoh: memudar tag "x-foo" saat didaftarkan:

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

Perlu diingat bahwa :unresolved hanya berlaku untuk elemen yang belum terselesaikan, bukan untuk elemen yang diwarisi dari HTMLUnknownElement (lihat Cara elemen diupgrade).

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

Dukungan histori dan browser

Deteksi fitur

Deteksi fitur adalah masalah memeriksa apakah document.registerElement() ada:

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

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

Dukungan browser

document.registerElement() pertama kali mulai muncul di balik tanda di Chrome 27 dan Firefox ~23. Namun, spesifikasinya telah berkembang cukup banyak sejak saat itu. Chrome 31 adalah versi pertama yang memiliki dukungan sebenarnya untuk spesifikasi yang diperbarui.

Hingga dukungan browser menjadi sangat baik, ada polyfill yang digunakan oleh Polymer Google dan X-Tag Mozilla.

Apa yang terjadi pada HTMLElementElement?

Bagi yang telah mengikuti pekerjaan standardisasi, Anda tahu bahwa pernah ada <element>. Itu adalah hal yang luar biasa. Anda dapat menggunakannya untuk mendaftarkan elemen baru secara deklaratif:

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

Sayangnya, ada terlalu banyak masalah pengaturan waktu dengan proses upgrade, kasus ekstrem, dan skenario seperti Armageddon untuk mengatasi semuanya. <element> harus ditangguhkan. Pada Agustus 2013, Dimitri Glazkov memposting ke public-webapps yang mengumumkan penghapusannya, setidaknya untuk saat ini.

Perlu diperhatikan bahwa Polymer menerapkan bentuk deklaratif pendaftaran elemen dengan <polymer-element>. Bagaimana caranya? Contoh ini menggunakan document.registerElement('polymer-element') dan teknik yang saya jelaskan dalam Membuat elemen dari template.

Kesimpulan

Elemen kustom memberi kita alat untuk memperluas kosakata HTML, mengajarkan trik baru, dan melewati wormhole platform web. Gabungkan dengan primitif platform baru lainnya seperti Shadow DOM dan <template>, dan kita mulai menyadari gambar Web Components. Markup dapat menjadi menarik lagi.

Jika Anda tertarik untuk mulai menggunakan komponen web, sebaiknya lihat Polymer. Aplikasi ini memiliki lebih dari cukup fitur untuk membantu Anda memulai.