0
votes

I am Working on creating a tab component with vue and noticed the following behavior.

If I create my component like this:

<tabs-component>
  <tab-component :title="1">
    <h1>1</h1>
    <p>Lorem ipsum dolor sit amet.</p>
  </tab-component>
  <tab-component :title="2">
    <h1>2</h1>
    <p>Lorem ipsum dolor sit amet.</p>
  </tab-component>
</tabs-component>

Life hooks are triggered in this order:

  • Tabs component created
  • Tab component created
  • Tab component mounted
  • Tabs component mounted

Which seems to be the correct behavior however, if instead of declaring the slots manually I use a v-for directive assuming I am dealing with some data from a service like this:

<tabs-component>
  <tab-component v-for="(type, index) in beerTypes" :key="type.slug" :title="type.slug">
    <h1>{{ index }}</h1>
    <p>Lorem ipsum dolor sit amet.</p>
  </tab-component>
</tabs-component>

Life hooks are triggered in this order:

  • Tabs component created
  • Tabs component mounted
  • Tab component created
  • Tab component mounted

The v-for directive seems to be messing with the order life hooks are triggered. And this is causing me trouble since I am firing the selectTab method on the mounted life hook to pick the default tab selected, but when is triggered tabs have not been mounted.

Does any one know how I can correctly listen for the events in the right order, or wait for the child components to be ready so I can pick the default panel?

Here is the rest of my code for reference:

App.vue

<template>
  <main>
    <h1>Discover beer styles</h1>
    <tabs-component>
      <tab-component v-for="(type, index) in beerTypes" :key="type.slug" :title="type.slug">
        <h1>{{ index }}</h1>
        <p>Lorem ipsum dolor sit amet.</p>
      </tab-component>
    </tabs-component>
  </main>
</template>

<script>
  import axios from 'axios';
  import TabsComponent from './components/tabs/Tabs.vue';
  import TabComponent from './components/tabs/Tab.vue';

  export default {
    components: {
      'tabs-component': TabsComponent,
      'tab-component': TabComponent,
    },

    data() {
      return {
        beerTypes: null,
        selectedType: null
      }
    },

    filters: {
      capitalize: function(value) {
        if (!value) return '';

        return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
      }
    },

    mounted() {
      axios.get('/data/data.json')
        .then((response) => {
          this.beerTypes = response.data;
          this.selectedType = this.beerTypes[0].slug;
        })
        .catch((error) => console.log(error));
    },

    methods: {
      selectActiveType: function(selected) {
        this.selectedType = selected;
      }
    }
  }
</script>

Tabs.vue

<template>
  <div class="tabs-component">
    <div>
      <ul>
        <li v-for="(tab, index) in tabs" :class="{'is-active': tab.isActive}" :key="index">
          <a href="#" @click="selectTab(index)">{{ tab.title }}</a>
        </li>
      </ul>
    </div>
    <div>
      <slot/>
    </div>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        tabs: [],
        activeTabIndex: 0
      }
    },

    created() {
      this.tabs = this.$children;
    },

    mounted() {
      this.selectTab(this.activeTabIndex);
    },

    methods: {
      selectTab(index) {
        this.tabs.forEach(function(tab) {
          tab.isActive = (this.tabs.indexOf(tab) === index);
        }.bind(this));
      }
    }
  };
</script>

Tab.vue

<template>
  <section v-show="isActive">
    <slot/>
  </section>
</template>

I appreciate if someone could take the time to help me.

Thanks!

1
I think that your service call is asynchronous and that is the reason that when App.vue is initialized there's no data within beerTypes and so tab-component doesn't get rendered and so life cycle hooks of tabs-component are initialized first. Please tell if the above case is the scenario.Ayush Gupta
Hello @AyushGupta, when the app is mounted I am using axios to call a JSON file that provides the info. Added to the question the fragment of code that performs that callJhonnatan Gonzalez Rodriguez

1 Answers

1
votes

I think uses Vue: provide/inject will be one solution at here.

For your case, if new <tab-component> is added (you stated beerTypes will be updated from your backend service), <tabs-component> will not know unless get newest this$children inside hook=updated().

If use provide/inject, we can let <tab-component> register its intance to one data property of <tabs-components>. Then <tabs-component> will not need to care about new tab is added, and is able to access the instance of <tab-component> directly.

Also you should implement one _unRegister, when the instance is destroied, remove it from the data property of <tabs-component>, below demo uses hook=beforeDestory to implement it.

Below is one dmeo:

Vue.productionTip = false
Vue.component('tabs-component', {
  template: `  <div class="tabs-component">
    <div>
      <h3>Selected Tab: {{activeTabIndex}}</h3>
      <ul>
        <li v-for="(tab, index) in tabs" :class="{'is-active': tab.isActive}" :key="index">
          <a href="#" @click="selectTab(index)">{{ tab.title }}</a>
        </li>
      </ul>
    </div>
    <div>
      <slot/>
    </div>
  </div>`,
  provide () {
    return {
      _tabs: this
    }
    },
    data() {
      return {
        tabs: [],
        activeTabIndex: 0
      }
    },
    mounted() {

    },

    methods: {
      selectTab(selectedIndex) {
        this.activeTabIndex = selectedIndex
        this.tabs.forEach((tab, tabIndex) => {
          tab.isActive = selectedIndex === tabIndex
        })
      },
      _registerTab(tab) {
        this.tabs.push(tab)
        this.selectTab(this.activeTabIndex)
      },
      _unRegisterTab(unRegTab) {
        this.tabs = this.tabs.filter(tab=>{
          return unRegTab !== tab
        })
        this.selectTab(0)
      }
    }
})

Vue.component('tab-component', {
  template: `  <section v-show="isActive" :title="title">
    <slot/>
  </section>`,
	inject: {
    _tabs: {
      default () {
        console.error('tab-component must be child of tab-components')
      }
		}
  },
  props: ['title'],
  mounted () {
    this._tabs._registerTab(this)
  },
  beforeDestroy(){
    this._tabs._unRegisterTab(this)
  },
  data () {
    return {
      isActive: false
    }
  }
})

new Vue({
  el: '#app',
  data () {
    return {
      beerTypes: []
    }
  },
  methods: {
    pushData () {
      setTimeout(()=>{
        this.beerTypes.push({'slug': 'test' + this.beerTypes.length})
      }, 1000)
    },
    popData () {
      this.beerTypes.pop()
    }
  }
})
.is-active {
  background-color: green;
}
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
<div id="app">
  <button @click="pushData()">Add Tab</button>
  <button @click="popData()">Pop one</button>
    <tabs-component>
      <tab-component v-for="(type, index) in beerTypes" :key="type.slug" :title="type.slug">
        <h1>{{ index }}: {{type.slug}}</h1>
        <p>Lorem ipsum dolor sit amet.</p>
      </tab-component>
    </tabs-component>
</div>

And pay an attention on this note in the Vue guide for provide/inject:

Note: the provide and inject bindings are NOT reactive. This is intentional. However, if you pass down an observed object, properties on that object do remain reactive.