仿造 elementUI 实现一个下拉选择器

news2024/12/25 15:33:44

前言: 近期在项目中遇到了一个设计需求,在 UI 给我提供的原图中有一个和 element UI 选择器功能基本一致的样式,但是由于我们是有自己的主体颜色和一些细节上的样式设计的,无法直接复用 element 组件库。所以需要自己动手实现一个下拉选择器,最开始以为很复杂,但其实在查阅了相关知识后,实现起来也谈不上很难,并且关键是自己又探索了一片空白知识区域,很开心,遂特来分享一下自己的实现思路🎁。(本文并不是解读 element 的源码,而是仿照它的功能来自己完成一个基础组件。)

(tips:本文不区分框架,无论你是 react 开发还是 vue 开发,思路是一致的)

一. 设计需求

  1. 由于我们项目的场景比较单一,所以 UI 让我直接参考 element基础多选这个效果去实现。
    image.png
    你可以点击链接先去官网体验一下最终效果,接下来我会一步一步讲解如何模仿这个功能和样式去实现一个精简版的下拉选择器。
    🔥element Plus 基础多选

  2. 样式方面,在这里我使用的是 UnoCSS ,将样式內联在了标签里,如果你还不了解这种写法,你可以点击下方的文章学习。不过即使你之前从未了解过 UnoCSS ,也不会影响你下面的阅读,因为样式不是本文的重点,并不影响整体阅读。
    🫱手把手教你如何创建一个代码仓库

二. 实现 selector 容器框

  1. 首先选择器肯定有一个最基础的容器框,这个容器框最开始的时候我选择使用原生的input 框去实现,但是其实这个想法是错误的,或者说是不容易达成的,因为 input 里写的内容很难去高度自定义化。
    image.png

  2. 那么我们就换一种思路,假设这不是一个选择器,我们就仅仅把它看作一个普通的容器,一个普通的 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>
    
    

    很简单的一个样式,实现效果如下:
    image.png

  3. 这里首先需要解释一下为什么最开始我会首先先到用 input 框去实现?因为选择框有一个最为基础的聚焦功能,所以我最真实的想法并不是想要 input 框这个标签本身,而是它身上自带的聚焦功能。可以测试一下,我们先用原生的 input 元素来测试一下 onFocus 事件。
    image.png
    11.gif

  4. 然后再来测试 div 标签。
    image.png
    可以看到,虽然我已经在疯狂点击 div 元素了,但是它依旧高冷,不给我们一点反馈。很遗憾的告诉你,不是你电脑卡了,而是 div 本身是没有聚焦属性的,但是我们有一个重要属性,可以让 div 实现聚焦的功能。
    1.gif

  5. 本文的第一个重点知识:tabIndex 属性。
    image.png
    不要被名字吓到了,看看你键盘上常用来切换窗口的那个键,是不是叫做 tab
    image.png

  6. 没错,tabIndex 中的 tab 就是对应你键盘上的那个 tab 键。index 的含义代表着你可以在 tab 键切换的时候设置聚焦的优先级。比如页面有三个 input
    image.png
    那么代表着按下键盘的 tab 的时候,会优先选择 tabIndex较小那个值对应的元素。
    2.gif
    也对应了 MDN 的解释
    image.png

  7. 接下来只需要给 div 设置 tabIndex 即可。
    image.png
    我们测试一下效果:
    3.gif

  8. 那么接下来的聚焦时改变 divborder 颜色还不是易如反掌?
    image.png

  9. 聚焦有了,对应的就有失焦效果,对应的事件是 onBlur 事件。和聚焦事件一样,不再过多赘述。
    image.png
    效果如下:
    5.gif

三. 实现 selectorItem 容器框

  1. 其实非常简单,使用上面的 isFocusing 变量,可以增加一个 div 通过设置 v-show 值来动态切换它的显示即可。这里需要在最外层增加一个 div 设置 relative 用来定位 selectorItem 容器框。
    image.png
    效果如下:
    6.gif

  2. 接下来做一个假数据来填充 item 容器框。
    image.png

  3. 很简单的 v-for,节省时间,我们跳过样式的书写过程,直接看效果。
    7.gif

