1

So I have a problem with parent-child component communication with vue. The thing is, after i navigate to a component, it should call an ajax to get data from the server. After receiving the data, the parent component supposed to send it to all the child components through props, but the props data isn't showing. The child component only start to show the props data, only after i change my code on my editor. So, here's the code for my parent component

<template>
  <div id="single-product-container">
    <product-header :name="singleProductName" :details="singleProductDetail" />
    <product-spec :spec="singleProductSpec" />
  </div>
</template>

<script>
import SingleProductHeader from '@/pages/SingleProductPage/single-product-header'
import SingleProductSpec from '@/pages/SingleProductPage/single-product-spec'
import singleProductApi from '@/api/product.api'

export default {
  data () {
    return {
      singleProductData: null,
      singleProductDetail: [],
      singleProductName: '',
      singleProductSpec: null
    }
  },
  methods: {
    getAllSingleProductDetail () {
      const productName = this.$route.params.product
      const location = this.location || 'jakarta'
      let vehicleType = null
      const path = this.$route.fullPath
      let self = this
      if (path.includes('motorcycle')) {
        vehicleType = 'motorcycle'
      } else if (path.includes('car')) {
        vehicleType = 'car'
      }
      singleProductApi.getSingleProductRequest(location, productName, vehicleType)
        .then(singleProductResponse => {
          console.log(singleProductResponse)
          let specObj = singleProductResponse.specification
          self.singleProductDetail = singleProductResponse.detail
          self.singleProductName = singleProductResponse.product_name
          self.singleProductSpec = specObj
          self.singleProductData = singleProductResponse
        })
        .catch(error => {
          throw error
        })
    }
  },
  mounted () {
    document.title = this.$route.params.product
  },
  created () {
     this.getAllSingleProductDetail()
  },
  components: {
    'product-header': SingleProductHeader,
    'product-spec': SingleProductSpec
  }
}
</script>

and this is my single-product-spec component that won't load the props data:

