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

การ Reflect Attribute

Custom element เปิดเผยสถานะของตัวเองในสองรูปแบบพร้อมกัน: ในรูปแบบ JavaScript property (เช่น element.label) และในรูปแบบ HTML attribute (เช่น label="Hello") การ reflect หมายความว่าทั้งสองรูปแบบนี้ซิงก์กันตลอดเวลา — เมื่อเขียนค่าในฝั่งใดฝั่งหนึ่ง อีกฝั่งก็จะอัปเดตตามโดยอัตโนมัติ

Element <input> ดั้งเดิมทำแบบนี้กับ value, disabled, checked และอื่น ๆ คุณสามารถมอบความสะดวกสบายแบบเดียวกันให้กับ element ของตัวเองได้

รูปแบบ reflection คือคู่ getter และ setter ใน JavaScript ที่ส่งต่อการทำงานไปยัง getAttribute และ setAttribute:

get label() {
return this.getAttribute('label') || '';
}
set label(v) {
this.setAttribute('label', v);
}

เมื่อโค้ดภายนอกอ่าน element.label getter จะคืนค่า attribute ปัจจุบัน เมื่อโค้ดกำหนด element.label = 'X' setter จะเรียก setAttribute ทำให้ HTML attribute ถูกอัปเดตทันที getAttribute('label') และ element.label จะตรงกันเสมอ

เพื่อให้ attributeChangedCallback ถูกเรียก — ไม่ว่า attribute จะถูกเปลี่ยนจาก HTML, DevTools หรือ JavaScript — ชื่อ attribute นั้นต้องอยู่ใน static getter ที่ชื่อ observedAttributes:

static get observedAttributes() {
return ['label', 'color'];
}

browser อ่านรายการนี้ครั้งเดียวเมื่อ class ถูกลงทะเบียน Attribute ที่ไม่อยู่ในรายการจะถูกละเว้นโดยไม่มีการแจ้งเตือน

เมื่อ attribute ที่ถูกเฝ้าดูถูกเพิ่ม เปลี่ยนแปลง หรือลบออก browser จะเรียก:

attributeChangedCallback(name, oldValue, newValue) {
// name — attribute ไหนที่เปลี่ยน เช่น 'label'
// oldValue — สตริงค่าก่อนหน้า หรือ null เมื่อกำหนดครั้งแรก
// newValue — สตริงค่าใหม่ หรือ null เมื่อถูกลบ
this._render();
}

เนื่องจาก setter เรียก setAttribute การกำหนด element.label = 'X' จึงเรียก attributeChangedCallback เช่นเดียวกับการเขียน element.setAttribute('label', 'X') โดยตรง callback นี้คือแหล่งข้อมูลเดียวที่เชื่อถือได้สำหรับการ re-render

class MyBadge extends HTMLElement {
static get observedAttributes() { return ['label', 'color']; }
get label() { return this.getAttribute('label') || ''; }
set label(v) { this.setAttribute('label', v); }
get color() { return this.getAttribute('color') || 'blue'; }
set color(v) { this.setAttribute('color', v); }
attributeChangedCallback(name, oldValue, newValue) {
this._render();
}
connectedCallback() { this._render(); }
_render() {
this.textContent = this.label;
this.style.color = this.color;
}
}
customElements.define('my-badge', MyBadge);

การ reflect attribute มอบข้อดีในทางปฏิบัติให้กับ element ของคุณหลายประการ:

  • มองเห็นใน DevTools — ค่า attribute ปรากฏโดยตรงในแผง Elements โดยไม่ต้องขยาย shadow DOM ใด ๆ
  • CSS attribute selector — คุณสามารถกำหนด style ให้ reflect-badge[color="#7c3aed"] หรือ reflect-badge[label] ได้โดยไม่ต้องใช้ class เพิ่มเติม
  • Server-side HTML — สถานะเริ่มต้นสามารถฝังไว้ใน markup ได้เลย ไม่จำเป็นต้องใช้ JavaScript เพื่อกำหนดค่าแรก
  • ความเข้ากันได้กับ Framework — Framework อย่าง Vue, Svelte และ Angular bind กับ attribute เป็นค่าเริ่มต้น Property ที่ reflect แล้วทำให้ <my-badge label={x}> ทำงานได้ทันที

หลังจาก 1.2 วินาที property label ของ element จะถูกกำหนดค่าจาก JavaScript บรรทัด #log ยืนยันว่า element.label และ getAttribute('label') คืนค่าสตริงเดียวกัน — หลักฐานว่า setter และ attribute ซิงก์กันอย่างสมบูรณ์แบบ

ตัวเลือกBenefitCost
Reflect property ไปยัง attributeสถานะมองเห็นได้ใน DevTools ใช้เป็น CSS attribute selector ได้ และรองรับ server-rendered HTML โดยไม่ต้องพึ่ง JavaScript ตอนโหลดหน้าแรกเสี่ยงเกิด infinite loop ถ้า setter กับ attributeChangedCallback เรียกกันไปมา และ attribute เก็บได้แค่ string เท่านั้น
ไม่ reflect (เก็บค่าไว้ใน private field เฉยๆ)หลีกเลี่ยงปัญหา infinite loop ได้ทั้งหมด และรองรับข้อมูลชนิดใดก็ได้ ไม่จำกัดแค่ stringสถานะไม่ปรากฏใน markup หรือ DevTools ใช้ CSS attribute selector เลือก element ตาม state ไม่ได้
  • ลืมประกาศชื่อ attribute ใน observedAttributes — ถ้าไม่อยู่ในรายการนี้ attributeChangedCallback จะไม่ถูกเรียกเลย ไม่ว่าจะเปลี่ยน attribute จาก HTML, DevTools หรือ JavaScript
  • ทำให้ setter เรียก setAttribute โดยไม่เช็คว่าค่าต่างจากเดิมก่อน — ทำให้เกิด infinite loop ระหว่าง setter กับ attributeChangedCallback ที่เรียก setter อีกที ต้องเช็ค if (this.getAttribute(name) !== v) ก่อนเขียนเสมอ
  • reflect property ที่เป็นข้อมูลซับซ้อน เช่น array หรือ object กลับไปเป็น attribute — attribute เป็น string เท่านั้น การพยายาม reflect object จะได้ [object Object] ที่ไร้ประโยชน์ ควร reflect เฉพาะ string, boolean หรือ number ธรรมดาเท่านั้น

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

Element <input> ดั้งเดิมของ browser reflect property อย่าง disabled และ checked ไปยัง attribute โดยตรง ทำให้ input:disabled ใช้เป็น CSS selector ได้ทันทีโดยไม่ต้องเขียน JavaScript เพิ่ม

GitHub’s Catalyst framework ใช้ decorator @attr เพื่อ generate getter/setter reflection pattern นี้อัตโนมัติ ลดโค้ด boilerplate ที่ต้องเขียนซ้ำในทุก custom element

setter `set label(v)` ทำหน้าที่อะไรในรูปแบบ reflection?
อะไรคือสิ่งที่จำเป็นเพื่อให้ `attributeChangedCallback` ถูกเรียก?
CSS selector ใดที่ใช้ได้เมื่อ attribute ถูก reflect แล้ว?