Category: reference
Web Components & Custom Elements
สร้าง reusable components ด้วย browser APIs โดยตรง: Custom Elements, Shadow DOM, HTML Templates — ไม่ต้องการ framework
สารบัญ
Web Components ประกอบด้วย 3 ส่วน
| API | หน้าที่ |
|---|---|
| Custom Elements | สร้าง HTML element ใหม่ เช่น <my-card> |
| Shadow DOM | encapsulate HTML + CSS ไม่ให้ leak ออกหรือเข้ามา |
| HTML Templates | <template> และ <slot> สำหรับ markup ที่ reuse ได้ |
Custom Element พื้นฐาน
class MyButton extends HTMLElement {
// browser เรียก ตอน element ถูกเพิ่มใน DOM
connectedCallback() {
this.innerHTML = `<button>${this.getAttribute('label') ?? 'Click'}</button>`;
this.querySelector('button').addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('my-click', { bubbles: true }));
});
}
// browser เรียก ตอน element ถูกลบออกจาก DOM
disconnectedCallback() {
// cleanup event listeners ที่ผูกกับ external elements
}
// browser เรียก ตอน attribute เปลี่ยน (ต้องระบุใน observedAttributes)
attributeChangedCallback(name, oldVal, newVal) {
if (name === 'label') {
const btn = this.querySelector('button');
if (btn) btn.textContent = newVal;
}
}
// ต้องระบุ attributes ที่ต้องการ observe
static get observedAttributes() {
return ['label', 'disabled'];
}
}
// ลงทะเบียน — ชื่อต้องมีเครื่องหมาย - (hyphen required)
customElements.define('my-button', MyButton);
<!-- ใช้งาน -->
<my-button label="Save"></my-button>
<my-button label="Cancel" disabled></my-button>
<script>
document.querySelector('my-button').addEventListener('my-click', (e) => {
console.log('clicked!');
});
</script>
Shadow DOM — CSS Encapsulation
class MyCard extends HTMLElement {
constructor() {
super();
// attachShadow ต้องเรียกใน constructor
this._shadow = this.attachShadow({ mode: 'open' });
// mode: 'open' → เข้าถึงได้จาก element.shadowRoot
// mode: 'closed' → ซ่อน shadow DOM จาก outside
}
connectedCallback() {
this._shadow.innerHTML = `
<style>
/* CSS นี้ไม่ leak ออกไปข้างนอก */
:host {
display: block;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 1rem;
}
:host([highlighted]) {
border-color: #2563eb;
}
h3 { color: #0f172a; margin: 0 0 0.5rem; }
</style>
<h3>${this.getAttribute('title') ?? ''}</h3>
<slot></slot> <!-- content จาก light DOM ถูก inject ที่นี่ -->
`;
}
}
customElements.define('my-card', MyCard);
<my-card title="Hello" highlighted>
<p>Content จาก light DOM — ถูก inject เข้า <slot></slot></p>
</my-card>
HTML Template
<!-- ประกาศ template — ไม่ render จนกว่าจะ clone -->
<template id="card-template">
<style>
.card { padding: 1rem; border: 1px solid #e2e8f0; border-radius: 8px; }
.card-title { font-weight: 700; }
</style>
<div class="card">
<h3 class="card-title"></h3>
<slot name="body"></slot>
<slot name="footer"></slot>
</div>
</template>
class TemplateCard extends HTMLElement {
connectedCallback() {
const template = document.getElementById('card-template');
const clone = template.content.cloneNode(true);
clone.querySelector('.card-title').textContent = this.getAttribute('title') ?? '';
this.attachShadow({ mode: 'open' }).appendChild(clone);
}
}
customElements.define('template-card', TemplateCard);
<template-card title="Project Update">
<p slot="body">เนื้อหาหลัก</p>
<a slot="footer" href="/more">อ่านเพิ่ม</a>
</template-card>
Named Slots
<template id="article-card">
<style>
:host { display: grid; gap: 0.5rem; }
.meta { font-size: 0.8rem; color: #64748b; }
</style>
<slot name="meta" class="meta"></slot>
<slot name="title"></slot>
<slot name="body"></slot>
<slot></slot> <!-- default slot สำหรับ content ที่ไม่ระบุ slot name -->
</template>
<article-card>
<span slot="meta">มิถุนายน 2026 · 5 นาที</span>
<h2 slot="title">ชื่อบทความ</h2>
<p slot="body">สรุปสั้นๆ</p>
</article-card>
Life Cycle และ Properties
class MyInput extends HTMLElement {
// JavaScript property ← → HTML attribute sync
get value() {
return this._input?.value ?? '';
}
set value(val) {
if (this._input) this._input.value = val;
}
connectedCallback() {
this._shadow = this.attachShadow({ mode: 'open' });
this._shadow.innerHTML = `
<input type="text" value="${this.getAttribute('value') ?? ''}" />
`;
this._input = this._shadow.querySelector('input');
this._input.addEventListener('input', () => {
this.dispatchEvent(
new CustomEvent('change', { detail: { value: this._input.value }, bubbles: true })
);
});
}
attributeChangedCallback(name, _, newVal) {
if (name === 'value' && this._input) this._input.value = newVal;
}
static get observedAttributes() { return ['value', 'placeholder']; }
}
customElements.define('my-input', MyInput);
// ใช้งานด้วย property แทน attribute
const input = document.querySelector('my-input');
input.value = 'Hello'; // ← JavaScript property
console.log(input.value);
Web Components กับ Framework อื่น
Web Components ทำงานใน HTML ธรรมดา — ใช้ร่วมกับ React, Vue, Astro, Svelte ได้
---
// Astro — import เพื่อ register
import '../components/my-button.js';
---
<my-button label="Save">Save</my-button>
เมื่อไรควรใช้ Web Components
ใช้เมื่อ:
- ต้องการ component ที่ทำงานในทุก framework (design system ที่แชร์ได้)
- ต้องการ style encapsulation จริงๆ ที่ CSS จาก parent เข้ามาไม่ได้
- ทำ micro-frontend ที่แต่ละ team ใช้ tech stack ต่างกัน
ไม่จำเป็นต้องใช้เมื่อ:
- ทำงานใน single framework — ใช้ component ของ framework ตรงๆ ดีกว่า
- ต้องการ server-side rendering — Shadow DOM ไม่รองรับ SSR ดีนัก (กำลังพัฒนา)