Search code examples
vue.jsvuejs2vuex

VUE.JS - Using the vNode argument to add unknown quantity of items to a dynamic navigation


I'm currently trying to create a component which manages several linked dropdowns and elements on a page. In addition, this element supplies a rather fancy navigation element, containing anchor links which automatically scroll to the desired element in this component.

The problem is that the actual contents of the component are completely dynamic and partially determined by the content manager in the CMS. There are several sub components that are always present, but apart from that the content manager can add any number of sections, (using various named and an unnamed ) and each of these sections should be added to the navigation of the component.

I see 3 options:

  1. For every component added, it's title and unique id is added to a property array on the parent component. This, however, is a bit prone to errors. (Unfortunately I will have no control over the eventual backend implementation, so I'm trying to create a foolproof system to avoid to much wasted time.)

  2. Due to other components, I'm already using Vuex to manage most of the data in the app. I figured I use a simple directive, to be added on each item in the parent component. This directive is responsible for adding that element to the Vuex store. The parent component simply reads the contents of the store and generates the navigation based on this. The problem here is that, as far as I can tell, I have to use the vNode argument in the bind hook of my directive to access the Vuex store. This seems... hacky. Is there anything wrong with this approach?

  3. In the mounted hook of my parent component, I traverse the DOM, searching for elements with a particular data-property, and add a link to each of these elements to my navigation. Seems prone to failure and fragile.

What is the preferred approach for this situation?

As a follow up question - What is the correct use case for the vNode argument in a vue directive? The documentation seems rather sparse on this subject.


Solution

  • I would steer away from using a directive in this case. In Vue 2, the primary use case for directives is for low level DOM manipulation.

    Note that in Vue 2.0, the primary form of code reuse and abstraction is components - however there may be cases where you just need some low-level DOM access on plain elements, and this is where custom directives would still be useful.

    Instead I would suggest a mixin approach, where your mixin essentially registers your components that should be included in navigation with Vuex.

    Consider the following code.

    const NavMixin = {
      computed:{
        navElement(){
          return this.$el
        },
        title(){
          return this.$vnode.tag.split("-").pop()
        }
      },
      mounted(){
        this.$store.commit('addLink', {
          element: this.navElement,
          title: this.title
        })
      }
    }
    

    This mixin defines a couple of computed values that determine the element that should be used for navigation and the title of the component. Obviously the title is a placeholder and you should modify it to suit your needs. The mounted hook registers the component with Vue. Should a component need a custom title or navElement, mixed in computed properties are overridden by the component's definition.

    Next I define my components and use the mixin.

    Vue.component("child1",{
      mixins:[NavMixin],
      template:`<h1>I am child1</h1>`
    })
    Vue.component("child2",{
      mixins:[NavMixin],
      template:`<h1>I am child2</h1>`
    })
    Vue.component("child3",{
     template:`<h1>I am child3</h1>`
    })
    

    Note that here I am not adding the mixin to the third component, because I could conceive of a situation where you may not want all components included in navigation.

    Here is a quick example of usage.

    console.clear()
    
    const store = new Vuex.Store({
      state: {
        links: []
      },
      mutations: {
        addLink (state, link) {
          state.links.push(link)
        }
      }
    })
    
    const NavMixin = {
      computed:{
        navElement(){
          return this.$el
        },
        title(){
          return this.$vnode.tag.split("-").pop()
        }
      },
      mounted(){
        this.$store.commit('addLink', {
          element: this.navElement,
          title: this.title
        })
      }
    }
    
    Vue.component("child1",{
      mixins:[NavMixin],
      template:`<h1>I am child1</h1>`,
    })
    Vue.component("child2",{
      mixins:[NavMixin],
      template:`<h1>I am child2</h1>`
    })
    Vue.component("child3",{
     template:`<h1>I am child3</h1>`
    })
    
    Vue.component("container",{
      template:`
        <div>
          <button v-for="link in $store.state.links" @click="scroll(link)">{{link.title}}</button>
          <slot></slot>
        </div>
      `,
      methods:{
        scroll(link){
          document.querySelector("body").scrollTop = link.element.offsetTop
        }
      },
    })
    
    new Vue({
      el:"#app",
      store
    })
    h1{
      height:300px
    }
    <script src="https://unpkg.com/vue@2.2.6/dist/vue.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/2.3.1/vuex.js"></script>
    <div id="app">
      <container>
        <child1></child1>
        <child3></child3>
        <child2></child2>
      </container>
    </div>

    This solution is pretty robust. You do not need to parse anything. You have control over which components are added to the navigation. You handle potentially nested components. You said you don't know which types of components will be added, but you should have control over the definition of the components that will be used, which means its relatively simple to include the mixin.