Özel Öğelerle Çalışma

Boris Smus
Boris Smus

Giriş

Web'de ifade eksikliği var. Ne demek istediğimi anlayabilmek için Gmail gibi "modern" bir web uygulamasına göz atın:

Gmail

<div> çorbasıyla ilgili modern bir şey yok. Yine de web uygulamalarını bu şekilde oluşturuyoruz. Çok üzgünüm. Platformumuzdan daha fazlasını istememiz gerekmez mi?

Seksi işaretleme. Birlikte harekete geçelim

HTML, bir belgeyi yapılandırmak için mükemmel bir araç sağlar, ancak kelime hazinesi HTML standardının tanımladığı öğelerle sınırlıdır.

Gmail işaretlemesi korkunç olmasa ne olurdu? Çok güzel olsaydı nasıl olurdu?

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

Ne kadar yeni? Bu uygulama da çok mantıklı. Anlamlı, anlaması kolay ve en önemlisi de sürdürülebilir olmasıdır. Gelecekte ben veya siz bildirim temelli omurgayı inceleyerek tam olarak ne işe yaradığını anlayacaksınız.

Başlarken

Özel Öğeler web geliştiricilerinin yeni HTML öğesi türleri tanımlamalarını sağlar. Bu spesifikasyon, Web Bileşenleri şemsiyesi altında bulunan birkaç yeni API temel öğesinden biridir, ancak muhtemelen en önemli olanıdır. Özel öğelerin kilidi açılan özellikler olmadan Web Bileşenleri görünmez:

  1. Yeni HTML/DOM öğeleri tanımlama
  2. Diğer öğelerden genişleyen öğeler oluşturun
  3. Özel işlevleri mantıksal olarak tek bir etiket altında bir araya getirin
  4. Mevcut DOM öğelerinin API'sini genişlet

Yeni öğeler kaydetme

Özel öğeler document.registerElement() kullanılarak oluşturulur:

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

document.registerElement() için ilk bağımsız değişken, öğenin etiket adıdır. Ad, kısa çizgi (-) içermelidir. Örneğin, <x-tags>, <my-element> ve <my-awesome-app> geçerli adlardır, <tabs> ve <foo_bar> ise geçerli değildir. Bu kısıtlama, ayrıştırıcının özel öğeleri normal öğelerden ayırt etmesine olanak tanır ancak aynı zamanda HTML'ye yeni etiketler eklendiğinde ileriye dönük uyumluluğu da sağlar.

İkinci bağımsız değişken, öğenin prototype özelliğini açıklayan (isteğe bağlı) bir nesnedir. Burası öğelerinize özel işlevler (ör. herkese açık mülkler ve yöntemler) ekleyebileceğiniz yerdir. İlerleyen zamanlarda bu konuda daha fazla bilgi edinebilirsiniz.

Varsayılan olarak, özel öğeler HTMLElement değerini devralır. Dolayısıyla, önceki örnek şuna eşdeğerdir:

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

document.registerElement('x-foo') çağrısı, tarayıcıya yeni öğe hakkında bilgi verir ve <x-foo> örneklerini oluşturmak için kullanabileceğiniz bir kurucuyu döndürür. Alternatif olarak, oluşturucuyu kullanmak istemiyorsanız diğer öğe oluşturma tekniklerini kullanabilirsiniz.

Öğe uzatma

Özel öğeler, diğer özel öğelerin yanı sıra mevcut (yerel) HTML öğelerini genişletmenize olanak tanır. Bir öğeyi genişletmek için, devralacak öğenin registerElement() adını ve prototype değerini iletmeniz gerekir.

Yerel öğeleri genişletme

Normal Joe <button> adlı şarkıdan memnun olmadığınızı düşünelim. Bir "Mega Düğme" olması için özelliklerini pekiştirmek istersiniz. <button> öğesini genişletmek için prototype öğesinin HTMLButtonElement ve extends öğesinin adını devralan yeni bir öğe oluşturun. Bu örnekte, "button":

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

Yerel öğelerden devralınan özel öğelere tür uzantısı özel öğeleri denir. Bunlar, "X öğesi Y'dir" anlamına gelen özel bir HTMLElement sürümünden devralır.

