Shadow DOM v1 - Bağımsız Web Bileşenleri

Gölge DOM, web geliştiricilerin web bileşenleri için bölümlere ayrılmış DOM ve CSS oluşturmasına olanak tanır.

Özet

Gölge DOM, web uygulamaları oluşturmanın kırılganlığını ortadan kaldırır. Bu kırılganlık, HTML, CSS ve JS'nin genel yapısından kaynaklanır. Yıllar içinde, sorunları aşmak için çok sayıda araç geliştirdik. Örneğin, yeni bir HTML kimliği/sınıfı kullandığınızda, sayfa tarafından kullanılan mevcut bir adla çakışıp çakışmayacağını bilemezsiniz. Küçük hatalar ortaya çıkar, CSS özgüllüğü büyük bir sorun haline gelir (!important her şey!), stil seçiciler kontrolden çıkar ve performans düşebilir. Liste bu şekilde uzayıp gidiyor.

Gölge DOM, CSS ve DOM'u düzeltir. Web platformuna kapsamlı stiller ekler. Araçlar veya adlandırma kuralları olmadan CSS'yi işaretlemeyle paketleyebilir, uygulama ayrıntılarını gizleyebilir ve vanilla JavaScript'te bağımsız bileşenler yazabilirsiniz.

Giriş

Shadow DOM, üç Web Bileşeni standardından biridir: HTML Şablonları, Shadow DOM ve Özel öğeler. HTML içe aktarmaları eskiden listenin bir parçasıydı ancak artık desteği sonlandırılmış olarak kabul ediliyor.

Gölge DOM kullanan web bileşenleri oluşturmanız gerekmez. Ancak kullandığınızda avantajlarından (CSS kapsamı, DOM kapsülleme, oluşturma) yararlanır ve dayanıklı, yüksek düzeyde yapılandırılabilir ve son derece yeniden kullanılabilir özel öğeler oluşturursunuz. Özel öğeler, yeni bir HTML oluşturmanın (JS API ile) yoluyken gölge DOM, HTML ve CSS'sini sağlamanın yoludur. Bu iki API bir araya gelerek bağımsız HTML, CSS ve JavaScript içeren bir bileşen oluşturur.

Gölge DOM, bileşen tabanlı uygulamalar oluşturmak için bir araç olarak tasarlanmıştır. Bu nedenle, web geliştirme alanındaki yaygın sorunlara çözümler sunar:

  • İzole DOM: Bir bileşenin DOM'u bağımsızdır (ör. document.querySelector(), bileşenin gölge DOM'undaki düğümleri döndürmez).
  • Kapsamlı CSS: Gölge DOM içinde tanımlanan CSS, gölge DOM ile sınırlıdır. Stil kuralları dışarı sızmaz ve sayfa stilleri içeri sızmaz.
  • Oluşturma: Bileşeniniz için açıklayıcı, işaretleme tabanlı bir API tasarlayın.
  • CSS'yi basitleştirir: Kapsamlı DOM, basit CSS seçicileri ve daha genel kimlik/sınıf adları kullanabileceğiniz ve adlandırma çakışmaları konusunda endişelenmenize gerek olmadığı anlamına gelir.
  • Üretkenlik: Uygulamaları tek bir büyük (global) sayfa yerine DOM parçaları olarak düşünün.

fancy-tabs demosu

Bu makale boyunca bir demo bileşenden (<fancy-tabs>) bahsedecek ve bu bileşenin kod snippet'lerine referans vereceğim. Tarayıcınız API'leri destekliyorsa hemen aşağıda canlı bir demo göreceksiniz. Aksi takdirde, GitHub'daki tam kaynağa göz atın.

Kaynağı GitHub'da görüntüle

Gölge DOM nedir?

DOM hakkında bilgi

HTML, kullanımı kolay olduğu için web'e güç verir. Birkaç etiket bildirerek hem sunumu hem de yapısı olan bir sayfayı saniyeler içinde oluşturabilirsiniz. Ancak HTML tek başına pek kullanışlı değildir. İnsanların metin tabanlı bir dili anlaması kolaydır ancak makinelerin daha fazlasına ihtiyacı vardır. Belge Nesne Modeli'ni (DOM) kullanabilirsiniz.

Tarayıcı bir web sayfasını yüklerken birçok ilginç işlem gerçekleştirir. Yaptığı şeylerden biri, yazarın HTML'sini canlı bir dokümana dönüştürmektir. Temel olarak, sayfanın yapısını anlamak için tarayıcı, HTML'yi (statik metin dizeleri) bir veri modeline (nesneler/düğümler) ayrıştırır. Tarayıcı, bu düğümlerden oluşan bir ağaç (DOM) oluşturarak HTML'nin hiyerarşisini korur. DOM'un en iyi yanı, sayfanızın canlı bir temsili olmasıdır. Yazdığımız statik HTML'den farklı olarak tarayıcı tarafından üretilen düğümler özellikler, yöntemler içerir ve en iyisi de programlar tarafından değiştirilebilir. Bu nedenle, JavaScript kullanarak doğrudan DOM öğeleri oluşturabiliyoruz:

const header = document.createElement('header');
const h1 = document.createElement('h1');
h1.textContent = 'Hello DOM';
header.appendChild(h1);
document.body.appendChild(header);

