前言: 近期在我的项目中遇到了一个设计需要,在 UI 给我提供的原图中有一个和 element UI 选择器性能基本一致的款式,然而因为咱们是有本人的主体色彩和一些细节上的款式设计的,无奈间接复用 element 组件库。所以须要本人入手实现一个下拉选择器,最开始认为很简单,但其实在查阅了相干常识后,实现起来也谈不上很难,并且要害是本人又摸索了一片空白常识区域,很开心,遂特来分享一下本人的实现思路🎁。(本文并不是解读 element 的源码,而是仿照它的性能来本人实现一个根底组件。)
(tips: 本文不辨别框架,无论你是 react 开发还是 vue 开发,思路是统一的)
一. 设计需要
- 因为咱们我的项目的场景比拟繁多,所以 UI 让我间接参考 element 的 根底多选 这个成果去实现。
你能够点击链接先去官网体验一下最终成果,接下来我会一步一步解说如何模拟这个性能和款式去实现一个精简版的下拉选择器。
🔥element Plus 根底多选 - 款式方面,在这里我应用的是
UnoCSS
,将款式內联在了标签里,如果你还不理解这种写法,你能够点击下方的文章学习。不过即便你之前从未理解过UnoCSS
,也不会影响你上面的浏览,因为款式不是本文的重点,并不影响整体浏览。
🫱手把手教你如何创立一个代码仓库
二. 实现 selector 容器框
- 首先选择器必定有一个最根底的容器框,这个容器框最开始的时候我抉择应用原生的input 框去实现,然而其实这个想法是谬误的,或者说是不容易达成的,因为 input 里写的内容很难去高度自定义化。
-
那么咱们就换一种思路,假如这不是一个选择器,咱们就仅仅把它看作一个一般的容器,一个一般的 div 元素,总应该会实现了吧。因为是解说思路,咱们就简略的模拟一下 element 的大抵款式,就不细扣款式相干的问题了。代码如下
<script setup lang="ts"></script> <template> <div class="w-100vw h-100vh text-14px text-black flex justify-center items-center"> <div class="w-300px h-40px rounded-4px border-1px border-solid border-#2ec1cc"></div> </div> </template>
很简略的一个款式,实现成果如下:
- 这里首先须要解释一下为什么最开始我会首先先到用 input 框去实现?因为 抉择框 有一个最为根底的 聚焦 性能,所以我最实在的想法并不是想要 input 框这个标签自身,而是它身上自带的 聚焦 性能。能够测试一下,咱们先用原生的 input 元素来测试一下 onFocus 事件。
- 而后再来测试 div 标签。
能够看到,尽管我曾经在疯狂点击 div 元素了,然而它仍旧高冷,不给咱们一点反馈。很遗憾的通知你,不是你电脑卡了,而是 div 自身是没有聚焦属性的,然而咱们有一个重要属性,能够让 div 实现聚焦的性能。
- 本文的第一个重点常识:tabIndex 属性。
不要被名字吓到了,看看你键盘上罕用来切换窗口的那个键,是不是叫做 tab?
- 没错,tabIndex 中的 tab 就是对应你键盘上的那个 tab 键。index 的含意代表着你能够在 tab 键切换的时候设置 聚焦的 优先级。比方页面有三个 input。
那么代表着按下键盘的 tab 的时候,会优先选择 tabIndex 较小 那个值对应的元素。
也对应了 MDN 的解释
- 接下来只须要给 div 设置
tabIndex
即可。咱们测试一下成果:
- 那么接下来的聚焦时扭转 div 的 border 色彩还不是大海捞针?
- 那 聚焦 有了,对应的就有 失焦 成果,对应的事件是 onBlur 事件。和聚焦事件一样,不再过多赘述。
成果如下:
三. 实现 selectorItem 容器框
- 其实非常简单,应用下面的 isFocusing 变量,能够减少一个 div 通过设置 v-show 值来动静切换它的显示即可。这里须要在最外层减少一个 div 设置 relative 用来定位 selectorItem 容器框。
成果如下:
- 接下来做一个假数据来填充 item 容器框。
- 很简略的 v-for,节省时间,咱们跳过款式的书写过程,间接看成果。
四. 实现点击抉择成果
- 这里咱们创立一个变量,用来包容抉择的元素。
- 而后在容器 div 里去 v-for 这个数组。
- 这里咱们测试一下成果。唉🤔?咱们抉择的数组如同没渲染啊?v-for 生效了吗?
-
如果你和我一起书写到了这里,你可能会非常困惑地查看本人的代码到底哪里产生了问题。其实造成这个起因十分出乎你的预料。其实是因为 失焦事件 的触发早一步咱们的 点击事件 造成的,让咱们梳理一下过程。
-
- 当咱们聚焦当前,selectorItem 框呈现。
-
- 咱们点击 item,依照现实状况下,它会触发咱们绑定的 click 事件。
-
- 要害就产生了在这里一步的两头过程,留神咱们之前的 失焦 事件,当咱们点击 item 的时候,导致容器 div 的失焦事件先一步触发。
- 4 . 紧接着咱们的 v-show 使上面的 item 框隐没,故而造成 click 事件来不及触发。
- 5. 证据就是咱们明明在点击的时候增加了 console 语句,然而控制台却没有正确的输入
只散落着之前的 失焦 & 聚焦 事件触发的打印。
-
- 明确了问题的所在,就晓得该如何正确下手去解决这个问题。既然 onBlur 会先一步触发,那咱们就先把 div 身上的 失焦 事件勾销掉,只留下聚焦事件。
让咱们看一下成果:
- 让咱们疾速调整一下抉择后款式,接下来的又该面临新的问题,当初咱们因为勾销了惟一的 失焦 事件,那么咱们该如何抉择实现后勾销掉这个框框呢?
- 咱们察看到 element 组件的做法是点击屏幕空白处就能够勾销显示,那么咱们就能够模拟这个做法,间接把 onBlur 事件要做的事件间接给 document 加上。
- 到这一步你会察看到一个奇怪的景象。点击后压根什么都不显示了。
- 造成这个后果的起因也很简略,因为事件的冒泡机制,你点击这个 div 当前,因为你给 document 绑定了 onBlur 事件,所以在短时间内
isFocusing
马上就由true
变为了false
,所以咱们的页面就会看似没有任何反馈。 - 要阻止这个状况的产生,就要阻止冒泡事件的产生,在 vue 中,咱们只须要给容器绑定一个空的 click 事件,设置一个 stop 修饰符即可。
成果如下:
五. 增加勾销按钮
- 至此咱们的选择器还差一个要害的性能就实现了,能够看到 element 是能够在抉择实现的时候,勾销掉某一项的抉择。
- 这里款式我就不齐全模拟 element 组件了。咱们间接实现需求即可,性能实现起来也非常简单,就是点击叉叉的时候,在曾经抉择的数组中找到对应的 index,而后调用 splice 办法即可,比拟根底,这里不再适度赘述。
成果如下:
- 小插曲,这里 抉择 同样须要断定一下是否曾经存在,不能够反复抉择。
- 至此,尽管无奈做到齐全媲美 element UI,然而仅仅不到 100 行代码就实现的这个 selector 组件,它的所有元素和款式都能够依据需要高度自定义,用来满足咱们我的项目的需要曾经入不敷出了,
六. 源码
<script setup lang="ts">
import {ref, onMounted} from "vue";
const mock = [{ id: 1, name: "韩振方"},
{id: 2, name: "vue"},
{id: 3, name: "react"},
{id: 4, name: "前端"},
{id: 5, name: "掘金"},
{id: 6, name: "CSDN"},
{id: 7, name: "知乎"},
];
interface MockType {
id: number;
name: string;
}
const isFocusing = ref<boolean>(false);
const selectedItem = ref<MockType[]>([]);
//tips: 点击元素,push 进数组即可
function clickItem(label: MockType) {const index = selectedItem.value.findIndex((item) => label.id === item.id);
if (index === -1) selectedItem.value.push(label);
}
function focusEvent(e: FocusEvent) {console.log("聚焦");
isFocusing.value = true;
}
function blurEvent() {console.log("失焦");
isFocusing.value = false;
}
function unSelectItem(label: MockType) {const index = selectedItem.value.findIndex((item) => label.id === item.id);
if (index !== -1) selectedItem.value.splice(index, 1);
}
onMounted(() => {document.addEventListener("click", () => {isFocusing.value = false;});
});
</script>
<template>
<div class="w-100vw h-100vh text-14px text-black">
<div class="relative w-full mt-100px flex justify-center items-center">
<div
@click.stop=""@focus="focusEvent"tabindex="1":class="isFocusing ? 'border-black' : 'border-#2ec1cc'"
class="w-300px h-40px rounded-4px border-1px border-solid flex items-center flex-nowrap"
>
<div
v-for="item in selectedItem"
class="inline-block px-10px leading-28px bg-#F7F9FA rounded-4px flex gap-8px items-center shrink-0"
>
<span class="text-#202020 text-14px">
{{item.name}}
</span>
<button
@click.stop="unSelectItem(item)"
class="cursor-pointer border-none flex items-center justify-center"
>
<span>x</span>
</button>
</div>
</div>
<div
v-show="isFocusing"
class="w-300px h-274px absolute overflow-y-auto bg-white z-999 rounded-6px border-1px border-#e4e6e5 flex flex-col py-10px"
:style="{
top: `60px`,
boxShadow: '0px 0px 10px rgba(0,0,0,0.1)',
}"
>
<div
v-for="item in mock"
@click.stop="clickItem(item)"
class="w-full leading-37px cursor-pointer hover:bg-#f6f7fa px-20px shrink-0 flex justify-between items-center"
>
<span class="text-14px">
{{item.name}}
</span>
</div>
</div>
</div>
</div>
</template>
七. 总结
其实在日常开发中,组件库的性能尽管全,然而有些状况下咱们仅仅只是用到了外面很少一部分的性能,这时候全副引进的话又显得很没有必要,这时候通过模拟组件库的性能来实现一个轻量级的组件还是十分有必要的。通过这着组件的设计和实现,又把握了很多之前没接触过的常识。比方 tabIndex、失焦 和汇集 事件的优先级🎁。