Örnek:

<button is="mega-button">

Özel öğe genişletme

<x-foo> özel öğesini genişleten bir <x-foo-extended> öğesi oluşturmak için prototipini devralmanız ve devraldığınız etiketi söylemeniz yeterlidir:

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

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

Öğe prototipleri oluşturma hakkında daha fazla bilgi için aşağıdaki JS özelliklerini ve yöntemlerini ekleme konusuna bakın.

Öğeler nasıl yükseltilir?

HTML ayrıştırıcısının standart olmayan etiketlere neden sığmadığını hiç merak ettiniz mi? Örneğin, sayfada <randomtag> öğesini tanımlarsak çok mutlu olur. HTML spesifikasyonuna göre:

Üzgünüm <randomtag>. Standart dışı ve HTMLUnknownElement politikasından devralıyorsunuz.

Aynı durum özel öğeler için geçerli değildir. Geçerli özel öğe adlarına sahip öğeler, HTMLElement değerini devralır. Bu durumu, Console'u (Ctrl + Shift + J veya Mac'te Cmd + Opt + J) etkinleştirip aşağıdaki kod satırlarını yapıştırarak doğrulayabilirsiniz; bu satırlarda true döndürülür:

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

Çözülmemiş öğeler

Özel öğeler komut dosyası tarafından document.registerElement() kullanılarak kaydedildiğinden, tanımları tarayıcı tarafından kaydedilmeden önce bildirilebilir veya oluşturulabilir. Örneğin, sayfada <x-tabs> işlevini belirtebilir ancak document.registerElement('x-tabs') öğesini çok daha sonra çağırabilirsiniz.

Öğeler tanımlarına yükseltilmeden önce bunlara çözümlenmemiş öğeler adı verilir. Bunlar, geçerli bir özel öğe adına sahip olan ancak kaydedilmemiş HTML öğeleridir.

Aşağıdaki tablo işleri yoluna koymaya yardımcı olabilir:

Ad Devralındığı kaynak Örnekler
Çözümlenmemiş öğe HTMLElement <x-tabs>, <my-element>
Bilinmeyen öğe HTMLUnknownElement <tabs>, <foo_bar>

Öğeleri örneklendirme

Yaygın olarak kullanılan öğe oluşturma teknikleri, özel öğeler için de geçerlidir. Tüm standart öğelerde olduğu gibi HTML'de bildirilebilir veya JavaScript kullanılarak DOM'de oluşturulabilir.

Özel etiketlere örnekler verme

Aşağıdakileri bildirin:

<x-foo></x-foo>

JS'de DOM oluşturun:

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

new operatörünü kullanın:

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

Tür uzantısı öğelerini örneklendirme

Tür uzantısı tarzı özel öğeleri örneklendirmek özel etiketlere son derece yakındır.

Aşağıdakileri bildirin:

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

JS'de DOM oluşturun:

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

Gördüğünüz gibi, is="" özelliğini ikinci parametre olarak alan, aşırı yüklenmiş bir document.createElement() sürümü var.

new operatörünü kullanın:

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

Şu ana kadar tarayıcıya yeni bir etiketi bildirmek için document.registerElement() özelliğini nasıl kullanacağımızı öğrendik... ancak bu etiket pek bir işe yaramaz. Şimdi özellikler ve yöntemler ekleyelim.

JS özellikleri ve yöntemleri ekleme

Özel öğelerin en güçlü yönü, öğe tanımında özellikler ve yöntemler tanımlayarak bu öğeyle özelleştirilmiş işlevleri bir araya getirebilmenizdir. Bunu, öğeniz için herkese açık bir API oluşturmanın yolu olarak düşünün.

Tam bir örneği aşağıda bulabilirsiniz:

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

Elbette prototype oluşturmanın binlerce yolu vardır. Bunun gibi prototipler oluşturmaktan hoşlanmıyorsanız, burada aynı şeyin daha kısa bir versiyonunu görebilirsiniz:

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