四. 实现点击选择效果

  1. 这里我们创建一个变量,用来容纳选择的元素。
    image.png

  2. 然后在容器 div 里去 v-for 这个数组。
    image.png

  3. 这里我们测试一下效果。唉🤔?我们选择的数组好像没渲染啊? v-for 失效了吗?
    8.gif

  4. 如果你和我一起书写到了这里,你可能会十分困惑地检查自己的代码到底哪里发生了问题。其实造成这个原因非常出乎你的意料。其实是由于失焦事件的触发早一步我们的点击事件造成的,让我们梳理一下过程。

      1. 当我们聚焦以后, selectorItem 框出现。
        image.png
      1. 我们点击 item,按照理想情况下,它会触发我们绑定的click事件。
        image.png
      1. 关键就发生了在这里一步的中间过程,注意我们之前的失焦事件,当我们点击 item 的时候,导致容器 div 的失焦事件先一步触发。
        image.png
    • 4 . 紧接着我们的 v-show 使下面的 item 框消失,故而造成 click 事件来不及触发。
      image.png

    • 5.证据就是我们明明在点击的时候添加了 console 语句,但是控制台却没有正确的输出
      image.png
      只散落着之前的失焦&聚焦事件触发的打印。
      image.png

  5. 明白了问题的所在,就知道该如何正确下手去解决这个问题。既然 onBlur 会先一步触发,那我们就先把 div 身上的失焦事件取消掉,只留下聚焦事件。
    image.png
    让我们看一下效果:
    9.gif

  6. 让我们快速调整一下选择后样式,接下来的又该面临新的问题,现在我们由于取消了唯一的失焦事件,那么我们该如何选择完成后取消掉这个框框呢?
    1.gif

  7. 我们观察到 element 组件的做法是点击屏幕空白处就可以取消显示,那么我们就可以模仿这个做法,直接把 onBlur 事件要做的事情直接给 document 加上。
    image.png

  8. 到这一步你会观察到一个奇怪的现象。点击后压根什么都不显示了。
    3.gif

  9. 造成这个结果的原因也很简单,因为事件的冒泡机制,你点击这个 div 以后,由于你给 document 绑定了 onBlur 事件,所以在短时间内 isFocusing 马上就由 true 变为了 false,所以我们的页面就会看似没有任何反应。

  10. 要阻止这个情况的发生,就要阻止冒泡事件的发生,在 vue 中,我们只需要给容器绑定一个空的 click 事件,设置一个 stop 修饰符即可。
    image.png
    效果如下:
    4.gif

五. 添加取消按钮

  1. 至此我们的选择器还差一个关键的功能就完成了,可以看到 element 是可以在选择完成的时候,取消掉某一项的选择。
    5.gif

  2. 这里样式我就不完全模仿 element 组件了。我们直接实现需求即可,功能实现起来也非常简单,就是点击叉叉的时候,在已经选择的数组中找到对应的 index,然后调用 splice 方法即可,比较基础,这里不再过度赘述。
    image.png
    效果如下:
    6.gif

  3. 小插曲,这里选择同样需要判定一下是否已经存在,不可以重复选择。
    image.png

  4. 至此,虽然无法做到完全媲美 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失焦聚集事件的优先级🎁。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/682619.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux3.权限

1.外壳程序(shell):通常情况下用户不直接与操作系统进行交互&#xff0c;而是通过外壳程序&#xff0c;在windows下叫做图形化界面&#xff0c;在Linux下叫做指令。 centos7使用的外壳程序是bash。 作用 :a.是用户和操作系统交互的中间软件层。b可以在一定程度起到保护操作系统…

HackTheBox - 学院【CPTS】复习3 - XSS、文件包含、文件上传、命令注入

