Web Components, kendi HTML öğelerinizi tanımlamanızı sağlayan tarayıcı API'lerinin ortak adıdır. Bir sınıfı HTMLElement sınıfından türetip customElements.define("kullanici-karti", KullaniciKarti) ile kaydettiğinizde, sayfada <kullanici-karti> yazdığınız her yerde tarayıcı sizin sınıfınızı çalıştırır. Öğenin iç yapısını sayfanın CSS ve JavaScript kodundan yalıtmak için this.attachShadow({ mode: "open" }) ile bir gölge kök (shadow root) eklersiniz. Öğe sayfaya eklendiğinde, çıkarıldığında veya gözlenen bir özniteliği değiştiğinde tarayıcı sınıfınızdaki yaşam döngüsü metotlarını çağırır.

Bu yazı konunun JavaScript tarafını anlatıyor: özel öğe türleri, kayıt kuralları, yaşam döngüsü geri çağrıları, öznitelik gözleme, attachShadow() seçenekleri, gölge DOM yalıtımı ve stil verme, slot içeriğine JavaScript ile erişim ve sık yapılan hatalar. template ve slot etiketlerinin HTML yazımı ile bildirimsel gölge DOM template ve slot etiketi yazısında anlatılıyor. Örnekler tarayıcı gerektirdiği için çalıştırılmadı; MDN belgelerine göre yazıldı. Serinin tamamı için Web API öğrenme rehberi sayfasına bakabilirsiniz.

Web Components neden var?

Web Components neden var?

Bir bileşen, farklı sayfalara bırakılıp orada da çalışması beklenen yeniden kullanılabilir bir parçadır. Bunun için iki şey gerekir: tarayıcının tanıdığı bir öğe adı ve sayfanın kodunun bileşenin içini yanlışlıkla bozamaması. Web Components bu ihtiyaçları üç teknolojiyle karşılar. Özel öğeler (custom elements) yeni etiket adlarını bir sınıfa bağlar. Gölge DOM (shadow DOM) öğeye gizli bir DOM ağacı ekler ve bu ağacın içini sayfanın stil ve betiklerinden ayırır. Şablonlar ve slotlar da yeniden kullanılabilir işaretlemeyi ve dışarıdan içerik yerleştirme noktalarını tanımlar.

MDN gölge DOM'un aslında web geliştiricilerine açılmadan önce de tarayıcıların içinde kullanıldığını hatırlatır. Örneğin denetimleri görünen bir video öğesinde DOM içinde yalnızca video etiketini görürsünüz; düğmeler ve kaydırıcılar öğenin gölge DOM'unda durur. Web Components size kendi öğeleriniz için aynı yalıtımı kurma imkânı verir. DOM ağacının temel yapısını hatırlamak isterseniz DOM nedir yazısına bakabilirsiniz.

Özel öğe türleri ve sınıf yazmak

İki tür özel öğe vardır. Bağımsız özel öğeler (autonomous) HTMLElement sınıfından türer ve davranışlarını sıfırdan siz yazarsınız; sayfada <popup-bilgi> gibi kendi adlarıyla kullanılırlar. Özelleştirilmiş yerleşik öğeler (customized built-in) ise HTMLParagraphElement veya HTMLUListElement gibi standart bir öğenin sınıfından türer, onun varsayılan davranışını devralır ve <ul is="acilir-liste"> biçiminde is özniteliğiyle kullanılır. MDN, Safari'nin özelleştirilmiş yerleşik öğeleri desteklemeyi planlamadığını not eder; bu yüzden geniş uyumluluk gereken bileşenlerde bağımsız öğeler daha güvenli bir seçimdir.

Sınıfın kurucusunu siz çağırmazsınız, tarayıcı çağırır. Kurucunun ilk satırı her zaman super() olmalıdır; böylece doğru prototip zinciri kurulur. MDN kurucuda başlangıç durumunu, varsayılan değerleri ve olay dinleyicilerini ayarlamanızı, isterseniz gölge kökü burada oluşturmanızı söyler; ama bu aşamada öğenin özniteliklerini veya çocuklarını incelememeniz, yeni öznitelik veya çocuk eklememeniz gerektiğini vurgular. Spesifikasyon da kurulum işini mümkün olduğunca connectedCallback() içine koymanızı önerir. JavaScript sınıf sözdizimi JavaScript öğrenme rehberi serisinde anlatılıyor.

customElements.define() ile kayıt

