Özel Öğelerle Çalışma

Boris Smus
Boris Smus

Giriş

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

Gmail

<div> çorbası modern bir yemek değildir. Yine de web uygulamalarını bu şekilde geliştiriyoruz. Bu üzücü bir durum. Platformumuzdan daha fazlasını talep etmemiz gerekmiyor mu?

Seksi işaretleme. Bunu bir alışkanlık haline getirelim

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

Gmail için işaretleme kötü değilse ne olur? Güzel bir resimse:

<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 ferahlatıcı! Bu uygulama da çok mantıklı. Anlamlı, anlaşılması kolay ve en önemlisi sürdürülebilir olmalıdır. Gelecekteki ben/siz, beyan temelli omurgasını inceleyerek tam olarak ne yaptığını anlayabilir.

Başlarken

Özel Öğeler, web geliştiricilerin yeni HTML öğesi türleri tanımlamasına olanak tanır. Bu spesifikasyon, Web Bileşenleri çatısı altında kullanıma sunulan birkaç yeni API ilkelinden biridir ancak büyük olasılıkla en önemlisidir. Web bileşenleri, özel öğelerin sunduğu özellikler olmadan var olamaz:

  1. Yeni HTML/DOM öğeleri tanımlama
  2. Diğer öğelerden uzanan öğeler oluşturma
  3. Özel işlevleri mantıksal olarak tek bir etikette bir araya getirme
  4. Mevcut DOM öğelerinin API'sini genişletme

Yeni öğeleri kaydetme

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

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

document.registerElement() işlevinin ilk bağımsız değişkeni, öğenin etiket adıdır. Ad kısa çizgi (-) içermelidir. Örneğin, <x-tags>, <my-element> ve <my-awesome-app> geçerli adlar iken <tabs> ve <foo_bar> 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 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. Öğelerinize özel işlevler (ör. herkese açık özellikler ve yöntemler) eklemek için bu bölümü kullanın. Bu konuyla ilgili daha fazla bilgiyi aşağıda bulabilirsiniz.

Özel öğeler varsayılan olarak HTMLElement öğesinden devralır. Bu nedenle, ö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> örneği oluşturmak için kullanabileceğiniz bir kurucu döndürür. Alternatif olarak, kurucuyu kullanmak istemiyorsanız diğer öğe oluşturma tekniklerini kullanabilirsiniz.

Öğeleri uzatma

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

Yerel öğeleri genişletme

<button> adlı müşteriden memnun olmadığınızı varsayalım. "Mega Düğme" olacak şekilde özelliklerini güçlendirmek istiyorsunuz. <button> öğesini genişletmek için HTMLButtonElement öğesinin prototype özelliğini ve öğenin adını devralan yeni bir öğe oluşturun.extends Bu durumda "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. "X öğesi bir Y öğesidir" demek için HTMLElement öğesinin özelleştirilmiş bir sürümünden devralınır.

Örnek:

<button is="mega-button">

Özel öğeleri genişletme

<x-foo> özel öğesini genişleten bir <x-foo-extended> öğesi oluşturmak için prototipini devralmanız ve hangi etiketten devraldığınızı belirtmeniz 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 özellikleri ve yöntemleri ekleme bölümüne bakın.

Öğeler nasıl yükseltilir?

HTML ayrıştırıcının standart olmayan etiketlere neden tepki vermediğini hiç merak ettiniz mi? Örneğin, sayfada <randomtag> tanımlamamızda herhangi bir sakınca yoktur. HTML spesifikasyonuna göre:

Üzgünüm <randomtag>. Standart dışıysanız ve HTMLUnknownElement kaynağından devralıyorsunuz.