aşağıdaki HTML işaretlemesini oluşturur:

<body>
    <header>
    <h1>Hello DOM</h1>
    </header>
</body>

Bunların hepsi çok güzel. Peki gölge DOM nedir?

Gölgelerdeki DOM

Gölge DOM, normal DOM'dan yalnızca iki açıdan farklıdır: 1) oluşturulma/kullanılma şekli ve 2) sayfanın geri kalanıyla ilişkili olarak nasıl davrandığı. Normalde DOM düğümleri oluşturur ve bunları başka bir öğenin alt öğeleri olarak eklersiniz. Gölge DOM ile öğeye eklenen ancak gerçek alt öğelerinden ayrı olan kapsamlı bir DOM ağacı oluşturursunuz. Bu kapsamlı alt ağaca gölge ağaç denir. Eklendiği öğe, gölge ana makinesidir. Gölgeye eklediğiniz her şey, <style> dahil olmak üzere barındırma öğesine özgü olur. Gölge DOM, CSS stil kapsamını bu şekilde sağlar.

Gölge DOM oluşturma

Gölge kök, bir "ana makine" öğesine eklenen belge parçasıdır. Öğe, gölge kökü eklenerek gölge DOM'unu elde eder. Bir öğe için gölge DOM oluşturmak istiyorsanız element.attachShadow() yöntemini çağırın:

const header = document.createElement('header');
const shadowRoot = header.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>'; // Could also use appendChild().

// header.shadowRoot === shadowRoot
// shadowRoot.host === header

Gölge kökü doldurmak için .innerHTML kullanıyorum ancak diğer DOM API'lerini de kullanabilirsiniz. Bu, web'dir. Seçim yapabiliriz.

Spesifikasyon, gölge ağaç barındıramayan öğelerin listesini tanımlar. Bir öğenin listede yer almasının birkaç nedeni olabilir:

  • Tarayıcı, öğe (<textarea>, <input>) için kendi dahili gölge DOM'unu zaten barındırıyor.
  • Öğenin gölge DOM (<img>) barındırması mantıklı değil.

Örneğin, şu komut çalışmaz:

    document.createElement('input').attachShadow({mode: 'open'});
    // Error. `<input>` cannot host shadow dom.

Özel öğe için gölge DOM oluşturma

Gölge DOM, özellikle özel öğeler oluştururken kullanışlıdır. Bir öğenin HTML, CSS ve JS'sini bölümlere ayırmak için gölge DOM'u kullanarak bir "web bileşeni" oluşturun.

Örnek: Özel bir öğe, DOM/CSS'sini kapsülleyerek gölge DOM'u kendisine ekler:

// Use custom elements API v1 to register a new HTML tag and define its JS behavior
// using an ES6 class. Every instance of <fancy-tab> will have this same prototype.
customElements.define('fancy-tabs', class extends HTMLElement {
    constructor() {
    super(); // always call super() first in the constructor.

    // Attach a shadow root to <fancy-tabs>.
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `
        <style>#tabs { ... }</style> <!-- styles are scoped to fancy-tabs! -->
        <div id="tabs">...</div>
        <div id="panels">...</div>
    `;
    }
    ...
});

Burada birkaç ilginç şey oluyor. İlk olarak, <fancy-tabs> örneği oluşturulduğunda özel öğe kendi gölge DOM'unu oluşturur. Bu işlem constructor() üzerinden yapılır. İkinci olarak, gölge kök oluşturduğumuz için <style> içindeki CSS kuralları <fancy-tabs> ile sınırlı olacak.

Oluşturma ve alanlar

Bileşim, gölge DOM'un en az anlaşılan özelliklerinden biridir ancak muhtemelen en önemlisidir.

Web geliştirme dünyasında oluşturma, uygulamaları HTML'den bildirimli olarak oluşturma şeklimizdir. Uygulamalar farklı yapı taşlarının (<div>, <header>, <form>, <input>) bir araya gelmesiyle oluşur. Bu etiketlerden bazıları birlikte de kullanılabilir. <select>, <details>, <form> ve <video> gibi yerel öğelerin bu kadar esnek olmasının nedeni oluşturmadır. Bu etiketlerin her biri, alt öğe olarak belirli HTML'leri kabul eder ve bunlarla özel bir işlem yapar. Örneğin, <select>, <option> ve <optgroup> öğelerini açılır liste ve çoklu seçim widget'larına nasıl dönüştüreceğini bilir. <details> öğesi, <summary> öğesini genişletilebilir bir ok olarak oluşturur. <video> bile belirli alt öğelerle nasıl başa çıkacağını bilir: <source> öğeleri oluşturulmaz ancak videonun davranışını etkiler. Sihir gibi!

Terminoloji: Işık DOM ve gölge DOM

Gölge DOM oluşturma, web geliştirmede bir dizi yeni temel kavram sunar. Ayrıntılara girmeden önce, aynı dili konuşabilmek için bazı terimleri standartlaştıralım.

Light DOM

Bileşeninizin kullanıcısının yazdığı işaretleme. Bu DOM, bileşenin gölge DOM'unun dışında bulunur. Öğenin gerçek alt öğeleridir.

<better-button>
    <!-- the image and span are better-button's light DOM -->
    <img src="gear.svg" slot="icon">
    <span>Settings</span>
