Özel Öğelerle Çalışma

Boris Smus
Boris Smus

Giriş

Web, ifadelere ciddi şekilde yer açmaz. 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?

Cinsel içerikli işaretleme. Hedeflerinize uygun hale getirelim

HTML, dokümanları yapılandırma için bize mükemmel bir araç sunar. Ancak dokümanın 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 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 da yenilikçi. Bu uygulama da gayet mantıklı. Anlamlı ve kolay anlaşılırdır. En önemlisi de sürdürebilir olması. Gelecekteki ben/siz, beyan temelli omurgasını inceleyerek tam olarak ne yaptığını anlayabilir.

Başlarken

Özel Öğeler web geliştiricilerinin yeni HTML öğesi türlerini tanımlamalarına olanak tanır. Bu spesifikasyon, Web Components ç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 etiket halinde gruplandırma
  4. Mevcut DOM öğelerinin API'sini genişletin

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ş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. Burası, öğelerinize özel işlevler (ör. herkese açık mülkler ve yöntemler) ekleyebileceğiniz yerdir. 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 registerElement() öğesine devralma yapılacak öğenin adını ve prototype değerini iletmeniz gerekir.

Yerel öğeleri genişletme

<button> adlı müşteriden memnun olmadığınızı varsayalım. Bu düğmenin özelliklerini iyileştirerek "Mega Düğme" olmasını istiyorsunuz. <button> öğesini genişletmek için HTMLButtonElement öğesinin prototype özelliğini ve öğenin adını devralan yeni bir öğe oluşturun. Bu durumda "button":

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

Yerel öğelerden devralan özel öğelere tür uzantısı özel öğeleri adı verilir. "X öğesi bir Y" diyebileceğimiz bir şekilde HTMLElement öğesinin özel bir sürümünden devralı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 uyum sağlamadığını hiç merak ettiniz mi? Örneğin, sayfada <randomtag> olduğunu beyan etmemiz son derece mutlu olur. HTML spesifikasyonuna göre:

Üzgünüm <randomtag>! Standart dışıysanız ve HTMLUnknownElement öğesinden miras alıyorsanız

Özel öğeler için aynı durum geçerli değildir. Geçerli özel öğe adlarına sahip öğeler HTMLElement öğesinden devralınır. Bu durumu 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

Sonlandırılmamış öğeler

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

Öğeler, tanımlarına yükseltilmeden önce çözülmüş öğeler olarak adlandırılır. Bunlar, geçerli bir özel öğe adına sahip ancak kaydedilmemiş HTML öğeleridir.

Şu tablo, işleri yoluna koymanıza yardımcı olabilir:

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

Öğeleri örnekleme

Yaygın öğe oluşturma teknikleri ö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ı belirtin:

<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ı stili özel öğeleri örneklendirmek, özel etiketlere son derece yakındır.

Bunları belirtin:

<!-- <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, artık is="" özelliğini ikinci parametresi olarak alan document.createElement() aşırı yüklenmiş bir 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ğinin nasıl kullanılacağını öğrendik. Ancak, bu araç pek işe yaramaz. Özellikler ve yöntemler ekleyelim.

JS özellikleri ve yöntemleri ekleme

Özel öğelerin avantajı, öğe tanımında özellikler ve yöntemler tanımlayarak özelleştirilmiş işlevleri öğeyle bir araya getirebilmenizdir. Bunu, öğeniz için herkese açık bir API oluşturma yöntemi 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 onlarca yolu var. Buna benzer prototipler oluşturmayı sevmiyorsanız aynı yöntemin daha kısa bir versiyonunu da burada 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ımına izin verir. İ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ç anlarından faydalanmak için özel yöntemler tanımlayabilir. Bu yöntemler, yaşam döngüsü geri çağırma olarak adlandırılır. Her birinin belirli bir adı ve amacı vardır:

Geri arama adı Çağrılma zamanı
createdCallback öğenin bir örneği oluşturulur
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> ürününde 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 olduğunda/uygun durumlarda 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. Veri DOM'den kaldırılmadan önce detachedCallback() bölümünde gerekli temizleme işlemini yapın. Not: Buna güvenmemelisiniz (örneğin, kullanıcı sekmeyi kapattıysa, ancak olası bir optimizasyon kancası olarak düşünebilirsiniz).

Diğer bir kullanım alanı yaşam döngüsü geri çağırması da öğede varsayılan etkinlik işleyiciler ayarlamaktır:

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

İşaretleme ekleme

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

Yaşam döngüsü geri aramaları bu noktada kullanışlıdır. Özellikle, bir öğeyi varsayılan HTML ile donatmak 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 etiket örneklendirildiğinde ve Geliştirici Araçları'nda incelenirken (sağ tıklayın, Öğeyi İncele'yi seçin) şunlar gösterilir:

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

Shadow DOM'de dahili öğeleri kapsülleme

Shadow DOM, tek başına içeriği kapsamaya yönelik güçlü bir araçtır. Özel öğelerle birlikte kullandığınızda işler büyülü bir hal alır.

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

  1. İçeriği gizleyerek kullanıcıları uygulamayla ilgili ayrıntılardan korumak için
  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. Geliştirici Araçları'nda "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'den 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ç kod satırı gerçekten çok etkilidir. Şimdi olup biten her şeyi anlayalım:

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

Çok iyi.

Özel öğelere stil uygulama

Herhangi bir HTML etiketinde olduğu gibi, özel etiketinizin kullanıcıları da seçicilerle etikete 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 öğelerin stilini belirleme

Gölge DOM'yi sürece dahil ettiğinizde tavşan deliği çok daha derine iner. Gölge DOM kullanan özel öğeler, bu özelliğin avantajlarından yararlanır.

Gölge DOM, bir öğeye stil kapsülleme 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 kapsüllemenin özellikleri, özel öğelerin kendileri için varsayılan stiller tanımlamasına da olanak tanır.

Gölge DOM stili çok önemli bir konu. Bu konu hakkında daha fazla bilgi edinmek için diğer makalelerimi okumanızı öneririm:

:unresolved kullanarak FOUC'yi önleme

FOUC'un etkisini azaltmak için özel öğeler yeni bir CSS sözde sınıfını (:unresolved) belirtir. Tarayıcının createdCallback() öğenizi çağırdığı ana kadar, çözülmüş öğ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 tamamlanmış ve öğe, tanımına dönüşmüştür.

Örnek: Kayıtlı olduklarında "x-foo" etiketlerinin kararması:

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

:unresolved işlevinin, HTMLUnknownElement öğesinden devralınan öğeler için değil, yalnızca çözülmemiş öğ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() öğesinin var 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 bayrağı kullanıma sunmaya başladı. Ancak bu spesifikasyon o zamandan beri epey gelişti. Chrome 31, güncellenmiş spesifikasyon için gerçek destek sunan ilk sürümdür.

Tarayıcı desteği mükemmel hale gelene kadar, Google'ın Polymer ve Mozilla'nın X-Tag'i tarafından kullanılan bir çoklu dolgu mevcuttur.

HTMLElementElement'e ne oldu?

Standartlaştırma çalışmalarını takip edenler, bir zamanlar <element> olduğunu bilir. Arıların dizleriydi. 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, uç durum ve Armageddon benzeri senaryo vardı. <element> adlı ürünün rafta bulunması gerekiyordu. Dimitri Glazkov, Ağustos 2013'te public-webapps adresinde, en azından şimdilik kaldırıldığını duyurdu.

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

Sonuç

Özel öğeler, HTML'nin kelime hazinesini genişletmemize, ona yeni numaralar öğretmemize ve web platformunun uzay tünelinden geçmemize olanak tanır. Bunları Shadow DOM ve <template> gibi diğer yeni platform bileşenleriyle birleştirdiğinizde Web Bileşenlerinin kavrayışını fark etmeye başlarız. İşaretleme yine çekici olabilir!

Web bileşenlerini kullanmaya başlamak istiyorsanız Polymer'i incelemenizi öneririz. Başlamanıza yardımcı olması için yeterli.