Özel öğeler için aynı durum geçerli değildir. Geçerli özel öğe adlarına sahip öğeler HTMLElement öğesinden devralınır. Bunu doğrulamak için Konsolu Ctrl + Shift + J (veya Mac'te Cmd + Opt + J) açıp aşağıdaki kod satırlarını yapıştırın. Bu satırların döndürdüğü değer true olacaktı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ümlenmemiş öğeler

Özel öğeler document.registerElement() kullanılarak komut dosyası tarafından kaydedildiğinden, tarayıcı tarafından tanımları kaydedilmeden önce tanımlanabilir veya oluşturulabilir. Örneğin, sayfa üzerinde <x-tabs>'ü tanımlayabilir ancak çok daha sonra document.registerElement('x-tabs')'u çağırabilirsiniz.

Öğeler, tanımları yapılana kadar çözümlenmemiş öğeler olarak adlandırılır. Bunlar, geçerli bir özel öğe adına sahip ancak kaydedilmemiş HTML öğeleridir.

Bu tablo, kafanızın karışmasını önlemeye yardımcı olabilir:

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

Öğeleri örnekleme

Öğe oluşturmayla ilgili yaygın teknikler, özel öğeler için de geçerlidir. Herhangi bir standart öğede olduğu gibi, bunlar HTML'de tanımlanabilir veya JavaScript kullanılarak DOM'da oluşturulabilir.

Özel etiketleri örneklendirme

Bunları beyan edin:

<x-foo></x-foo>

JS'de DOM oluşturma:

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

Tür uzantısı stilinde özel öğeleri örneklendirmek, özel etiketlere oldukça benzer.

Bunları beyan edin:

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

JS'de DOM oluşturma:

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

Gördüğünüz gibi, artık document.createElement() işlevinin ikinci parametresi olarak is="" özelliğini alan aşırı yüklenen bir sürümü var.

new operatörünü kullanın:

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

Şimdiye kadar, tarayıcıya yeni bir etiket hakkında bilgi vermek için document.registerElement()'ü nasıl kullanacağımızı öğrendik. Ancak bu, çok fazla işe yaramaz. Özellikler ve yöntemler ekleyelim.

JS özellikleri ve yöntemleri ekleme

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

Tam ö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 bir prototype oluşturmanın binlerce yolu vardır. Bu tür prototipler oluşturmak istemiyorsanız aynı şeyin daha yoğun bir versiyonunu aşağıda bulabilirsiniz:

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ılmasına olanak tanır. İkincisi, get/set işlevinin kullanılmasına olanak tanır.

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

Öğeler, varlıklarının ilginç zamanlarına erişmek için özel yöntemler tanımlayabilir. Bu yöntemlere uygun şekilde yaşam döngüsü geri çağırma adı verilir. Her birinin belirli bir adı ve amacı vardır:

Geri arama adı Aşağıdaki durumlarda çağrılır:
createdCallback Öğenin bir örneği oluşturulur.
attachedCallback Dokümana bir örnek eklendi
detachedCallback Bir örnek dokümandan kaldırıldı
attributeChangedCallback(attrName, oldVal, newVal) Bir özellik eklendi, kaldırıldı veya güncellendi

Örnek: <x-foo> üzerinde createdCallback() ve attachedCallback()'i tanımlama:

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 mantıklı olduğunda/olursa bunları tanımlayın. Örneğin, öğenizin yeterince karmaşık olduğunu ve createdCallback() içinde IndexedDB bağlantısı açtığını varsayalım. DOM'dan kaldırılmadan önce detachedCallback() içinde gerekli temizleme işlemlerini yapın. Not: Örneğin, kullanıcı sekmeyi kapatırsa bu yönteme güvenmemelisiniz. Bunun yerine, olası bir optimizasyon kancası olarak düşünün.

Yaşam döngüsü geri çağırmalarının bir diğer kullanım alanı, öğede varsayılan etkinlik işleyicileri ayarlamaktır:

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

İşaretleme ekleme

<x-foo> öğesini oluşturduk ve ona bir JavaScript API verdik ancak boş görünüyor. Oluşturması için ona biraz HTML verelim mi?

Bu noktada yaşam döngüsü geri çağırma yöntemleri işinize yarayabilir. Özellikle, bir öğeye 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 örneklendirip DevTools'da incelediğinizde (sağ tıklayın, Öğeyi İncele'yi seçin) aşağıdakiler gösterilir:

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

Dahili öğeleri gölge DOM'da kapsama

Gölge DOM, içerikleri kapsayan güçlü bir araçtır. Özel öğelerle birlikte kullandığınızda harika sonuçlar elde edebilirsiniz.

Gölge DOM, özel öğelere şunları sağlar:

  1. İçeriği gizleyerek kullanıcıları uygulamayla ilgili ayrıntılardan koruma
  2. Stil kapsülleme…ücretsiz.

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

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 ardından bunu işaretlemeyle doldurdum. DevTools'ta "Gölge DOM'u göster" ayarı etkinleştirildiğinde genişletilebilen bir #shadow-root görürsünüz:

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

