องค์ประกอบที่กำหนดเอง

องค์ประกอบที่กำหนดเองคือคลาสที่ลงทะเบียนกับชื่อแท็กผ่าน customElements.define() การลงทะเบียนและวงจรพื้นฐานเป็นที่เข้าใจกันดีอยู่แล้ว กฎด้านล่างจึงกำหนดเป้าหมายไปยังส่วนที่มักจะมีการนำไปใช้อย่างไม่ถูกต้อง

วงจร: ส่วนที่ใช้งานไม่ถูกต้อง

Callback มีการบันทึกไว้อย่างดี แต่ความเข้าใจผิดต่อไปนี้อาจทำให้เกิดข้อบกพร่องได้

  • connectedCallback สามารถทำงานได้มากกว่า 1 ครั้ง โดยจะเริ่มทำงานทุกครั้งที่มีการแทรก ดังนั้นการย้ายองค์ประกอบจะเรียกใช้โค้ดอีกครั้ง ทำให้การตั้งค่าเป็นแบบ Idempotent โดยอย่าคิดว่าการเรียกใช้ครั้งแรกจะเกิดขึ้นเพียงครั้งเดียว
  • connectedMoveCallback จะข้ามการยกเลิกการเชื่อมต่อ/เชื่อมต่อใหม่เมื่อมีการย้าย เมื่อย้ายองค์ประกอบด้วย Node.moveBefore() ระบบจะเรียกใช้ Callback นี้แทน disconnectedCallback → connectedCallback ดังนั้นสถานะ โฟกัส และการเล่น iframe/สื่อจะยังคงอยู่หลังจากการย้าย กำหนดเมื่อการตั้งค่าหรือการแยกส่วนมีค่าใช้จ่ายสูงหรือสังเกตเห็นได้ว่ามีการเรียกใช้ซ้ำ
  • รื้อถอนใน disconnectedCallback นำ Listener, ตัวจับเวลา และ Observer ส่วนกลางออก ไม่เช่นนั้นจะเกิดการรั่วไหล แต่องค์ประกอบอาจถูกแทรกซ้ำ ดังนั้นอย่าถือว่าการแยกส่วนเป็นแบบถาวร
  • เรียกใช้ super() ก่อนในคอนสตรัคเตอร์ และอย่าพึ่งพา DOM แบบเต็มที่มีอยู่ การเรียกใช้ super() หลังจากแตะ this จะแสดง ReferenceError โดยปกติแล้วแอตทริบิวต์จะแสดงอยู่ (แต่ไม่เสมอไป) รายการย่อยที่ยาวอาจยังแยกวิเคราะห์ไม่เสร็จ ใช้ connectedCallback สำหรับการเริ่มต้นแบบครั้งเดียว slotchange เพื่อตรวจสอบองค์ประกอบย่อยโดยตรง และ attributeChangedCallback เพื่อตรวจสอบแอตทริบิวต์ connectedCallback อาจยังคงเริ่มทำงานก่อนที่จะแยกวิเคราะห์รายการย่อยทั้งหมด ดังนั้นอย่าละเว้นการเรียก slotchange / attributeChangedCallback ที่ตามมา
  • attributeChangedCallback จะเริ่มทำงานสำหรับ observedAttributes เท่านั้น และควรมีค่าใช้จ่ายต่ำสำหรับแอตทริบิวต์ที่ไม่ส่งผลต่อเอาต์พุต
  • ระบบจะจับภาพฮุกวงจรและ observedAttributes ไว้เมื่อลงทะเบียน customElements.define() จะอ่านทั้ง 2 อย่างเพียงครั้งเดียว การเพิ่มฮุกหรือขยาย observedAttributes ในคลาสหลังจากการเรียกจะถูกละเว้นโดยไม่มีการแจ้งเตือน โดยเฉพาะอย่างยิ่ง attributeChangedCallback จะไม่เริ่มทำงานเว้นแต่ observedAttributes จะแสดงอยู่แล้วในเวลา define()

ช่วงเวลาอัปเกรดและ:defined

องค์ประกอบอาจมีอยู่ใน DOM ก่อนที่จะมีการลงทะเบียนคลาส (ใน HTML ที่แสดงผลฝั่งเซิร์ฟเวอร์ หรือเมื่อสคริปต์โหลดช้า) จนกว่าจะมีการกำหนด HTMLElement ที่ไม่มีการทำงานใดๆ เมื่อ customElements.define() ทำงาน เบราว์เซอร์จะอัปเกรดองค์ประกอบที่ตรงกันทุกรายการที่มีอยู่ในเอกสาร โดยเรียกใช้คอนสตรัคเตอร์และ connectedCallback จากนั้น

