Home » vue » Building reusable components with custom methods

Building reusable components with custom methods

Posted by: admin November 26, 2021 Leave a comment

Questions:

I am trying to build a reusable tab component with vuejs. I am still learning some basic consepts of vue and hardly managed to finish tab generation and switch logic. It is OK to switch between tabs in component itself now. But I have some problems with making my component to listen outside triggers.

For now, I can switch my tabs outside of the component with the help of $refs. But as I am trying to make it reusable this method doesn’t sound practical. What should I do?

Here is the JSFiddle

Vue.component('tabs', {
  template: `
  <div class="tabs">
    <div class="tab-titles">
          <a :class="{'active':tab.active}" v-for="tab in tablist" href="#" class="tab-title" @click.prevent="activateTab(tab)">{{tab.title}}</a>
      </div>
      <div class="tab-contents">
          <slot></slot>
      </div>
  </div>    
  `,
  data() {
    return {
      tablist: [],
    }
  },

  methods: {

    activateTab: function(tab) {
      this.tablist.forEach(t => {
        t.active = false;
        t.tab.is_active = false
      });

      tab.active = true;
      tab.tab.is_active = true;
    },

    activateTabIndex: function(index) {
      this.activateTab(this.tablist[index]);
    },

    collectTabData: function(tabData) {
      this.tablist.push(tabData)
    }
  },

});

//==============================================================================

Vue.component('tab', {
  template: `
  <div :class="{'active':is_active}" class="tab-content">
    <slot></slot>
  </div>
  `,

  data() {
    return {
      is_active: this.active
    }
  },

  mounted() {
    this.$parent.collectTabData({
      tab: this,
      title: this.title,
      active: this.active,
      is_active: this.is_active
    });
  },

  props: {
    title: {
      type: String,
      required: true
    },
    active: {
      type: [Boolean, String],
      default: false
    },
  }

});

//==============================================================================

Vue.component('app', {
  template: `
  <div class="container">

    <tabs ref="foo">

      <tab title="tab-title-1">
        <h3>content-1</h3>
        Initial content here
      </tab>

      <tab title="tab-title-2" active>
        <h3>content-2</h3>
        Some content here
      </tab>

      <tab title="tab-title-3">
        <h3>content-3</h3>
        Another content here
      </tab>

    </tabs>

    <a href="#" @click='switchTab(0)'>switch to tab(index:0)</a>

    </div>
  `,

  methods: {
    switchTab: function () {
      vm.$children[0].$refs.foo.activateTabIndex(0);
    }
  },

});

//==============================================================================

const vm = new Vue({
  el: '#inner-body',
});
@import url('https://fonts.googleapis.com/css?family=Lato');

#inner-body{
 font-family: 'Lato', sans-serif; 
 background-color:#ffffff;
 padding:20px;
}

.tab-titles {

}

.tab-title {
  display: inline-block;
  margin-right: 10px;
  color: #bbb;
  text-decoration: none;
}

.tab-title.active {
  color: #06cd92;
  border-bottom:1px solid #06cd92;
}

.tab-contents{
    border: 1px solid #ddd;
    border-width:1px 0;
    margin-top:-1px;
    margin-bottom:20px;
}

.tab-content {
  display: none;
  padding-bottom:20px;
}

.tab-content.active {
  display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.5/vue.min.js"></script>
<div id="inner-body">
  <app></app>
</div>

Answers:

Vue Components are supposed communicate one-way down to children via Props (where only parents mutate the props on the children, never the children themselves) and children communicate to parents by emitting events. The makes nesting components much easier to reason about, and decouples components properly. So what do you do when the parent wants to change a tab? Let me walk you through a process:

1) Imagine we add a prop called activeTab to the tabs component (I’m not following your code in your question directly here, just basing loosely off it to demonstrate the process easier). The parent will change the value of this prop whenever it wants. The tabs component (aka child component in this case) should not alter the value of the activeTab prop. Instead, inside the tabs component, add a watcher for this prop:

in child component (ie. tabs)

    props: {

      /**
       * Prop parent can use to set active tab
       */
      'activeTab': {
        type: Number
      }
    },

    watch: {

      /**
       * Watch for changes on the activeTab prop.
       *
       * @param {boolean} val The new tab value.
       * @param {boolean} oldVal The old tab value.
       * @return {void}
       */
      activeTab: function (val, oldVal) {
        console.log('new: %s, old: %s', val, oldVal)
        // do something here to make the tabs change
        // and never alter the prop itself.
        this.switchTab(val)
      }
    },

2) Now on your parent, you should have a reactive data property that can be named the same as your prop if you want:

in parent component (ie. app)

    data: {
      activeTab: 0
    }

3) Then we need to make it where when you alter the data property above, the prop gets altered too. Here’s what it would look like on the tabs component tag:

in parent component (ie. app)

    <tabs :activeTab="activeTab" ...

4) What do you do if you want to allow the tabs component to also alter the active tab sometimes? Easy, just emit an event whenever an active tab is changed:

in the child component (ie. tabs)

  methods: {
    switchTab (newActiveTabValue) {

      // store the new active tab in some data property in this component, but not the "activeTab" prop, as mentioned earlier.
      this.whatever = newActiveTabValue

      // ... now emit the event
      this.$emit('switched', newActiveTabValue)

  }

5) Your parent should now listen for this emitted event and update its own data property:

in parent component (ie. app)

    <tabs :activeTab="activeTab" @switched="activeTab = arguments[0]" ...

Seems a little bit more effort, but it’s all worth it as your app grows in complexity and more things become nested. You can read more on Composing Components in the official docs here.