preface

Similar to the browser window as the route switch logic, looking or quite lofty, this thought there are a lot of advanced stuff, but complex things are always made up of simple things, this function is no exception.

This article mainly describes two issues:

  1. How to achieve this Tab routing effect
  2. How to add transition animation for route switching.

This feature was developed using the Tab component of the AntDesignVue component library and Animate. CSS

The effect is as follows:

Tab Tab page implementation

The first is the template part of the component. The ContextMenu component is our custom right click menu, which we’ll talk about later. The A-tabs component is an Ant component. See the official documentation for details. There’s also a PageToggleTransition component, which is the component we use to do the animation switch, very simple.

/** * Tablayout.vue template section, a brief look at the impression */<template>
  <PageLayout>
    <ContextMenu
      :list="menuItems"
      :visible.sync="menuVisible"
      @select="onMenuSelect"
    />
    <! -- Tag section -->
    <a-tabs
      type="editable-card"
      :hide-add="true"
      :active-key="activePage"
      @change="changePage"
      @edit="editPage"
      @contextmenu="onContextmenu"
    >
      <a-tab-pane v-for="page in pageList" :key="page.fullPath">
        <template #tab>
          <span :data-key="page.fullPath">
            {{ page.name }}
          </span>
        </template>
      </a-tab-pane>
    </a-tabs>
    <! -- Route exit -->
    <PageToggleTransition name="fadeIn">
      <keep-alive :exclude="dustbin">
        <router-view />
      </keep-alive>
    </PageToggleTransition>
  </PageLayout>
</template>
Copy the code

The principle of

Maintains a pageList that dynamically adds and removes pages by listening for route changes. The so-called page is the page routing object ($route), we are through $route.fullPath as the unique identifier of the page. To delete a page, not only pageList is operated, but also the exclude attribute of the keep-alive component is used to delete the cache. As for the slot of the A-Tabs component, it is mainly to bind a data key so that when contextMenu event is triggered, the key value of the corresponding page can be easily obtained (fullPath).

Theory exists, practice begins.

Routing to monitor

 watch: {
    $route: {
      handler (route) {
        this.activePage = route.fullPath
        this.putCache(route)
        const index = this.pageList.findIndex(item= > item.fullPath === route.fullPath)
        if (index === - 1) {
          this.pageList.push(route)
        }
      },
      immediate: true}}Copy the code

When a route is changed, the following three things are done:

  1. Set the current page (activePage)
  2. Add current page to cache, i.e. remove dustbin
  3. If the current page is not in pageList, add it to the list.

Page jump

