Composed Events
Shadow DOM สร้างขอบเขตของ Event
หัวข้อที่มีชื่อว่า “Shadow DOM สร้างขอบเขตของ Event”Shadow root ทุกตัวเป็นขอบเขตของ event โดย event ที่เกิดขึ้นภายใน shadow root จะ ไม่ bubble ผ่าน shadow host โดยค่าเริ่มต้น — จะถูกกักไว้ภายใน shadow tree นี่คือส่วนหนึ่งของการรับประกัน encapsulation: โลกภายนอกไม่สามารถมองเห็นสิ่งที่เกิดขึ้นภายใน component ของคุณได้ เว้นแต่คุณจะอนุญาตอย่างชัดเจน
เพื่อให้ event ข้ามขอบเขตนั้นได้ คุณต้องตั้งค่า composed: true ตอน dispatch event นั้น
Flag composed
หัวข้อที่มีชื่อว่า “Flag composed”ไม่มี composed — event หยุดที่ shadow root
หัวข้อที่มีชื่อว่า “ไม่มี composed — event หยุดที่ shadow root”// ภายใน shadow root — event นี้จะไม่ถึง documentthis._shadow.querySelector('button').addEventListener('click', () => { this.dispatchEvent(new CustomEvent('shadow-click', { bubbles: true, // composed เป็น false โดยค่าเริ่มต้น — event หยุดที่ shadow boundary }));});แม้ว่าจะตั้งค่า bubbles: true ไว้ แต่ event จะไม่ออกจาก shadow tree ผู้รับฟัง (listener) บน document หรือ ancestor ใน light DOM จะไม่มีทางเห็น event นั้นเลย
มี composed: true — event ข้าม shadow boundary
หัวข้อที่มีชื่อว่า “มี composed: true — event ข้าม shadow boundary”// Composed — event นี้จะข้าม shadow boundarythis._shadow.querySelector('button').addEventListener('click', () => { this.dispatchEvent(new CustomEvent('shadow-click', { bubbles: true, composed: true, // ← อนุญาตให้ข้าม shadow boundary }));});ตอนนี้ event จะ bubble ตามปกติผ่าน ancestor ทุกตัวใน light DOM จนถึง window
event.composedPath()
หัวข้อที่มีชื่อว่า “event.composedPath()”composedPath() คืนค่า array เต็มของ node ที่ event เดินทางผ่าน รวมถึง node ภายใน shadow root ด้วย สิ่งนี้มีประโยชน์เฉพาะขณะที่ event กำลัง dispatch อยู่เท่านั้น — เมื่อ event handler return แล้ว array จะถูกล้าง
// อ่าน composedPathdocument.addEventListener('shadow-click', (e) => { console.log(e.composedPath()); // รวม shadow nodes ด้วย console.log(e.target); // retarget ไปยัง host element จากภายนอก});เมื่อคุณอ่าน e.target จากภายนอก shadow tree คุณจะเห็น shadow host ไม่ใช่ element ต้นกำเนิดที่ยิง event นี้เรียกว่า target retargeting — ซึ่งซ่อนโครงสร้างภายในของ component จากผู้รับฟังภายนอก
Target Retargeting
หัวข้อที่มีชื่อว่า “Target Retargeting”Target retargeting เป็นกลไกความเป็นส่วนตัวที่ออกแบบมาโดยตั้งใจ ลองนึกภาพ component ที่ซับซ้อนซึ่งมีปุ่มภายในหลายปุ่ม โค้ดภายนอกไม่ควรสามารถบอกได้ว่า node ภายในใดถูกคลิก — แค่รู้ว่า component นั้นถูกโต้ตอบเท่านั้น browser จะเขียน event.target ใหม่โดยอัตโนมัติให้ชี้ไปที่ host element เมื่อ event ถูกสังเกตจากภายนอก shadow tree
ภายใน shadow tree target ต้นฉบับยังคงมองเห็นได้ แต่จากภายนอก event.target จะชี้ไปที่ shadow host เสมอ
เมื่อไหรควรใช้ composed: true
หัวข้อที่มีชื่อว่า “เมื่อไหรควรใช้ composed: true”กฎง่ายๆ: ใช้ composed: true สำหรับ event ที่แสดงถึง action ที่ผู้ใช้มองเห็น ซึ่ง component ของคุณเผยแพร่เป็นส่วนหนึ่งของ public API (เช่น value-change, submit, close) รักษา event ภายในระดับล่างให้ไม่มี composed เพื่อให้ event เหล่านั้นยังคงเป็นเรื่องภายในของ component
ตัวอย่างสด
หัวข้อที่มีชื่อว่า “ตัวอย่างสด”ปุ่มสีม่วง dispatch event composed-click ด้วย composed: true listener ของ document จะยิงและยืนยันว่า event มาถึงแล้ว โดยแสดงว่า event.target ถูก retarget ไปที่ host element <composed-demo>
ปุ่มสีเทา dispatch event not-composed-click โดยไม่มี composed: true listener ของ document สำหรับชื่อ event นั้นถูกลงทะเบียนไว้ — แต่ไม่เคยยิง ข้อความที่คุณเห็นถูกตั้งค่าโดยตรงบน listener ของ host element เอง ซึ่งพิสูจน์ว่า event ถูกหยุดที่ shadow boundary
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
composed: true | event ทะลุออกจาก shadow boundary ทำให้โค้ดภายนอก เช่น analytics หรือ parent app ฟัง event ได้โดยไม่ต้องรู้โครงสร้างภายในของ component | เผย public event surface มากขึ้น เปลี่ยนชื่อหรือจังหวะของ event ทีหลังจะกระทบ consumer ภายนอกที่ผูก listener ไว้แล้ว |
ไม่ตั้ง composed (ปล่อยเป็นค่าเริ่มต้น false) | event ยังคงเป็นรายละเอียดภายในของ component เปลี่ยนแปลงหรือลบได้อย่างอิสระโดยไม่กระทบใคร | โค้ดภายนอก รวมถึง test ที่ query จาก document ไม่มีทางฟัง event นั้นได้เลย แม้จะตั้ง bubbles: true ไว้ก็ตาม |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- ตั้ง
bubbles: trueแต่ลืมcomposed: true— คิดว่า event จะออกจาก shadow root ได้เพราะตั้ง bubble แล้ว แต่จริงๆ ยังหยุดที่ shadow boundary เหมือนเดิม ต้องตั้งทั้งสอง flag พร้อมกันถ้าต้องการให้ listener ภายนอก shadow tree ฟังได้ - ตั้ง
composed: trueให้ event ภายในทุกตัวโดยไม่เลือก — ทำให้รายละเอียดการทำงานภายใน component รั่วไหลออกสู่ภายนอกโดยไม่จำเป็น ควรเลือกเฉพาะ event ที่เป็นส่วนหนึ่งของ public API เท่านั้น - เก็บผลลัพธ์จาก
composedPath()ไว้ใช้ทีหลัง —composedPath()คืนค่าที่ถูกต้องเฉพาะขณะ event handler กำลังทำงานอยู่เท่านั้น ถ้าเก็บ reference ไว้ใช้ใน callback แบบ async ในภายหลัง จะได้ array ว่างเปล่า
💡 ตัวอย่างจากของจริง
Shoelace / Web Awesome design system — component อย่าง
sl-dialogdispatch event เช่นsl-show,sl-hideด้วยcomposed: trueเสมอ เพื่อให้ framework ภายนอกอย่าง React หรือ Vue ผูก event handler ได้ตามปกติโดยไม่ต้องเจาะเข้าไปใน shadow treeYouTube web components — ปุ่ม like และ subscribe ที่อยู่ภายใน shadow DOM dispatch custom event ด้วย
composed: trueเพื่อให้ analytics script ระดับหน้าเว็บดักฟังการโต้ตอบของผู้ใช้ได้