原生js实现常规ui组件之checkbox篇

前言

一直以来,我们都是使用框架来实现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();// 调用事件监听器的方法

通过注释,我们可以总结构造函数内部主要做了哪些如下:

  1. 初始化配置属性。
  2. 初始化onChange回调。
  3. 初始化最终生成的dom结构,然后通过mount方法挂载到容器元素上。
  4. 初始化选中状态。
  5. 调用事件监听器的方法。

接下来,我们来看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个操作。

  1. 设置选中状态的数据。
  2. 获取input并更改input的选中状态属性。
  3. 回调最终的选中状态。

最后,我们来看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);
});

以上代码,我们做了如下操作:

  1. 获取内部的type为checkbox的input元素,以及获取当前的容器元素。
  2. 监听容器元素的点击事件,判断如果不是type为checkbox的input元素,则更改选中状态,并执行回调onChange,将选中状态参数传递。
  3. 监听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组件。

  1. 实现一个checkbox。
  2. 实现一个复选框组。

感谢阅读,如果觉得有用,望不吝啬点赞收藏。

相关推荐
编程二级爱好者4 小时前
2025年9月计算机二级Web程序设计——选择题打卡Day5
前端·计算机二级
Tanjc5184 小时前
uniapp H5预览图片组件
前端·vue.js·uni-app
ᥬ 小月亮4 小时前
uniapp中输入金额的过滤(只允许输入数字和小数点)
前端·css·uni-app
共享ui设计和前端开发4 小时前
UI前端大数据可视化实战策略:如何设计符合用户认知的数据可视化界面?
前端·ui·信息可视化
Akshsjsjenjd4 小时前
Ansible 变量与加密文件全解析:从基础定义到安全实践
前端·安全·ansible
2503_928411564 小时前
9.2 BOM对象
前端·javascript
chinesegf4 小时前
浏览器内存 (JavaScript运行时内存)存储的优劣分析
开发语言·javascript·ecmascript
whysqwhw4 小时前
JavaScript 动态代理全面指南
前端
Highcharts.js5 小时前
Highcharts Stock 股票图在交易系统中的应用思路
前端·数据可视化·股票图