从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 提供了严格的类型检查,确保组件的健壮性和可维护性。

相关推荐
腾讯TNTWeb前端团队5 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰9 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪9 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪9 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy9 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom10 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom10 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom10 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom10 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom11 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试