</better-button>

Gölge DOM

Bileşen yazarının yazdığı DOM. Gölge DOM, bileşene özeldir ve bileşenin iç yapısını, kapsamlı CSS'sini tanımlar ve uygulama ayrıntılarınızı kapsar. Ayrıca, bileşeninizin tüketicisi tarafından yazılan işaretlemenin nasıl oluşturulacağını da tanımlayabilir.

#shadow-root
    <style>...</style>
    <slot name="icon"></slot>
    <span id="wrapper">
    <slot>Button</slot>
    </span>

Düzleştirilmiş DOM ağacı

Tarayıcının, kullanıcının ışık DOM'unu gölge DOM'unuza dağıtarak nihai ürünü oluşturmasının sonucudur. Düzleştirilmiş ağaç, Geliştirici Araçları'nda gördüğünüz ve sayfada oluşturulan ağaçtır.

<better-button>
    #shadow-root
    <style>...</style>
    <slot name="icon">
        <img src="gear.svg" slot="icon">
    </slot>
    <span id="wrapper">
        <slot>
        <span>Settings</span>
        </slot>
    </span>
</better-button>

<slot> öğesi

Gölge DOM, <slot> öğesini kullanarak farklı DOM ağaçlarını bir araya getirir. Alanlar, bileşeninizde kullanıcıların kendi işaretlemeleriyle doldurabileceği yer tutuculardır. Bir veya daha fazla alan tanımlayarak dış işaretlemeyi bileşeninizin gölge DOM'unda oluşturulmaya davet edersiniz. Aslında "Kullanıcının işaretlemesini burada oluştur" demiş olursunuz.

Öğelerin, bir <slot> tarafından davet edildiklerinde gölge DOM sınırını "aşmasına" izin verilir. Bu öğelere dağıtılmış düğümler denir. Kavramsal olarak, dağıtılmış düğümler biraz tuhaf görünebilir. Yuvalar, DOM'u fiziksel olarak taşımaz; gölge DOM'un içindeki başka bir konumda oluşturur.

Bir bileşen, gölge DOM'unda sıfır veya daha fazla alan tanımlayabilir. Alanlar boş olabilir veya yedek içerik sağlayabilir. Kullanıcı light DOM içeriği sağlamazsa slot, ikame içeriğini oluşturur.

<!-- Default slot. If there's more than one default slot, the first is used. -->
<slot></slot>

<slot>fallback content</slot> <!-- default slot with fallback content -->

<slot> <!-- default slot entire DOM tree as fallback -->
    <h2>Title</h2>
    <summary>Description text</summary>
</slot>

Adlandırılmış alanlar da oluşturabilirsiniz. Adlandırılmış yuvalar, gölge DOM'unuzda kullanıcıların adıyla referans verdiği belirli boşluklardır.

Örnek: <fancy-tabs> öğesinin gölge DOM'undaki alanlar:

#shadow-root
    <div id="tabs">
    <slot id="tabsSlot" name="title"></slot> <!-- named slot -->
    </div>
    <div id="panels">
    <slot id="panelsSlot"></slot>
    </div>

Bileşen kullanıcıları <fancy-tabs> öğesini şu şekilde bildirir:

<fancy-tabs>
    <button slot="title">Title</button>
    <button slot="title" selected>Title 2</button>
    <button slot="title">Title 3</button>
    <section>content panel 1</section>
    <section>content panel 2</section>
    <section>content panel 3</section>
</fancy-tabs>

<!-- Using <h2>'s and changing the ordering would also work! -->
<fancy-tabs>
    <h2 slot="title">Title</h2>
    <section>content panel 1</section>
    <h2 slot="title" selected>Title 2</h2>
    <section>content panel 2</section>
    <h2 slot="title">Title 3</h2>
    <section>content panel 3</section>
</fancy-tabs>

Düzleştirilmiş ağaç şu şekilde görünür:

<fancy-tabs>
    #shadow-root
    <div id="tabs">
        <slot id="tabsSlot" name="title">
        <button slot="title">Title</button>
        <button slot="title" selected>Title 2</button>
        <button slot="title">Title 3</button>
        </slot>
    </div>
    <div id="panels">
        <slot id="panelsSlot">
        <section>content panel 1</section>
        <section>content panel 2</section>
        <section>content panel 3</section>
        </slot>
    </div>
</fancy-tabs>

Bileşenimizin farklı yapılandırmaları işleyebildiğini ancak düzleştirilmiş DOM ağacının aynı kaldığını görebilirsiniz. <button> değerinden <h2> değerine de geçiş yapabiliriz. Bu bileşen, farklı alt öğe türlerini işlemek için oluşturuldu. Tıpkı <select> gibi.

Stil

Web bileşenlerini stillendirmek için birçok seçenek vardır. Gölge DOM kullanan bir bileşen, ana sayfa tarafından stillendirilebilir, kendi stillerini tanımlayabilir veya kullanıcıların varsayılanları geçersiz kılması için kancalar (CSS özel özellikleri biçiminde) sağlayabilir.

Bileşen tarafından tanımlanan stiller

Gölge DOM'un en kullanışlı özelliği şüphesiz kapsamlı CSS'tir:

  • Dış sayfadaki CSS seçiciler bileşeninizde geçerli olmaz.
  • İçeride tanımlanan stiller dışarı taşmaz. Ana öğe kapsamındadırlar.

