从0搭建Vue3组件库之Dropdown组件

在前端开发中,一个功能齐全、灵活易用的 Dropdown 组件是必不可少的。本文将带你从零搭建一个基于 Vue3 的 DropdownP 组件,展示从需求分析到代码实现的完整流程。

需求分析

  • 基于现有 Tooltip 组件扩展功能:复用已有组件,提升开发效率。

  • 显示菜单列表:支持多选项菜单,内容可灵活配置。

  • 自定义复杂选项:用户可传入复杂的自定义节点。

  • 支持语义化结构:提高可访问性与易用性。

确定方案

  • 属性
typescript 复制代码
import type { VNode } from "vue";
import type { TooltipProps } from "../Tooltip/types";
// 需要在Tooltip组件的属性上进行继承和拓展
export interface DropdownProps extends TooltipProps {
  // 传入的菜单项,用一个数组保存
  menuOptions: MenuOption[];
  // 点击某一项之后关闭菜单展示
  afterClickItem?: boolean;
}
// 每一项列表具有的属性
export interface MenuOption {
   // 字符串或自定义节点
  label: string | VNode;
  key: string | number;
  disabled?: boolean;
  // 分割线 
  divided?: boolean;
}
  • 事件

DropdownP 支持以下事件:

  • visible-change:当菜单显隐状态变化时触发。
  • select:当用户选择菜单项时触发。
typescript 复制代码
export interface DropdownEmits {
   // 打开列表派发事件
  (e: "visible-change", value: boolean): void;
  // 选中选项派发事件
  (e: "select", value: MenuOption): void;
}
  • 实例

通过暴露实例方法,用户可以在外部手动控制菜单显隐状态:

dart 复制代码
export interface DropdownInstance {
  show: () => void;
  hide: () => void;
}
  • 组件
xml 复制代码
<template>
    <div class="yv-dropdown">
        <Tooltip>
        <!-- 触发区域 -->
            <slot></slot>
            <!-- 内容区域 -->
            <template #content>
                <ul>
                    <template >
                        <!-- 分割线 -->
                        <hr >
                        <!-- 列表 -->
                        <li>
                            <RenderVnode  />
                        </li>
                    </template>
                </ul>
            </template>
        </Tooltip>
    </div>
</template>

设计思路

  • 使用Tooltip组件进行二次开发
  • 菜单列表使用Vnode节点,用户可自定义
  • 列表之间可以添加分割线
  • 默认插槽为触发区域,触发区域设置为Tooltip中预留的content部分
xml 复制代码
<template>
  <div class="jd-dropdown">
    <Tooltip
      :trigger="trigger"
      :placement="placement"
      :popper-options="popperOptions"
      :open-delay="openDelay"
      :close-delay="closeDelay"
      @visible-change="visibleChange"
      :manual="manual"
      ref="tooltipRef"
    >
      <slot />
      <template #content>
        <ul class="jd-dropdown__menu">
          <template v-for="item in menuOptions" :key="item.key">
            <!-- 分割线 -->
            <li
              v-if="item.divided"
              role="separator"
              class="divided-placeholder"
            />
            <!-- 菜单项 -->
            <li
              class="jd-dropdown__item"
              @click="itemClick(item)"
              :class="{
                'is-disabled': item.disabled,
                'is-divided': item.divided,
              }"
              :id="`dropdown-item-${item.key}`"
            >
              <!-- 中介组件,用户可自定义 -->
              <RenderVnode :vNode="item.label" />
            </li>
          </template>
        </ul>
      </template>
    </Tooltip>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import type { Ref } from "vue";
import type {
  DropdownProps,
  DropdownInstance,
  DropdownEmits,
  MenuOption,
} from "./types";
import type { TooltipInstance } from "../Tooltip/types";
import Tooltip from "../Tooltip/Tooltip.vue";
import RenderVnode from "../Common/RenderVnode";

defineOptions({
  name: "JdDropdown",
});
const props = withDefaults(defineProps<DropdownProps>(), {
  afterClickItem: true,
});
const emits = defineEmits<DropdownEmits>();

const tooltipRef = ref() as Ref<TooltipInstance>;

const visibleChange = (e: boolean) => {
  emits("visible-change", e);
};

const itemClick = (e: MenuOption) => {
  if (e.disabled) return;
  emits("select", e);
  if (props.afterClickItem) {
    tooltipRef.value?.hide();
  }
};

defineExpose<DropdownInstance>({
  // 通过闭包的形式,直接赋值会拿收不到对应的节点,因为是在setup函数中,此时实例还未被挂载
  show: () => tooltipRef.value?.show(),
  hide: () => tooltipRef.value?.hide(),
});
</script>
typescript 复制代码
//types.ts
import type { VNode } from "vue";
import type { TooltipProps } from "../Tooltip/types";

export interface DropdownProps extends TooltipProps {
  // 传入的菜单项,用一个数组保存
  menuOptions: MenuOption[];
  // 点击某一项之后关闭菜单展示
  afterClickItem?: boolean;
}
// 每一项列表具有的属性
export interface MenuOption {
  // 字符串或自定义节点
  label: string | VNode;
  key: string | number;// 每个选项的唯一标识
  disabled?: boolean;
  // 分割线
  divided?: boolean;
}

export interface DropdownEmits {
  // 打开列表派发事件
  (e: "visible-change", value: boolean): void;
  // 选中选项派发事件
  (e: "select", value: MenuOption): void;
}

export interface DropdownInstance {
  show: () => void;
  hide: () => void;
}

总结

通过上述步骤,我们成功地从零搭建了一个高度可定制的 Vue 3 Dropdown 组件。该组件不仅继承了 Tooltip 组件的弹出层管理功能,还增加了菜单列表、自定义选项、分割线等特性,能够满足大多数实际项目中的需求。此外,我们还通过 TypeScript 提供了严格的类型检查,确保组件的健壮性和可维护性。

相关推荐
hhw19911213 分钟前
JavaScript知识点1
开发语言·前端·javascript
患得患失94917 分钟前
【前端】【功能函数】treeMapEach,对每个节点进行自定义转换的实用函数
前端
卡夫卡的小熊猫2 小时前
vue前端菜单权限控制
前端
祈澈菇凉3 小时前
什么是 Vue 的自定义事件?如何触发和监听?
前端·javascript·vue.js
2301_766536055 小时前
调试无痛入手
开发语言·前端
@大迁世界6 小时前
构建 Next.js 应用时的安全保障与风险防范措施
开发语言·前端·javascript·安全·ecmascript
IT、木易7 小时前
ES6 新特性,优势和用法?
前端·ecmascript·es6
is今夕7 小时前
postcss.config.js 动态配置基准值
javascript·vue.js·postcss
青茶绿梅*27 小时前
500字理透react的hook闭包问题
javascript·react.js·ecmascript