Vue cli3 学习

vue 自定义指令封装一个手风琴嵌套组件

2020-02-17  本文已影响0人  一颗数据小白菜

很多教程的手风琴组件都是一个v-for数组来实现手风琴组件,v-for封装起来很简单,但是我认为并不好。
理由如下:

所以自己用自定义指令实现了一个手风琴组件。

代码很长,不想学习的可以直接github下载代码run serve直接使用。

效果图
  • 支持初始化撑开多个折叠版,点击任意一个之后会将其他撑开的都关闭。
  • 初始撑开为props 参数visible=true
  • 后续会更新支持多个不同手风琴,支持开启手风琴模式

结构

<!-- 由于本身是封装的一个边侧导航栏,所有组件中有
NLY-accordionNav
NLY-accordionNavItem
NLY-accordionNavTree
 -->
<NLY-accordionNav>
  <NLY-accordionNavItem icon="nlyblog nly-blog-home">
    Nejinn
  </NLY-accordionNavItem>
  <NLY-accordionNavTree icon="nlyblog nly-blog-book" v-nly-accordion.sss>
    Nejinn
  </NLY-accordionNavTree>
  <NLY-accordionNavCollapse id="sss" visible>
    <NLY-accordionNavItem icon="nlyblog nly-blog-home">
      lerity
    </NLY-accordionNavItem>
    <NLY-accordionNavItem icon="nlyblog nly-blog-home">
      blog
    </NLY-accordionNavItem>
  </NLY-accordionNavCollapse>
</NLY-accordionNav>
<任意元素 v-nly-accordion.collapseId>
</任意元素元素>
<NLY-accordionNavCollapse id='collapseId'>
    ...嵌套元素,随意插入
</NLY-accordionNavCollapse>

demo:
<div v-nly-accordion.collapse1>点击我收起或展开 collapse1</div>
<NLY-accordionNavCollapse id='collapse1'>
  <a>我是折叠版中的元素</a>
</NLY-accordionNavCollapse>

<div v-nly-accordion.collapse2>点击我收起或展开collapse2</div>
<NLY-accordionNavCollapse id='collapse2'>
  <a>我是折叠版中的元素</a>
</NLY-accordionNavCollapse>
使用这种结构的时候,请注意自己写css。可以在accordion.vue中修改就行。

组件目录结构

目录结构.jpg

自定义指令 v-nly-accordion

// nlyaccordion.js
import Vue from "vue";

/**
 * 差集函数
 */
function getDifference(allCollapseId, idKeys) {
  let mixArray = [];
  allCollapseId.forEach(item => {
    if (idKeys.indexOf(item) == -1) {
      mixArray.push(item);
    }
  });
  return mixArray;
}

