[Web] 從 Rating 元件理解 Web Component
/ 5 min read
Updated:Table of Contents
Rating Component 簡單但完整的 Web Component
為什麼用 Web Component
- 不綁任何框架,任何地方都能用
- 元件行為可封裝,外部只管屬性與事件
- 可以逐步引入,不需要大改既有技術堆疊
元件的四個核心區塊
這個 Rating 元件在 Rating.astro 裡可以看到四個區塊:
- frontmatter:讀 props、算出星星數量與尺寸
- HTML:輸出星星 button + hidden input
- CSS:用 data-* 切換顏色與互動狀態
- Script:定義 custom element 的行為
接下來逐段說明
1) frontmatter:把設定變成可用的資料
const { id, name = "rating", label = "Rating", value = 0, numItems = 5, size = "md", readOnly = false, disabled = false, allowClear = false,} = Astro.props;
const sizeMap = { sm: 20, md: 28, lg: 36 } as const;const starSize = sizeMap[size as keyof typeof sizeMap] ?? sizeMap.md;const stars = Array.from({ length: numItems }, (_, index) => index + 1);- props 統一在這裡處理,後面就不用擔心 undefined
stars是1..numItems的陣列,用來 map 出星星starSize用 CSS 變數丟給樣式,避免寫死尺寸
2) HTML:把結構寫成可互動的 DOM
<rating-input class="rating-root" data-max={numItems} data-value={value} data-readonly={readOnly} data-disabled={disabled} data-allow-clear={allowClear} style={`--star-size: ${starSize}px`}> <div class="rating-stars" role="radiogroup" aria-label={label}> {stars.map((star) => ( <button class="rating-star" data-value={star} type="button" role="radio" aria-checked="false"> <svg aria-hidden="true" viewBox="0 0 24 24" focusable="false"> <path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"></path> </svg> </button> ))} </div> <input type="hidden" name={name} value={value} /></rating-input>幾個重點:
- 用
data-*把狀態從 HTML 交給 Web Component 讀取 role="radiogroup"+role="radio"讓無障礙工具知道這是單選- hidden input 可以直接搭配
<form>提交分數
3) CSS:狀態全部用 data-* 控制
.rating-star[data-on="true"] { color: var(--rating-on);}
.rating-star[data-active="true"] { transform: scale(1.06);}- UI 狀態變化完全由 data 屬性驅動
- JS 不直接操作 class,只有更新 data,CSS 自己決定要怎麼顯示
行為與視覺分離,後續改樣式不影響邏輯
4) Script:定義 Web Component 的行為
核心類別:
class RatingInput extends HTMLElement { connectedCallback() { this.buttons = Array.from(this.querySelectorAll("button[data-value]")); this.hiddenInput = this.querySelector("input[type='hidden']");
this.max = Number(this.dataset.max ?? this.buttons.length); this.value = Number(this.dataset.value ?? 0); this.readOnly = this.dataset.readonly === "true"; this.disabled = this.dataset.disabled === "true"; this.allowClear = this.dataset.allowClear === "true";
this.buttons.forEach((button) => { button.addEventListener("click", () => { if (this.readOnly || this.disabled) return; const nextValue = Number(button.dataset.value ?? 0); if (this.allowClear && nextValue === this.value) { this.setValue(0); return; } this.setValue(nextValue); }); });
this.updateUI(); }}這段做了三件事:
- 找 DOM:把所有星星 button 和 hidden input 抓出來
- 讀狀態:從
data-*初始化元件狀態 - 綁事件:點擊時改分數並更新 UI
更新 UI 的方法
updateUI() { const displayValue = this.hoverValue ?? this.value; const focusValue = this.value > 0 ? this.value : 1;
this.buttons.forEach((button) => { const starValue = Number(button.dataset.value ?? 0); const isOn = displayValue > 0 && starValue <= displayValue; const isActive = displayValue > 0 && starValue === displayValue; const isChecked = this.value > 0 && starValue === this.value; const isFocusable = starValue === focusValue;
button.dataset.on = String(isOn); button.dataset.active = String(isActive); button.setAttribute("aria-checked", String(isChecked)); button.tabIndex = isFocusable ? 0 : -1; });}Web Component 的「單一資料來源」概念:
- 狀態只有
value / hoverValue / disabled這些欄位 - UI 永遠只跟狀態同步,不自己決定
對外發事件
this.dispatchEvent( new CustomEvent("rating-change", { detail: { value: this.value }, bubbles: true, }));外部想知道分數變化,只要監聽 rating-change 就好
對外事件監聽範例
<rating-input id="movie-rating" value="3" max="5"></rating-input><script> const rating = document.querySelector("#movie-rating"); rating.addEventListener("rating-change", (event) => { const { value } = event.detail; console.log("new rating", value); });</script>Web Component 的注意事項
這些是做 Web Component 的坑,這個元件也剛好提供了對應的設計方向
-
狀態與 DOM 要同步
- 不要在事件中直接改 class,請集中到
updateUI() - 可預期的狀態會讓你更容易 debug
- 不要在事件中直接改 class,請集中到
-
要有無障礙規範
role="radiogroup"+role="radio"+aria-checkedtabIndex只保留一個可聚焦目標,鍵盤操作才合理
-
資料輸出要友善
- hidden input 讓它可以直接被表單提交
CustomEvent讓非表單場景也好用
結語
Web Component 三件事:
- 讓狀態清楚
- 讓 DOM 可預期
- 讓外部好整合
這個 Rating 元件雖然不大,但該有的點都有了 未來可以慢慢加上更多能力,例如動態更新、API 驅動、Shadow DOM