「全選/反選」是表單操作中最常見的互動需求。本文從原生 JavaScript 實作切入,逐步優化至現代寫法,並延伸至批量刪除等實務場景。
<!-- HTML 結構 -->
<div id="checkbox-group">
<label><input type="checkbox" id="select-all"> 全選</label><br>
<label><input type="checkbox" name="items[]" value="1"> 項目 1</label><br>
<label><input type="checkbox" name="items[]" value="2"> 項目 2</label><br>
<label><input type="checkbox" name="items[]" value="3"> 項目 3</label><br>
<label><input type="checkbox" name="items[]" value="4"> 項目 4</label><br>
</div>
<script>
// 原生 JS 實作
document.addEventListener('DOMContentLoaded', () => {
const selectAll = document.getElementById('select-all');
const items = document.querySelectorAll('input[name="items[]"]');
selectAll.addEventListener('change', () => {
items.forEach(item => item.checked = selectAll.checked);
});
// 反向同步:手動取消任一項目時,取消全選
items.forEach(item => {
item.addEventListener('change', () => {
selectAll.checked = [...items].every(i => i.checked);
});
});
});
</script>
<div id="advanced-group">
<button type="button" id="btn-select-all">全選</button>
<button type="button" id="btn-invert">反選</button>
<button type="button" id="btn-clear">清空</button>
<hr>
<label><input type="checkbox" name="data[]" value="10"> 資料 10</label><br>
<label><input type="checkbox" name="data[]" value="20"> 資料 20</label><br>
<label><input type="checkbox" name="data[]" value="30"> 資料 30</label><br>
</div>
<script>
const group = document.getElementById('advanced-group');
const checkboxes = group.querySelectorAll('input[type="checkbox"][name="data[]"]');
group.querySelector('#btn-select-all').onclick = () =>
checkboxes.forEach(cb => cb.checked = true);
group.querySelector('#btn-invert').onclick = () =>
checkboxes.forEach(cb => cb.checked = !cb.checked);
group.querySelector('#btn-clear').onclick = () =>
checkboxes.forEach(cb => cb.checked = false);
</script>
<form id="batch-form" action="delete.php" method="post">
<input type="hidden" name="ids" id="batch-ids">
<button type="button" id="btn-delete">批量刪除</button>
</form>
<script>
document.getElementById('btn-delete').addEventListener('click', () => {
const checked = document.querySelectorAll('input[name="items[]"]:checked');
const ids = [...checked].map(cb => cb.value).join(',');
if (!ids) return alert('請至少選擇一項');
if (!confirm(`確定刪除 ${checked.length} 筆資料?`)) return;
document.getElementById('batch-ids').value = ids;
document.getElementById('batch-form').submit();
});
</script>
<?php
// delete.php
require_once 'conn.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ids'])) {
$ids = array_map('intval', explode(',', $_POST['ids']));
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $conn->prepare("DELETE FROM `messages` WHERE `id` IN ($placeholders)");
$stmt->bind_param(str_repeat('i', count($ids)), ...$ids);
if ($stmt->execute()) {
echo "成功刪除 {$stmt->affected_rows} 筆資料";
} else {
echo "刪除失敗:" . $stmt->error;
}
$stmt->close();
}
?>
<script setup>
import { ref, computed } from 'vue'
const items = ref([
{ id: 1, label: '項目一' },
{ id: 2, label: '項目二' },
{ id: 3, label: '項目三' },
])
const selected = ref([])
const allSelected = computed({
get: () => selected.value.length === items.value.length,
set: (val) => selected.value = val ? items.value.map(i => i.id) : []
})
function deleteSelected() {
if (!selected.value.length) return alert('請選擇項目')
// 呼叫 API 刪除...
}
</script>
<template>
<label><input type="checkbox" v-model="allSelected"> 全選</label>
<div v-for="item in items" :key="item.id">
<label><input type="checkbox" :value="item.id" v-model="selected"> {{ item.label }}</label>
</div>
<button @click="deleteSelected">批量刪除</button>
</template>
| 問題 | 原因 | 解決 |
|---|---|---|
| 全選框狀態不同步 | 未監聽子項目變化 | 加入 change 事件同步 every() 判斷 |
| 動態新增項目失效 | 事件綁定在舊元素 | 使用事件代理 parent.addEventListener('change', ...) |
| 表單送出無值 | 未勾選的 checkbox 不送出 | 隱藏欄位收集 ID,或後端檢查 isset($_POST['items']) |
全選功能核心在於「狀態同步」與「資料收集」。原生 JS 適合輕量專案,框架專案則善用資料綁定。後端務必使用預處理語句防範 SQL 注入,並驗證權限再執行刪除。