skip to content
BlogZzz

[Web] 從 Rating 元件理解 Web Component

/ 5 min read

Updated:
Table of Contents

Rating Component 簡單但完整的 Web Component

為什麼用 Web Component

  • 不綁任何框架,任何地方都能用
  • 元件行為可封裝,外部只管屬性與事件
  • 可以逐步引入,不需要大改既有技術堆疊

元件的四個核心區塊

這個 Rating 元件在 Rating.astro 裡可以看到四個區塊:

  1. frontmatter:讀 props、算出星星數量與尺寸
  2. HTML:輸出星星 button + hidden input
  3. CSS:用 data-* 切換顏色與互動狀態
  4. 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
  • stars1..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();
}
}

這段做了三件事:

  1. 找 DOM:把所有星星 button 和 hidden input 抓出來
  2. 讀狀態:從 data-* 初始化元件狀態
  3. 綁事件:點擊時改分數並更新 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 的坑,這個元件也剛好提供了對應的設計方向

  1. 狀態與 DOM 要同步

    • 不要在事件中直接改 class,請集中到 updateUI()
    • 可預期的狀態會讓你更容易 debug
  2. 要有無障礙規範

    • role="radiogroup" + role="radio" + aria-checked
    • tabIndex 只保留一個可聚焦目標,鍵盤操作才合理
  3. 資料輸出要友善

    • hidden input 讓它可以直接被表單提交
    • CustomEvent 讓非表單場景也好用

結語

Web Component 三件事:

  • 讓狀態清楚
  • 讓 DOM 可預期
  • 讓外部好整合

這個 Rating 元件雖然不大,但該有的點都有了 未來可以慢慢加上更多能力,例如動態更新、API 驅動、Shadow DOM