多选题表单的复杂度不在外观,而在提交结构。同一组选项要能被服务端当成数组接收,未选中的项不能出现在请求里,用户对选中项的个数限制也要落到可校验的位置。

结构与命名
<fieldset>
<legend>常用来源渠道</legend>
<label><input type="checkbox" name="source[]" value="search"> 搜索</label>
<label><input type="checkbox" name="source[]" value="social"> 社交</label>
<label><input type="checkbox" name="source[]" value="friend"> 推荐</label>
</fieldset>
- 同一组的 name 完全一致,带
[]后缀便于服务端解析为数组; - value 必须写且各不相同,空 value 提交时只剩
source[]=on,无法区分选项; - label 包裹控件,点击文字即可切换,不必手写 for 与 id。
服务端接收
$sources = $_POST['source'] ?? [];
$allowed = ['search', 'social', 'friend'];
$sources = array_values(array_intersect($sources, $allowed));
未勾选的复选框不参与提交,服务端收不到任何同名参数,因此取值前要给出默认值。取值后与白名单求交集,避免客户端伪造的 value 进入后续逻辑。
required 的边界
required 加在复选框上时,含义是「这一项必须被勾选」。给同组每一项都加上,等于要求全部勾选,与「至少一个」的诉求相反。组级别的个数限制没有原生属性可用,需要走 constraint validation API:
form.addEventListener('change', () => {
const checked = form.querySelectorAll('input[name="source[]"]:checked').length;
const ok = checked >= 1 && checked <= 2;
form.querySelector('[name="source[]"]').setCustomValidity(ok ? '' : '请选择一至两项');
});
组校验挂在同组第一个控件上,提示浮层才会出现在正确位置。
「其他」选项怎么配
「其他」由独立字段承载文本内容,不与复选框本身共用一份数据:
<label><input type="checkbox" name="source[]" value="other"> 其他</label>
<input type="text" name="source_other" maxlength="40" disabled>
勾选时移除 disabled,取消勾选时清空并重新禁用。服务端遇到 value 为 other 时读取 source_other,两者一起做长度与字符校验。
检查清单
- 同组 name 一致且带数组后缀;
- 每项 value 非空、互不相同;
- 服务端按数组取值并过白名单;
- 个数限制走 setCustomValidity;
- 「其他」内容字段独立且联动启用。