Gölge DOM içinde kullanılan CSS seçiciler, bileşeninize yerel olarak uygulanır. Pratikte bu, sayfanın başka yerlerindeki çakışmalar konusunda endişelenmeden ortak kimlik/sınıf adlarını tekrar kullanabileceğimiz anlamına gelir. Gölge DOM'da daha basit CSS seçiciler kullanmak en iyi uygulamalardan biridir. Ayrıca performans açısından da faydalıdır.

Örnek: Gölge kökte tanımlanan stiller yereldir

#shadow-root
    <style>
    #panels {
        box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
        background: white;
        ...
    }
    #tabs {
        display: inline-flex;
        ...
    }
    </style>
    <div id="tabs">
    ...
    </div>
    <div id="panels">
    ...
    </div>

Stil sayfaları da gölge ağacı kapsamındadır:

#shadow-root
    <link rel="stylesheet" href="styles.css">
    <div id="tabs">
    ...
    </div>
    <div id="panels">
    ...
    </div>

multiple özelliğini eklediğinizde <select> öğesinin çoklu seçim widget'ını (açılır liste yerine) nasıl oluşturduğunu hiç merak ettiniz mi?

<select multiple>
  <option>Do</option>
  <option selected>Re</option>
  <option>Mi</option>
  <option>Fa</option>
  <option>So</option>
</select>

<select>, belirttiğiniz özelliklere göre kendisini farklı şekilde biçimlendirebilir. Web bileşenleri, :host seçicisini kullanarak kendilerini de biçimlendirebilir.

Örnek: Kendini stillendiren bir bileşen

<style>
:host {
    display: block; /* by default, custom elements are display: inline */
    contain: content; /* CSS containment FTW. */
}
</style>

:host ile ilgili bir sorun, üst sayfadaki kuralların, öğede tanımlanan :host kurallarından daha yüksek özgüllüğe sahip olmasıdır. Yani dış stiller kazanır. Bu sayede kullanıcılar, üst düzey stilinizi dışarıdan geçersiz kılabilir. Ayrıca, :host yalnızca gölge kök bağlamında çalışır. Bu nedenle, gölge DOM dışında kullanamazsınız.

:host(<selector>) işlevsel biçimi, bir <selector> ile eşleşmesi durumunda ana makineyi hedeflemenize olanak tanır. Bu, bileşeninizin kullanıcı etkileşimine tepki veren davranışları kapsüllemek veya ana makineye göre dahili düğümleri şekillendirmek için harika bir yöntemdir.

<style>
:host {
    opacity: 0.4;
    will-change: opacity;
    transition: opacity 300ms ease-in-out;
}
:host(:hover) {
    opacity: 1;
}
:host([disabled]) { /* style when host has disabled attribute. */
    background: grey;
    pointer-events: none;
    opacity: 0.4;
}
:host(.blue) {
    color: blue; /* color host when it has class="blue" */
}
:host(.pink) > #tabs {
    color: pink; /* color internal #tabs node when host has class="pink". */
}
</style>

Bağlama göre stil oluşturma

:host-context(<selector>), kendisi veya üst öğelerinden herhangi biri <selector> ile eşleşiyorsa bileşenle eşleşir. Bunun yaygın bir kullanımı, bir bileşenin çevresine göre tema oluşturmaktır. Örneğin, birçok kişi <html> veya <body> öğesine bir sınıf uygulayarak tema oluşturur:

<body class="darktheme">
    <fancy-tabs>
    ...
    </fancy-tabs>
</body>

:host-context(.darktheme), .darktheme öğesinin alt öğesi olduğunda <fancy-tabs> öğesine stil uygular:

:host-context(.darktheme) {
    color: white;
    background: black;
}

:host-context(), tema oluşturmak için kullanışlı olabilir ancak CSS özel niteliklerini kullanarak stil kancaları oluşturmak daha iyi bir yaklaşımdır.

Dağıtılmış düğümleri stillendirme

::slotted(<compound-selector>), bir <slot> içinde dağıtılan düğümlerle eşleşir.

Bir yaka kartı bileşeni oluşturduğumuzu varsayalım:

<name-badge>
    <h2>Eric Bidelman</h2>
    <span class="title">
    Digital Jedi, <span class="company">Google</span>
    </span>
</name-badge>

Bileşenin gölge DOM'u, kullanıcının <h2> ve .title öğelerini biçimlendirebilir:

<style>
::slotted(h2) {
    margin: 0;
    font-weight: 300;
    color: red;
}
::slotted(.title) {
    color: orange;
}
/* DOESN'T WORK (can only select top-level nodes).
::slotted(.company),
::slotted(.title .company) {
    text-transform: uppercase;
}
*/
</style>
<slot></slot>

Daha önce de belirttiğimiz gibi, <slot> kullanıcının light DOM'unu taşımaz. Düğümler bir <slot> içine dağıtıldığında <slot>, DOM'larını oluşturur ancak düğümler fiziksel olarak yerinde kalır. Dağıtımdan önce uygulanan stiller, dağıtımdan sonra da uygulanmaya devam eder. Ancak ışık DOM dağıtıldığında ek stiller (gölge DOM tarafından tanımlananlar) alabilir.