customElements.define() ile kayıt: Ad geçerli bir özel öğe adı değil (örneğin kısa çizgisiz "kart"), Aynı ad veya aynı kurucu daha önce tanımlanmış, extends değeri geçerli bir...

Bir özel öğeyi sayfada kullanılabilir yapmak için window.customElements üzerindeki define(ad, kurucu, secenekler) metodunu çağırırsınız. Üçüncü parametre yalnızca özelleştirilmiş yerleşik öğelerde gerekir ve extends özelliği genişletilen etiketin adını taşır; örneğin { extends: "ul" }. Bu durumda sınıfınızın da o etiketin arayüzünden, yani HTMLUListElement sınıfından türemesi gerekir. define() çağrıldığında sayfada zaten bulunan ve tanıma uyan öğeler otomatik olarak yükseltilir (upgrade), yani betiğiniz HTML'den sonra yüklense bile mevcut etiketler canlanır.

Öğe adının kuralları katıdır. MDN geçerli bir özel öğe adının küçük bir ASCII harfiyle başlaması, en az bir kısa çizgi içermesi, büyük ASCII harf, boşluk, / veya > içermemesi ve annotation-xml gibi ayrılmış adlardan biri olmaması gerektiğini listeler. Kısa çizgi kuralı sayesinde özel öğe adları gelecekte eklenecek standart HTML etiketleriyle çakışmaz.

DurumSonuç
Ad geçerli bir özel öğe adı değil (örneğin kısa çizgisiz "kart")SyntaxError (DOMException)
Aynı ad veya aynı kurucu daha önce tanımlanmışNotSupportedError (DOMException)
extends değeri geçerli bir özel öğe adıNotSupportedError
extends bilinmeyen bir öğeyi gösteriyorNotSupportedError
İkinci parametre bir kurucu değilTypeError

Bir modül iki kez yüklendiğinde veya iki kütüphane aynı adı kullandığında "aynı ad zaten tanımlı" hatası sık görülür. Tanımlamadan önce customElements.get(ad) ile kontrol edebilirsiniz. Bir öğenin tanımlanmasını beklemek için whenDefined(ad) vardır: öğe tanımlandığında kurucuyla çözülen bir söz döndürür, öğe zaten tanımlıysa söz hemen tamamlanır. MDN'nin örneği bu sözle bir menünün yer tutucusunu bileşenler hazır olunca kaldırır. MDN, büyük uygulamalarda ad çakışmalarını önlemek için yalnızca belirli bir gölge ağacında geçerli olan kapsamlı kayıt defterlerinden (scoped custom element registries) de söz eder; tarayıcı desteği için MDN uyumluluk tablosuna bakın.

javascript
if (!customElements.get("kullanici-karti")) {
  customElements.define("kullanici-karti", KullaniciKarti);
}

customElements.whenDefined("kullanici-karti").then(() => {
  document.querySelector(".yer-tutucu")?.remove();
});

Yaşam döngüsü geri çağrıları

Öğe kaydedildikten sonra sayfadaki kod öğeyle belirli şekillerde etkileşime girdiğinde tarayıcı sınıfınızdaki şu metotları çağırır:

Geri çağrıNe zaman çağrılır
connectedCallback()Öğe belgeye her eklendiğinde
disconnectedCallback()Öğe belgeden her çıkarıldığında
adoptedCallback()Öğe yeni bir belgeye her taşındığında
attributeChangedCallback(ad, eski, yeni)observedAttributes içindeki bir öznitelik eklendiğinde, değiştiğinde, silindiğinde veya yerine başkası konduğunda
connectedMoveCallback()Tanımlıysa, öğe Element.moveBefore() ile taşındığında diğer iki geri çağrının yerine

"Her" sözcüğü önemlidir. Bir öğeyi insertBefore() veya append() ile başka bir yere taşırsanız MDN'ye göre önce disconnectedCallback(), sonra connectedCallback() çalışır, çünkü öğe DOM'dan çıkarılıp yeniden bağlanır. Bu geri çağrılara başlatma ve temizlik kodu koyduysanız taşıma işlemi öğenin durumunu sıfırlayabilir. MDN bunun için connectedMoveCallback() tanımlayıp öğeyi Element.moveBefore() ile taşımayı önerir; o zaman diğer iki geri çağrı çalışmaz. Tarayıcı desteği için MDN uyumluluk tablosuna bakın. Öğe ekleme ve taşıma yöntemleri DOM eleman oluşturma ve ekleme yazısında.