<template>
  <div id="product-spec">
    <div class="product-spec-title">
      Spesifikasi
    </div>
    <div class="produk-laris-wrapper">
      <div class="tab-navigation-wrapper tab-navigation-default">
        <div class="tab-navigation tab-default" v-bind:class="{ 'active-default': mesinActive}" v-on:click="openSpaceTab(event, 'mesin')">
          <p class="tab-text tab-text-default">Mesin</p>
        </div>
        <div class="tab-navigation tab-default" v-bind:class="{ 'active-default': rangkaActive}" v-on:click="openSpaceTab(event, 'rangka')">
          <p class="tab-text tab-text-default">Rangka & Kaki</p>
        </div>
        <div class="tab-navigation tab-default" v-bind:class="{ 'active-default': dimensiActive}" v-on:click="openSpaceTab(event, 'dimensi')">
          <p class="tab-text tab-text-default">Dimensi & Berat</p>
        </div>
        <div class="tab-navigation tab-default" v-bind:class="{ 'active-default': kapasitasActive}" v-on:click="openSpaceTab(event, 'kapasitas')">
          <p class="tab-text tab-text-default">Kapasitas</p>
        </div>
        <div class="tab-navigation tab-default" v-bind:class="{ 'active-default': kelistrikanActive}" v-on:click="openSpaceTab(event, 'kelistrikan')">
          <p class="tab-text tab-text-default">Kelistrikan</p>
        </div>
      </div>
      <div id="tab-1" class="spec-tab-panel" v-bind:style="{ display: mesinTab }">
        <table class="spec-table">
          <tbody>
            <tr class="spec-row" v-for="(value, name) in mesinData" :key="name">
              <td> {{ name }} </td>
              <td> {{ value }} </td>
            </tr>
          </tbody>
        </table>
      </div>
      <div id="tab-2" class="spec-tab-panel" v-bind:style="{ display: rangkaTab }">
        <table class="spec-table">
          <tbody>
            <tr class="spec-row" v-for="(value, name) in rangkaData" :key="name">
              <td> {{ name }} </td>
              <td> {{ value }} </td>
            </tr>
          </tbody>
        </table>
      </div>
      <div id="tab-3" class="spec-tab-panel" v-bind:style="{ display: dimensiTab }">
        <table class="spec-table">
          <tbody>
            <tr class="spec-row" v-for="(value, name) in dimensiData" :key="name">
              <td> {{ name }} </td>
              <td> {{ value }} </td>
            </tr>
          </tbody>
        </table>
      </div>
      <div id="tab-4" class="spec-tab-panel" v-bind:style="{ display: kapasitasTab }">
        <table class="spec-table">
          <tbody>
            <tr class="spec-row" v-for="(value, name) in kapasitasData" :key="name">
              <td> {{ name }} </td>
              <td> {{ value }} </td>
            </tr>
          </tbody>
        </table>
      </div>
      <div id="tab-5" class="spec-tab-panel" v-bind:style="{ display: kelistrikanTab }">
        <table class="spec-table">
          <tbody>
            <tr class="spec-row" v-for="(value, name) in kelistrikanData" :key="name">
              <td> {{ name }} </td>
              <td> {{ value }} </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    location: String,
    spec: Object
  },
  data () {
    return {
      mesinActive: true,
      rangkaActive: false,
      dimensiActive: false,
      kapasitasActive: false,
      kelistrikanActive: false,
      mesinTab: 'block',
      rangkaTab: 'none',
      dimensiTab: 'none',
      kapasitasTab: 'none',
      kelistrikanTab: 'none',
      mesinData: {},
      rangkaData: {},
      dimensiData: {},
      kapasitasData: {},
      kelistrikanData: {}
    }
  },
  methods: {
    openSpaceTab (evt, tab) {
      if (tab === 'mesin') {
        this.mesinActive = true
        this.rangkaActive = false
        this.dimensiActive = false
        this.kapasitasActive = false
        this.kelistrikanActive = false
        this.mesinTab = 'block'
        this.rangkaTab = 'none'
        this.dimensiTab = 'none'
        this.kapasitasTab = 'none'
        this.kelistrikanTab = 'none'
      } else if (tab === 'rangka') {
        this.mesinActive = false
        this.rangkaActive = true
        this.dimensiActive = false
        this.kapasitasActive = false
        this.kelistrikanActive = false
        this.mesinTab = 'none'
        this.rangkaTab = 'block'
        this.dimensiTab = 'none'
        this.kapasitasTab = 'none'
        this.kelistrikanTab = 'none'
      } else if (tab === 'dimensi') {
        this.mesinActive = false
        this.rangkaActive = false
        this.dimensiActive = true
        this.kapasitasActive = false
        this.kelistrikanActive = false
        this.mesinTab = 'none'
        this.rangkaTab = 'none'
        this.dimensiTab = 'block'
        this.kapasitasTab = 'none'
        this.kelistrikanTab = 'none'
      } else if (tab === 'kapasitas') {
        this.mesinActive = false
        this.rangkaActive = false
        this.dimensiActive = false
        this.kapasitasActive = true
        this.kelistrikanActive = false
        this.mesinTab = 'none'
        this.rangkaTab = 'none'
        this.dimensiTab = 'none'
        this.kapasitasTab = 'block'
        this.kelistrikanTab = 'none'
      } else if (tab === 'kelistrikan') {
        this.mesinActive = false
        this.rangkaActive = false
        this.dimensiActive = false
        this.kapasitasActive = false
        this.kelistrikanActive = true
        this.mesinTab = 'none'
        this.rangkaTab = 'none'
        this.dimensiTab = 'none'
        this.kapasitasTab = 'none'
        this.kelistrikanTab = 'block'
      }
    }
  },
  created () {
    this.mesinData = this.spec.mesin
    this.rangkaData = this.spec.rangka
    this.dimensiData = this.spec.dimensi
    this.kapasitasData = this.spec.kapasitas
    this.kelistrikanData = this.spec.kelistrikan
  }
}
</script>

