vue 自定義指令封裝一個手風琴嵌套組件

很多教程的手風琴組件都是一個v-for數組來實現手風琴組件,v-for封裝起來很簡單,但是我認為并不好。
理由如下:

  • 這種方式很不優雅
  • 其實我沒用過這種實現方式的手風琴,不知道到底怎么實現配合router來實現展開和激活?感覺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??梢栽赼ccordion.vue中修改就行。

組件目錄結構

目錄結構.jpg

自定義指令 v-nly-accordion

  • nlyaccordion.js
// 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");
            }
          });
        });
      }
    });
  };
});

動畫過渡組件

  • 動畫過渡組件是借鑒element-ui的,但是說實話,我不是很喜歡這個。
  • collapse動畫在NLY-accordionNavCollapse組件中引入
<script>
    import collapse from "./collapse.js";
    export default {
        name: "AccordionNavCollapse",
        components: {
            collapse: collapse
        },
  • collapse.js
// 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>

注冊組件

  • index.js
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);
  }
};

全局注冊指令和組件

  • main.js
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

  • 請把less放到app.vue或者手風琴的父組件中
  • 也可以編譯成css,然后再引入
<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>

這時候運行就可以看到效果圖的大手風琴折疊板。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 基本用法 一、vuejs簡介 是一個構建用戶界面的框架 是一個輕量級的MVVM(Model-View-ViewMo...
    深度剖析JavaScript閱讀 18,262評論 0 8
  • 這個命題作文考生容易下筆,沒有審題障礙,不會跑題偏題。 我們發散思維,可以看見自我,比如看見最好的自己,看見自...
    詩語人生閱讀 1,040評論 0 0
  • 年不知不覺就走到了你跟前,你或許還沒有反應,但必須適應年的到來,年尾是一年之中最忙碌的日子,也是記憶中最難...
    浪子青海閱讀 258評論 0 1
  • 誦讀,不是簡單的讀,它是一門藝術。只會讀不會誦最高只能達到誦讀的初級水平,會讀會誦才有可能達到誦讀的最高水平。誦讀...
    安定區張虎閱讀 1,154評論 4 4
  • 今天正式開始了我的洋蔥閱讀課。小六老師在開營儀式上講了為什么要閱讀,要怎樣閱讀,以及洋蔥閱讀的體系,總結起來就是實...
    嘮叨妖的雞娃日記閱讀 689評論 3 6