Öznitelikleri gözlemek

Bir özniteliğin değişikliğinden haberdar olmak için iki şey gerekir: gözlenecek adları içeren statik observedAttributes dizisi ve attributeChangedCallback() metodu. Geri çağrı üç parametre alır: özniteliğin adı, eski değeri ve yeni değeri. Listede olmayan öznitelikler için geri çağrı çalışmaz. MDN önemli bir ayrıntıyı da belirtir: öğenin HTML'inde gözlenen bir öznitelik zaten yazılıysa, belge ilk kez ayrıştırıldığında öznitelik başlatılırken de geri çağrı çalışır. Yani <sayac-dugme baslangic="5"> yazdığınızda hiçbir değişiklik yapmasanız bile attributeChangedCallback("baslangic", null, "5") çağrılır. define() sayfasına göre bu statik özellik ve metotlar define() çağrıldığı anda yalnızca bir kez okunur; tanımdan sonra sınıfa eklenen bir geri çağrı dikkate alınmaz.

Aşağıdaki sayaç düğmesi bu parçaları bir araya getiriyor. Gölge kökü kurucuda oluşturuyoruz, çünkü MDN gölge kök oluşturmayı kurucuda yapılabilecek işler arasında sayar; öznitelik okuma ve dinleyici ekleme ise connectedCallback() içinde:

javascript
class SayacDugme extends HTMLElement {
  static observedAttributes = ["baslangic"];

  constructor() {
    super();
    this.sayi = 0;
    const golge = this.attachShadow({ mode: "open" });
    golge.innerHTML = `
      <style>button { font: inherit; padding: 0.5rem 1rem; }</style>
      <button type="button"><slot>Sayaç</slot>: <span>0</span></button>
    `;
    this.artir = () => {
      this.sayi += 1;
      this.ciz();
      this.dispatchEvent(new CustomEvent("degisti", { detail: this.sayi, bubbles: true, composed: true }));
    };
  }

  connectedCallback() {
    this.shadowRoot.querySelector("button").addEventListener("click", this.artir);
    this.ciz();
  }

  disconnectedCallback() {
    this.shadowRoot.querySelector("button").removeEventListener("click", this.artir);
  }

  attributeChangedCallback(ad, eski, yeni) {
    if (ad === "baslangic") {
      this.sayi = Number(yeni) || 0;
      this.ciz();
    }
  }

  ciz() {
    this.shadowRoot.querySelector("span").textContent = String(this.sayi);
  }
}

customElements.define("sayac-dugme", SayacDugme);
html
<sayac-dugme baslangic="5">Beğeni</sayac-dugme>

Bu örnekte disconnectedCallback() dinleyiciyi kaldırarak temizlik yapıyor, connectedCallback() ise öğe her bağlandığında dinleyiciyi yeniden ekliyor; böylece taşınan öğede dinleyici ikiye katlanmıyor. Olay, composed: true sayesinde gölge sınırını aşıp sayfadaki dinleyicilere ulaşabiliyor. Kendi olaylarınızı üretmek CustomEvent ve EventTarget, olayların gölge sınırından geçişi ve composedPath() ise olay yayılımı ve delegasyon yazısında anlatılıyor.

attachShadow() ve gölge DOM

attachShadow() ve gölge DOM: Yalıtım: JavaScript ve CSS sınırda durur, Gölge DOM'a stil vermek

attachShadow(secenekler) bir öğeye gölge DOM ağacı ekler ve ShadowRoot nesnesini döndürür. Gölge DOM'un terimleri şunlardır: gölgenin bağlandığı normal DOM düğümü gölge ana makinesi (shadow host), içindeki ağaç gölge ağacı, ağacın kökü gölge kök, gölge DOM'un bittiği ve normal DOM'un başladığı yer de gölge sınırıdır. Gölge ağacındaki düğümleri normal DOM'daki gibi değiştirebilirsiniz; fark, içerideki kodun dışarıyı etkileyememesidir.

SeçenekDeğerlerAnlamı
modeopen veya closedopen ise host.shadowRoot gölge kökü verir; closed ise null döner
delegatesFocusboolean, varsayılan falseOdaklanamayan bir iç parçaya tıklanınca ilk odaklanabilir parçaya odak verilir
clonableboolean, varsayılan falsetrue ise cloneNode() ve importNode() gölge kökü de kopyalar
serializableboolean, varsayılan falsetrue ise getHTML() ile gölge kök serileştirilebilir
slotAssignmentnamed veya manualnamed (varsayılan) slot özniteliğine göre otomatik atar; manual ise HTMLSlotElement.assign() ile elle