ผลลัพธ์ 2 อย่างที่โมเดลมักจะพลาด ได้แก่

  1. แอตทริบิวต์และองค์ประกอบย่อยอาจมีอยู่แล้วเมื่อตัวสร้าง/connectedCallback ทำงานในการอัปเกรด อ่านแอตทริบิวต์ที่มีอยู่ใน connectedCallback เพื่อซิงค์สถานะเริ่มต้น และอย่าถือว่าองค์ประกอบเริ่มต้นด้วยค่าว่าง
  2. องค์ประกอบย่อยอาจยังขาดหายไปแม้จะอยู่ใน connectedCallback เนื่องจากโปรแกรมแยกวิเคราะห์ HTML สามารถแสดงผลระหว่างการแยกวิเคราะห์รายการย่อยที่ยาวได้ ดังนั้น Callback จึงอาจทำงานก่อนที่จะแนบองค์ประกอบย่อยทั้งหมดในบางครั้ง ดูหัวข้อตัวสร้างในวงจรด้านบนสำหรับรูปแบบ (slotchange + attributeChangedCallback)

ซ่อนองค์ประกอบที่ไม่ได้กำหนดเพื่อหลีกเลี่ยงเนื้อหาที่ไม่มีการจัดรูปแบบแบบฉับพลัน แต่เปิดไม่สำเร็จ เนื้อหาจึงยังคงเข้าถึงได้หากสคริปต์ไม่โหลด สร้างภาพเคลื่อนไหว opacity โดยมีการหน่วงเวลาแทนที่จะซ่อนอย่างเข้มงวดด้วย visibility: hidden เพื่อให้องค์ประกอบแสดงตัวเองหลังจากหมดเวลาแม้ว่าจะไม่เคยอัปเกรดก็ตาม

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

กำหนดขอบเขตของ Guard เหล่านี้ให้กับองค์ประกอบที่ต้องการ หรือกำหนดให้กับซับทรีที่เฉพาะเจาะจงผ่าน :has(:not(:defined)) แทนที่จะเป็น body ทั้งหมด วิดเจ็ตที่ไม่สำคัญและทำงานช้า (เช่น ปุ่มแชร์) ไม่ควรทำให้การแสดงผลทั้งหน้าล่าช้า

การแมปแอตทริบิวต์ ⇄ พร็อพเพอร์ตี้

ซิงค์แอตทริบิวต์ HTML และพร็อพเพอร์ตี้ JS เพื่อให้ใช้องค์ประกอบได้จากทั้งมาร์กอัปและสคริปต์ ค่าแอตทริบิวต์จะเป็นสตริงเสมอ ให้แปลงประเภท

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

แหล่งที่มาของมูลค่า

ตัวอย่างด้านบนจะเก็บ DOM ไว้เป็นแหล่งข้อมูลที่ถูกต้องเพียงแหล่งเดียว โดย Getter แต่ละรายการจะอ่าน getAttribute ซึ่งเป็นค่าเริ่มต้นที่เหมาะสมสำหรับแอตทริบิวต์ที่เรียบง่ายและคล้ายสตริงจำนวนหนึ่ง เนื่องจากมีเพียงที่เดียวที่สถานะจะอยู่ได้ จึงไม่สามารถยกเลิกการซิงค์ได้

สำหรับคอมโพเนนต์ที่สมบูรณ์ยิ่งขึ้น ให้เก็บแหล่งข้อมูลที่เชื่อถือได้ไว้ในช่อง JS และแสดงออกไปภายนอกเป็นแอตทริบิวต์เฉพาะเมื่อมีสิ่งอื่นที่ต้องเห็น เช่น การกำหนดเป้าหมาย CSS หรือผู้บริโภคที่อ่านข้อมูลนั้น การอ่านฟิลด์มีค่าใช้จ่ายน้อยกว่าการแยกวิเคราะห์และการบังคับแอตทริบิวต์ในทุกการเข้าถึง และค่าที่ไม่ใช่สตริงจะไม่ไปกลับผ่านการแปลงเป็นอนุกรม นี่คือเหตุผลที่ไลบรารีอย่าง Lit เก็บสถานะไว้ใน JS และแสดงผลแบบเลือกแทนที่จะถือว่าแอตทริบิวต์เป็นพื้นที่เก็บข้อมูล โดยจะแสดงเฉพาะสิ่งที่โลกภายนอกต้องสังเกต ไม่ใช่พร็อพเพอร์ตี้ทั้งหมด

ข้อควรระวัง:

  • observedAttributes discipline: แสดงเฉพาะแอตทริบิวต์ที่การเปลี่ยนแปลงต้องแสดงผลอีกครั้งหรือทริกเกอร์ผลข้างเคียง การสังเกตทุกอย่างจะเพิ่ม Callback ให้กับการเปลี่ยนแปลงทุกรายการโดยไม่มีประโยชน์
  • ลูปการสะท้อน: Setter ที่เขียนแอตทริบิวต์จะเข้าสู่ attributeChangedCallback อีกครั้ง ป้องกันด้วยการตรวจสอบ oldVal !== newVal ตามที่อธิบายไว้ข้างต้น
  • สถานะเฉพาะสไตล์: สำหรับสถานะภายในที่มีอยู่เพื่อขับเคลื่อนการจัดสไตล์เท่านั้น (กำลังโหลด ทำงานอยู่ ขยาย) ให้เลือกสถานะที่กำหนดเองแทนการแสดงแอตทริบิวต์สังเคราะห์ โปรดดูสถานะที่กำหนดเองด้านล่าง แสดงผลเป็นแอตทริบิวต์เฉพาะเมื่อผู้บริโภคต้องตั้งค่าสถานะจากมาร์กอัปได้

