ข้ามไปยังเนื้อหา

การประกอบ Component

รูปแบบที่ง่ายที่สุดของ composition คือการซ้อน (nesting) นั่นคือ custom element ชั้นนอกสร้าง custom element ชั้นในหนึ่งตัวหรือมากกว่าไว้ภายใน connectedCallback ให้นิยาม element ชั้นในก่อน เพื่อให้ registry รู้จัก element ชั้นในก่อนที่ element ชั้นนอกจะเรียกใช้

// Inner element — defined first
class InnerWidget extends HTMLElement {
connectedCallback() {
this.textContent = 'I am the inner widget';
}
}
customElements.define('inner-widget', InnerWidget);
// Outer element — creates the inner one programmatically
class OuterWidget extends HTMLElement {
connectedCallback() {
var inner = document.createElement('inner-widget');
this.appendChild(inner);
}
}
customElements.define('outer-widget', OuterWidget);

วาง element ชั้นนอกไว้ใน HTML แล้ว browser จะจัดการส่วนที่เหลือให้เอง:

<outer-widget></outer-widget>

วิธีที่ตรงไปตรงมาที่สุดในการที่ element แม่ส่งข้อมูลให้ลูกคือผ่าน HTML attribute element แม่เรียก setAttribute บน element ลูกที่สร้างขึ้น ส่วน element ลูกอ่านค่านั้นด้วย getAttribute ภายใน connectedCallback ของตัวเอง

// Parent sets the child's attribute
var child = document.createElement('name-tag');
child.setAttribute('name', this.getAttribute('name') || 'friend');
this.appendChild(child);
// Child reads it
NameTag.prototype.connectedCallback = function() {
var name = this.getAttribute('name') || 'stranger';
this.textContent = name;
};

attribute เป็น string เสมอ ดังนั้นจึงต้องแปลงเป็นชนิดข้อมูลที่ต้องการ (เช่น parseInt สำหรับตัวเลข)

เมื่อ element ลูกต้องการบอกแม่ว่ามีบางอย่างเกิดขึ้น เช่น ปุ่มถูกคลิก หรือมีค่าถูกเลือก จะส่ง (dispatch) CustomEvent ออกไป ส่วน element แม่จะคอยฟัง (listen) event นั้นภายใน connectedCallback ของตัวเอง

มีสองออปชันบน constructor ของ event ที่สำคัญในที่นี้:

  • bubbles: true — event จะเดินทางขึ้นไปบน DOM tree ผ่าน element ลูก
  • composed: true — event จะข้ามขอบเขตของ shadow DOM ได้
// Child dispatches an event
this.addEventListener('click', function() {
self.dispatchEvent(new CustomEvent('item-selected', {
bubbles: true,
composed: true,
detail: { value: self.getAttribute('value') }
}));
});
// Parent listens in connectedCallback
OuterWidget.prototype.connectedCallback = function() {
var self = this;
// ... build children ...
self.addEventListener('item-selected', function(e) {
console.log('selected:', e.detail.value);
});
};

เพราะตั้งค่า bubbles: true ไว้ element แม่จึงสามารถผูก listener เพียงตัวเดียวบนตัวเอง และรับ event จาก element ลูกตัวใดก็ได้ที่ซ้อนอยู่ภายในโดยไม่ต้องอ้างอิงโดยตรง

เดโมด้านล่างประกอบ <star-rating> ขึ้นจาก <rating-star> ห้าตัว แต่ละดาวจะคอยฟังการคลิกและส่ง star-select CustomEvent ส่วน <star-rating> ชั้นนอกจะคอยฟัง event เหล่านั้นและอัปเดต span สำหรับแสดงผลด้วยคะแนนที่เลือก

ตัวเลือกBenefitCost
สื่อสารผ่าน attribute (parent ตั้งค่า attribute ให้ child)เขียนง่าย ตรวจสอบค่าได้ตรง ๆ ผ่าน DOM inspectorattribute เป็น string ล้วน ต้อง serialize/deserialize เองเมื่อข้อมูลซับซ้อนขึ้น
สื่อสารผ่าน custom event (child dispatch event ขึ้นไปหา parent)ส่งข้อมูลชนิดใดก็ได้ผ่าน detail และไม่ผูก parent เข้ากับ implementation ภายในของ childต้องตั้งค่า bubbles/composed ให้ถูกต้อง ไม่งั้น event จะไม่ไปถึงที่ที่ควรไปถึง
  • ลืมตั้ง composed: true — event ที่ dispatch จากภายใน shadow DOM แต่ไม่ตั้ง composed จะไม่ข้ามออกไปนอก shadow boundary ทำให้ parent ที่ listen อยู่ใน light DOM ไม่เคยได้รับ event เลย
  • ส่งข้อมูลซับซ้อนผ่าน attribute แทน property หรือ event — attribute เก็บได้แค่ string ทำให้ต้อง JSON.stringify/parse ไปมาโดยไม่จำเป็น ทั้งที่ควรส่งผ่าน property หรือ event.detail แทน
  • Parent เข้าถึง internals ของ child ผ่าน shadowRoot โดยตรง — ทำลาย encapsulation และผูก parent เข้ากับ implementation ภายในของ child เมื่อโครงสร้างภายในของ child เปลี่ยน parent จะพังตาม

💡 ตัวอย่างจากของจริง

GitHub — Catalyst elements ใช้ custom event เป็นช่องทางหลักในการสื่อสารระหว่าง component เพื่อคง encapsulation ไว้ ไม่ให้ parent เข้าไปยุ่งกับ internals ของ child โดยตรง

Ionic Framework — component ที่ซ้อนกัน (เช่น <ion-select> กับ <ion-select-option>) dispatch custom event ขึ้นไปหา parent เพื่อรายงานการเปลี่ยนแปลง state แทนที่จะให้ parent poll ค่าจาก child เอง

property ใดบนออปชันของ CustomEvent ที่ทำให้ event ข้ามขอบเขตของ shadow DOM ได้?
ในเดโม star-rating element ใดเป็นตัวส่ง event star-select?
element แม่ใช้ method ใดในการ subscribe event ของลูกภายใน connectedCallback?