<fancy-tabs>'dan daha ayrıntılı bir örnek:

const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
    <style>
    #panels {
        box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
        background: white;
        border-radius: 3px;
        padding: 16px;
        height: 250px;
        overflow: auto;
    }
    #tabs {
        display: inline-flex;
        -webkit-user-select: none;
        user-select: none;
    }
    #tabsSlot::slotted(*) {
        font: 400 16px/22px 'Roboto';
        padding: 16px 8px;
        ...
    }
    #tabsSlot::slotted([aria-selected="true"]) {
        font-weight: 600;
        background: white;
        box-shadow: none;
    }
    #panelsSlot::slotted([aria-hidden="true"]) {
        display: none;
    }
    </style>
    <div id="tabs">
    <slot id="tabsSlot" name="title"></slot>
    </div>
    <div id="panels">
    <slot id="panelsSlot"></slot>
    </div>
`;

Bu örnekte iki alan vardır: sekme başlıkları için adlandırılmış bir alan ve sekme paneli içeriği için bir alan. Kullanıcı bir sekme seçtiğinde seçimi kalın harflerle gösterilir ve sekmenin paneli açılır. Bu işlem, selected özelliğine sahip dağıtılmış düğümler seçilerek yapılır. Özel öğenin JS'si (burada gösterilmemiştir) bu özelliği doğru zamanda ekler.

Bir bileşeni dışarıdan stillendirme

Bir bileşeni dışarıdan stillendirmenin birkaç yolu vardır. En kolay yöntem, etiket adını seçici olarak kullanmaktır:

fancy-tabs {
    width: 500px;
    color: red; /* Note: inheritable CSS properties pierce the shadow DOM boundary. */
}
fancy-tabs:hover {
    box-shadow: 0 3px 3px #ccc;
}

Dış stiller, gölge DOM'da tanımlanan stillere göre her zaman önceliklidir. Örneğin, kullanıcı fancy-tabs { width: 500px; } seçicisini yazarsa bu seçici, bileşenin kuralını (:host { width: 650px;}) geçersiz kılar.

Bileşenin kendisini biçimlendirmek yalnızca bir yere kadar işe yarar. Peki bir bileşenin iç kısımlarını stillemek isterseniz ne olur? Bunun için CSS özel özelliklerini kullanmamız gerekir.

CSS özel niteliklerini kullanarak stil kancaları oluşturma

Bileşenin yazarı, CSS özel özellikleri kullanarak stil kancaları sağlıyorsa kullanıcılar dahili stilleri değiştirebilir. Kavramsal olarak bu fikir, <slot> ile benzerdir. Kullanıcıların geçersiz kılması için "stil yer tutucuları" oluşturursunuz.

Örnek: <fancy-tabs>, kullanıcıların arka plan rengini geçersiz kılmasına olanak tanır:

<!-- main page -->
<style>
    fancy-tabs {
    margin-bottom: 32px;
    --fancy-tabs-bg: black;
    }
</style>
<fancy-tabs background>...</fancy-tabs>

Gölge DOM'u içinde:

:host([background]) {
    background: var(--fancy-tabs-bg, #9E9E9E);
    border-radius: 10px;
    padding: 10px;
}

Bu durumda bileşen, kullanıcı tarafından sağlandığı için arka plan değeri olarak black değerini kullanır. Aksi takdirde varsayılan olarak #9E9E9E kullanılır.

İleri düzey konular

Kapalı gölge kökleri oluşturma (kaçınılmalıdır)

Gölge DOM'un "kapalı" mod adı verilen başka bir türü daha vardır. Kapalı bir gölge ağacı oluşturduğunuzda, dışarıdaki JavaScript, bileşeninizin dahili DOM'sine erişemez. Bu, <video> gibi yerel öğelerin işleyiş şekline benzer. Tarayıcı, kapalı mod gölge kökü kullanarak uyguladığı için JavaScript, <video> öğesinin gölge DOM'una erişemiyor.

Örnek: Kapalı bir gölge ağacı oluşturma:

const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'closed'}); // close shadow tree
// div.shadowRoot === null
// shadowRoot.host === div

Kapalı moddan etkilenen diğer API'ler:

  • Element.assignedSlot/TextNode.assignedSlot, null döndürür
  • Event.composedPath(), gölge DOM içindeki öğelerle ilişkili etkinlikler için [] döndürür.

{mode: 'closed'} ile neden hiçbir zaman web bileşenleri oluşturmamanız gerektiğine dair özetimi aşağıda bulabilirsiniz:

  1. Yapay güvenlik hissi. Saldırganların Element.prototype.attachShadow adresini ele geçirmesini engelleyen bir şey yok.

  2. Kapalı mod, özel öğe kodunuzun kendi gölge DOM'sine erişmesini engeller. Bu tam bir başarısızlık. Bunun yerine, querySelector() gibi öğeleri kullanmak istiyorsanız daha sonra kullanmak üzere bir referans saklamanız gerekir. Bu durum, kapalı modun asıl amacını tamamen ortadan kaldırır.

        customElements.define('x-element', class extends HTMLElement {
        constructor() {
        super(); // always call super() first in the constructor.
        this._shadowRoot = this.attachShadow({mode: 'closed'});
        this._shadowRoot.innerHTML = '<div class="wrapper"></div>';
        }
        connectedCallback() {
        // When creating closed shadow trees, you'll need to stash the shadow root
        // for later if you want to use it again. Kinda pointless.
        const wrapper = this._shadowRoot.querySelector('.wrapper');
        }
        ...
    });
    
  3. Kapalı mod, bileşeninizi son kullanıcılar için daha az esnek hale getirir. Web bileşenleri oluştururken bir özelliği eklemeyi unuttuğunuz zamanlar olacaktır. Yapılandırma seçeneği. Kullanıcının istediği bir kullanım alanı. Dahili düğümler için yeterli stil kancası eklemeyi unutmak, sık karşılaşılan bir örnektir. Kapalı modda kullanıcılar varsayılanları geçersiz kılamaz ve stilleri değiştiremez. Bileşenin dahili öğelerine erişebilmek çok faydalıdır. Sonuç olarak, kullanıcılar bileşeninizi çatallayacak, başka bir bileşen bulacak veya istediklerini yapmıyorsa kendi bileşenlerini oluşturacaktır :(

JS'de alanlarla çalışma

Gölge DOM API, yuvalar ve dağıtılmış düğümlerle çalışmak için yardımcı programlar sağlar. Bunlar, özel bir öğe oluştururken kullanışlıdır.

slotchange etkinliği

slotchange etkinliği, bir alanın dağıtılmış düğümleri değiştiğinde tetiklenir. Örneğin, kullanıcı light DOM'a alt öğe ekler veya kaldırırsa.

const slot = this.shadowRoot.querySelector('#slot');
slot.addEventListener('slotchange', e => {
    console.log('light dom children changed!');
});

Light DOM'daki diğer değişiklik türlerini izlemek için öğenizin oluşturucusunda bir MutationObserver oluşturabilirsiniz.

Bir alanda hangi öğeler oluşturuluyor?

Bazen bir alanla hangi öğelerin ilişkilendirildiğini bilmek yararlı olabilir. Alan tarafından hangi öğelerin oluşturulduğunu öğrenmek için slot.assignedNodes() yöntemini çağırın. {flatten: true} seçeneği, bir alanın ikame içeriğini de döndürür (hiçbir düğüm dağıtılmıyorsa).

Örneğin, gölge DOM'unuzun aşağıdaki gibi olduğunu varsayalım:

<slot><b>fallback content</b></slot>
KullanımTelefonSonuç
<my-component>bileşen metni</my-component> slot.assignedNodes(); [component text]
<my-component></my-component> slot.assignedNodes(); []
<my-component></my-component> slot.assignedNodes({flatten: true}); [<b>fallback content</b>]

Bir öğe hangi yuvaya atanır?

Ters soruyu yanıtlamak da mümkündür. element.assignedSlot, öğenizin hangi bileşen alanlarına atandığını gösterir.

Gölge DOM etkinlik modeli

Bir etkinlik gölge DOM'dan yukarı doğru çıktığında hedefi, gölge DOM'un sağladığı kapsüllemeyi koruyacak şekilde ayarlanır. Yani etkinlikler, gölge DOM'unuzdaki dahili öğeler yerine bileşenden gelmiş gibi görünecek şekilde yeniden hedeflenir. Bazı etkinlikler gölge DOM'un dışına bile yayılmaz.

Gölge sınırını aşan etkinlikler şunlardır:

  • Odaklanma Etkinlikleri: blur, focus, focusin, focusout
  • Fare Etkinlikleri: click, dblclick, mousedown, mouseenter, mousemove vb.
  • Tekerlek etkinlikleri: wheel
  • Giriş Etkinlikleri: beforeinput, input
  • Klavye Etkinlikleri: keydown, keyup
  • Oluşturma Etkinlikleri: compositionstart, compositionupdate, compositionend
  • DragEvent: dragstart, drag, dragend, drop vb.

İpuçları

Gölge ağaç açıksa event.composedPath() çağrıldığında etkinliğin geçtiği düğümlerden oluşan bir dizi döndürülür.

Özel etkinlikleri kullanma

Gölge ağacındaki dahili düğümlerde tetiklenen özel DOM etkinlikleri, etkinlik composed: true işareti kullanılarak oluşturulmadığı sürece gölge sınırının dışına çıkmaz:

// Inside <fancy-tab> custom element class definition:
selectTab() {
    const tabs = this.shadowRoot.querySelector('#tabs');
    tabs.dispatchEvent(new Event('tab-select', {bubbles: true, composed: true}));
}

composed: false (varsayılan) ise tüketiciler, gölge kökünüzün dışında etkinliği dinleyemez.

<fancy-tabs></fancy-tabs>
<script>
    const tabs = document.querySelector('fancy-tabs');
    tabs.addEventListener('tab-select', e => {
    // won't fire if `tab-select` wasn't created with `composed: true`.
    });
</script>

Odaklanmayı yönetme

Gölge DOM'un etkinlik modelinden hatırlayacağınız üzere, gölge DOM içinde tetiklenen etkinlikler, barındırma öğesinden geliyormuş gibi görünecek şekilde ayarlanır. Örneğin, bir gölge kök içindeki <input> öğesini tıkladığınızı varsayalım:

<x-focus>
    #shadow-root
    <input type="text" placeholder="Input inside shadow dom">

focus etkinliği, <input>'den değil <x-focus>'den gelmiş gibi görünür. Benzer şekilde, document.activeElement <x-focus> olacak. Gölge kök mode:'open' ile oluşturulduysa (kapalı mod bölümüne bakın) odaklanan dahili düğüme de erişebilirsiniz:

document.activeElement.shadowRoot.activeElement // only works with open mode.

Birden fazla gölge DOM düzeyi varsa (ör. başka bir özel öğenin içindeki özel öğe) activeElement öğesini bulmak için gölge köklerini yinelemeli olarak incelemeniz gerekir:

function deepActiveElement() {
    let a = document.activeElement;
    while (a && a.shadowRoot && a.shadowRoot.activeElement) {
    a = a.shadowRoot.activeElement;
    }
    return a;
}

Odaklanma için başka bir seçenek de gölge ağacındaki öğelerin odaklanma davranışını genişleten delegatesFocus: true seçeneğidir:

  • Gölge DOM'un içindeki bir düğümü tıklarsanız ve düğüm odaklanılabilir bir alan değilse ilk odaklanılabilir alan odaklanır.
  • Gölge DOM içindeki bir düğüm odaklandığında, odaklanan öğeye ek olarak ana makineye de :focus uygulanır.

Örnek: delegatesFocus: true, odak davranışını nasıl değiştirir?

<style>
    :focus {
    outline: 2px solid red;
    }
</style>

<x-focus></x-focus>

<script>
customElements.define('x-focus', class extends HTMLElement {
    constructor() {
    super(); // always call super() first in the constructor.

    const root = this.attachShadow({mode: 'open', delegatesFocus: true});
    root.innerHTML = `
        <style>
        :host {
            display: flex;
            border: 1px dotted black;
            padding: 16px;
        }
        :focus {
            outline: 2px solid blue;
        }
        </style>
        <div>Clickable Shadow DOM text</div>
        <input type="text" placeholder="Input inside shadow dom">`;

    // Know the focused element inside shadow DOM:
    this.addEventListener('focus', function(e) {
        console.log('Active element (inside shadow dom):',
                    this.shadowRoot.activeElement);
    });
    }
});
</script>

Sonuç

delegatesFocus: true davranışı.

Yukarıda, <x-focus> öğesine odaklanıldığında (kullanıcı tıklaması, sekmeyle geçiş, focus() vb.), "Tıklanabilir gölge DOM metni" tıklandığında veya dahili <input> öğesine odaklanıldığında (autofocus dahil) elde edilen sonuç gösterilmektedir.

delegatesFocus: false değerini ayarlarsanız bunun yerine şunları görürsünüz:

delegatesFocus: false ve dahili giriş odaklanmış.
delegatesFocus: false ve dahili <input> odaklanmış.
delegatesFocus: false ve x-focus
    odağı alır (ör. tabindex='0' değerine sahiptir).
delegatesFocus: false ve <x-focus> odaklanır (ör. tabindex="0").
delegatesFocus: false ve &quot;Tıklanabilir gölge DOM metni&quot; tıklanır (veya öğenin gölge DOM'undaki başka bir boş alan tıklanır).
delegatesFocus: false ve "Tıklanabilir gölge DOM metni" tıklanır (veya öğenin gölge DOM'undaki başka bir boş alan tıklanır).

İpuçları ve püf noktaları

Yıllar içinde web bileşenleri yazma hakkında birkaç şey öğrendim. Bileşen yazma ve gölge DOM'da hata ayıklama konusunda bu ipuçlarından bazılarının faydalı olacağını düşünüyorum.

CSS sınırlaması kullanma

Genellikle bir web bileşeninin düzeni/stili/boyası oldukça bağımsızdır. Performans artışı için :host içinde CSS kapsama kullanın:

<style>
:host {
    display: block;
    contain: content; /* Boom. CSS containment FTW. */
}
</style>

Devralınabilir stilleri sıfırlama

Devralınabilir stiller (background, color, font, line-height vb.) gölge DOM'da devralınmaya devam eder. Yani varsayılan olarak gölge DOM sınırını aşarlar. Temiz bir başlangıç yapmak istiyorsanız devralınabilir stilleri gölge sınırını geçtiklerinde ilk değerlerine sıfırlamak için all: initial; kullanın.

<style>
    div {
    padding: 10px;
    background: red;
    font-size: 25px;
    text-transform: uppercase;
    color: white;
    }
</style>

<div>
    <p>I'm outside the element (big/white)</p>
    <my-element>Light DOM content is also affected.</my-element>
    <p>I'm outside the element (big/white)</p>
</div>

<script>
const el = document.querySelector('my-element');
el.attachShadow({mode: 'open'}).innerHTML = `
    <style>
    :host {
        all: initial; /* 1st rule so subsequent properties are reset. */
        display: block;
        background: white;
    }
    </style>
    <p>my-element: all CSS properties are reset to their
        initial value using <code>all: initial</code>.</p>
    <slot></slot>