สถานะที่กำหนดเอง

สถานะที่กำหนดเองคือแฟล็กบูลีนที่องค์ประกอบแสดงเกี่ยวกับตัวมันเองเพื่อให้ CSS กำหนดเป้าหมาย โดยตั้งค่าผ่าน ElementInternals และจับคู่กับคลาสจำลอง :state() ใช้สำหรับสถานะ UI รันไทม์ภายใน เช่น busy, expanded, invalid แทนที่จะแสดงแอตทริบิวต์สังเคราะห์ เนื่องจากจะไม่ขัดแย้งกับแอตทริบิวต์ของผู้ใช้ ไม่ปรากฏใน DOM และเป็นแบบอ่านอย่างเดียวจากภายนอก (ผู้ใช้สามารถจัดรูปแบบ :state() ได้ แต่จะปลอมแปลงไม่ได้)

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 เป็นชุด ดังนั้นสถานะอิสระจะประกอบกัน (busy บวก error) โดยไม่ต้องสร้าง enum data-status แบบรวม

สถานะที่กำหนดเองจะใช้ได้เฉพาะในรันไทม์เท่านั้น ระบบไม่ได้จัดรูปแบบเป็นอนุกรมไปยัง DOM ดังนั้นจึงไม่คงอยู่หลังจากการโหลดซ้ำ การสร้างองค์ประกอบใหม่ หรือการแสดงผลฝั่งเซิร์ฟเวอร์ หากต้องตั้งค่าสถานะอย่างชัดเจนในมาร์กอัปหรือคงอยู่ตลอดการโหลด นั่นเป็นหน้าที่ของแอตทริบิวต์ (ดูตำแหน่งที่ค่าอยู่) โดยให้กู้คืนใน connectedCallback จากแหล่งข้อมูลที่เชื่อถือได้ หลักการทั่วไป: สถานะที่กำหนดเองสำหรับสิ่งที่คอมโพเนนต์ได้รับและเป็นเจ้าของในรันไทม์ และจำเป็นต้องจัดสไตล์เท่านั้น แอตทริบิวต์สำหรับสิ่งที่ต้องตั้งค่าจากมาร์กอัปหรือทำให้เป็นอนุกรม

ดูวิธีจัดรูปแบบสถานะเหล่านี้ทั้งภายในคอมโพเนนต์และจากสไตล์ชีตของผู้ใช้ได้ที่การจัดรูปแบบคอมโพเนนต์เว็บ

รูปแบบการตั้งชื่อ

  • สถานะส่วนตัวที่แท้จริง: ฟิลด์ # แบบเนทีฟ (#count) สำหรับการห่อหุ้มแบบเข้มงวด โดยใช้คำนำหน้า _ เฉพาะเมื่อคลาสย่อยยังคงต้องเข้าถึงฟิลด์ดังกล่าว โปรดทราบว่าระบบจะไม่รับค่าช่อง # โดยคลาสย่อยจะมองไม่เห็นช่องส่วนตัวของคลาสพื้นฐาน
  • พร็อพเพอร์ตี้ที่มีคีย์เป็นสัญลักษณ์เป็นตัวกลาง โดยซ่อนจากการแจงนับปกติ แต่เข้าถึงได้ด้วยโค้ดใดก็ตามที่มีสัญลักษณ์ (ส่งออกหรือแสดงเป็นฟิลด์ static ในคลาส) เนื่องจากสัญลักษณ์แต่ละตัวไม่ซ้ำกัน สัญลักษณ์ 2 ตัวที่ใช้คำอธิบายร่วมกันจึงไม่ขัดแย้งกัน ซึ่งมีประโยชน์เมื่อคลาสย่อยหรือคลาสพี่น้องต้องการการเข้าถึงที่มีการควบคุมซึ่งฟิลด์ # ไม่สามารถให้ได้
  • อย่าใช้แอตทริบิวต์ส่วนกลาง (style, class, id, slot, part, title, lang, dir) กับพร็อพเพอร์ตี้ของคุณเอง เนื่องจากจะทำให้ลักษณะการทำงานในตัวเสียหาย โปรดทราบว่า disabled จะเป็นแบบส่วนกลางเฉพาะในตัวควบคุมแบบฟอร์มเท่านั้น ส่วนในองค์ประกอบอื่นๆ คุณต้องใช้เอฟเฟกต์ด้วยตนเอง

เมื่อตั้งชื่อแพลตฟอร์ม API ใหม่ ให้ทำตามแบบแผนของแพลตฟอร์มเอง โดยใช้ Kebab Case ตัวพิมพ์เล็กที่มีขีดกลางอย่างน้อย 1 ขีดสำหรับชื่อแท็กองค์ประกอบที่กำหนดเอง (เช่น my-toggle) lowerCamelCase สำหรับพร็อพเพอร์ตี้และเมธอด และชื่อแอตทริบิวต์ตัวพิมพ์เล็กที่ซิงค์กับพร็อพเพอร์ตี้ที่เกี่ยวข้อง