İlk biçim ES5 Object.defineProperty kullanımına olanak tanır. İkincisi, get/set kullanımına olanak tanır.

Yaşam döngüsü geri çağırma yöntemleri

Öğeler, var oldukları ilginç zamanlardan faydalanmak için özel yöntemler tanımlayabilir. Bu yöntemlere uygun bir şekilde yaşam döngüsü geri çağırmaları adı verilir. Her birinin belirli bir adı ve amacı vardır:

Geri arama adı Şu durumda aranır:
createdCallback öğenin bir örneği oluşturulduğunda
attachedCallback dokümana bir örnek eklendi
detachedCallback dokümandan bir örnek kaldırıldı
attributeChangedCallback(attrName, oldVal, newVal) Bir özellik eklendiğinde, kaldırıldığında veya güncellendi

Örnek: <x-foo> üzerinde createdCallback() ve attachedCallback() tanımlanıyor:

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

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

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

Yaşam döngüsü geri çağırmalarının tümü isteğe bağlıdır ancak uygun olup olmadığını veya ne zaman mantıklı olduğunu belirtin. Örneğin, öğenizin yeterince karmaşık olduğunu ve createdCallback() bölgesinde IndexedDB ile bağlantı açtığını varsayalım. DOM'den kaldırılmadan önce detachedCallback() üzerinde gerekli temizleme işlemini yapın. Not: Buna bel bağlamamanız gerekir. Örneğin, kullanıcı sekmeyi kapatıyorsa ancak bunu olası bir optimizasyon kancası olarak düşünebilirsiniz.

Bir diğer kullanım alanı yaşam döngüsü geri çağırması da öğede varsayılan etkinlik işleyicilerin ayarlanmasıdır:

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

İşaretleme ekleme

JavaScript API'si verildiğinde <x-foo> URL'sini oluşturduk ancak bu alan boş! Oluşturması için biraz HTML verelim mi?

Yaşam döngüsü geri çağırmaları burada kullanışlıdır. Özellikle, bir öğeye bazı varsayılan HTML eklemek için createdCallback() kullanabiliriz:

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

Bu etiketi göstermek ve Geliştirici Araçları'nda incelemek (sağ tıklayıp Öğeyi İncele'yi seçin) aşağıdakileri göstermelidir:

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

Dahili öğeleri Gölge DOM içine dahil etme

Shadow DOM, tek başına içeriği kapsamak için güçlü bir araçtır. Özel öğelerle birlikte kullanın, muhteşem bir deneyim elde edeceksiniz.

Gölge DOM, özel öğeler sağlar:

  1. Cesaretlerini gizleyerek kullanıcıları korkunç uygulama ayrıntılarından korumanın bir yolu.
  2. Stil kapsülleme ...ücretsiz.

Gölge DOM'dan bir öğe oluşturmak, temel işaretleme oluşturan bir öğe oluşturmaya benzer. Fark createdCallback() cinsinden:

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

Öğenin .innerHTML değerini ayarlamak yerine <x-foo-shadowdom> için bir Gölge Kökü oluşturdum ve daha sonra, bunu işaretlemeyle doldurdum. Geliştirici Araçları'nda "Gölge DOM'u Göster" ayarı etkinken genişletilebilir bir #shadow-root görürsünüz:

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

Shadow Root işte bu!

Şablondan öğe oluşturma

HTML Şablonları, özel öğeler dünyasına çok uygun olan bir başka yeni API öğesidir.

Örnek: <template> ve Gölge DOM'dan oluşturulmuş bir öğeyi kaydetme:

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

Bu birkaç satırlık kod, çok yardımcı olabilir. Neler olup bittiğini görelim:

  1. HTML'ye yeni bir öğe kaydettik: <x-foo-from-template>
  2. Öğenin DOM'u bir <template> öğesinden oluşturuldu
  3. Öğenin korkunç ayrıntıları Gölge DOM kullanılarak gizlenir
  4. Gölge DOM, öğe stili kapsülleme işlemi sağlar (ör. p {color: orange;}, sayfanın tamamını turuncuya çevirmiyor)

Çok iyi!

Özel öğelerin stilini belirleme