XSS 登录表单 document.write(<h3>Please login to continue</h3><form actionhttp://OUR_IP><input type"username" name"username" placeholder"Username"><input type"password" name"password&…

动态规划:积木画

积木画 问题描述 小明最近迷上了积木画, 有这么两种类型的积木, 分别为 I I I 型&#xff08;大小为 2 个单位面积) 和 L L L 型 (大小为 3 个单位面积): 同时, 小明有一块面积大小为 2 N 2 \times N 2N 的画布, 画布由 2 N 2 \times N 2N 个 1 1 1 \times 1 11 区域…

如何解读 Java 的继承和多态特性?

&#x1f482; 个人网站:【海拥】【游戏大全】【神级源码资源网】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 寻找学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】 目录 前言继承类的继承继承的…

Gartner宣布,亚马逊云科技全球数据库市场份额超四分之一

对比常规的基础设施上云和应用上云,企业对于数据上云一直保持最为慎重的态度。不过也不是一成不变的,Gartner前不久公布的一组数据显示,在2022年全球数据库管理系统的市场份额排名中,作为纯云厂商的亚马逊云科技,超越了老牌传统数据库厂商甲骨文和微软,首次位居第一。 降低企业…

Qt信号和信号槽(二)

目录 信号槽使用拓展 示例1&#xff08;一个信号可以对应多个槽函数&#xff09;&#xff08;在上篇文章的代码中进行修改&#xff09;&#xff1a; 示例2(用信号连接信号)&#xff1a; 信号槽的连接方式 示例&#xff1a; Lambda表达式 语法格式 定义和调用 信号槽使用拓…

【金融量化】如何筛选基金?

基金的评价与筛选 1 筛选步骤 1.1 股票型基金 &#xff08;1&#xff09;构建备选池 优先考虑股票配置较为稳定的基金&#xff0c;这样才能预估基金未来一段时间的表现&#xff0c;及其对基准股票指数的跟踪情况。因此&#xff0c;首先应该剔除那些仓位变化较大、本身在进行…

[刷题] 删除有序数组中的重复项

系列文章目录 删除有序数组中的重复项 文章目录 系列文章目录在这里插入图片描述 前言1、题目1.2、判题标准1.3、示例 2、解题2.1、双指针正向查找2.1、逆序删除 3、总结 前言 数据结构在程序世界里非常重要&#xff0c;尤其大厂面试是必考项目&#xff0c;今天随机到[删除有…

“三步走”构建全链路数据能力,助力企业全面唤醒数据价值

01 企业数字化转型加速前进&#xff0c;数据价值唤醒仍面临多重挑战 数字经济蓬勃发展时代&#xff0c;数据已成为关键生产要素。随着国家政策对数据要素价值释放的方向引领&#xff0c;数据赋能企业经营决策、业务模式创新的需求不断强化&#xff0c;以及新兴技术工具的有效支…

[LeetCode 1401]圆和矩形是否有重叠

题目描述 给你一个以 (radius, xCenter, yCenter) 表示的圆和一个与坐标轴平行的矩形 (x1, y1, x2, y2) &#xff0c;其中 (x1, y1) 是矩形左下角的坐标&#xff0c;而 (x2, y2) 是右上角的坐标。 如果圆和矩形有重叠的部分&#xff0c;请你返回 true &#xff0c;否则返回 f…

音乐格式转换器mp3免费方法?分享四个实用的!

在日常工作和娱乐中&#xff0c;我们经常会遇到并使用MP3这种音频格式。它以小文件尺寸和优秀音质为特点&#xff0c;成为许多音频文件的首选格式。然而&#xff0c;当我们面对其他音频格式时&#xff0c;可能需要进行转换为MP3的操作。因此&#xff0c;接下来我将向您分享4种简…

篇章十一 Vuex

文章目录 一、理解 Vuex1. 是什么2. 什么时候用 二、Vuex 工作原理三、Vuex 环境搭建四、四个 map 方法的使用五、模块化 命名空间 一、理解 Vuex 1. 是什么 Vue 中实现集中式状态&#xff08;数据&#xff09;管理的一个 Vue 插件&#xff0c;对 vue 应用中多个组件的共享状…

蓝牙室内定位|蓝牙信标iBeacon部署原则

室内定位导航给我们的工作生活带来了诸多的便利&#xff0c;越来越多的企业愿意来SKYLAB了解室内定位方案&#xff0c;并根据自己的实际应用需求来选择米级蓝牙室内定位方案和厘米级UWB室内定位方案。今天SKYLAB君就来简单介绍一下蓝牙Beacon室内定位导航方案中Beacon的部署原则…

JAVA基础:线程池的使用

目录 1.概述 2.线程池的优势​​​​​​​ 2.1.线程池为什么使用自定义方式&#xff1f; 2.2.封装的线程池工具类有什么好处&#xff1f; 3.线程池的七大参数 3.线程池的创建 3.1. 固定数量的线程池 3.2. 带缓存的线程池 3.3. 执⾏定时任务 3.4. 定时任务单线程 3.…

掌握唯米系统ChatGPT批量生成文章的操作技巧

以下是重写后的操作步骤&#xff1a; 1. 购买会员并添加个人的ChatGPT密钥&#xff1a; 首先&#xff0c;您需要购买唯米系统的会员&#xff0c;并获得访问ChatGPT的权限。随后&#xff0c;您可以将个人的ChatGPT密钥添加到系统中&#xff0c;以便使用该功能进行自然语言生成和…

Spring Session使用

一.使用场景 前后端不分离的情况下&#xff0c;往登陆页auth.gulimall.com的session中存放一个用户信息&#xff0c;想要在首页gulimall.com中取出该数据并展示出来 GetMapping("/oauth2.0/gitee/success")public String oauth2(RequestParam("code") Str…

组态王与PLC通过RJ45口建立无线以太网通讯

本文以组态王和2台三菱FX5u PLC为例&#xff0c;介绍组态王与多台 PLC的无线以太网通信实现过程。在本方案中采用了三菱PLC无线通讯终端DTD419MB&#xff0c;作为实现无线通讯的硬件设备。 在这一无线以太网通讯系统的搭建中&#xff0c;用户无需更改网络参数和原有程序&#…

Ubuntu的USB相关操作

这里写目录标题 0.信息查看1. 串口设备设置2. 串口调试助手 0.信息查看 指令lsusb输出Bus 004 Device 002: ID 05e3:0620 Genesys Logic, Inc. USB3.2 Hub Bus 004 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub Bus 003 Device 006: ID 5986:115f Acer, Inc Integ…

佩戴最舒服的蓝牙耳机推荐,好用、佩戴体验很不错的蓝牙耳机分享

​面对市面上不同场景使用、不同类型的蓝牙耳机&#xff0c;我们选购蓝牙耳机时应该如何选&#xff1f;最怕遇到耳机延迟高、不防水防汗、音质表现差以及佩戴体验差的蓝牙耳机&#xff0c;针对这些经常面临的问题&#xff0c;我这次精选了四款市面上热销质量不错的蓝牙耳机分享…

PMP考试经验分享,准备不要超过三个月‼️

因为各种原因&#xff08;拖延、贪玩、上课 哈哈&#xff09; 我是用一个半月时间准备的PMP考试 3A通过 努力➕幸运的结果 资料准备&#xff1a; PMBOK第六版和第七版&#xff08;PMI官方教材&#xff09; 机构的视频材料&#xff08;巩固基础&#xff09; 模拟题库&#…