As I said, the only problem with my single-product-spec component isn't that it won't load the props data. The problem is, it only loads the props data, when I change the code in my text editor (it's strange, I know). I began to realize this when I start to debugging, and when I change my code in single-product-spec component, the props data then began start to load. And if i don't change my single-product-spec component code, the props data won't load no matter how long i wait.

cantdocpp
  • 350
  • 7
  • 18
  • Use the created() hook instead of mounted(), beacuse mounted is called after the dom is rendered. More [info](https://stackoverflow.com/a/45814848/10781526) – Andreas Oct 18 '19 at 15:54
  • did you paste it incorrectly? your last example is closed with `` also `mesinTab` is not defined in your code. – Flame Oct 18 '19 at 15:55
  • also you do not need to use `let self = this` reference when you're using ES6 lambda syntax – Flame Oct 18 '19 at 15:56
  • hi @Flame, i deliberately paste my code incomplete, there's a method that handles mesinTab, but it's so long and redundant, also it not related to the problem I face. – cantdocpp Oct 18 '19 at 16:00
  • we cant tell when the code sample shows obvious errors – Flame Oct 18 '19 at 16:08
  • hi @Flame, i've edit and posted my full code – cantdocpp Oct 18 '19 at 16:24
  • hi @andreas, I can't try it because I don't have the server code with me right now, but I think I've tried it before. Tomorrow i'll try it again. – cantdocpp Oct 18 '19 at 16:27

1 Answers1

3

OK, so let's step through what happens in order:

  1. The parent component is created, triggering the created hook and initiating the data load from the server.
  2. The parent component renders, creating the child components. The prop value for spec will be null as the data hasn't loaded yet and singleProductSpec is still null.
  3. The created hook for single-product-spec runs. As this.spec is null I'd imagine this throws an error, though no error was mentioned in the question.
  4. At some point in the future the data load completes, updating the value of singleProductSpec. It is a rendering dependency of the parent component, so that component will be added to the rendering queue.
  5. The parent component will re-render. The new value of singleProductSpec will be passed as the spec prop to single-product-spec. A new instance of single-product-spec will not be created, it will just re-use the one it created it first rendered.

At that point nothing else will happen. The created hook of single-product-spec won't re-run as it hasn't just been created.

When you edit the source code of the child component it will trigger a hot-reload of that component. The exact effect of such a change will vary but often it will cause that child to be re-created without re-creating the parent. As the parent already has the data loaded from the server the newly created child will be have been passed the fully-populated spec value. This allows it to be read within the created hook.

There are a number of ways to solve this.

Firstly, we could avoid creating the single-product-spec until the data is ready:

<product-spec v-if="singleProductSpec" :spec="singleProductSpec" />

This will simply avoid creating the component during the initial render, so that when the child's created hook is run it has access to the data you want. This is probably the approach you should use.

A second way to do it would be to use a key. Keys are used to pair up components across re-renders so that Vue knows which old component matches which new component. If the key changes then Vue will throw away the old child component and create a new one instead. As a new component is created it will run the created hook. This probably isn't the best approach for your scenario as it isn't clear what the child component should do when passed a spec of null.

A third approach would be to use a watch in the child component. This would watch for when the value of spec changes and copy across the relevant values to the component's local data properties. While there are some occasions when using a watch like this is appropriate it usually indicates an underlying weakness in a component's design.

However, there are other problems in your code...

  1. It isn't clear why you're copying the values from the prop into local data in the first place. You can just use the prop directly. If you're doing it just to give them shorter names then just use a computed property instead. The only legitimate reason for copying them like this is if the property values can be changed within the child and the prop is only used to pass an initial value. Even in that scenario you wouldn't use a created hook, you'd just do it inside the data function. See https://v2.vuejs.org/v2/guide/components-props.html#One-Way-Data-Flow.
  2. You're duplicating everything 5 times for the 5 tabs. This should be implemented using an array of objects, with each object containing all the relevant details for a tab.
  3. The properties mesinActive and mesinTab both represent the same underlying data. You shouldn't have both in data. At the very least one should be a computed property, though personally I'd probably just get rid of mesinTab altogether. Instead use CSS classes to apply the relevant styling and just use mesinActive to decide which classes to apply (as you have elsewhere). Obviously the same applies to the other xActive/xTab properties.
  4. Your tabs are a form of single selection. Using 5 boolean values to represent one selection is not an appropriate data structure. The correct way to do this is to have a single property that identifies the current tab. The specifics can vary, it might hold the tab index, or the object representing the tab data, or an id representing the tab.
  5. You don't need to use let self = this with arrow functions. The this value is preserved from the surrounding scope.

Correctly implemented the code for single-product-spec should collapse down to almost nothing. You should be able to get rid of about 80% of the code. I would expect the method openSpaceTab to be a one-liner if you just use the appropriate data structures to hold all of your data.

Update:

As requested, here is a rewrite of your component taking into account points 1-4 from the 'other problems' section of my answer.

const ProductSpecTitle = {
  template: `
    <div>
      <div class="product-spec-title">
        Spesifikasi
      </div>
      <div class="produk-laris-wrapper">    
        <div class="tab-navigation-wrapper tab-navigation-default">
          <div
            v-for="tab of tabs"
            :key="tab.id"
            class="tab-navigation tab-default"
            :class="{ 'active-default': tab.active }"
            @click="openSpaceTab(tab.id)"
          >
            <p class="tab-text tab-text-default">{{ tab.text }}</p>
          </div>
        </div>
        <div
          v-for="tab in tabs"
          class="spec-tab-panel"
          :class="{ 'spec-tab-panel-active': tab.active }"
        >
          <table class="spec-table">
            <tbody>
              <tr
                v-for="(value, name) in tab.data"
                :key="name"
                class="spec-row"
              >
                <td> {{ name }} </td>
                <td> {{ value }} </td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>
    </div>
  `,
  
  props: {
    spec: Object
  },
  
  data () {
    return {
      selectedTab: 'mesin'
    }
  },
  
  computed: {
    tabs () {
      const tabs = [
        { id: 'mesin', text: 'Mesin' },
        { id: 'rangka', text: 'Rangka & Kaki' },
        { id: 'dimensi', text: 'Dimensi & Berat' },
        { id: 'kapasitas', text: 'Kapasitas' },
        { id: 'kelistrikan', text: 'Kelistrikan' }
      ]
      
      for (const tab of tabs) {
        tab.active = tab.id === this.selectedTab
        tab.data = this.spec[tab.id]
      }
      
      return tabs
    }
  },

  methods: {
    openSpaceTab (tab) {
      this.selectedTab = tab
    }
  }
}

new Vue({
  el: '#app',
  
  components: {
    ProductSpecTitle
  },
  
  data () {
    return {
      spec: {
        mesin: { a: 1, b: 2 },
        rangka: { c: 3, d: 4 },
        dimensi: { e: 5, f: 6 },
        kapasitas: { g: 7, h: 8 },
        kelistrikan: { i: 9, j: 10 }
      }
    }
  }
})
.tab-navigation-wrapper {
  display: flex;
  margin-top: 10px;
}

.tab-navigation {
  border: 1px solid #000;
  cursor: pointer;
}

.tab-text {
  margin: 10px;
}

.active-default {
  background: #ccf;
}

.spec-tab-panel {
  display: none;
}

.spec-tab-panel-active {
  display: block;
  margin-top: 10px;
}

.spec-table {
  border-collapse: collapse;
}

.spec-table td {
  border: 1px solid #000;
  padding: 5px;
}
<script src="https://unpkg.com/vue@2.6.10/dist/vue.js"></script>

<div id="app">
  <product-spec-title :spec="spec"></product-spec-title>
</div>
tony19
  • 125,647
  • 18
  • 229
  • 307
skirtle
  • 27,868
  • 4
  • 42
  • 57
  • 2. So you're saying that I should make an array of objects in component data, and make the tabs by looping that array of objects? 4. I still can't understand how I can show the tab data, based on the opened tab. How do I know that this tab is open, while the others are not? except, I make a long if-else in the template. And how can i show xData when xTab is opened/true, when i only have to make one tab on the template ? i'd be grateful if you have an example or reference guide about points 2, 3, and 4. – cantdocpp Oct 18 '19 at 21:53
  • @BeingShame I have updated my answer with an example of how your component could be rewritten to take into account those points. However, this is just one way it could be done. It should not be interpreted as the 'one true way'. – skirtle Oct 18 '19 at 23:57
  • hi, thank you very much for your answer. It really open my eyes ! as a junior frontend, i don't have anywhere to ask, so your answer really help me, thank you :) – cantdocpp Oct 19 '19 at 05:25
  • This explanation is perfect! Good job! Thanks! – Vasilije Bursac Sep 05 '21 at 01:14