Halaman website yang ditampilkan terbentuk dari HTML. HTML sangat membantu kita karena ia cukup mudah dipelajari dan digunakan. HTML mudah dipahami oleh kita namun tidak untuk mesin.
sehingga terciptalah DOM (Document Object Model) sebagai penghubung antara HTML dengan bahasa pemrograman.
Di dalam DOM seluruh struktur HTML dapat digambarkan dalam bentuk objek yang dapat dimanipulasi melalui bahasa pemrograman, salah satunya JavaScript.
Ketika browser memuat halaman, HTML akan secara otomatis dimodelkan menjadi sebuah object dan nodes hingga membentuk “DOM Tree”. Berikut contoh DOM Tree yang terbuat:
Dari struktur HTML berikut:
Web Components
Mari belajar Shadow DOM
Object dan nodes yang dihasilkan dari DOM akan memiliki properti dan method yang dapat kita manfaatkan untuk memanipulasi konten di dalamnya.
Seluruh elemen dan style pada HTML (apapun yang berada di dalam DOM) akan terekspos secara global dan nilainya dapat kita peroleh dari mana saja.
Biasanya untuk mendapatkan element kita gunakan document.querySelector, setelah itu kita dapat leluasa mengontrol elemen, mengubah konten di dalamnya ataupun mengubah styling yang diterapkan.
Encapsulation
Saat ini banyak web yang dibangun melalui arsitektur berbasis komponen sehingga diharapkan komponen tersebut dapat digunakan kembali.
Namun bukankah tidak baik jika komponen tersebut dapat diganggu dan diubah dari luar? Sebaiknya komponen dapat bertahan dari gangguan luar agar secara visual atau fungsinya agar tetap dalam keadaan aslinya. Maka dari itu, kita perlu menerapkan konsep enkapsulasi pada komponen tersebut
What is Shadow DOM?
Saat ini kita mungkin bisa menerapkan konsep enkapsulasi dengan menggunakan agar komponen terpisah dari gangguan luar. Namun teknik ini bukan cara yang baik, berat, dan dapat menimbulkan masalah. Lantas bagaimana solusinya? Gunakanlah Shadow DOM.
Shadow DOM dapat mengisolasi sebagian struktur DOM di dalam komponen sehingga tidak dapat disentuh dari luar komponen atau nodenya. Singkatnya kita bisa sebut Shadow DOM sebagai “DOM dalam DOM”. Bagaimana ia bekerja? Perhatikan ilustrasi berikut:
Shadow DOM dapat membuat DOM Tree lain terbentuk secara terisolasi melalui host yang merupakan komponen dari regular DOM Tree (Document Tree). Shadow DOM Tree ini dimulai dari root bayangan (Shadow root), yang dibawahnya dapat memiliki banyak element lagi layaknya Document Tree.
Terdapat beberapa terminologi yang perlu kita ketahui dari ilustrasi di atas:
Shadow host : Merupakan komponen/node yang terdapat pada regular DOM di mana shadow DOM terlampir pada komponen/node ini.
Shadow tree : DOM Tree di dalam shadow DOM.
Shadow boundary : Batas dari shadow DOM dengan regular DOM.
Shadow root : Root node dari shadow tree.
Kita dapat memanipulasi elemen yang terdapat di dalam shadow tree layaknya pada document tree, namun cakupannya selama kita berada di dalam shadow boundary. Dengan kata lain, jika kita berada di document tree kita tidak dapat memanipulasi elemen bahkan menerapkan styling pada elemen yang terdapat di dalam shadow tree. Itulah mengapa shadow DOM dapat membuat komponen terenkapsulasi
Jika kita lihat pada browser, maka struktur HTML yang akan dihasilkan adalah seperti ini:
Dan struktur DOM tree yang terbentuk akan tampak seperti ini:
Dalam penggunaan attachShadow() kita melampirkan objek dengan properti mode yang memiliki nilai ‘open’. Sebenarnya terdapat dua opsi nilai yang dapat digunakan dalam properti mode, yaitu “open” dan “closed”.
Menggunakan nilai open berarti kita memperbolehkan untuk mengakses properti shadowRoot melalui elemen yang melampirkan Shadow DOM.
divElement.attachShadow;
properti shadowRoot mengembalikan struktur DOM yang berada pada shadow tree.
Namun jika kita menggunakan nilai closed maka properti shadowRoot akan mengembalikan nilai null.
Karena Shadow DOM terisolasi dari document tree maka element yang terdapat di dalamnya pun tidak akan terpengaruh oleh styling yang berada diluar dari shadow root-nya.
// Memasukkan element heading ke dalam shadow root
shadowRoot.appendChild(headingElement);
// Memasukkan elemen shadow host ke regular DOM
document.body.appendChild(divElement);
Jika dilihat pada browser maka hasilnya akan seperti ini:
Berdasarkan hasil di atas, styling hanya akan diterapkan pada elemen yang berada di document tree. Sedangkan elemen yang berada pada shadow dom tidak akan terpengaruh dengan styling tersebut. Lantas, bagaimana caranya kita melakukan styling pada Shadow DOM?
Kita dapat melakukannya dengan menambahkan template di dalam shadowRoot.innerHTML. Contohnya seperti ini:
// menetapkan styling pada Shadow DOM
shadowRoot.innerHTML +=`
h1 {
color: green;
}
`;
Maka element tersebut akan berada di dalam shadow tree dan akan berdampak pada elemen yang ada di dalamnya.
Untuk membantu menerapkan enkapsulasi pada custom element, Shadow DOM berperan sebagai salah satu API standar yang digunakan dalam membuat Web Component (Hal ini distandarisasi oleh W3C). Kita sudah belajar bagaimana menerapkan Shadow DOM pada elemen yang berada pada Document Tree, namun bagaimana caranya bila itu diterapkan pada custom element?
Ketika kode tersebut dijalankan pada browser, kita bisa melihat terdapat dua komponen yang ditampilkan, salah satunya adalah custom element.
Kita bisa melihat juga bahwa keduanya memiliki styling yang sama, padahal kita hanya menetapkan styling di dalam komponen ImageFigure saja.
Yup, hal tersebut wajar terjadi karena pada custom element kita tidak menetapkan Shadow DOM sehingga styling pada custom element akan berdampak juga terhadap komponen di luarnya.
Dalam melampirkan Shadow DOM pada custom element sama seperti pada elemen biasanya, yaitu menggunakan attachShadow.
Namun dalam custom element, kita lakukan pada constructor class-nya seperti ini:
Agar nilai shadowRoot dapat diakses pada fungsi mana saja di class, maka kita perlu memasukkan nilai shadowRoot pada properti class menggunakan this.
Kita bebas menentukan nama properti sesuai keinginan, namun untuk memudahkan kita gunakan nama _shadowRoot. Lalu mengapa penamaannya menggunakan tanda underscore (_) di depannya? Jawabannya, this pada konteks class ini merupakan HTMLElement dan ia sudah memiliki properti dengan nama shadowRoot.
Untuk membedakan properti _shadowRoot asli dengan properti baru yang kita buat, kita bisa tambahkan underscore di awal penamaannya.
Hal ini dibutuhkan karena jika kita menerapkan mode closed pada Shadow DOM, nilai properti shadowRoot akan mengembalikan null, sehingga tidak ada cara lain untuk kita mengakses Shadow Tree.
Setelah menerapkan Shadow DOM pada constructor, ketika ingin mengakses apapun yang merupakan properti dari DOM kita harus melalui _shadowRoot.
Contohnya ketika ingin menerapkan template HTML, kita tidak bisa menggunakan langsung this.innerHTML, namun perlu melalui this._shadowRoot.innerHTML.
Sehingga kita perlu menyesuaikan kembali beberapa kode yang terdapat pada fungsi render menjadi seperti ini:
render(){
this._shadowRoot.innerHTML =`
figure {
border: thin #c0c0c0 solid;
display: flex;
flex-flow: column;
padding: 5px;
max-width: 220px;
margin: auto;
}
figure > img {
max-width: 220px;
}
figure > figcaption {
background-color: #222;
color: #fff;
font: italic smaller sans-serif;
padding: 3px;
text-align: center;
}
<img src="${this.src}"
alt=”${this.alt}”>
${this.caption}
`;
}
Dengan begitu sekarang styling pada komponen hanya berlaku pada komponen itu sendiri. Begitu juga sebaliknya, styling yang dituliskan di luar dari komponen tidak akan berdampak pada elemen di dalam komponen.
Solution: Menerapkan Shadow DOM pada Proyek Club Finder
Apakah Anda berhasil menerapkan shadow DOM pada custom element di proyek Club Finder? Jika belum, mari kita lakukan bersama-sama.
Menerapkan Shadow DOM pada App Bar
Kita mulai dari component yuk. Pertama kita buka dulu proyek club finder dengan text editor yang kita gunakan.
Kemudian buka berkas script -> component -> app-bar.js, buat constructor dari class tersebut dan di dalamnya kita tetapkan shadow root seperti ini:
classAppBarextendsHTMLElement{
constructor(){
super();
this.shadowDOM =this.attachShadow({mode:“open”});
}
connectedCallback(){
this.render();
}
render(){
this.innerHTML =`
Club Finder
`
;
}
}
customElements.define(“app-bar”,AppBar);
Karena kita sudah menerapkan Shadow DOM pada AppBar, jangan lupa pada fungsi render(), kita ubah this.innerHTML menjadi this.shadowDOM.innerHTML.
classAppBarextendsHTMLElement{
constructor(){
super();
this.shadowDOM =this.attachShadow({mode:"open"});
}
connectedCallback(){
this.render();
}
render(){
this.shadowDOM.innerHTML =`
Club Finder
`;
}
}
customElements.define("app-bar",AppBar);
Kemudian buka berkas style -> appbar.css dan pindahkan (cut) seluruh kode yang ada pada berkas tersebut.
app-bar {
display: block;
padding:16px;
width:100%;
background-color: cornflowerblue;
color: white;
box-shadow:04px8px0 rgba(0,0,0,0.2);
}
Lalu tempel (paste) pada nilai this.shadowDOM.innerHTML dengan dibungkus oleh element tepat sebelum element pada fungsi render() di berkas app-bar.js seperti ini:
classAppBarextendsHTMLElement{
constructor(){
super();
this.shadowDOM =this.attachShadow({mode:“open”});
}
connectedCallback(){
this.render();
}
render(){
this.shadowDOM.innerHTML =`
app-bar {
display: block;
padding: 16px;
width: 100%;
background-color: cornflowerblue;
color: white;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
}
Club Finder
`
;
}
}
customElements.define(“app-bar”,AppBar);
Coba kita simpan perubahan yang diterapkan kemudian lihat perubahannya pada browser.
Ups, pada browser kita dapat melihat title yang ditampilkan pada tampak berantakan. Untuk menanganinya, kita perlu menyesuaikan kembali style yang diterapkan pada custom element menjadi seperti ini:
classAppBarextendsHTMLElement{
constructor(){
super();
this.shadowDOM =this.attachShadow({mode:“open”});
}
connectedCallback(){
this.render();
}
render(){
this.shadowDOM.innerHTML =`
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:host {
display: block;
width: 100%;
background-color: cornflowerblue;
color: white;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
}
h2 {
padding: 16px;
}
Club Finder
`
;
}
}
customElements.define(“app-bar”,AppBar);
Pada perubahan styling tersebut kita menambahkan
*{
margin:0;
padding:0;
box-sizing: border-box;
}
Yang digunakan untuk menghilangkan seluruh margin dan padding standar yang diterapkan pada element html. Dan kita juga mengubah pengaturan box-sizing menjadi border-box.
Lalu kode pada kode styling lainnya juga kita melihat bahwa selector app-bar digantikan dengan :host. Apa itu :host? Selector :host merupakan selector yang digunakan untuk menunjuk element :host yang menerapkan Shadow DOM. Pada host kita tidak dapat mengatur padding sehingga kita perlu memindahkannya pada elemen .
Setelah melakukan perubahan tersebut simpan (save) kembali perubahannya dan lihat hasilnya pada browser, seharusnya sudah ditampilkan dengan baik.
Karena kita sudah tidak membutuhkan lagi berkas src -> styles -> appbar.css, kita dapat menghapus berkas tersebut.
Jangan lupa untuk menghapus import css tersebut pada src -> styles -> style.css.
@import"clublist.css";
@import"searchbar.css";
*{
padding:0;
margin:0;
box-sizing: border-box;
}
body {
font-family: sans-serif;
}
main {
width:90%;
max-width:800px;
margin:32pxauto;
}
Menerapkan Shadow DOM pada Search Bar
Setelah berhasil menerapkan Shadow DOM pada App Bar, selanjutnya kita terapkan Shadow DOM pada search bar. Silakan buka berkas src -> script -> component -> search-bar.js, kemudian buat constructor dan terapkan Shadow DOM di dalamnya.
Kemudian buka berkas src -> styles -> searchbar.css, pindahkan (cut) seluruh kode yang terdapat pada berkas tersebut.
.search–container {
max–width:800px;
box–shadow:04px8px0 rgba(0,0,0,0.2);
padding:16px;
border–radius:5px;
display: flex;
position: sticky;
top:10px;
background–color: white;
}
.search–container > input {
width:75%;
padding:16px;
border:0;
border–bottom:1px solid cornflowerblue;
font–weight: bold;
}
.search–container > input:focus {
outline:0;
border–bottom:2px solid cornflowerblue;
}
.search–container > input:focus::placeholder {
font–weight: bold;
}
.search–container > input::placeholder {
color: cornflowerblue;
font–weight: normal;
}
.search–container > button {
width:23%;
cursor: pointer;
margin–left:auto;
padding:16px;
background–color: cornflowerblue;
color: white;
border:0;
text–transform: uppercase;
}
@media screen and(max–width:550px){
.search–container {
flex–direction: column;
position:static;
}
.search–container > input {
width:100%;
margin–bottom:12px;
}
.search–container > button {
width:100%;
}
}
Lalu tempel (paste) pada nilai this.shadowDOM.innerHTML dengan dibungkus oleh element tepat sebelum element
pada fungsi render() di berkas search-bar.js seperti ini:
classSearchBarextendsHTMLElement{
………
render(){
this.shadowDOM.innerHTML =`
.search-container {
max-width: 800px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
padding: 16px;
border-radius: 5px;
display: flex;
position: sticky;
top: 10px;
background-color: white;
}
.search-container > input {
width: 75%;
padding: 16px;
border: 0;
border-bottom: 1px solid cornflowerblue;
font-weight: bold;
}
.search-container > input:focus {
outline: 0;
border-bottom: 2px solid cornflowerblue;
}
.search-container > input:focus::placeholder {
font-weight: bold;
}
.search-container > input::placeholder {
color: cornflowerblue;
font-weight: normal;
}
.search-container > button {
width: 23%;
cursor: pointer;
margin-left: auto;
padding: 16px;
background-color: cornflowerblue;
color: white;
border: 0;
text-transform: uppercase;
}
@media screen and (max-width: 550px){
.search-container {
flex-direction: column;
position: static;
}
.search-container > input {
width: 100%;
margin-bottom: 12px;
}
.search-container > button {
width: 100%;
}
}
Search
`;
…….
}
}
customElements.define(“search-bar”,SearchBar);
Simpan perubahan yang dilakukan kemudian lihat hasilnya pada browser.
Komponen Search Bar tampak normal dan berfungsi dengan baik sehingga kita tidak perlu menyesuaikan lagi styling-nya.
Karena kita sudah tidak membutuhkan lagi berkas src -> styles -> searchbar.css, kita dapat menghapus berkas tersebut.
Jangan lupa untuk menghapus import css tersebut pada src -> styles -> style.css.
@import"clublist.css";
*{
padding:0;
margin:0;
box-sizing: border-box;
}
body {
font-family: sans-serif;
}
main {
width:90%;
max-width:800px;
margin:32pxauto;
}
Menerapkan Shadow DOM pada Club List dan Club Item
Terakhir kita terapkan Shadow DOM pada komponen club list dan club item. Silakan buka berkas src -> script -> component -> club-list.js, kemudian buat constructor dan terapkan Shadow DOM di dalamnya.
Kemudian buka berkas src -> styles -> clublist.css dan pindahkan (cut) kode styling dengan selector club-list > .placeholder
club-list >.placeholder {
font-weight: lighter;
color: rgba(0,0,0,0.5);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
Lalu tempel (paste) pada nilai this.shadowDOM.innerHTML dengan dibungkus oleh element tepat sebelum element fungsi renderError() di berkas club-list.js seperti ini:
import‘./club-item.js’;
classClubListextendsHTMLElement{
………
renderError(message){
this.shadowDOM.innerHTML =`
club-list > .placeholder {
font-weight: lighter;
color: rgba(0,0,0,0.5);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
`;
this.shadowDOM.innerHTML +=`
${message}
`
;
}
…….
}
customElements.define(“club-list”,ClubList);
Hapus child selector (>) beserta kombinatornya, sisakan .placeholder sebagai selector dari styling tersebut. Sehingga kode pada berkas ini seluruhnya tampak seperti:
Simpan perubahan tersebut dan lihat hasilnya pada browser, tampilan dari daftar club akan sangat berantakan.
Tenang kita akan memperbaikinya dengan beranjak ke berkas src -> script -> component -> club-item.js.
Pada berkas tersebut buat sebuah constructor dan terapkan Shadow DOM di dalamnya.
classClubItemextendsHTMLElement{
constructor(){
super();
this.shadowDOM =this.attachShadow({mode:“open”});
}
set club(club){
this._club = club;
this.render();
}
render(){
this.innerHTML =`
${this._club.name}
${this._club.description}
`
;
}
}
customElements.define(“club-item”,ClubItem);
Seperti biasa jangan lupa untuk mengubah this.innerHTML menjadi this.shadowDOM.innerHTML ya.
classClubItemextendsHTMLElement{
constructor(){
super();
this.shadowDOM =this.attachShadow({mode:“open”});
}
set club(club){
this._club = club;
this.render();
}
render(){
this.shadowDOM.innerHTML =`
${this._club.name}
${this._club.description}
`
;
}
}
customElements.define(“club-item”,ClubItem);
Selanjutnya buka kembali berkas src -> styles -> clublist.css dan pindahkan styling berikut:
club–item {
display: block;
margin–bottom:18px;
box–shadow:04px8px0 rgba(0,0,0,0.2);
border–radius:10px;
overflow: hidden;
}
club–item .fan–art–club {
width:100%;
max–height:300px;
object–fit: cover;
object–position: center;
}
.club–info {
padding:24px;
}
.club–info > h2 {
font–weight: lighter;
}
.club–info > p {
margin–top:10px;
overflow: hidden;
text–overflow: ellipsis;
display:–webkit–box;
–webkit–box–orient: vertical;
–webkit–line–clamp:10;/* number of lines to show */
}
Tempel pada nilai this.shadowDOM.innerHTML dengan dibungkus oleh element tepat sebelum element pada fungsi render() di berkas club-item.js seperti ini:
classClubItemextendsHTMLElement{
…….
render(){
this.shadowDOM.innerHTML =`
club-item {
display: block;
margin-bottom: 18px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
border-radius: 10px;
overflow: hidden;
}
club-item .fan-art-club {
width: 100%;
max-height: 300px;
object-fit: cover;
object-position: center;
}
.club-info {
padding: 24px;
}
.club-info > h2 {
font-weight: lighter;
}
.club-info > p {
margin-top: 10px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 10; /* number of lines to show */
}
${this._club.name}
${this._club.description}
`
;
}
}
……
Sesuaikan kembali selector pada styling tersebut menjadi seperti ini:
classClubItemextendsHTMLElement{
…..
render(){
this.shadowDOM.innerHTML =`
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:host {
display: block;
margin-bottom: 18px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
border-radius: 10px;
overflow: hidden;
}
.fan-art-club {
width: 100%;
max-height: 300px;
object-fit: cover;
object-position: center;
}
.club-info {
padding: 24px;
}
.club-info > h2 {
font-weight: lighter;
}
.club-info > p {
margin-top: 10px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 10; /* number of lines to show */
}
${this._club.name}
${this._club.description}
`
;
}
}
……..
Simpan perubahan tersebut dan lihat pada browser, seharusnya tampilan daftar tim sudah kembali normal.
Oh ya, sebelum beranjak kita buka kembali berkas src -> styles -> clublist.css. Di sana masih terdapat satu rule styling berikut:
club-list {
display: block;
margin-top:32px;
width:100%;
padding:16px;
}
Jangan hapus rule styling tersebut karena kita masih menggunakannya untuk mengatur jarak daftar liga yang ditampilkan. Namun sebaiknya kita pindahkan rule styling tersebut pada berkas src -> styles -> style.css.
@import“clublist.css”;
*{
padding:0;
margin:0;
box–sizing: border–box;
}
body {
font–family: sans–serif;
}
main {
width:90%;
max–width:800px;
margin:32pxauto;
}
club–list {
display: block;
margin–top:32px;
width:100%;
padding:16px;
}
Dengan begitu kita dapat leluasa menghapus berkas clublist.css dan menghapus @import pada berkas style.css.
Selamat! Kita sudah berhasil menerapkan Shadow DOM pada seluruh custom element yang digunakan di proyek Club Finder. Sampai ketemu di materi selanjutnya ya!