`;
</script>

Bir sayfa tarafından kullanılan tüm özel öğeleri bulma

Bazen sayfada kullanılan özel öğeleri bulmak yararlı olabilir. Bunu yapmak için sayfada kullanılan tüm öğelerin gölge DOM'unu yinelemeli olarak incelemeniz gerekir.

const allCustomElements = [];

function isCustomElement(el) {
    const isAttr = el.getAttribute('is');
    // Check for <super-button> and <button is="super-button">.
    return el.localName.includes('-') || isAttr && isAttr.includes('-');
}

function findAllCustomElements(nodes) {
    for (let i = 0, el; el = nodes[i]; ++i) {
    if (isCustomElement(el)) {
        allCustomElements.push(el);
    }
    // If the element has shadow DOM, dig deeper.
    if (el.shadowRoot) {
        findAllCustomElements(el.shadowRoot.querySelectorAll('*'));
    }
    }
}

findAllCustomElements(document.querySelectorAll('*'));

<template> öğesinden öğe oluşturma

.innerHTML kullanarak bir gölge kökü doldurmak yerine, açıklayıcı bir <template> kullanabiliriz. Şablonlar, bir web bileşeninin yapısını bildirmek için ideal bir yer tutucudur.

"Özel öğeler: yeniden kullanılabilir web bileşenleri oluşturma" başlıklı makaledeki örneğe bakın.

Geçmiş ve tarayıcı desteği

Son birkaç yıldır web bileşenlerini takip ediyorsanız Chrome 35+/Opera'nın bir süredir gölge DOM'un eski bir sürümünü sunduğunu biliyorsunuzdur. Blink, bir süre boyunca her iki sürümü de paralel olarak desteklemeye devam edecek. v0 spesifikasyonu, gölge kök oluşturmak için farklı bir yöntem (v1'in element.attachShadow yöntemi yerine element.createShadowRoot) sunuyordu. Eski yöntemin çağrılması, v0 semantiğiyle gölge kök oluşturmaya devam eder. Bu nedenle, mevcut v0 kodu bozulmaz.

Eski v0 spesifikasyonuyla ilgileniyorsanız html5rocks makalelerine göz atın: 1, 2, 3. Ayrıca, gölge DOM v0 ve v1 arasındaki farkların karşılaştırıldığı harika bir kaynak da mevcuttur.

Tarayıcı desteği

Shadow DOM v1; Chrome 53 (durum), Opera 40, Safari 10 ve Firefox 63'te kullanıma sunulmuştur. Edge geliştirme sürecini başlattı.

Gölge DOM'u özellik algılamak için attachShadow öğesinin varlığını kontrol edin:

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

Polyfill

Tarayıcı desteği yaygın olarak kullanılabilir hale gelene kadar shadydom ve shadycss polyfill'leri v1 özelliğini sağlar. Shady DOM, Shadow DOM'un DOM kapsamını ve shadycss polyfill'lerini taklit eder. CSS özel özellikleri ve yerel API'nin sağladığı stil kapsamı.

Polyfill'leri yükleyin:

bower install --save webcomponents/shadydom
bower install --save webcomponents/shadycss

Polyfill'leri kullanma:

function loadScript(src) {
    return new Promise(function(resolve, reject) {
    const script = document.createElement('script');
    script.async = true;
    script.src = src;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
    });
}

// Lazy load the polyfill if necessary.
if (!supportsShadowDOMV1) {
    loadScript('/bower_components/shadydom/shadydom.min.js')
    .then(e => loadScript('/bower_components/shadycss/shadycss.min.js'))
    .then(e => {
        // Polyfills loaded.
    });
} else {
    // Native shadow dom v1 support. Go to go!
}

Stillerinizi nasıl dolgulayacağınız/kapsamlandıracağınızla ilgili talimatlar için https://github.com/webcomponents/shadycss#usage adresine bakın.

Sonuç

İlk kez, doğru CSS kapsamı ve DOM kapsamı olan ve gerçek bir oluşturma özelliğine sahip bir API temel öğemiz var. Özel öğeler gibi diğer web bileşeni API'leriyle birleştirildiğinde gölge DOM, hilelere veya <iframe> gibi eski yöntemlere başvurmadan gerçekten kapsüllenmiş bileşenler oluşturmanın bir yolunu sunar.

Beni yanlış anlamayın. Gölge DOM kesinlikle karmaşık bir canavardır. Ancak öğrenmeye değer bir canavardır. Biraz zaman ayırın. Öğrenin ve soru sorun.

Daha fazla bilgi

SSS

Gölge DOM v1'i hemen kullanabilir miyim?

Polyfill ile evet. Tarayıcı desteği başlıklı makaleyi inceleyin.

Gölge DOM hangi güvenlik özelliklerini sağlar?

Gölge DOM bir güvenlik özelliği değildir. CSS'yi kapsamlandırmak ve bileşendeki DOM ağaçlarını gizlemek için kullanılan basit bir araçtır. Gerçek bir güvenlik sınırı istiyorsanız <iframe> kullanın.

Bir web bileşeninin gölge DOM kullanması gerekir mi?

Hayır. Gölge DOM kullanan web bileşenleri oluşturmanız gerekmez. Ancak gölge DOM kullanan özel öğeler oluşturmak, CSS kapsamı, DOM kapsülleme ve oluşturma gibi özelliklerden yararlanabileceğiniz anlamına gelir.

Açık ve kapalı gölge kökler arasındaki fark nedir?

Kapalı gölge kökleri bölümüne bakın.