mode: "closed" sık yanlış anlaşılır. MDN bunun güçlü bir güvenlik mekanizması sayılmaması gerektiğini, örneğin tarayıcı eklentileri gibi yollarla aşılabileceğini, daha çok "sayfa bu bileşenin içine dokunmamalı" anlamında bir işaret olduğunu söyler. Ayrıca kapalı kökte bile attachShadow() metodunun döndürdüğü değeri saklarsanız kendi kodunuz köke erişebilir.

Her öğeye gölge eklenemez. MDN geçerli adlı bağımsız özel öğelerin yanında article, aside, blockquote, body, div, footer, h1 ile h6 arası başlıklar, header, main, nav, p, section ve span öğelerini listeler; a gibi bazı öğeler güvenlik nedeniyle gölge alamaz. Listede olmayan bir öğede veya bildirimsel olarak oluşturulmamış bir gölge kökü zaten olan bir öğede çağrı NotSupportedError fırlatır. Sunucuda bildirimsel olarak oluşturulmuş bir gölge kök varsa ve mode aynıysa mevcut kök temizlenip döndürülür; bu, sunucu tarafında oluşturulan bileşenlerin istemcide yeniden bağlanmasını kolaylaştırır.

Yalıtım: JavaScript ve CSS sınırda durur

MDN'nin örneği yalıtımı açıkça gösterir: sayfadaki bütün span öğelerini document.querySelectorAll("span") ile bulup büyük harfe çeviren bir düğme, gölge DOM içindeki span öğesini bulamaz. Aynı şekilde sayfa CSS'indeki span { color: blue; } kuralı gölge ağacındaki öğeleri etkilemez. Açık modda içeriye bilerek ulaşmak için host.shadowRoot.querySelectorAll("span") kullanırsınız. Seçici yöntemlerinin ayrıntıları querySelector ve getElementById yazısında.

javascript
const host = document.querySelector("#host");
const golge = host.attachShadow({ mode: "open" });
const span = document.createElement("span");
span.textContent = "Gölge DOM içindeyim";
golge.appendChild(span);

// Sayfa genelindeki arama gölge içini görmez
document.querySelectorAll("span");
// Açık kökte bilinçli erişim
host.shadowRoot.querySelectorAll("span");

Gölge DOM'a stil vermek

MDN iki yol gösterir. Programatik yolda boş bir CSSStyleSheet oluşturur, içeriğini replaceSync() veya replace() ile doldurur ve gölge kökün adoptedStyleSheets dizisine atarsınız. Bildirimsel yolda bir template içine style öğesi koyar ve şablon içeriğini gölge köke eklersiniz. İki durumda da stiller yalnızca o gölge ağacında geçerlidir. MDN, adoptedStyleSheets yolunun tek bir stil sayfasını birçok bileşen arasında paylaşmaya, tarayıcının bunu bir kez ayrıştırmasına ve yapılan değişikliklerin bütün bileşenlere yayılmasına imkân verdiğini; style öğesi yolunun ise az stil içeren, paylaşım gerektirmeyen bileşenler için uygun olduğunu belirtir. Dışarıdan link ile stil dosyası bağlamak da mümkündür, ama MDN bu öğelerin gölge kökün boyanmasını engellemediğini, dosya yüklenene kadar stilsiz içerik görülebileceğini not eder.

javascript
const sayfa = new CSSStyleSheet();
sayfa.replaceSync("span { color: red; border: 2px dotted black; }");

const golge = document.querySelector("#host").attachShadow({ mode: "open" });
golge.adoptedStyleSheets = [sayfa];

Gölge ağacının içinden ana öğeye stil vermek için :host, slot içeriğine stil vermek için ::slotted() gibi CSS seçicileri vardır; bunlar CSS serisinin konusudur.

Slot içeriğine JavaScript ile erişmek

Slotlar, bileşeni kullanan kişinin dışarıdan içerik yerleştirebileceği noktalardır. HTML tarafında slot öğesinin name özniteliği ile içerideki öğenin slot özniteliği eşleşir; adı olmayan slot varsayılan slottur ve slot özniteliği taşımayan çocuklar oraya gider. Aynı adlı iki slot varsa eşleşen bütün öğeler ilkine atanır. Yazım kuralları template ve slot etiketi yazısında.