Vue.directive("nly-accordion", function(el, binding, vnode) {
  /**
   * 初始化指令时监控collapseStatus事件,collapseStatus事件由NLY-accordionNavCollapse组件发出,有2个参数,
   * 一个是NLY-accordionNavCollapse事件props参数id,
   * 一个是NLY-accordionNavCollapse折叠状态show
   * function(a,b)中a是show,b是id
   */
  vnode.context.$root.$on("collapseStatus", function(a, b) {
    // 将所有提交collapseStatus事件的NLY-accordionNavCollapse组件的id放入allCollapseId
    if (allCollapseId.indexOf(b) == -1) {
      allCollapseId.push(b);
    }

    /**
     * 获取当前指令的modifiers,如果当前指令的modifiers中包含提交collapseStatus事件的NLY-accordionNavCollapse组件的id
     * 则初始化挂载指令的组件或者element的class,且修改当前指令modifiers为对应的show值
     * 对应的初始化show为true,则在当前挂载指令的element的class中添加open
     * 对应的初始化show为false,则在当前挂载指令的element的class中移除open
     */
    let idKeys = Object.keys(binding.modifiers);
    if (idKeys.indexOf(b) != -1) {
      binding.modifiers[String(b)] = a;
      if (a) {
        el.classList.add("open");
      } else {
        el.classList.remove("open");
      }
    }
  });

  /**
   * 新建一个array储存组件NLY-accordionNavCollapse的id
   * 注意会先执行这里的代码再执行上面的代码。
   */
  let allCollapseId = [];

  /**
   * 点击事件
   */
  el.onclick = function() {
    // 获取指令的modifiers
    let idKeys = Object.keys(binding.modifiers);
    // 求出modifiers和储存所有id的数组的差集
    let mixArray = getDifference(allCollapseId, idKeys);

    /**
     * 循环当前指令的modifiers,并循环当前挂载指令实例的父组件的所有子组件
     * 以组件的id找出指令对应的组件,执行对应的展开折叠动作
     */
    idKeys.forEach(idKeysItem => {
      vnode.componentInstance.$parent.$children.forEach(childrenItem => {
        if (childrenItem.id == idKeysItem) {
          if (binding.modifiers[idKeysItem]) {
            childrenItem.show = false;
            el.classList.remove("open");
          } else {
            childrenItem.show = true;
            el.classList.add("open");
          }
        }
      });
    });
    /**
     * 判断当前指令的modifiers是否有对应的NLY-accordionNavCollapse组件
     * 如果有就执行关闭其他NLY-accordionNavCollapse组件的动作,如果没有,就不进行操作
     */
    idKeys.forEach(idKeysItem => {
      if (allCollapseId.indexOf(idKeysItem) != -1) {
        mixArray.forEach(mixArrayItem => {
          vnode.componentInstance.$parent.$children.forEach(childrenItem => {
            if (childrenItem.id == mixArrayItem) {
              childrenItem.show = false;
              el.classList.remove("open");
            }
          });
        });
      }
    });
  };
});

动画过渡组件