Herhangi bir HTML etiketinde olduğu gibi, özel etiketinizin kullanıcıları etiketi seçicilerle biçimlendirebilir:

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

Gölge DOM kullanan öğelerin stil özelliklerini ayarlama

Gölge DOM'yi dahil ettiğinizde tavşan deliği çok daha derine iner. Gölge DOM kullanan özel öğeler harika avantajlarını kullanır.

Gölge DOM, bir öğeyi stil kapsüllemesiyle doldurur. Gölge Kökü'nde tanımlanan stiller ana makineden sızmaz ve sayfadan taşmaz. Özel öğe kullanılması durumunda, öğenin kendisi ana makinedir. Stil kapsüllemenin özellikleri, özel öğelerin kendileri için varsayılan stilleri tanımlamasına da olanak tanır.

Gölge DOM stili önemli bir konudur. Bu konuda daha fazla bilgi edinmek isterseniz diğer makalelerimden birkaçını okumanızı öneririz:

:unresolved kullanarak FOUC önleme

Özel öğeler, FOUC kaynaklı yeni bir CSS sözde sınıfı (:unresolved) belirtir. Bunu, tarayıcının createdCallback() öğenizi çağırdığı noktaya kadar çözümlenmemiş öğeleri hedeflemek için kullanın (yaşam döngüsü yöntemlerine bakın). Bu durumda öğe, çözümlenmemiş öğe olmaktan çıkar. Yükseltme işlemi tamamlanmıştır ve öğe tanımına dönüştürülmüştür.

Örnek: Kaydedildiklerinde "x-foo" etiketleri görünür:

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

:unresolved değerinin, HTMLUnknownElement öğesinden devralan öğeler için değil, yalnızca çözümlenmemiş öğeler için geçerli olduğunu unutmayın (Öğeler nasıl yükseltilir? bölümüne bakın).

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

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

Özellik algılama

Özellik algılama, document.registerElement() ürününün mevcut olup olmadığını kontrol eder:

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

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

Tarayıcı desteği

document.registerElement() ilk olarak Chrome 27 ve Firefox ~23 sürümlerinde bir bayrak arkasına inmeye başladı. Ancak, bu spesifikasyon o zamandan beri epey gelişti. Chrome 31, güncellenmiş spesifikasyon için gerçek desteğe sahip olan ilk sürümdür.

Tarayıcı desteği mükemmel hale gelene kadar Google'ın Polymer ve Mozilla X-Tag ürünleri tarafından kullanılan bir polyfill vardır.

HTMLElementElement öğesine ne oldu?

Standartlaştırma çalışmasını takip edenler için bir zamanlar <element> vardı. Arıların dizleri vardı. Yeni öğeleri bildirimli olarak kaydetmek için bunu kullanabilirsiniz:

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

Ne yazık ki yükseltme sürecinde, köşe davalarında ve Mahşer saldırısına benzer senaryolarda her şeyin üstesinden gelmek için çok fazla zamanlama sorunu yaşandı. <element> rafa kaldırıldı. Ağustos 2013'te, Dimitri Glazkov en azından şimdilik herkese açık webapps uygulamasında uygulamanın kaldırıldığını duyurdu.

Polymer'in <polymer-element> ile bildirim temelli bir öğe kaydı biçimi uyguladığını unutmayın. Nasıl mı? Bu şablonda document.registerElement('polymer-element') ve Şablondan öğe oluşturma bölümünde açıkladığım teknikler kullanılmaktadır.

Sonuç

Özel öğeler, HTML'nin kelime dağarcığını genişletmemizi, ona yeni püf noktaları öğretmeyi ve web platformunun boşluklarından atlamamızı sağlayacak bir araç sunuyor. Bunları Gölge DOM ve <template> gibi diğer yeni platform temel öğeleriyle birleştirdiğinizde Web Bileşenlerinin ne olduğunu anlamaya başlarız. İşaretleme yine çekici olabilir!

Web bileşenlerini kullanmaya başlamakla ilgileniyorsanız Polymer'e göz atmanızı öneririz. Başlamanıza yardımcı olacak daha fazlası var.