JavaScript tarafında iki araç vardır. HTMLSlotElement.assignedNodes() slota atanmış düğümleri döndürür; { flatten: true } verirseniz iç içe slotlara atanmış düğümleri de içerir ve hiç atanmış düğüm yoksa slotun yedek içeriğini döndürür. slotchange olayı ise slottaki düğümler değiştiğinde slot öğesinde tetiklenir. MDN, atanmış bir düğümün kendi çocukları değiştiğinde bu olayın gelmediğini, yalnızca atanan düğümlerin kendisi eklenip çıkarıldığında geldiğini vurgular.

javascript
const slot = this.shadowRoot.querySelector("slot[name=baslik]");
slot.addEventListener("slotchange", () => {
  const dugumler = slot.assignedNodes();
  console.log(`Başlık slotunda ${dugumler.length} düğüm var.`);
});

Bir slotun atanmış öğesi ebeveynini değiştirmez; MDN, slota atanan bir öğenin lang ve dir değerlerini gölge ağacından değil, dışarıdaki ebeveyninden aldığını açıklar. Bileşeninizi çok dilli bir sayfada kullanırken bu ayrıntı önemlidir.

Sık yapılan hatalar

  • Kurucuda öznitelik okumak veya çocuk eklemek. MDN bunu açıkça yasaklar; bu işleri connectedCallback() içinde yapın.
  • connectedCallback() içinde koşulsuz attachShadow() çağırmak. Geri çağrı öğe her bağlandığında çalışır; öğe taşındığında ikinci çağrı, gölge kökü zaten olan öğe için NotSupportedError fırlatır. Kökü kurucuda oluşturun veya önce this.shadowRoot değerini kontrol edin.
  • Kısa çizgisiz ad kullanmak. define("kart", ...) SyntaxError verir; uygulama-kart gibi bir ad seçin.
  • observedAttributes listesini unutmak. Liste yoksa attributeChangedCallback() hiç çalışmaz.
  • İlk ayrıştırmadaki geri çağrıyı hesaba katmamak. HTML'de yazılı gözlenen öznitelikler için geri çağrı eski değer null ile bir kez çalışır.
  • mode: "closed" ile güvenlik sağladığını sanmak. Kapalı mod bir işarettir, güvenlik sınırı değildir.
  • Dinleyicileri temizlememek. connectedCallback() içinde eklenen dinleyiciler disconnectedCallback() içinde kaldırılmazsa taşınan öğede birikir.
  • Gölge içine güvenilmeyen HTML yazmak. innerHTML gölge kökte de HTML yorumlar; kullanıcı verisi için textContent kullanın. Ayrıntı için innerHTML ve textContent farkı yazısına bakın.

Sık Sorulan Sorular

Web Components kullanmak için bir kütüphane gerekir mi?

Hayır. Özel öğeler, gölge DOM ve şablonlar tarayıcının kendi API'leridir; bu yazıdaki örneklerin hepsi kütüphanesiz çalışacak şekilde MDN'ye göre yazıldı.

Tanım yüklenmeden önce özel öğe ne olur?

Tanım yüklenmeden önce öğe, tarayıcının tanımadığı bir öğe olarak durur. define() çağrıldığında mevcut öğeler yükseltilir. Arada geçen süreyi yönetmek için whenDefined() sözünü bekleyebilir veya CSS tarafında tanımlanmamış öğeleri hedefleyen :defined sözde sınıfını kullanabilirsiniz.

open ve closed modu arasında hangisini seçmeliyim?

Çoğu bileşen için open pratik bir seçimdir; test ve hata ayıklama sırasında shadowRoot üzerinden içeriye ulaşabilirsiniz. closed modu, sayfanın iç yapıya dokunmaması gerektiğini belirtmek için kullanılır ama MDN'nin uyardığı gibi güvenlik garantisi vermez.

Özel öğe form alanı gibi davranabilir mi?

define() sayfası, sınıfta statik formAssociated özelliği true olduğunda formAssociatedCallback(), formResetCallback(), formDisabledCallback() ve formStateRestoreCallback() geri çağrılarının da tanımlanabileceğini listeler. Form gönderimi tarafı form gönderme ve FormData yazısında anlatılıyor.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Web API öğrenme rehberi · template ve slot etiketi · DOM nedir · CustomEvent ve EventTarget · Olay yayılımı · classList ve setAttribute