Web Components คืออะไร
Web Components เป็นชุดมาตรฐานของ Web Browser ที่ให้สร้าง Reusable Custom HTML Elements ได้ ประกอบด้วย 4 เทคโนโลยีหลัก คือ Custom Elements (สร้าง HTML Tag ใหม่), Shadow DOM (แยก Style และ DOM), HTML Templates (Template ที่ไม่ Render จนกว่าจะใช้) และ Slots (ช่องรับ Content จากภายนอก)
สำหรับ SaaS Architecture Web Components เหมาะอย่างยิ่งเพราะ SaaS มักมีหลาย Product, Micro-frontends หรือ Embeddable Widgets ที่ต้องทำงานข้าม Framework Web Components เป็น Framework-agnostic ใช้ได้ทุกที่ที่มี Browser
สร้าง Web Components พื้นฐาน
// === Custom Element พื้นฐาน ===
// saas-pricing-card.js
class SaaSPricingCard extends HTMLElement {
// Observed Attributes — React เมื่อ Attribute เปลี่ยน
static get observedAttributes() {
return ['plan', 'price', 'currency', 'period', 'featured'];
}
constructor() {
super();
// สร้าง Shadow DOM
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
// เรียกเมื่อ Element ถูกเพิ่มใน DOM
this.render();
}
attributeChangedCallback(name, oldValue, newValue) {
// เรียกเมื่อ Observed Attribute เปลี่ยน
if (oldValue !== newValue) {
this.render();
}
}
get plan() { return this.getAttribute('plan') || 'Basic'; }
get price() { return this.getAttribute('price') || '0'; }
get currency() { return this.getAttribute('currency') || '$'; }
get period() { return this.getAttribute('period') || '/mo'; }
get featured() { return this.hasAttribute('featured'); }
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
font-family: system-ui, -apple-system, sans-serif;
}
.card {
border: 1px solid ;
border-radius: 12px;
padding: 2rem;
text-align: center;
background: ;
color: ;
transition: transform 0.2s, box-shadow 0.2s;
position: relative;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0,0,0,0.1);
}
.plan-name {
font-size: 1.25rem;
font-weight: 600;
margin: 0 0 1rem;
}
.price {
font-size: 3rem;
font-weight: 800;
line-height: 1;
}
.currency { font-size: 1.5rem; vertical-align: super; }
.period {
font-size: 0.875rem;
opacity: 0.7;
margin-top: 0.25rem;
}
.features {
list-style: none;
padding: 0;
margin: 1.5rem 0;
text-align: left;
}
.features ::slotted(li) {
padding: 0.5rem 0;
border-bottom: 1px solid ;
}
.cta {
display: inline-block;
padding: 0.75rem 2rem;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
border: none;
font-size: 1rem;
background: ;
color: ;
transition: opacity 0.2s;
}
.cta:hover { opacity: 0.9; }
.badge {
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
background: #f59e0b;
color: #fff;
padding: 4px 16px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
display: ;
}
</style>
<div class="card">
<div class="badge">Most Popular</div>
<h3 class="plan-name"></h3>
<div class="price">
<span class="currency"></span>
</div>
<div class="period"></div>
<ul class="features">
<slot name="features"></slot>
</ul>
<button class="cta">
<slot name="cta">Get Started</slot>
</button>
</div>
`;
// Event Handling
this.shadowRoot.querySelector('.cta').addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('plan-selected', {
bubbles: true,
composed: true, // ข้าม Shadow DOM boundary
detail: { plan: this.plan, price: this.price },
}));
});
}
}
// Register Custom Element
customElements.define('saas-pricing-card', SaaSPricingCard);
// === ใช้งานใน HTML ===
// <saas-pricing-card plan="Pro" price="29" featured>
// <li slot="features">Unlimited Projects</li>
// <li slot="features">Priority Support</li>
// <li slot="features">API Access</li>
// <span slot="cta">Start Free Trial</span>
// </saas-pricing-card>
Design System ด้วย Web Components สำหรับ SaaS
// === Design System — Base Component Class ===
// lib/base-element.js
export class BaseElement extends HTMLElement {
static styles = ''; // Override ใน Subclass
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._state = {};
}
// Reactive State
setState(newState) {
this._state = { ...this._state, ...newState };
this.render();
}
// Theme Support
get theme() {
return {
primary: getComputedStyle(this).getPropertyValue('--ds-primary')
|| '#6366f1',
background: getComputedStyle(this).getPropertyValue('--ds-bg')
|| '#ffffff',
text: getComputedStyle(this).getPropertyValue('--ds-text')
|| '#1f2937',
border: getComputedStyle(this).getPropertyValue('--ds-border')
|| '#e5e7eb',
radius: getComputedStyle(this).getPropertyValue('--ds-radius')
|| '8px',
font: getComputedStyle(this).getPropertyValue('--ds-font')
|| 'system-ui, sans-serif',
};
}
connectedCallback() {
this.render();
}
render() {
// Override ใน Subclass
}
}
// === Design Token CSS Variables ===
// ตั้ง Theme ที่ Root Level
// :root {
// --ds-primary: #6366f1;
// --ds-bg: #ffffff;
// --ds-text: #1f2937;
// --ds-border: #e5e7eb;
// --ds-radius: 8px;
// --ds-font: 'Inter', system-ui, sans-serif;
// }
//
// /* Dark Theme */
// [data-theme="dark"] {
// --ds-primary: #818cf8;
// --ds-bg: #111827;
// --ds-text: #f9fafb;
// --ds-border: #374151;
// }
// === Button Component ===
// components/ds-button.js
import { BaseElement } from '../lib/base-element.js';
class DSButton extends BaseElement {
static get observedAttributes() {
return ['variant', 'size', 'disabled', 'loading'];
}
attributeChangedCallback() { this.render(); }
get variant() { return this.getAttribute('variant') || 'primary'; }
get size() { return this.getAttribute('size') || 'md'; }
get isLoading() { return this.hasAttribute('loading'); }
render() {
const t = this.theme;
const sizes = { sm: '8px 16px', md: '10px 20px', lg: '14px 28px' };
const fontSizes = { sm: '0.875rem', md: '1rem', lg: '1.125rem' };
this.shadowRoot.innerHTML = `
<style>
:host { display: inline-block; }
button {
font-family: ;
padding: ;
font-size: ;
border-radius: ;
border: 1px solid transparent;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 8px;
}
button[disabled] { opacity: 0.5; cursor: not-allowed; }
.primary {
background: ;
color: white;
}
.outline {
background: transparent;
color: ;
border-color: ;
}
.ghost {
background: transparent;
color: ;
}
.spinner {
width: 16px; height: 16px;
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
<button class=""
>
<slot></slot>
</button>
`;
}
}
customElements.define('ds-button', DSButton);
Embeddable Widget สำหรับ SaaS
// === Embeddable Chat Widget สำหรับ SaaS ===
// widget/saas-chat-widget.js
// ลูกค้าฝังบนเว็บด้วย <script> tag เดียว
class SaaSChatWidget extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._isOpen = false;
this._messages = [];
}
static get observedAttributes() {
return ['api-key', 'position', 'theme-color'];
}
get apiKey() { return this.getAttribute('api-key') || ''; }
get position() { return this.getAttribute('position') || 'bottom-right'; }
get themeColor() { return this.getAttribute('theme-color') || '#6366f1'; }
connectedCallback() {
this.render();
this.setupWebSocket();
}
disconnectedCallback() {
if (this._ws) this._ws.close();
}
setupWebSocket() {
this._ws = new WebSocket(
`wss://chat.saas-app.com/ws?key=`
);
this._ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
this._messages.push(msg);
this.updateMessages();
};
}
toggleChat() {
this._isOpen = !this._isOpen;
this.render();
}
sendMessage(text) {
if (!text.trim()) return;
this._messages.push({ role: 'user', text, time: new Date() });
this._ws.send(JSON.stringify({ text }));
this.updateMessages();
}
render() {
const pos = this.position === 'bottom-left'
? 'left: 20px' : 'right: 20px';
this.shadowRoot.innerHTML = `
<style>
:host { position: fixed; bottom: 20px; ; z-index: 99999;
font-family: system-ui, sans-serif; }
.fab {
width: 56px; height: 56px; border-radius: 50%;
background: ; border: none; cursor: pointer;
color: white; font-size: 24px; display: flex;
align-items: center; justify-content: center;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.panel {
display: ;
flex-direction: column; width: 360px; height: 500px;
background: white; border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.15);
margin-bottom: 12px; overflow: hidden;
}
.header {
background: ; color: white;
padding: 16px; font-weight: 600;
}
.messages {
flex: 1; overflow-y: auto; padding: 16px;
}
.input-area {
display: flex; padding: 12px; border-top: 1px solid #e5e7eb;
}
.input-area input {
flex: 1; padding: 8px 12px; border: 1px solid #e5e7eb;
border-radius: 8px; outline: none;
}
.input-area button {
margin-left: 8px; padding: 8px 16px;
background: ; color: white;
border: none; border-radius: 8px; cursor: pointer;
}
</style>
<div class="panel">
<div class="header">Chat Support</div>
<div class="messages" id="messages"></div>
<div class="input-area">
<input type="text" placeholder="Type a message..."
id="chatInput" />
<button id="sendBtn">Send</button>
</div>
</div>
<button class="fab" id="fabBtn">
</button>
`;
// Events
this.shadowRoot.getElementById('fabBtn')
.addEventListener('click', () => this.toggleChat());
const input = this.shadowRoot.getElementById('chatInput');
const sendBtn = this.shadowRoot.getElementById('sendBtn');
if (input && sendBtn) {
sendBtn.addEventListener('click', () => {
this.sendMessage(input.value);
input.value = '';
});
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.sendMessage(input.value);
input.value = '';
}
});
}
this.updateMessages();
}
updateMessages() {
const container = this.shadowRoot.getElementById('messages');
if (!container) return;
container.innerHTML = this._messages.map(m => `
<div style="margin:8px 0;text-align:">
<span style="display:inline-block;padding:8px 12px;border-radius:12px;
max-width:80%;background:;
color:">
</span>
</div>
`).join('');
container.scrollTop = container.scrollHeight;
}
}
customElements.define('saas-chat-widget', SaaSChatWidget);
// ลูกค้าฝังด้วย:
// <script src="https://cdn.saas-app.com/widget.js"></script>
// <saas-chat-widget api-key="abc123"
// theme-color="#6366f1"></saas-chat-widget>
ใช้ Web Components กับ React, Vue, Angular
- React: ใช้ได้โดยตรงใน JSX แต่ Event Handling ต้องใช้ ref เพราะ React ไม่ Forward Custom Events อัตโนมัติ หรือใช้ @lit/react Wrapper
- Vue: รองรับ Web Components โดยตรง ใช้ได้เลยใน Template ตั้ง Config ให้ Vue ไม่ Resolve Custom Elements
- Angular: เพิ่ม CUSTOM_ELEMENTS_SCHEMA ใน Module แล้วใช้ได้เลย รองรับ Property Binding ด้วย
- Svelte: ใช้ได้โดยตรง Svelte ไม่จัดการ Custom Elements ปล่อยให้ Browser จัดการ
- Lit: Library สำหรับสร้าง Web Components ง่ายขึ้น มี Reactive Properties, Templates และ Scoped Styles
Build และ Distribution
- Bundling: ใช้ Vite หรือ Rollup Bundle เป็น Single File สำหรับ CDN Distribution
- CDN: Host บน CDN เช่น CloudFlare, jsDelivr ให้ลูกค้าใช้ Script Tag เดียว
- NPM: Publish เป็น NPM Package สำหรับ Developer ที่ใช้ Build Tools
- Tree Shaking: Export แต่ละ Component แยกให้ Import เฉพาะที่ต้องการ
- Versioning: ใช้ Semantic Versioning CDN URL มี Version เช่น /v2/widget.js
Web Components คืออะไร
Web Components เป็นมาตรฐาน Browser สร้าง Custom HTML Elements ประกอบด้วย Custom Elements, Shadow DOM, HTML Templates และ Slots ทำงานได้ทุก Framework ไม่ต้องมี Runtime เพิ่ม เหมาะสำหรับ Shared Components ข้าม Framework
ทำไมต้องใช้ Web Components กับ SaaS
SaaS มีหลาย Product และ Micro-frontends ที่ใช้ต่าง Framework Web Components สร้าง Shared Components ใช้ได้ทุก Framework เช่น Design System, Embeddable Widget สำหรับลูกค้าฝัง ลด Duplicate Code และ Maintenance Cost
Shadow DOM คืออะไร
Shadow DOM เป็น Encapsulated DOM Tree แยกจาก Main DOM CSS ภายในไม่กระทบภายนอกและภายนอกไม่กระทบภายใน เหมาะกับ SaaS Widget ที่ Embed ในเว็บลูกค้า Style ไม่ชนกัน ใช้ CSS Variables สำหรับ Customization
Web Components ต่างจาก React Components อย่างไร
Web Components เป็นมาตรฐาน Browser ใช้ได้ทุก Framework ไม่ต้อง Runtime แต่ State Management ซับซ้อนกว่า React Components ผูกกับ React มี Virtual DOM และ State Management ดีกว่า เลือกตามความต้องการของ Project
สรุป
Web Components เป็นทางเลือกที่ดีสำหรับ SaaS Architecture ที่ต้องการ Shared UI Components ข้าม Framework ใช้ Shadow DOM สำหรับ Style Encapsulation, Custom Events สำหรับ Communication และ Slots สำหรับ Content Projection เหมาะกับ Design System, Embeddable Widgets และ Micro-frontends Distribute ผ่าน CDN หรือ NPM ให้ลูกค้าใช้ Script Tag เดียว
