前言
一直以来,我们都是使用框架来实现ui组件,而本系列的文章将带大家使用js实现ui组件,掌握组件实现的背后原理,首先我们来看第一个组件checkbox组件。
实现思路
checkbox组件,我们可以直接使用<input type="checkbox" />
来实现,但该组件的样式ui还是差强人意,并不能满足高要求,因此,我们需要自定义实现它。通常我们实现自定义的复选框,只是把该元素给隐藏,然后采用自定义的元素来模拟该元素的功能。也因此,我们最终实现的元素结构应该如下所示:
html
<div class="ew-checkbox">
<input type="checkbox" class="checkbox-input" /> <!-- 隐藏的原生复选框 -->
<span class="checkbox-checkmark"></span> <!-- 自定义视觉样式 -->
<span class="checkbox-label">标签文本</span> <!-- 显示文本 -->
</div>
默认,我们会将<input type="checkbox" />
给隐藏掉,然后通过给其它元素添加事件来完成选中态效果。
然后我们就可以通过样式来自定义复选框的样式,从而达到美化复选框的效果。接下来,我们需要分两步实现该插件,首先是单个复选框插件,然后则是复选框组。
实现单个复选框
我们将采用面向对象的设计模式来实现单个复选框插件,对于单个复选框插件,我们只需要3个配置属性,即选中展示的文案,代表选中的状态,以及状态改变的回调,所以我们最终封装的插件使用示例如下所示:
js
const checkbox = new Checkbox({
label: "同意条款",
checked: false,
onChange: (checked) => {
console.log('复选框状态:', checked);
}
});
checkbox.mount('#container');
- label: 选中文本。
- checked: 是否选中
- onChange: 选中事件触发的回调。
这里我们还额外实现了一个mount方法,用于将插件挂载到某个容器元素中。
接下来,我们来看js代码,如下所示:
js
class Checkbox {
// 构造函数,初始化单个复选框
constructor(options){
// ....
}
// 创建DOM元素
createCheckboxElement(){
// ...
}
// 设置事件监听器
setupEventListeners(){
// ...
}
// 设置选中状态
setChecked(checked){
//....
}
// 获取选中状态
isChecked(){
//...
}
// 挂载到容器
mount(container){
//...
}
}
对于以上代码,我们创建了一个Checkbox类,包括了1个构造函数和5个方法,并且也对每一部分做了注释说明。
接下来,我们就来一步一步实现每一部分。首先是构造函数部分,很明显我们通常会做一些初始化处理,比如初始化配置属性,初始化dom元素等。如下所示:
js
// 构造函数内部
this.options = {
label: options.label || "",
checked: options.checked || false,
...options
}; // 初始化配置属性
this.onChange = options.onChange || (() => {}); // 初始化onChange回调
this.element = this.createCheckboxElement(); // 初始化最终生成的dom结构,然后通过mount方法挂载到容器元素上
this.checked = this.options.checked; // 初始化选中状态
this.setupEventListeners();// 调用事件监听器的方法
通过注释,我们可以总结构造函数内部主要做了哪些如下:
- 初始化配置属性。
- 初始化onChange回调。
- 初始化最终生成的dom结构,然后通过mount方法挂载到容器元素上。
- 初始化选中状态。
- 调用事件监听器的方法。
接下来,我们来看createCheckboxElement方法,前面我们也知道了checkbox的dom结构,接下来,我们就是通过js创建这些元素。如下:
js
// createCheckboxElement的实现
// 创建一个容器元素
const container = document.createElement("div");
container.className = "ew-checkbox";
// 创建一个input元素
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.className = "checkbox-input";
checkbox.checked = this.options.checked;
// 创建一个用于展示选中状态的元素
const checkmark = document.createElement("span");
checkmark.className = "checkbox-checkmark";
// 创建一个label元素
const label = document.createElement("span");
label.className = "checkbox-label";
label.textContent = this.options.label;
container.appendChild(checkbox);
container.appendChild(checkmark);
container.appendChild(label);
return container;
这一步,我们分别创建了input元素,展示选中状态的元素以及展示文案的label元素,然后创建了一个容器元素,并将前面3种元素添加到了容器元素中,然后返回这个容器元素。
接下来,我们来看setChecked和isChecked方法,这2个方法的实现比较简单,所以优先讲这2个方法,无非就是设置选中状态和获取选中状态。
js
// 获取选中状态
return this.checked; // 直接返回我们初始化的选中状态即可
js
// 设置选中状态
this.checked = checked;
const checkbox = this.element.querySelector(".checkbox-input");
checkbox.checked = checked;
this.onChange(checked);
对于获取选中状态没什么好说的,主要是设置选中状态,我们做了3个操作。
- 设置选中状态的数据。
- 获取input并更改input的选中状态属性。
- 回调最终的选中状态。
最后,我们来看setupEventListeners方法。如下:
js
const checkbox = this.element.querySelector(".checkbox-input");
const container = this.element;
container.addEventListener("click", (e) => {
if (e.target !== checkbox) {
checkbox.checked = !checkbox.checked;
this.checked = checkbox.checked;
this.onChange(this.checked);
}
});
checkbox.addEventListener("change", (e) => {
this.checked = e.target.checked;
this.onChange(this.checked);
});
以上代码,我们做了如下操作:
- 获取内部的type为checkbox的input元素,以及获取当前的容器元素。
- 监听容器元素的点击事件,判断如果不是type为checkbox的input元素,则更改选中状态,并执行回调onChange,将选中状态参数传递。
- 监听type为checkbox的input元素的change事件,然后更改选中状态,并回调出去。
如此一来,我们就完成了一个复选框组件。而我们的mount方法,则非常简单。如下所示:
js
if (typeof container === "string") {
container = document.querySelector(container);
}
container.appendChild(this.element);
说白了就是获取挂载的元素,然后将当前的checkbox的容器元素添加到挂载元素中。
然后就是样式的实现,样式主要就是画一个正方形,然后通过伪元素实现一个✅的效果。如下:
css
.ew-checkbox {
display: inline-flex;
align-items: center;
position: relative;
cursor: pointer;
margin: 4px 8px;
}
// 隐藏默认的input元素
.checkbox-input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.checkbox-checkmark {
height: 18px;
width: 18px;
background-color: #fff;
border: 2px solid #ddd;
border-radius: 3px;
margin-right: 8px;
position: relative;
transition: all 0.2s ease;
}
// 选中效果
.checkbox-input:checked ~ .checkbox-checkmark {
background-color: #3498db;
border-color: #3498db;
}
.checkbox-input:checked ~ .checkbox-checkmark:after {
content: '';
position: absolute;
left: 4px;
top: 0;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.checkbox-label {
font-size: 14px;
color: #333;
}
融合以上的代码,你就会得到如下图所示的复选框。


实现复选框组
实现复选框组也很简单,我们只需要在单个复选框的基础上做一些操作就行了。我们首先需要初始化一个全选复选框,然后再根据子复选框数据初始化。这就是静态方法的含义。整体代码如下所示:
js
static createCheckboxGroup(options = {}) {
const container = document.createElement("div");
container.className = "checkbox-group";
const checkboxes = [];
let isUpdatingFromAll = false; // 防止递归调用的标志
const allCheckbox = new Checkbox({
label: "全选",
checked: options.items ? options.items.every(item => item.checked) : false,
onChange: (checked) => {
if (isUpdatingFromAll) return; // 防止递归
isUpdatingFromAll = true;
checkboxes.forEach((cb) => {
cb.setChecked(checked);
});
// 确保全选复选框本身也正确设置状态
const allCheckboxInput = allCheckbox.element.querySelector(".checkbox-input");
allCheckboxInput.checked = checked;
allCheckboxInput.indeterminate = false; // 清除中间状态
isUpdatingFromAll = false;
if (options.onChange) {
options.onChange(checkboxes.map((cb) => cb.isChecked()));
}
},
});
allCheckbox.mount(container);
if (options.items) {
options.items.forEach((item) => {
const checkbox = new Checkbox({
...item,
onChange: (checked) => {
if (isUpdatingFromAll) return; // 防止递归
// 更新对应的选项状态
if (options.onChange) {
options.onChange(checkboxes.map((cb) => cb.isChecked()));
}
// 检查是否所有子复选框都被选中
const allChecked = checkboxes.every((cb) => cb.isChecked());
const noneChecked = checkboxes.every((cb) => !cb.isChecked());
// 更新全选checkbox的状态
isUpdatingFromAll = true;
allCheckbox.setChecked(allChecked);
const allCheckboxInput = allCheckbox.element.querySelector(".checkbox-input");
allCheckboxInput.indeterminate = !allChecked && !noneChecked;
isUpdatingFromAll = false;
},
});
checkbox.mount(container);
checkboxes.push(checkbox);
});
// 初始化全选状态
const allChecked = checkboxes.every((cb) => cb.isChecked());
const noneChecked = checkboxes.every((cb) => !cb.isChecked());
const allCheckboxInput = allCheckbox.element.querySelector(".checkbox-input");
allCheckboxInput.indeterminate = !allChecked && !noneChecked;
}
return {
container,
checkboxes,
allCheckbox,
// 添加便捷方法
setAllChecked: (checked) => {
isUpdatingFromAll = true;
checkboxes.forEach((cb) => {
cb.setChecked(checked);
});
allCheckbox.setChecked(checked);
// 确保全选复选框清除中间状态
const allCheckboxInput = allCheckbox.element.querySelector(".checkbox-input");
allCheckboxInput.indeterminate = false;
isUpdatingFromAll = false;
if (options.onChange) {
options.onChange(checkboxes.map((cb) => cb.isChecked()));
}
},
getAllChecked: () => checkboxes.map((cb) => cb.isChecked()),
isAllChecked: () => checkboxes.every((cb) => cb.isChecked()),
isNoneChecked: () => checkboxes.every((cb) => !cb.isChecked())
};
}
对于复选框组,我们需要分析有如下三种效果。
状态 | 条件 | 视觉效果 | 实现方式 |
---|---|---|---|
未选中 | 所有子复选框都未选中 | 空框 | checked = false, indeterminate = false |
全选中 | 所有子复选框都选中 | 对勾 | checked = true, indeterminate = false |
部分选中 | 部分子复选框选中 | 横线 | checked = false, indeterminate = true |
其实核心难点就是将全选复选框和子复选框进行联动,我们通过插件本身就可以实现多个复选框,复选框组无非就是多了全选复选框和子复选框进行联动而已。复选框组会涉及到半选状态,因此我们需要增加半选的样式,如下所示:
css
.checkbox-input:indeterminate ~ .checkbox-checkmark {
background-color: #3498db;
border-color: #3498db;
}
.checkbox-input:indeterminate ~ .checkbox-checkmark:after {
content: '';
position: absolute;
left: 4px;
top: 7px;
width: 8px;
height: 2px;
background: white;
border: none;
transform: none;
}
// 复选框组容器元素的样式
.checkbox-group {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px 0;
}
这里还要注意一点,那就是避免递归操作,需要通过一个变量来控制。使用静态方法创建复选框组的逻辑也比较简单,如下所示:
js
const group = Checkbox.createCheckboxGroup({
items: [
{ label: '显示详细信息', checked: true },
{ label: '包含日期分析', checked: true },
{ label: '显示可视化', checked: true },
{ label: '自动保存历史', checked: true },
{ label: '显示代码片段', checked: true },
{ label: '严格验证模式', checked: false }
],
onChange: (values) => {
// ... 复选框的状态回调
}
});
document.body.appendChild(group.container);
如此一来,我们就实现了一个复选框组和复选框插件。
总结
本解我们学会了如何实现一个checkbox组件。
- 实现一个checkbox。
- 实现一个复选框组。
感谢阅读,如果觉得有用,望不吝啬点赞收藏。