标题完整内容应该是“就在星期天的下午,我在书房突发奇想,要是可以让元素(如<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状态关联似乎是可行的呀,脑子里盘了盘,有了个雏形,然后就开始开搞。
目标效果
实现的目标效果如下:
- 引入一段JS代码;
- 任意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下就可以了。
下拉列表效果
这个效果绝对是CSS :focus-within伪类实现最佳,纯CSS,现代浏览器兼容性还是可以的,我已经在实际项目中使用了,Safari浏览器注意使用<a>
元素+tabindex="0"
。
<details>
/<summary>
元素也可以实现下拉列表效果,本文的checked状态同步也可以实现,但是,点击空白区域隐藏这个处理,在复杂页面场景下可能会有层级混乱的问题。
算了,想了想,还是整个demo吧,万一可以帮到需要的人呢,毕竟兼容性要比:focus-within
伪类好很多,IE11+都支持。
您可以狠狠地点击这里:checked状态同步与下拉列表交互demo
下面的GIF录屏就是demo页面实现的交互效果:
优点除了兼容性好之外,还有就是下拉列表浮层元素的位置是可以任意的。其他几个CSS方法都有DOM位置和层级的限制。
大家有兴趣可以研究下demo页面中的源代码。
选项卡切换效果
有了本文的smart-for.js,选项卡效果再也不需要复杂而又奇怪的DOM层级结构了。
您可以狠狠地点击这里:checked状态同步选项卡效果demo (内有完整的源代码)
此时的HTML结构就是正常的了,符合我们的理解和认知:
实现效果如下GIF所示:
更多细节参见demo页面,这里不展开。
总之,只要引入一小段JS代码,Min + Gzip后 < 1K,借助隐藏的单选框或者复选框元素,各类点击交互效果就无需额外的JS代码了,层级无限制,位置无限制,元素几乎无限制,非常灵活。
反正这年头语义化就是个梦,大家应该会用得很开心的。
三、实现的原理、难点在哪
要想让元素和单复选框元素的:checked状态关联还是有一些挑战的。
因为单复选框:checked状态变化的场景太多了:
- 点击行为选中(无任何属性变化)
- JS设置:radio.checked = true // 或false
- HTML属性设置:radio.setAttribute(‘checked’, ”)
- 单选框组中其他元素check导致自身uncheck
- 页面新增一个单复选框
- 删除一个单复选框
- 页面新增一个for关联元素
必须观察上面所有的场景,而且要立即识别,无需用户主动触发。
对于checked状态变化,我一开始的思路是:
:checked {
padding: 0.1px;
}
然后使用ResizeObserver观察元素的尺寸是否变化,这样就知道状态可能发生了变化。但是这样做,需要元素非display:none
隐藏,而且,最重要的是ResizeObverse iOS 13才开始支持,兼容性不佳,于是放弃了这个想法,改为采用下面的策略:
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);
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。