Gölge kökü budur.

Şablondan öğe oluşturma

HTML Şablonları, özel öğeler dünyasına mükemmel şekilde uyan yeni bir API ilkel öğesidir.

Örnek: <template> ve Gölge DOM'dan oluşturulan 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ır kod çok etkilidir. Neler olduğunu anlayalım:

  1. HTML'de yeni bir öğe kaydettik: <x-foo-from-template>
  2. Öğenin DOM'u bir <template>
  3. Öğenin korkutucu ayrıntıları, Gölge DOM kullanılarak gizlenir.
  4. Gölge DOM, öğe stili kapsüllemesi sağlar (ör.p {color: orange;} sayfanın tamamını turuncu renge boyamaz).

Çok iyi.

Özel öğelere stil uygulama

Herhangi bir HTML etiketinde olduğu gibi, özel etiketinizin kullanıcıları da seçicilerle stil verebilir:

<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 öğeleri biçimlendirme

Gölge DOM'u da işin içine kattığınızda bu konu çok çok daha derinlere iner. Gölge DOM kullanan özel öğeler, bu özelliğin avantajlarından yararlanır.

Gölge DOM, bir öğeye stil kapsayıcı özelliği ekler. Gölge kökünde tanımlanan stiller, ana makineden sızmaz ve sayfadan sızmaz. Özel öğelerde, öğenin kendisi barındırıcıdır. Stil sarmalama özelliklerinin, özel öğelerin kendileri için varsayılan stiller tanımlamasına da olanak tanır.

Gölge DOM stili çok önemli bir konudur. Bu konu hakkında daha fazla bilgi edinmek isterseniz diğer makalelerime göz atmanızı öneririm:

:unresolved kullanarak FOUC'yi önleme

FOUC'yi azaltmak için özel öğeler yeni bir CSS sözde sınıfı olan :unresolved'yi belirtir. 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 işlemden sonra öğe artık çözülmemiş öğe olarak kabul edilmez. Yükseltme işlemi tamamlandı ve öğe, tanımına dönüştürüldü.

Örnek: "x-foo" etiketleri kayıtlıyken yavaşça görünür hale getirme:

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

:unresolved'ün yalnızca HTMLUnknownElement'ten devralınan öğeler için değil, yalnızca çözümlenmemiş öğeler için geçerli olduğunu unutmayın (Öğelerin nasıl yükseltildiği 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() değerinin olup olmadığını kontrol etmektir:

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 işaretin arkasına yerleştirilmeye başladı. Ancak bu tarihten bu yana spesifikasyon oldukça gelişti. Chrome 31, güncellenmiş spesifikasyonu gerçek anlamda destekleyen ilk sürümdür.

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

HTMLElementElement ne oldu?

Standartlaştırma çalışmalarını takip edenler, bir zamanlar <element> olduğunu bilir. Çok iyiydi. Yeni öğeleri açıklayıcı bir şekilde kaydetmek için kullanabilirsiniz:

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

Maalesef yükseltme süreci ile ilgili çok fazla zamanlama sorunu, özel durum ve Armageddon'a benzer senaryolar vardı. <element> rafa kaldırıldı. Dimitri Glazkov, Ağustos 2013'te public-webapps'de, en azından şimdilik kaldırıldığını duyurdu.

Polymer'in <polymer-element> ile öğe kaydının açıklayıcı bir biçimini uyguladığını belirtmek gerekir. Nasıl mı? Bu örnekte document.registerElement('polymer-element') ve Şablondan öğe oluşturma bölümünde açıklanan teknikler kullanılmaktadır.

Sonuç

Özel öğeler, HTML'nin kelime hazinesini genişletmemize, ona yeni numaralar öğretmemize ve web platformunun uzay tünellerindeki engelleri aşmamıza olanak tanır. Bunları Gölge DOM ve <template> gibi diğer yeni platform temel öğeleriyle birleştirdiğimizde Web Bileşenleri'nin resmini görmeye başlarız. İşaretleme tekrar seksi olabilir.

Web bileşenlerini kullanmaya başlamak istiyorsanız Polymer'i incelemenizi öneririz. Başlamak için ihtiyacınız olan her şeye sahiptir.