Özel Öğeler

Web özellikleri: Otonom özel öğeler

Özel öğe, customElements.define() aracılığıyla bir etiket adına kaydedilen sınıftır. Kayıt ve temel yaşam döngüsü iyi anlaşılmıştır. Aşağıdaki kurallar, rutin olarak yanlış uygulanan bölümleri hedeflemektedir.

Yaşam döngüsü: Yanlış uygulanan kısımlar

Geri çağırmalarla ilgili yeterli doküman mevcuttur. Hatalara neden olan yanlış anlamalar şunlardır:

  • connectedCallback birden fazla kez çalıştırılabilir. Her ekleme işleminde tetiklenir. Bu nedenle, öğeyi taşımak bu işlemi yeniden çalıştırır. Kurulumu idempotent yapın. Tek bir ilk çalıştırma olduğunu varsaymayın.
  • connectedMoveCallback, taşıma işleminde bağlantıyı kesme/yeniden bağlama çiftini atlar. Bir öğe Node.moveBefore() ile yeniden konumlandırıldığında bu geri çağırma, disconnectedCallback → connectedCallback yerine tetiklenir. Böylece durum, odak ve iFrame/medya oynatma, taşıma işleminden etkilenmez. Kurulum veya kaldırma işleminiz pahalıysa ya da gözle görülür şekilde yeniden çalışıyorsa bunu tanımlayın.
  • disconnectedCallback içinde sökme. Genel dinleyicileri, zamanlayıcıları ve gözlemcileri kaldırın, aksi takdirde sızıntı olur. Ancak öğe yeniden eklenebileceğinden, kaldırma işlemini kalıcı olarak değerlendirmeyin.
  • Oluşturucuda önce super() yöntemini çağırın ve tam DOM'un mevcut olmasına bağlı kalmayın. this öğesine dokunduktan sonra super() çağrıldığında ReferenceError hatası oluşur. Özellikler genellikle (ancak her zaman değil) mevcuttur; uzun alt öğe listelerinin ayrıştırma işlemi tamamlanmamış olabilir. Tek seferlik başlatma için connectedCallback, doğrudan alt öğeleri izlemek için slotchange ve özellikleri izlemek için attributeChangedCallback kullanın. connectedCallback, tüm alt öğeler ayrıştırılmadan önce tetiklenebilir. Bu nedenle, sonraki slotchange / attributeChangedCallback çağrılarını yoksaymayın.
  • attributeChangedCallback yalnızca observedAttributes için tetiklenir ve çıkışı etkilemeyen özellikler için ucuz kalmalıdır.
  • Yaşam döngüsü kancaları ve observedAttributes, kayıt sırasında anlık görüntü olarak kaydedilir. customElements.define() her ikisini de bir kez okur; çağrıdan sonra sınıfa bir kanca eklemek veya observedAttributes sınıfını genişletmek sessizce yoksayılır. Özellikle attributeChangedCallback, observedAttributes define() zamanında mevcut değilse hiçbir zaman tetiklenmez.

Yükseltme zamanlaması ve :defined

Bir öğe, sınıfı kaydedilmeden önce DOM'da bulunabilir (sunucu tarafından oluşturulan HTML'de veya komut dosyası geç yüklendiğinde). Tanımlanana kadar, davranışlarınızın hiçbirini içermeyen hareketsiz bir HTMLElement olur. customElements.define() çalıştığında tarayıcı, dokümanda bulunan her eşleşen öğeyi yükseltir, kurucusunu çalıştırır ve ardından connectedCallback.

Modellerin genellikle gözden kaçırdığı iki sonuç:

  1. Oluşturucunuz/connectedCallback yükseltme sırasında çalıştırıldığında özellikler ve alt öğeler zaten mevcut olabilir. İlk durumu senkronize etmek için connectedCallback öğesindeki mevcut özellikleri okuyun ve öğenin boş başladığını varsaymayın.
  2. connectedCallback içinde bile alt öğeler eksik olabilir. HTML ayrıştırıcı, uzun alt öğe listelerinin ayrıştırılması sırasında sonuç verebilir. Bu nedenle, geri çağırma bazen her alt öğe eklenmeden önce çalışır. Kalıp (slotchange + attributeChangedCallback) için yukarıdaki Yaşam döngüsü bölümünde yer alan oluşturucu maddesine bakın.

