การประกอบ Component
การซ้อน custom element
หัวข้อที่มีชื่อว่า “การซ้อน custom element”รูปแบบที่ง่ายที่สุดของ composition คือการซ้อน (nesting) นั่นคือ custom element ชั้นนอกสร้าง custom element ชั้นในหนึ่งตัวหรือมากกว่าไว้ภายใน connectedCallback ให้นิยาม element ชั้นในก่อน เพื่อให้ registry รู้จัก element ชั้นในก่อนที่ element ชั้นนอกจะเรียกใช้
// Inner element — defined firstclass InnerWidget extends HTMLElement { connectedCallback() { this.textContent = 'I am the inner widget'; }}customElements.define('inner-widget', InnerWidget);
// Outer element — creates the inner one programmaticallyclass 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>การสื่อสารผ่าน attribute
หัวข้อที่มีชื่อว่า “การสื่อสารผ่าน attribute”วิธีที่ตรงไปตรงมาที่สุดในการที่ element แม่ส่งข้อมูลให้ลูกคือผ่าน HTML attribute element แม่เรียก setAttribute บน element ลูกที่สร้างขึ้น ส่วน element ลูกอ่านค่านั้นด้วย getAttribute ภายใน connectedCallback ของตัวเอง
// Parent sets the child's attributevar child = document.createElement('name-tag');child.setAttribute('name', this.getAttribute('name') || 'friend');this.appendChild(child);
// Child reads itNameTag.prototype.connectedCallback = function() { var name = this.getAttribute('name') || 'stranger'; this.textContent = name;};attribute เป็น string เสมอ ดังนั้นจึงต้องแปลงเป็นชนิดข้อมูลที่ต้องการ (เช่น parseInt สำหรับตัวเลข)
การสื่อสารผ่าน event
หัวข้อที่มีชื่อว่า “การสื่อสารผ่าน event”เมื่อ element ลูกต้องการบอกแม่ว่ามีบางอย่างเกิดขึ้น เช่น ปุ่มถูกคลิก หรือมีค่าถูกเลือก จะส่ง (dispatch) CustomEvent ออกไป ส่วน element แม่จะคอยฟัง (listen) event นั้นภายใน connectedCallback ของตัวเอง
มีสองออปชันบน constructor ของ event ที่สำคัญในที่นี้:
bubbles: true— event จะเดินทางขึ้นไปบน DOM tree ผ่าน element ลูกcomposed: true— event จะข้ามขอบเขตของ shadow DOM ได้
// Child dispatches an eventthis.addEventListener('click', function() { self.dispatchEvent(new CustomEvent('item-selected', { bubbles: true, composed: true, detail: { value: self.getAttribute('value') } }));});
// Parent listens in connectedCallbackOuterWidget.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 สำหรับแสดงผลด้วยคะแนนที่เลือก
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| สื่อสารผ่าน attribute (parent ตั้งค่า attribute ให้ child) | เขียนง่าย ตรวจสอบค่าได้ตรง ๆ ผ่าน DOM inspector | attribute เป็น 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 เอง