突发奇想,同步单复选框checked态岂不点击通杀?

这篇文章发布于 2020年11月26日,星期四,01:51,归类于 JS实例。 阅读 15684 次, 今日 2 次 12 条评论

 

思考的狐狸,突发奇想的张鑫旭

标题完整内容应该是“就在星期天的下午,我在书房突发奇想,要是可以让元素(如<label>元素)无论在页面什么位置都能响应单选框或复选框元素的状态变化,那么几乎页面所有的点击交互岂不是都可以通杀了,那就牛逼大啦!”

到底牛不牛逼呢,大家可以跟过来一起看看评一评。

一、起步、背景和目标效果

几乎所有常见的点击交互的本质就是单选或者复选。

例如选项卡是典型的单选,展开收起或者下拉就是典型的复选(只有1个选项的复选),树结构是多选等。

因此radio/checkbox配合:checked伪类可以纯CSS实现大量的点击交互效果,这个技术我早在8年前就介绍过了:“CSS radio/checkbox单复选框元素显隐技术”。

但是这个技术有个限制,由于CSS选择器中的+或者~选择符只能选择后面的兄弟元素,因此,交互事件的主体需要和单复选框元素是兄弟关系,这就导致DOM结构有了明显的限制。

有一种方法可以一定程度上绕开这种限制,就是使用<label>元素,通过for属性与单选复选元素进行关联,这样,单选框或者复选框就可以在页面的任意的位置。

但是这种方法虽然功能OK,但是DOM的结构却很奇怪,语义不符,结构混乱,例如实现选项卡效果的时候,选项卡按钮和选项卡面板元素需要公用一个祖先元素,如下截图红框框所示,这太奇怪了,完全不能用在实际项目中。

奇怪的单复选框实现效果

突发奇想的背景

我的上一篇文章就是和单选框技术相关的,实现下图这个单选变色效果:

列表点击效果

因为HTML代码有限制,如下所示:

<ul>
    <li><input type="radio" name="item" checked>选项1</li> 
    <li><input type="radio" name="item">选项2</li> 
    <li><input type="radio" name="item">选项3</li> 
    <li><input type="radio" name="item">选项4</li> 
    <li><input type="radio" name="item">选项5</li> 
</ul>

所以用了“高级的”mix-blend-mode属性实现的。

当时,我就琢磨着,要是父元素<li>可以实时响应里面单选框元素的checked状态,什么屁事都没了,关键就是不支持,毕竟CSS中没有父选择器。

心有不甘,一直盘在心里,然后周日晚上9点多的时候,看着鱼缸里的自由自在的小鱼,就突然来了灵感:嘿!以我目前的技术储备,想要让任意元素和单复选框的checked状态关联似乎是可行的呀,脑子里盘了盘,有了个雏形,然后就开始开搞。

目标效果

实现的目标效果如下:

  1. 引入一段JS代码;
  2. 任意for/id属性关联元素状态实时联动;

例如:

<ul>
    <li for="$1"><input type="radio" id="$1" name="item" checked>选项1</li> 
    <li for="$2"><input type="radio" id="$2" name="item">选项2</li> 
    <li for="$3"><input type="radio" id="$3" name="item">选项3</li> 
    <li for="$4"><input type="radio" id="$4" name="item">选项4</li> 
    <li for="$5"><input type="radio" id="$5" name="item">选项5</li> 
</ul>

无论通过何种方式改变了单选框元素的选中态,for属性值等于这个单选框元素id值的任意元素都会有对应的状态列表,例如toggle一个类名.active

这样,就可以使用li.active选择器轻松改变文字的颜色了。

二、代码出场、基本效果

按照心中的雏形,盘啊盘,测啊测,代码撸出来了。

该JS地址为:smart-for.js

直接在页面中引入下面的JS,然后万能点击效果就有了。

<script src="smart-for.js"></script>

眼见为实,上面那个列表选择效果,您可以狠狠地点击这里:父元素响应单选框checked状态demo

打开控制台,就可以看到,单选框在点击的时候,父元素的类名.active会自动添加删除。

类名自动同步示意

状态关联的机制很简单,就是需要状态同步的元素的for属性值就是单选框或复选框元素的id属性值就可以了,类似于<label>元素和单复选框元素的关联。

有了smart-for.js,几乎所有的点击交互效果就不需要额外的JS代码去实现了,通杀。

