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

::part และ exportparts

Shadow DOM ปิดกั้นไม่ให้ CSS ภายนอก select เข้าไปจัดสไตล์ internals ได้ตามปกติ นั่นดีต่อ encapsulation แต่บางครั้งผู้เขียน component ก็อยากเปิดจุดให้ผู้ใช้ theme ได้แบบควบคุมได้ — part attribute คือกลไกที่ให้ทำสิ่งนั้น

attribute part="name" ทำเครื่องหมาย element ภายใน shadow root ให้เป็น styling hook ที่ตั้งชื่อไว้ จากนั้น CSS ของหน้าจะกำหนดเป้าหมายไปที่ element นั้นด้วย pseudo-element ::part(name) บน host ได้ ทำให้ข้ามขอบเขต shadow boundary ได้โดยไม่สูญเสียการ encapsulation ของส่วนอื่น ๆ ผู้เขียน component เป็นผู้ควบคุมว่า internals ใดบ้างที่จะถูกเปิดเผย

<theme-card></theme-card>

CSS ของหน้าใช้ ::part() เพื่อจัดสไตล์ internals ที่ถูกเปิดเผย:

theme-card::part(title) { color: #7c3aed; font-size: 1.2rem; font-weight: bold; }
theme-card::part(body) { color: #374151; font-size: 0.95rem; margin-top: 0.5rem; }

shadow stylesheet ของ component เอง ไม่ควร มีสไตล์ part เหล่านี้ — ให้กำหนดเพียง rule เชิงโครงสร้างหรือเลย์เอาต์เริ่มต้นเท่านั้น rule ::part() อยู่ใน CSS ของหน้าทั้งหมด (หรือใน prop css ของ LivePreview) และ rule เหล่านั้นชนะเพราะเป็นการ opt-in อย่างชัดเจน

เมื่อ component ประกอบ custom element อื่นไว้ภายใน parts ของ children ใน shadow DOM ของตัวเองจะไม่ปรากฏให้หน้าเห็นโดยอัตโนมัติ attribute exportparts ส่งออก inner parts ใหม่ภายใต้ชื่อใหม่:

<outer-card exportparts="label: card-label"></outer-card>

ตอนนี้ CSS ของหน้าสามารถเขียน outer-card::part(card-label) เพื่อเข้าถึง part label ของ component ภายในได้โดยไม่จำเป็นต้องรู้ชื่อ tag ของ element ภายใน

prop css ด้านล่างถูก inject เป็นบล็อก <style> ในหน้า ภายนอก shadow root ของ component — ตรงกับที่ rule ::part() ควรอยู่

ตัวเลือกBenefitCost
part + ::part()เป็น styling API แบบ opt-in ที่ชัดเจน — เฉพาะ element ที่ทำเครื่องหมายไว้เท่านั้นที่จัดสไตล์จากภายนอกได้ รักษา encapsulation ของส่วนที่เหลือชื่อ part กลายเป็น public contract ที่ต้อง maintain เหมือน API — เปลี่ยนชื่อหรือลบ part ทีหลังคือ breaking change สำหรับผู้ใช้ที่พึ่งพา ::part() อยู่
exportpartsส่งออก part ของ component ที่ถูกประกอบซ้อนกันได้ โดยไม่ต้อง expose shadow tree ทั้งหมดของ component ภายในเพิ่มชั้นของ mapping ที่ต้อง sync ด้วยมือ หาก inner component เปลี่ยนชื่อ part เดิม exportparts ของ outer component จะพังตาม
  • มองว่า part เป็นแค่รายละเอียดภายใน — เปลี่ยนชื่อหรือลบ part="name" โดยไม่คิดว่าเป็น public API เวอร์ชันหนึ่งที่ CSS ภายนอกพึ่งพาอยู่ ทำให้ ::part() ของผู้ใช้พังแบบไม่มีการแจ้งเตือนใด ๆ
  • ลืมใช้ exportparts เมื่อ compose component — part ของ custom element ที่ซ้อนอยู่ภายในจะไม่ถูกส่งออกให้ host ภายนอกเห็นโดยอัตโนมัติ ต้องประกาศ exportparts="inner: outer-name" อย่างชัดเจนเสมอ
  • ใส่ part attribute ผิดตำแหน่งpart ต้องอยู่บน element ภายใน shadow root เท่านั้น จะใช้ ::part() อ้างถึงตัว host เองไม่ได้ ให้ใช้ :host / :host() แทนสำหรับสไตล์ของ host

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

Adobe Spectrum (Spectrum Web Components) — ใช้ ::part อย่างกว้างขวางเป็น styling API สำหรับ theme internals ของทุก component พร้อมเอกสารกำกับชื่อ part แต่ละตัวไว้อย่างเป็นทางการ เหมือน public API จริง ๆ

Shoelace — เอกสาร part names ทั้งหมดของแต่ละ component ไว้ในหน้า docs และถือว่าการเปลี่ยนชื่อ part ในเวอร์ชันถัดไปคือ breaking change

attribute part='name' วางไว้ที่ไหน?
selector ::part(name) เขียนไว้ที่ไหน?
exportparts='label: card-label' ทำอะไร?