<script>
    import collapse from "./collapse.js";
    export default {
        name: "AccordionNavCollapse",
        components: {
            collapse: collapse
        },
// collapse.js
const elTransition =
  "0.3s height ease-in-out, 0.3s padding-top ease-in-out, 0.3s padding-bottom ease-in-out";
const Transition = {
  "before-enter"(el) {
    el.style.transition = elTransition;
    if (!el.dataset) el.dataset = {};

    el.dataset.oldPaddingTop = el.style.paddingTop;
    el.dataset.oldPaddingBottom = el.style.paddingBottom;

    el.style.height = 0;
    el.style.paddingTop = 0;
    el.style.paddingBottom = 0;
  },

  enter(el) {
    el.dataset.oldOverflow = el.style.overflow;
    if (el.scrollHeight !== 0) {
      el.style.height = el.scrollHeight + "px";
      el.style.paddingTop = el.dataset.oldPaddingTop;
      el.style.paddingBottom = el.dataset.oldPaddingBottom;
    } else {
      el.style.height = "";
      el.style.paddingTop = el.dataset.oldPaddingTop;
      el.style.paddingBottom = el.dataset.oldPaddingBottom;
    }

    el.style.overflow = "hidden";
  },

  "after-enter"(el) {
    el.style.transition = "";
    el.style.height = "";
    el.style.overflow = el.dataset.oldOverflow;
  },

  "before-leave"(el) {
    if (!el.dataset) el.dataset = {};
    el.dataset.oldPaddingTop = el.style.paddingTop;
    el.dataset.oldPaddingBottom = el.style.paddingBottom;
    el.dataset.oldOverflow = el.style.overflow;

    el.style.height = el.scrollHeight + "px";
    el.style.overflow = "hidden";
  },

  leave(el) {
    if (el.scrollHeight !== 0) {
      el.style.transition = elTransition;
      el.style.height = 0;
      el.style.paddingTop = 0;
      el.style.paddingBottom = 0;
    }
  },

  "after-leave"(el) {
    el.style.transition = "";
    el.style.height = "";
    el.style.overflow = el.dataset.oldOverflow;
    el.style.paddingTop = el.dataset.oldPaddingTop;
    el.style.paddingBottom = el.dataset.oldPaddingBottom;
  }
};

export default {
  name: "collapseTransition",
  functional: true,
  render(h, { children }) {
    const data = {
      on: Transition
    };
    return h("transition", data, children);
  }
};

组件

NLY-accordionNav

// AccordionNav.vue
<template>
  <nav class="nly-blog-sider-nav flex-column">
    <ul class="nly-blog-sider-menu flex-column">
      <slot />
    </ul>
  </nav>
</template>
<script>
export default {
  name: "AccordionNav"
};
</script>

NLY-accordionNavItem

// AccordionNavItem.vue
<template>
  <li class="nly-blog-sider-menu-item">
    <a class="nly-blog-sider-menu-title">
      <i :class="iconClass" v-if="icon"> </i>
      <p>
        <slot />
      </p>
    </a>
  </li>
</template>

<script>
export default {
  name: "AccordionNavItem",
  props: {
    icon: {
      type: String
    }
  },
  computed: {
    iconClass: function() {
      return ["nly-blog-sider-menu-icon", this.icon];
    }
  }
};
</script>

NLY-accordionNavTree

// AccordionNavTree.vue
<template>
  <li class="nly-blog-sider-menu-item">
    <a class="nly-blog-sider-menu-title">
      <i :class="iconClass" v-if="icon"> </i>
      <p>
        <slot />
      </p>
      <i class="nly-blog-sider-menu-arrow"> </i>
    </a>
  </li>
</template>

<script>
export default {
  name: "AccordionNavTree",
  props: {
    icon: {
      type: String
    }
  },
  computed: {
    iconClass: function() {
      return ["nly-blog-sider-menu-icon", this.icon];
    }
  }
};
</script>

NLY-accordionNavCollapse

// AccordionNavCollapse.vue
<template>
  <collapse>
    <ul class="nly-blog-sider-menu menu-tree" v-show="show">
      <slot />
    </ul>
  </collapse>
</template>

<script>
import collapse from "./collapse.js";
export default {
  name: "AccordionNavCollapse",
  components: {
    collapse: collapse
  },
  data() {
    return {
      show: this.visible
    };
  },
  model: {
    prop: "visible",
    event: "input"
  },
  props: {
    visible: {
      type: Boolean,
      default: false
    },
    id: {
      type: [String, Number]
    }
  },
  created() {
    this.show = this.visible;
    this.$nextTick(function() {
      this.emitState();
    });
  },
  computed: {},
  methods: {
    emitState: function emitState() {
      // 告诉指令当前id和show
      this.$root.$emit("collapseStatus", this.show, this.id);
    }
  },
  mounted() {},
  watch: {
    visible: function(newval, oldval) {
      if (newval != oldval) {
        this.show = newval;
      }
    },
    show: function show(newVal, oldVal) {
      if (newVal !== oldVal) {
        this.emitState();
      }
    }
  }
};
</script>

注册组件

import AccordionNav from "./AccordionNav.vue";
import AccordionNavItem from "./AccordionNavItem.vue";
import AccordionNavTree from "./AccordionNavTree.vue";
import AccordionNavCollapse from "./AccordionNavCollapse.vue";

export default {
  install: Vue => {
    Vue.component("NLY-accordionNav", AccordionNav);
    Vue.component("NLY-accordionNavItem", AccordionNavItem);
    Vue.component("NLY-accordionNavTree", AccordionNavTree);
    Vue.component("NLY-accordionNavCollapse", AccordionNavCollapse);
  }
};

全局注册指令和组件

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";

Vue.config.productionTip = false;

// 自定义图标,阿里巴巴矢量图标库
import "./assets/nlyblogfont/iconfont.css";

// 全局注册组件
import NLYblog from "./nlyaccordion";
Vue.use(NLYblog);

// 全局注册指令
import "./nlyaccordion/nlyaccordion.js";

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");

Less

<style lang="less">
.flex-column {
  flex-direction: column !important;
}
.nly-blog-sider-nav {
  padding: 0.5rem 1.5rem 0.5rem 1rem;
  display: flex;
  flex-wrap: wrap;
  margin-bottom: 0;
  list-style: none;

  .nly-blog-sider-menu {
    margin-top: 1rem;
    list-style: none;

    &.menu-tree {
      margin-top: 0;
      margin-left: 1rem;
    }

    .nly-blog-sider-menu-item {
      padding: 0 1rem 0 1rem;

      &.open {
        > .nly-blog-sider-menu-title {
          i.nly-blog-sider-menu-arrow {
            transform: translateY(-2px);
          }
          i.nly-blog-sider-menu-arrow::after {
            transform: rotate(-45deg) translateX(-2px);
          }
          i.nly-blog-sider-menu-arrow::before {
            transform: rotate(45deg) translateX(2px);
          }
        }
      }
    }

    .nly-blog-sider-menu-title:hover {
      color: #0fbcf9;
      transition: color 0.3s ease-in;

      .nly-blog-sider-menu-arrow::after {
        background-color: #0fbcf9;
      }
      .nly-blog-sider-menu-arrow::before {
        background-color: #0fbcf9;
      }
    }

    .nly-blog-sider-menu-title {
      // color: inherit;
      white-space: nowrap;
      cursor: pointer;
      display: block;
      color: #f97f51;
      position: relative;
      padding: 0.2rem 0.5rem 0.2rem 0.5rem;
      transition: color 0.3s ease-in;

      .nly-blog-sider-menu-icon {
        // font-size: 1.8rem;
        margin-right: 0.5rem;
        color: inherit;
        // vertical-align: -0.3rem;
      }

      p {
        display: inline-block;
        // font-size: 1.3rem;
        color: inherit;
      }

      .nly-blog-sider-menu-arrow {
        position: absolute;
        top: 50%;
        right: 16px;
        width: 10px;
        transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
      }

      .nly-blog-sider-menu-arrow::before {
        transform: rotate(-45deg) translateX(2px);
        position: absolute;
        width: 6px;
        height: 1.5px;
        background-color: #f97f51;
        border-radius: 2px;
        transition: background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
          transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
          top 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
        content: "";
      }

      .nly-blog-sider-menu-arrow::after {
        transform: rotate(45deg) translateX(-2px);
        position: absolute;
        width: 6px;
        height: 1.5px;
        background-color: #f97f51;
        border-radius: 2px;
        transition: background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
          transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
          top 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
        content: "";
      }
    }
  }
}
</style>

Demo

<template>
  <NLY-accordionNav>
    <NLY-accordionNavItem icon="nlyblog nly-blog-home">
      Nejinn
    </NLY-accordionNavItem>

    <NLY-accordionNavTree icon="nlyblog nly-blog-book" v-nly-accordion.sss>
      Nejinn
    </NLY-accordionNavTree>
    <NLY-accordionNavCollapse id="sss" visible>
      <NLY-accordionNavItem icon="nlyblog nly-blog-home">
        lerity
      </NLY-accordionNavItem>
      <NLY-accordionNavItem icon="nlyblog nly-blog-home">
        blog
      </NLY-accordionNavItem>
    </NLY-accordionNavCollapse>

    <NLY-accordionNavTree icon="nlyblog nly-blog-book" v-nly-accordion.zzz>
      一颗
    </NLY-accordionNavTree>
    <NLY-accordionNavCollapse id="zzz">
      <NLY-accordionNavItem icon="nlyblog nly-blog-home">
        数据
      </NLY-accordionNavItem>
      <NLY-accordionNavItem icon="nlyblog nly-blog-home">
        小白菜
      </NLY-accordionNavItem>
    </NLY-accordionNavCollapse>

    <NLY-accordionNavTree icon="nlyblog nly-blog-book" v-nly-accordion.ccc>
      测试
    </NLY-accordionNavTree>
    <NLY-accordionNavCollapse id="ccc">
      <NLY-accordionNavItem icon="nlyblog nly-blog-home">
        黄色
      </NLY-accordionNavItem>
      <NLY-accordionNavItem icon="nlyblog nly-blog-home">
        蓝色
      </NLY-accordionNavItem>
    </NLY-accordionNavCollapse>

    <NLY-accordionNavTree icon="nlyblog nly-blog-book" v-nly-accordion.ddd>
      大巴
    </NLY-accordionNavTree>
    <NLY-accordionNavCollapse id="ddd">
      <NLY-accordionNavItem icon="nlyblog nly-blog-home">
        上车
      </NLY-accordionNavItem>
      <NLY-accordionNavItem icon="nlyblog nly-blog-home">
        不开车就下车
      </NLY-accordionNavItem>
    </NLY-accordionNavCollapse>
  </NLY-accordionNav>
</template>

<script>
export default {
  name: "accordion"
};
</script>

<style lang="less">
.flex-column {
  flex-direction: column !important;
}
.nly-blog-sider-nav {
  padding: 0.5rem 1.5rem 0.5rem 1rem;
  display: flex;
  flex-wrap: wrap;
  margin-bottom: 0;
  list-style: none;

  .nly-blog-sider-menu {
    margin-top: 1rem;
    list-style: none;

    &.menu-tree {
      margin-top: 0;
      margin-left: 1rem;
    }

    .nly-blog-sider-menu-item {
      padding: 0 1rem 0 1rem;

      &.open {
        > .nly-blog-sider-menu-title {
          i.nly-blog-sider-menu-arrow {
            transform: translateY(-2px);
          }
          i.nly-blog-sider-menu-arrow::after {
            transform: rotate(-45deg) translateX(-2px);
          }
          i.nly-blog-sider-menu-arrow::before {
            transform: rotate(45deg) translateX(2px);
          }
        }
      }
    }

    .nly-blog-sider-menu-title:hover {
      color: #0fbcf9;
      transition: color 0.3s ease-in;

      .nly-blog-sider-menu-arrow::after {
        background-color: #0fbcf9;
      }
      .nly-blog-sider-menu-arrow::before {
        background-color: #0fbcf9;
      }
    }

    .nly-blog-sider-menu-title {
      // color: inherit;
      white-space: nowrap;
      cursor: pointer;
      display: block;
      color: #f97f51;
      position: relative;
      padding: 0.2rem 0.5rem 0.2rem 0.5rem;
      transition: color 0.3s ease-in;

      .nly-blog-sider-menu-icon {
        // font-size: 1.8rem;
        margin-right: 0.5rem;
        color: inherit;
        // vertical-align: -0.3rem;
      }

      p {
        display: inline-block;
        // font-size: 1.3rem;
        color: inherit;
      }

      .nly-blog-sider-menu-arrow {
        position: absolute;
        top: 50%;
        right: 16px;
        width: 10px;
        transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
      }

      .nly-blog-sider-menu-arrow::before {
        transform: rotate(-45deg) translateX(2px);
        position: absolute;
        width: 6px;
        height: 1.5px;
        background-color: #f97f51;
        border-radius: 2px;
        transition: background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
          transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
          top 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
        content: "";
      }

      .nly-blog-sider-menu-arrow::after {
        transform: rotate(45deg) translateX(-2px);
        position: absolute;
        width: 6px;
        height: 1.5px;
        background-color: #f97f51;
        border-radius: 2px;
        transition: background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
          transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
          top 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
        content: "";
      }
    }
  }
}
</style>

这时候运行就可以看到效果图的大手风琴折叠板。

上一篇 下一篇

猜你喜欢

热点阅读