popup或侧边栏效果

例如移动端常见的底部popup浮层效果,或者aside侧边栏效果,就不需要额外的JS来实现了。

眼见为实,您可以狠狠地点击这里:无业务JS实现的Popup和Aside浮层交互demo

可以看到如下GIF所示的效果(点击播放-183K):

浮层出现隐藏效果示意

相关代码如下,主要是HTML部分,侧边栏效果示意,无关紧要代码删除了:

<label class="ui-button"><input type="checkbox" id="zxxAside" hidden>点击我显示侧边栏</label>
<aside class="aside" for="zxxAside">
    <label for="zxxAside" class="aside-overlay"></label>
    <div class="aside-content">点击黑色蒙层可以收起</div>
</aside>

显隐控制的CSS代码主要就是:

.aside {
    visibility: hidden;
}
.aside.active {
    visibility: visible;
}

浏览器有个特性,点击<label>元素,里面的单选框或者复选框元素就会选中,此时就会触发任意for="zxxAside"的元素添加类名.active,于是浮层显示。

黑色蒙层使用的是指向复选框的<label>元素,因此点击时候就会取消复选框的选中,此时<aside>元素的.active类名自动删除,浮层隐藏。

如果希望点击其他元素或者按钮让隐藏,但是又不想使用<label>元素。

则需要使用JS改变复选框元素的选中态,例如:

someEle.addEventListener('click', function () {
    zxxAside.checked = false;
});

元素状态会自动同步,无需开发者去触发。

展开更多效果

这个demo页面是老的:checked伪类实现的效果,不过需要固定的层级,有了本文的JS代码,可以无视层级,更自由了,实现自然不在话下。

例子就不举了,因为这种交互更推荐使用“<details>、<summary>元素实现”,纯CSS,语义更好,不支持的浏览器简单几行JS Polyfill下就可以了。

更多展开收起gif效果

下拉列表效果

这个效果绝对是CSS :focus-within伪类实现最佳,纯CSS,现代浏览器兼容性还是可以的,我已经在实际项目中使用了,Safari浏览器注意使用<a>元素+tabindex="0"

<details>/<summary>元素也可以实现下拉列表效果,本文的checked状态同步也可以实现,但是,点击空白区域隐藏这个处理,在复杂页面场景下可能会有层级混乱的问题。

算了,想了想,还是整个demo吧,万一可以帮到需要的人呢,毕竟兼容性要比:focus-within伪类好很多,IE11+都支持。

您可以狠狠地点击这里:checked状态同步与下拉列表交互demo

下面的GIF录屏就是demo页面实现的交互效果:

checked状态实现的下拉列表效果GIF

优点除了兼容性好之外,还有就是下拉列表浮层元素的位置是可以任意的。其他几个CSS方法都有DOM位置和层级的限制。

大家有兴趣可以研究下demo页面中的源代码。

选项卡切换效果

有了本文的smart-for.js,选项卡效果再也不需要复杂而又奇怪的DOM层级结构了。

您可以狠狠地点击这里:checked状态同步选项卡效果demo (内有完整的源代码)

此时的HTML结构就是正常的了,符合我们的理解和认知:

正常选项卡结构示意

实现效果如下GIF所示:

选项卡切换效果GIF录屏

更多细节参见demo页面,这里不展开。


总之,只要引入一小段JS代码,Min + Gzip后 < 1K,借助隐藏的单选框或者复选框元素,各类点击交互效果就无需额外的JS代码了,层级无限制,位置无限制,元素几乎无限制,非常灵活。

反正这年头语义化就是个梦,大家应该会用得很开心的。

三、实现的原理、难点在哪

要想让元素和单复选框元素的:checked状态关联还是有一些挑战的。

因为单复选框:checked状态变化的场景太多了:

  1. 点击行为选中(无任何属性变化)
  2. JS设置:radio.checked = true // 或false
  3. HTML属性设置:radio.setAttribute(‘checked’, ”)
  4. 单选框组中其他元素check导致自身uncheck
  5. 页面新增一个单复选框
  6. 删除一个单复选框
  7. 页面新增一个for关联元素

必须观察上面所有的场景,而且要立即识别,无需用户主动触发。

对于checked状态变化,我一开始的思路是:

:checked {
    padding: 0.1px;
}

