当前位置 : 主页 > 网络编程 > JavaScript >

手写实现vue2下拉菜单dropdown组件实例

来源:互联网 收集:自由互联 发布时间:2023-02-08
目录 概述 最终效果(动图没显示出来,请稍定会儿,可以先看后面) 实现原理 具体实现 目录结构 emitter.js MyDropdown.vue MyDropdownMenu.vue MyDropdownItem.vue 总结 概述 一般后台项目结合分页组
目录
  • 概述
  • 最终效果(动图没显示出来,请稍定会儿,可以先看后面)
  • 实现原理
  • 具体实现
    • 目录结构
    • emitter.js
    • MyDropdown.vue
    • MyDropdownMenu.vue
    • MyDropdownItem.vue
  • 总结

    概述

    一般后台项目结合分页组件用到这个dropdown组件,用来做显示分页条数,其他用到这个组件的地方倒不是很多,其实现思路和select组件有那么些相似,现记录下这个组件的实现。

    最终效果(动图没显示出来,请稍定会儿,可以先看后面)

    实现原理

    这个组件和select组件记起相似,可以参考我之前的文章【手写vue2select下拉组件】,要做这个组件,需要注意以下几点:

    组件分为两部分:

    • 供我们点击的文字,按钮,超链接等等(当成插槽供用户提供)
    • 下拉菜单项(支持边框,禁用等)

    使用该组件应当提供的事件应该是点击item项,然后将对应的item的对应value暴露出来,供用户使用。

    组件菜单项的显示隐藏需要过渡动画。

    默认菜单项方向向下,当下方可视区的高度不足以容纳下拉菜单的高度的时候,自动让菜单方向向上。

    具体实现

    目录结构

    emitter.js

    这个在之前的组件实现过程中介绍过这个文件,主要是为了解决跨多层级父子组件之前数据通信的,本质上实现原理为发布订阅模式。

    /**
     * @Description 由于涉及到跨组件之间通信,因此我们只有自己实现发布订阅的模式,来实现组件之间通信,灵感主要来源于element-ui组件库源码中跨层级父子组件通信方案,本质上也是发布订阅和$emit和$on
     * @param { String } componentName 组件名
     * @param { String } eventName 事件名
     * @param { argument } params 参数
     **/
    // 广播通知事件
    function _broadcast(componentName, eventName, params) {
      // 遍历当前组件的子组件
      this.$children.forEach(function (child) {
        // 取出componentName,组件options上面可以自己配置
        var name = child.$options.componentName;
        // 如果找到了需要通知的组件名,触发组件上面的$eimit方法,触发自定义事件
        if (name === componentName) {
          child.$emit.apply(child, [eventName].concat(params));
        } else {
          // 没找到,递归往下找
          _broadcast.apply(child, [componentName, eventName].concat([params]));
        }
      });
    }
    const emiiter = {
      methods: {
        // 派发事件(通知父组件)
        dispatch(componentName, eventName, params) {
          var parent = this.$parent || this.$root;
          var name = parent.$options.componentName;
          // 循环往上层父组件,知道知道组件名和需要触发的组件名相同即可,然后触发对应组件的事件
          while (parent && (!name || name !== componentName)) {
            parent = parent.$parent;
            if (parent) {
              name = parent.$options.componentName;
            }
          }
          if (parent) {
            parent.$emit.apply(parent, [eventName].concat(params));
          }
        },
        // 广播事件(通知子组件)
        broadcast(componentName, eventName, params) {
          _broadcast.call(this, componentName, eventName, params);
        },
      },
    };
    export default emiiter;
    

    MyDropdown.vue

    主要暴露给用户使用的组件

    <template>
      <div
        class="my-dropdown"
        @click.stop="trigger == 'click' ? (showMenu = !showMenu) : ''"
        @mouseenter="trigger == 'hover' ? (showMenu = true) : ''"
        @mouseleave="trigger == 'hover' ? (showMenu = false) : ''"
        ref="myDropDdown"
      >
        <div class="tip-text" ref="tipText">
          <slot></slot>
        </div>
        <slot name="list"></slot>
      </div>
    </template>
    <script>
    import emitter from "./emitter";
    export default {
      name: "MyDropdown",
      componentName: "MyDropdown",
      mixins: [emitter],
      props: {
        // 触发显示方式
        trigger: {
          type: String,
          default: "click",
        },
        // 下来菜单的出现位置(上方,下方)
        placement: {
          type: String,
          default: "bottom",
          validator: function (value) {
            // 这个值必须匹配下列字符串中的一个
            return ["bottom", "top"].includes(value);
          },
        },
      },
      data() {
        return {
        //控制菜单是否显示
          showMenu: false,
        };
      },
      mounted() {
          //初始化自定义事件
        this.initEvent();
      },
      methods: {
        // 初始化
        initEvent() {
        //订阅当item点击的时候,发布on-click事件,告知外部
          this.$on("item-click", (params) => {
            this.$emit("on-click", params);
            this.showMenu = false;
          });
          //空白点击要隐藏菜单,需要执行的函数需要绑定this指向
          this.handleEmptyDomElementClickBindThis =
            this.handleEmptyDomElementClick.bind(this);
          window.addEventListener("click", this.handleEmptyDomElementClickBindThis);
        },
        // 处理空白区域点击,隐藏菜单列表
        handleEmptyDomElementClick(e) {
          if (!Array.from(this.$refs.myDropDdown.childNodes).includes(e.target)) {
            this.showMenu = false;
          }
        },
      },
      beforeDestroy() {
        // 移除window上面的事件
        window.removeEventListener(this.handleEmptyDomElementClickBindThis);
      },
      watch: {
      //变化的时候,通知子组件隐藏菜单列表
        showMenu() {
          this.broadcast("MyDropdownMenu", "set-menu-status", this.showMenu);
        },
      },
    };
    </script>
    <style lang="less">
    .my-dropdown {
      position: relative;
    }
    </style>
    

    MyDropdownMenu.vue

    主要暴露给用户使用的组件,菜单列表组件

    <template>
    <!-- 涉及到高度,位移,过渡使用js钩子函数的方式比较好处理些 -->
      <transition
        @before-enter="beforeEnter"
        @enter="enter"
        @leave="leave"
        v-bind:css="false"
      >
        <div class="my-dropdown-menu" v-if="showMeune" ref="myDroupdownMenu">
          <slot></slot>
        </div>
      </transition>
    </template>
    <script>
    import emitter from "./emitter";
    export default {
      name: "MyDropdownMenu",
      componentName: "MyDropdownMenu",
      mixins: [emitter],
      data() {
        return {
          showMeune: false,
          timer: null,
        };
      },
      mounted() {
        this.$on("set-menu-status", (status) => {
          this.showMeune = status;
        });
      },
      methods: {
      //进入前,初始化需要过渡的属性
        beforeEnter: function (el) {
          // 初始化
          el.style.opacity = 0;
          el.style.transform = "scaleY(0)";
          el.style.transformOrigin = "top center";
        },
      //dom进入
        enter: function (el, done) {
        //获取文档可视区高度
          const htmlClientHeight = document.documentElement.clientHeight;
          //菜单列表相对于父元素的top偏移量
          const offsetTop = el.offsetTop;
          const scrollHeight = el.scrollHeight;
          //获取当前元素和可视区的一些长度(top,left,bottom等)
          const { bottom } = el.getBoundingClientRect();
          // 说明底部高度不够显示菜单了,这时候我们需要调整菜单朝上面显示
          if (htmlClientHeight - bottom < scrollHeight) {
            el.style.transformOrigin = "bottom center";
            el.style.top = -(scrollHeight + 20) + "px";
          } else {
          //查看是否placement属性,是的话我们主动处理
            if (this.$parent.placement == "top") {
              el.style.transformOrigin = "bottom center";
              el.style.top = -(scrollHeight + 20) + "px";
            } else {
              el.style.top = offsetTop + "px";
            }
          }
          el.style.transform = "scaleY(1)";
          el.style.opacity = 1;
        //根据官网事例,必须在enter和leave里面调用done函数,不然过渡动画不生效(切记)
          done();
        },
        //dom元素离开 
        leave: function (el, done) {
          el.style.transform = "scaleY(0)";
          el.style.opacity = 0;
          clearTimeout(this.timer);
          this.timer = setTimeout(() => {
          //根据官网事例,必须在enter和leave里面调用done函数,不然过渡动画不生效(切记)
            done();
          }, 250);
        },
      },
    };
    </script>
    <style lang="less">
    .my-dropdown-menu {
      min-width: 100px;
      max-height: 200px;
      overflow: auto;
      margin: 5px 0;
      padding: 5px 0;
      background-color: #fff;
      box-sizing: border-box;
      border-radius: 4px;
      box-shadow: 0 1px 6px rgb(0 0 0 / 20%);
      z-index: 900;
      transform-origin: top center;
      position: absolute;
      transition: transform 0.25s ease, opacity 0.25s ease;
    }
    </style>
    

    MyDropdownItem.vue

    主要暴露给用户使用的组件,菜单列表项组件,组件内容很简单,主要就是展示item数据和绑定点击事件。

    <template>
      <div
        :class="[
          'my-dropdownItem',
          divided ? 'my-dropdownItem-divided' : '',
          disabled ? 'my-dropdownItem-disabled' : '',
        ]"
        @click.stop="handleItemClick"
      >
        <slot></slot>
      </div>
    </template>
    <script>
    import emitter from "./emitter";
    export default {
      name: "MyDropdownItem",
      componentName: "MyDropdownItem",
      mixins: [emitter],
      props: {
        divided: {
          type: Boolean,
          default: false,
        },
        disabled: {
          type: Boolean,
          default: false,
        },
        name: {
          type: String,
          default: "",
        },
      },
      data() {
        return {};
      },
      methods: {
        handleItemClick() {
          if (this.disabled) return;
          // item项点击通知dropdown组件派发到外部的自定义事件
          this.dispatch("MyDropdown", "item-click", this.name);
        },
      },
    };
    </script>
    <style lang="less">
    .my-dropdownItem {
      margin: 0;
      line-height: normal;
      padding: 7px 16px;
      clear: both;
      color: #515a6e;
      font-size: 14px !important;
      white-space: nowrap;
      list-style: none;
      cursor: pointer;
      transition: background 0.2s ease-in-out;
      &:hover {
        background: #f3f3f3;
      }
    }
    .my-dropdownItem-divided {
      border-bottom: 1px solid #eee;
    }
    .my-dropdownItem-disabled {
      color: #cacdd2;
      cursor: not-allowed;
      &:hover {
        background: #fff;
      }
    }
    </style>
    

    总结

    类似组件库中的这种经常出现跨多层级组件通信,需要特殊处理,一般emitter.js文件里面的封装我们在开发中一般是用不到的,我们写组件经常也就父子组件之间,很少会跨祖孙级别,但是在组件库中,这种关系就很多,因此需要单独利用发布订阅来处理,这种模式用到实际项目里面也是很管用的。

    以上就是手写实现vue2下拉菜单dropdown组件实例的详细内容,更多关于vue 下拉菜单dropdown的资料请关注易盾网络其它相关文章!

    上一篇:手把手教你Vue3实现路由跳转
    下一篇:没有了
    网友评论