Home > Net >  How to update the child component b-tabs value from parent component in vuejs
How to update the child component b-tabs value from parent component in vuejs

Time:06-30

I am trying to update the child component value from the parent component, when I set the value in the prop, I do get the following error,

Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value.

I tried the below solution found on the internet but by doing so my child component prop is not getting changed when I update the parent component

Parent component

<template>
  <child :index-val="indexVal"> </child>
</template>

<script>
export default {
  props: [indexVal],
  data() {
    return {
      indexVal: 0,
    }
  },
  methods: {
    updateVal() {
      // updating value from parent
      this.indexVal = 2
    },
  },
}
</script>

Child component

<template>
  <div>
    <!-- Tabs with card integration -->
    <b-card no-body>
      <b-tabs v-model="tabIndex" small card>
        <b-tab title="tab1">tab1</b-tab>
        <b-tab title="tab2">tab2</b-tab>
      </b-tabs>
    </b-card>
  </div>
</template>

<script>
export default {
  props: [indexVal],
  data() {
    return {
      tabIndex: this.indexVal,
    }
  },
}
</script>

CodePudding user response:

The error you're facing happens when you're trying to mutate the value of a prop, because prop values are coming from parents. If the parent changes it, your change inside the child will be overwritten.

Thus, when a child needs to change a prop, it has to emit an event to the parent, update the parent value which, in turn flows the change back into the child, via the prop.

Props down, events up!

To eliminate part of the boilerplate for this mechanism, Vue 2 provided the .sync modifier.

In Vue3, .sync was deprecated and removed (breaking change)!
someProp.sync has been replaced by v-model:someProp, because:

  • they were providing the same functionality (dual binding),
  • this increased the confusion between the two syntaxes and their particular implementation details.
  • now v-model behaviour is aligned across native form elements and Vue components.

Vue's author (Evan You), has declared over the past years that, going forward, the core team's intention is to reduce the API surface, by removing unnecessary or confusing syntax options.


Vue 2 example:

Vue.component('tab', {
  template: `#tab-tpl`,
  props: ['index', 'text'],
  computed: {
    tIndex: {
      get() { return this.index },
      set(val) { this.$emit('update:index', val); }
    }
  }
})
new Vue({
  el: '#app',
  data: () => ({
    tabs: [{text: 'one'}, {text: 'two'}, {text: 'three'}],
    tabIndex: 0
  }),
  computed: {
    currentIndex: {
      get() { return this.tabIndex },
      set(val) { 
        this.tabIndex = (this.tabs.length   val) % this.tabs.length; 
      }
    }
  }
})
.tab-pills {
  display: flex;
  border-bottom: 1px solid #ccc;
  cursor: pointer;
}
.active {
  color: red;
}
.tab-pills > div { padding: .3rem .7rem; }
.tab-content { padding: 1rem .7rem; }
.tab-body { padding: 1rem 0 }
<script src="https://v2.vuejs.org/js/vue.min.js"></script>

<div id="app">
  <div >
    <div v-for="(tab, key) in tabs"
         v-text="tab.text"
         :
         :key="key"
         @click="currentIndex = key">   
    </div>
  </div>
  <tab :index.sync="currentIndex" :text="tabs[currentIndex].text" ></tab>
</div>
<template id="tab-tpl">
  <div>
    <div v-text="text" ></div>
    <button @click="tIndex -= 1">Prev</button>
    <button @click="tIndex  = 1">Next</button>
  </div>    
</template>

Same thing, in Vue 3 (Composition API):

const { defineComponent, toRefs, reactive, computed, createApp } = Vue;

const Tab = defineComponent({
  template: '#tab-tpl',
  props: ['index', 'text'],
  setup: (props, { emit }) => ({
    tIndex: computed({
      get: () => props.index,
      set: val => emit('update:index', val)
    })
  })
});

const Tabs = defineComponent({
  template: '#tabs-tpl',
  components: { Tab },
  setup() {
    const state = reactive({
      tabs: ['one', 'two', 'three'].map(text => ({ text })),
      tabIndex: 0,
      currentIndex: computed({
        get: () => state.tabIndex,
        set: val => state.tabIndex = (state.tabs.length   val) % state.tabs.length
      })
    })
    return {...toRefs(state)}
  }
});

createApp(Tabs).mount('#app')
.tab-pills {
  display: flex;
  border-bottom: 1px solid #ccc;
  cursor: pointer;
}
.active {
  color: red;
}
.tab-pills > div { padding: .3rem .7rem; }
.tab-content { padding: 1rem .7rem; }
.tab-body { padding: 1rem 0 }
<script src="https://unpkg.com/[email protected]/dist/vue.global.prod.js"></script>

<div id="app"></div>

<template id="tabs-tpl">
  <div >
    <div v-for="(tab, key) in tabs"
         v-text="tab.text"
         :
         :key="key"
         @click="currentIndex = key">   
    </div>
  </div>
  <tab v-model:index="currentIndex"
       :text="tabs[currentIndex].text"
       ></tab>
</template>

<template id="tab-tpl">
  <div v-text="text" ></div>
  <button @click="tIndex -= 1">Prev</button>
  <button @click="tIndex  = 1">Next</button>
</template>


In both examples, the tab controls at the top change tabIndex at parent level.

The buttons inside the tab, change it from the child (by emitting the updated value to parent).

The important part is that tabIndex changes in parent component, which drives the update in the child. Don't mind the currentIndex computed in parent, that's just an implementation detail (it allows going to first tab when clicking Next on last tab and going to last when hitting Prev on first tab - that's the only reason why I added it, it's not necessary for all of this to work).

If something is unclear, check the doc links I posted above, for the Vue version you're using.

  • Related