然后使用ResizeObserver观察元素的尺寸是否变化,这样就知道状态可能发生了变化。但是这样做,需要元素非display:none隐藏,而且,最重要的是ResizeObverse iOS 13才开始支持,兼容性不佳,于是放弃了这个想法,改为采用下面的策略:

  1. dom.checked引起的状态变化通过重置单复选框元素的checked属性实现的,代码如下:
    var propsChecked = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'checked');
    var propsCheckedNew = {};
    
    for (key in propsChecked) {
        propsCheckedNew[key] = propsChecked[key];
    }
    
    propsCheckedNew.set = function (value) {
        propsChecked.set.call(this, value);
        // 同步对应label元素的状态
        funCheckedSync(this);
    };

    补充于翌日

    感谢XboxYan的反馈,观察checked状态变化还可以使用animationend回调,兼容性会好很多,IE10+,不足就是display:none隐藏无法感知。

  2. setAttribute/removeAttribute导致的属性变化、DOM元素的增删,全部使用MutationObserver方法实现,这个在之前“聊聊JS DOM变化的监听检测与应用”一文中有介绍,IE11+支持。

    具体代码这里就不展示了。

  3. 点击行为导致的状态变化采用委托的方式进行监听,代码如下:
    document.addEventListener('click', function (event) {
        var eleTarget = event && event.target;
        if (eleTarget && eleTarget.matches('[type="radio"], [type="checkbox"]')) {
            funCheckedSync(eleTarget);
        }
    });

更多细节大家仔细参阅源码。


算了算了,直接浏览器打开的代码一坨糊,很不利于阅读。

我找了个地方开源了下:https://gitee.com/zhangxinxu/smart-for

gitee速度杠杠的,比github快多了,欢迎关注我 https://gitee.com/zhangxinxu)。

关注我的gitee

以后非国际化的项目我都会放在gitee上,国际形势不定,放在github上的代码说不定哪天都不是自己的。

JS源码参阅直接戳这里:https://gitee.com/zhangxinxu/smart-for/blob/master/smart-for.js

吼吼~

四、测试、降维打击、结语

为了验证品质,我还专门写了个测试页面

点击测试按钮,一排绿的感觉真好,然后有改动,测一下,也放心一点。

理论上,IE9+浏览器也是可以对DOM和属性进行观察的,但是那几个window事件性能太差,早就要淘汰了,我就没支持。

因此,本JS兼容到IE11+,也就是MutationObserver方法支持的版本。

例如下图是IE11浏览器下的测试结果:

IE11下测试结果示意

降维打击

本文的这个JS覆盖点击交互的方法,感觉就是一个完全的不同层面的思路,有种降维打击的感觉。

无需针对每一种交互效果去写具体的代码,只要抽象出一个规则,然后利用浏览器原生的特性,配合已知的一些浏览器API能力,就可以实现全覆盖的交互增强支持。

根据这么多年的时间,单选复选的浏览器原生特性是非常稳健的,因此,代码出坑的可能性并不大。

以后要是有个运营活动之类的项目、或者内部项目,我会尝试用用看,感受下到底香不香。

OK,以上就是本文的全部内容,周末突然的一点奇思妙想,居然扯这么多内容。

研究、学习、成长不止!

感谢您的阅读,如果你觉得本文内容还不错,欢迎转发。

(本篇完)

分享到:


发表评论(目前12 条评论)

  1. FanYa说道:

    旭哥, 用着怎么样, 爽不爽?

  2. 阿龙说道:

    和之前的 attr-polyfill 差不多呢

  3. Lisianthus说道:

    代码在严格模式会报错,56行key未声明

  4. 前端学习中说道:

    666,先mark一记

  5. ziven27说道:

    确实是完全不一样的思路。
    将 UI 逻辑和 业务逻辑完美的独立出来,
    拓展性刚刚的。

  6. 埼玉人柱力King说道:

    请问 这里面的测试Demo会一保存吗,说不定我三四年后想起来回来看一下。
    如果会的话我就不写在自己笔记里了。

  7. Jack说道:

    for (key in propsCheckedNew) { // propsChecked?
    propsCheckedNew[key] = propsChecked[key];
    }

  8. 说道:

    真心羡慕大佬可以研究的这么细致

  9. 橙子说道:

    沙发,太棒了,又多了一个思路