Stilsiz içeriğin anlık olarak görünmesini önlemek için tanımlanmamış öğeleri gizleyin ancak açık bırakın. Böylece komut dosyası hiç yüklenmezse içeriğe erişilebilir. opacity öğesini visibility: hidden ile tamamen gizlemek yerine gecikmeli olarak canlandırın. Böylece öğe, hiçbir zaman yükseltilmese bile zaman aşımından sonra kendini gösterir:

my-element:not(:defined) {
  opacity: 0;
  /* Reveal anyway after 2s, so a failed or slow script never hides content for good. */
  animation: defined-fallback 0.2s 2s forwards;
}
@keyframes defined-fallback { to { opacity: 1; } }

Bu korumaları tüm body yerine, ihtiyaç duyan öğelerle veya :has(:not(:defined)) aracılığıyla belirli bir alt ağaçla sınırlayın. Yavaş ve kritik olmayan bir widget (ör. paylaş düğmesi), tüm sayfanın boyanmasını geciktirmemelidir.

Özellik ⇄ özellik yansıması

Öğenin hem işaretlemeden hem de komut dosyasından kullanılabilmesi için HTML özelliğini ve JS özelliğini senkronize edin. Özellik değerleri her zaman dizedir; bunları dönüştürün.

class MyToggle extends HTMLElement {
  static observedAttributes = ['label', 'count', 'disabled'];

  // String attribute reflected via getter/setter.
  get label() { return this.getAttribute('label') ?? ''; }
  set label(v) {
    v ? this.setAttribute('label', v) : this.removeAttribute('label');
  }

  // Numeric attribute: coerce from string on read, serialize on write.
  get count() { return Number(this.getAttribute('count')) || 0; }
  set count(v) { this.setAttribute('count', String(v)); }

  // Boolean attribute: present = true, absent = false. Never set it to "false".
  get disabled() { return this.hasAttribute('disabled'); }
  set disabled(on) { this.toggleAttribute('disabled', !!on); }

  attributeChangedCallback(name, oldVal, newVal) {
    if (oldVal !== newVal) this.render();
  }
}

Değerin bulunduğu yer

Yukarıdaki örnekte DOM, doğruluk kaynağı olarak tutulur. Her alıcı getAttribute değerini okur. Bu, birkaç basit, dize benzeri özellik için doğru varsayılandır: Durumun bulunabileceği tek bir yer vardır, bu nedenle senkronizasyonu bozulamaz.

Daha zengin bileşenler için doğruluk kaynağını bir JS alanında tutun ve yalnızca başka bir şeyin görmesi gerektiğinde (CSS hedefleme veya okuyan bir tüketici) bir özelliğe dışa doğru yansıtın. Bir alanı okumak, her erişimde bir özelliği ayrıştırıp zorlamaktan çok daha ucuzdur ve dize olmayan değerler hiçbir zaman serileştirme yoluyla gidiş dönüş yapmaz. Bu nedenle, Lit gibi kitaplıklar JS'de durumu korur ve özellikleri depolama alanı olarak ele almak yerine seçici olarak yansıtır: Her özelliği değil, yalnızca dış dünyanın gözlemlemesi gerekenleri yansıtır.

Güçlükler:

  • observedAttributes disiplini: Yalnızca değişikliği yeniden oluşturma veya yan etki tetiklemesi gereken özellikleri listeleyin. Her şeyi gözlemlemek, her mutasyona hiçbir fayda sağlamadan bir geri çağırma ekler.
  • Yansıma döngüleri: Bir özelliği yazan ayarlayıcı, attributeChangedCallback öğesini yeniden girer. Yukarıda gösterildiği gibi oldVal !== newVal kontrolüyle koruyun.
  • Yalnızca stil durumu: Yalnızca stil oluşturmak için kullanılan dahili durumlar (yükleniyor, etkin, genişletilmiş) için yapay bir özellik yansıtmak yerine özel durumları tercih edin. Aşağıdaki Özel durumlar bölümüne bakın. Yalnızca tüketicilerin durumu işaretlemeden ayarlayabilmesi gerektiğinde bir özelliğe yansıtın.