methods: {
    changePage (key) {
      this.activePage = key
      this.$router.push(key)
    },
    editPage (key, action) {
      if (action === 'remove') {
        this.remove(key)
      }
    },
    remove (key) {
      if (this.pageList.length <= 1) {
        return message.info('This is the last page.')}let curIndex = this.pageList.findIndex(item= > item.fullPath === key)
      const { matched } = this.pageList[curIndex]
      const componentName = last(matched).components.default.name
      this.dustbin.push(componentName)
      this.pageList.splice(curIndex, 1)
      // Jump only if you want to delete the current page
      if (key === this.activePage) {
        // Determine whether to jump left or right
        curIndex = curIndex >= this.pageList.length ? this.pageList.length - 1 : curIndex
        const page = this.pageList[curIndex]
        this.$router.push(page.fullPath).finally((a)= > {
          this.dustbin.splice(0) // reset, otherwise it will affect the cache of some components}}})... . }Copy the code

Here is the main main talk about remove method:

  1. If it is the last page, ignore it
  2. $keep-alive ($route.matched); $matched ($route. Matched);
  3. If you want to delete the current page, do you need to jump to the left or right?

Note that the keep-alive exclude attribute clears the cache as soon as the component name is matched, so remember to reset it after dustbin is added, otherwise the cache will not be cached next time.

Custom ContextMenu event

The contextmenu event is a contextmenu event, and we can listen for the event to display our custom menu when the contextmenu event is triggered.

methods: {
    // Customize the right menu to close the function
    onContextmenu (e) {
      const key = getTabKey(e.target) // Use the data-key attribute added to the span tag
      if(! key)return // The main function is to control the display or hiding of the menu

      e.preventDefault() // Organize the default behavior to display our custom mail menu
      this.menuVisible = true}... . }/** * Because the TabPane component of the Ant-Design-Vue component library does not support custom listeners, you cannot directly access the key of the target TAB. Therefore, add this method to * query the identity key of the TAB page where the right-click target resides to customize the event processing of the right-click menu. * note: The TabPane component can remove this method when it supports custom listeners and refactor 'event handling for custom right-click menus' * @param Target query start target * @param depth query level depth (search level is no more than 3 levels, Returns null if the depth exceeds 3 levels
function getTabKey (target, depth = 0) {
  if (depth > 2| |! target) {return null
  }
  return target.dataset.key || getTabKey(target.firstElementChild, ++depth)
}
Copy the code

Dom.dataset. Key = dom.dataset. Key = dom.dataset

Here is our ContextMenu component:

Effect:

The code is as follows:

<template>
  <a-menu
    v-show="visible"
    class="contextmenu"
    :style="style"
    :selectedKeys="selectedKeys"
    @click="handleClick"
  >
    <a-menu-item v-for="item in list" :key="item.key">
      <a-icon v-if="item.icon" :type="item.icon"/>
      <span>{{ item.text }}</span>
    </a-menu-item>
  </a-menu>
</template>

<script>
export default {
  name: 'ContextMenu'.props: {
    visible: {
      type: Boolean.required: false.default: false
    },
    list: {
      type: Array.required: true.default: (a)= > []
    }
  },
  data () {
    return {
      left: 0.top: 0.target: null.selectedKeys: []}},computed: {
    style () {
      return {
        left: this.left + 'px'.top: this.top + 'px'
      }
    }
  },
  created () {
    const clickHandler = (a)= > this.closeMenu()
    const contextMenuHandler = e= > this.setPosition(e)
    window.addEventListener('click', clickHandler)
    window.addEventListener('contextmenu', contextMenuHandler)
    this.$emit('hook:beforeDestroy', () = > {window.removeEventListener('click', clickHandler)
      window.removeEventListener('contextmenu', contextMenuHandler)
    })
  },
  methods: {
    closeMenu () {
      this.$emit('update:visible'.false)
    },
    setPosition (e) {
      this.left = e.clientX
      this.top = e.clientY
      this.target = e.target
    },
    handleClick ({ key }) {
      this.$emit('select', key, this.target)
      this.closeMenu()
    }
  }
}
</script>

<style lang="stylus" scoped>
  .contextmenu
    position fixed
    z-index 1000
    border-radius 4px
    border 1px lightgrey solid
    box-shadow 4px 4px 10px lightgrey !important
  .ant-menu-item
    margin0!important
</style>
Copy the code

The key here is that the hook function is created:

  1. First, global events need to come in pairs, either added or removed, otherwise memory leaks and other bugs can occur. For example, in module hot replacement projects, repeated binding can be a problem.
  2. Why bind the Window contextMenu event and click event? The click event is used to close the menu. The feature of the right click menu is that it can be closed by clicking on it. The ContextMenu event is used to retrieve the event objectevent“To set the location of the menu. Whereas before it was bound toa-tabsThe ContextMenu event on the component is primarily intended to block the default event, and we only intercept the component, not the global scope.

The main purpose of the custom right-click menu is to get the key we need from the event. Target and pass it as an event to facilitate the distribution of the following logic:

onMenuSelect (key, target) {
    const tabKey = getTabKey(target)
    switch (key) {
        case '1': this.closeLeft(tabKey); break
        case '2': this.closeRight(tabKey); break
        case '3': this.closeOthers(tabKey); break
        default: break}}Copy the code

The logic of these three cases is basically the same, which mainly does three things:

  1. Clear the cache
  2. Delete the page and set the current page
  3. Page jump

Take closeOthers as an example:

closeOthers (tabKey) {
    const index = this.pageList.findIndex(item= > item.fullPath === tabKey) Hover over the TAB to find the trigger event
    for (const route of this.pageList) {
        if(route.fullPath ! == tabKey) {this.clearCache(route) / / the cache}}const page = this.pageList[index]
    this.pageList = [page] // Set pageList so that everything else is cleared
    this.activePage = page.fullPath
    this.$router.push(this.activePage).catch(e= > e)
}
Copy the code

The cache control

This part of the logic is relatively simple and can be understood with comments

methods: {
    clearCache (route) {
        const componentName = last(route.matched).components.default.name // The last method comes from Lodash and gets the last element of the array
        this.dustbin.push(componentName) / / remove
    },
    putCache (route) {
        const componentName = last(route.matched).components.default.name
        if (this.dustbin.includes(componentName)) {
            this.dustbin = this.dustbin.filter(item= >item ! == componentName)// Remove the current component name from dustbin and restore its caching mechanism}}}Copy the code

So, the main logic is done, the following is a brief talk about the transition animation implementation

Transition animation implementation

Transition is mainly used in Animate. CSS with Vue transition component implementation, component complete code is as follows, extremely simple:

<template>
  <transition :enter-active-class="`animate__animated animate__${name}`">
    <slot />
  </transition>
</template>

<script>
export default {
  name: 'PageToggleTransition'.props: {
    name: String}}</script>
Copy the code

For details, see the transition component documentation

The last

  1. Borrowing from vue – antd – admin
  2. Github.com/1446445040/…

The complete code

<template>
  <PageLayout>
    <ContextMenu
      :list="menuItems"
      :visible.sync="menuVisible"
      @select="onMenuSelect"
    />
    <! -- Tag section -->
    <a-tabs
      type="editable-card"
      :hide-add="true"
      :active-key="activePage"
      @change="changePage"
      @edit="editPage"
      @contextmenu="onContextmenu"
    >
      <a-tab-pane v-for="page in pageList" :key="page.fullPath">
        <template #tab>
          <span :data-key="page.fullPath">
            {{ page.name }}
          </span>
        </template>
      </a-tab-pane>
    </a-tabs>
    <! -- Route exit -->
    <PageToggleTransition name="fadeIn">
      <keep-alive :exclude="dustbin">
        <router-view />
      </keep-alive>
    </PageToggleTransition>
  </PageLayout>
</template>

<script>
import { message } from 'ant-design-vue'
import { last } from 'lodash'
import PageLayout from './PageLayout'
import ContextMenu from '.. /components/common/ContextMenu'
import PageToggleTransition from '.. /components/transition/PageToggleTransition'

export default {
  name: 'TabLayout'.components: { PageToggleTransition, ContextMenu, PageLayout },
  data () {
    return {
      pageList: [].dustbin: [].activePage: ' '.menuVisible: false.menuItems: [{key: '1'.icon: 'arrow-left'.text: 'Close left' },
        { key: '2'.icon: 'arrow-right'.text: 'Close right side' },
        { key: '3'.icon: 'close'.text: 'Close other'}}},watch: {
    $route: {
      handler (route) {
        this.activePage = route.fullPath
        this.putCache(route)
        const index = this.pageList.findIndex(item= > item.fullPath === route.fullPath)
        if (index === - 1) {
          this.pageList.push(route)
        }
      },
      immediate: true}},methods: {
    changePage (key) {
      this.activePage = key
      this.$router.push(key)
    },
    editPage (key, action) {
      if (action === 'remove') {
        this.remove(key)
      }
    },
    remove (key) {
      if (this.pageList.length <= 1) {
        return message.info('This is the last page.')}let curIndex = this.pageList.findIndex(item= > item.fullPath === key)
      const { matched } = this.pageList[curIndex]
      const componentName = last(matched).components.default.name
      this.dustbin.push(componentName)
      this.pageList.splice(curIndex, 1)
      // Jump only if you want to delete the current page
      if (key === this.activePage) {
        // Determine whether to jump left or right
        curIndex = curIndex >= this.pageList.length ? this.pageList.length - 1 : curIndex
        const page = this.pageList[curIndex]
        this.$router.push(page.fullPath).finally((a)= > {
          this.dustbin.splice(0) // reset, otherwise it will affect the cache of some components})}},// Customize the right menu to close the function
    onContextmenu (e) {
      const key = getTabKey(e.target)
      if(! key)return

      e.preventDefault()
      this.menuVisible = true
    },
    onMenuSelect (key, target) {
      const tabKey = getTabKey(target)
      switch (key) {
        case '1': this.closeLeft(tabKey); break
        case '2': this.closeRight(tabKey); break
        case '3': this.closeOthers(tabKey); break
        default: break
      }
    },
    closeOthers (tabKey) {
      const index = this.pageList.findIndex(item= > item.fullPath === tabKey)
      for (const route of this.pageList) {
        if(route.fullPath ! == tabKey) {this.clearCache(route)
        }
      }
      const page = this.pageList[index]
      this.pageList = [page]
      this.activePage = page.fullPath
      this.$router.push(this.activePage).catch(e= > e)
    },
    closeLeft (tabKey) {
      const index = this.pageList.findIndex(item= > item.fullPath === tabKey)
      this.pageList.forEach((route, i) = > {
        if (i < index) {
          this.clearCache(route)
        }
      })
      const restPages = this.pageList.slice(index)
      this.pageList = restPages
      // Determine whether the current activePage is in the page to be deleted
      const curActivePage = restPages.find(item= > item.fullPath === this.activePage)
      if(! curActivePage) {this.activePage = restPages[0].fullPath
        this.$router.push(this.activePage).catch(e= > e)
      }
    },
    closeRight (tabKey) {
      const index = this.pageList.findIndex(item= > item.fullPath === tabKey)
      this.pageList.forEach((route, i) = > {
        if (i > index) {
          this.clearCache(route)
        }
      })
      const restPages = this.pageList.slice(0, index + 1)
      this.pageList = restPages
      // Determine whether the current activePage is in the page to be deleted
      const curActivePage = restPages.find(item= > item.fullPath === this.activePage)
      if(! curActivePage) {this.activePage = last(restPages).fullPath
        this.$router.push(this.activePage).catch(e= > e)
      }
    },
    /** * cache control */
    clearCache (route) {
      const componentName = last(route.matched).components.default.name
      this.dustbin.push(componentName) / / remove
    },
    putCache (route) {
      const componentName = last(route.matched).components.default.name
      if (this.dustbin.includes(componentName)) {
        this.dustbin = this.dustbin.filter(item= >item ! == componentName) } } } }/** * Get the custom data in the DOM node under the Tab Tab, recursively search down up to 3 layers (observe the DOM after Tab component rendering) * This method is a hack method, @param{HTMLElement} target event. Target * @param depth */
function getTabKey (target, depth = 0) {
  if (depth > 2| |! target) {return null
  }
  return target.dataset.key || getTabKey(target.firstElementChild, ++depth)
}
</script>
Copy the code