Özel durumlar

Özel durumlar, bir öğenin CSS'nin hedeflemesi için kendisi hakkında gösterdiği, ElementInternals aracılığıyla ayarlanan ve :state() sözde sınıfıyla eşleşen boole işaretleridir. Sentetik bir özelliği yansıtmak yerine bunları dahili, çalışma zamanı kullanıcı arayüzü durumu (busy, expanded, invalid) için kullanın: Tüketicinin özellikleriyle çakışamazlar, DOM'da hiçbir zaman görünmezler ve dışarıdan salt okunurdurlar (tüketici :state()'i stilleştirebilir ancak taklit edemez).

class AsyncButton extends HTMLElement {
  #internals;
  #action = async () => {};

  constructor() {
    super();
    this.#internals = this.attachInternals();
  }

  set action(fn) { this.#action = fn; } // host supplies the work to run

  async run() {
    if (this.#internals.states.has('busy')) return; // guard re-entry
    this.#internals.states.add('busy');
    this.#internals.states.delete('success');
    try {
      await this.#action();
      this.#internals.states.add('success');
    } catch {
      this.#internals.states.add('error');
    } finally {
      this.#internals.states.delete('busy');
    }
  }
}

CustomStateSet bir küme olduğundan bağımsız durumlar, birleşik bir data-status enum oluşturmadan birleştirilir (busy artı error).

Özel durum yalnızca çalışma zamanında geçerlidir. DOM'ye serileştirilmediğinden yeniden yükleme, öğenin yeniden oluşturulması veya sunucu oluşturma işleminden sonra kalıcı olmaz. Durumun işaretlemede bildirimli olarak ayarlanması veya yüklemeler arasında kalıcı olması gerekiyorsa bu bir özellik işidir (Değerin bulunduğu yer bölümüne bakın). Doğruluk kaynağınızdan connectedCallback içinde geri yükleyin. Genel kural: Bileşenin çalışma zamanında türettiği ve sahip olduğu ve yalnızca stil vermesi gerekenler için özel durum; işaretlemeden veya serileştirilmiş olarak ayarlanması gerekenler için bir özellik.

Bu durumları hem bileşen içinde hem de tüketicinin stil sayfasından stillendirmek için Web Bileşenlerini Stillendirme başlıklı makaleyi inceleyin.

Adlandırma kuralları

  • Gerçek gizli durum: Tam kapsülleme için yerel # alanları (#count); yalnızca alt sınıfların erişmesi gereken durumlarda _ ön eki. # alanlarının devralınmadığını unutmayın. Alt sınıf, temel sınıfın özel alanlarını göremez.
  • Sembol anahtarlı özellikler, sıradan numaralandırmadan gizlenir ancak sembolü tutan herhangi bir kod tarafından erişilebilir (dışa aktarın veya sınıfta static alanı olarak gösterin). Her sembol benzersiz olduğundan, aynı açıklamaya sahip iki sembol hiçbir zaman çakışmaz. Bu, bir alt sınıfın veya kardeş sınıfın, # alanlarının sağlayamadığı kontrollü erişime ihtiyaç duyduğu durumlarda kullanışlıdır.
  • Global özellikleri (style, class, id, slot, part, title, lang, dir) kendi özelliklerinizle gölgelemeyin. Aksi takdirde yerleşik davranışları bozulur. disabled özelliğinin yalnızca form kontrollerinde global olduğunu, diğer öğelerde bu özelliğin etkisini kendiniz uygulamanız gerektiğini unutmayın.

Yeni bir API yüzeyini adlandırırken platformun kendi kurallarına uyun: Özel öğe etiketi adları için en az bir tire içeren küçük harfli tireli yazım (ör. my-toggle), özellikler ve yöntemler için lowerCamelCase ve karşılık gelen özellikleriyle senkronize edilmiş küçük harfli özellik adları kullanın.