Issue with loading dynamic data via Dynamic tabs

Greetings!

I’m new to Vue and am attempting to build a UI that contains each page within a “tab” that is located within “toolbar” component. Think “code editor”, such as, for example, codesandbox.io, Visual Studio Code. Those kind of tabs. :slight_smile:

I apologize for any lack of information. Please let me know if I’m missing any information as I’m attempting to put my issue into more words than:

Dynamically generated tabs only show the data of the first tab “clicked” on. Subsequently generated tabs will show content of first tab clicked. Only happens with tabs that reference [_id.vue | mysite.com/ticket/1] and not pages that have a static value, such as [new-ticket.vue | mysite.com/ticket/new-ticket]

I’m still in the process of learning and experimenting with idea’s and am currently stuck on an issue where, when I click on the “tab” to view the contents of the page, it only displays the contents of the first tab opened. Rather than the data that is corresponding to the slug in the url.

For example, if I grabbed Json data from an API with the following information in it:

[
	{
		"id": 1,
		"subject": "Hello World",
		"content": "This is content for hello world with id of 1."
	},
	{
		"id": 2,
		"subject": "Goodbye World",
		"content": "This is content for goodbye world with id of 2."
  },
]

And clicked on links that opened up tabs for mysite.com/ticket/1 and mysite.com/ticket/2. If I clicked on tab 2, it would show the data of ticket/2. Then if I clicked on another tab, such as ticket/1, it would still show the content of ticket/2. However, if I created a new tab that wasnt passing a parameter such as _id. For example, ticket/new-ticket, it would load the data for that tab just fine. Then if I went to either ticket/1 or ticket/2 it would show ticket/2 data as before.

Again I apologize for the fragmentation of this issue as I’m attempting to learn a lot about a lot of different components that make the whole thing work and appreciate everyone’s time in schooling my @$$. :smiley:

Basic Default.vue layout

<template lang="pug">
  v-app
    // Display toolbar
    AppTabToolbar
    // Main (left) navigation
    AppNavigationLeft
    // Display Router-View Content
    AppContent
</template>
<script>
import AppTabToolbar from "../components/AppTabToolbar";
import AppNavigationLeft from "../components/AppNavigationLeft";
import AppContent from "../components/AppContent";
export default {
  components: {
    AppContent,
    AppNavigationLeft,
    AppTabToolbar
  },
  data: () => ({}),
  computed: {
    isHome() {
      return this.$route.path === "/";
    },
    namespace() {
      return this.$route.name;
    }
  }
};
</script>

Toolbar Component that holds my tabs

<template lang="pug">
  v-toolbar.outline-border-bottom(app clipped-right fixed flat dense color='white')
    // Tabs
    v-tabs(left)
      v-tab(v-model="tabsList" v-for='item in tabsList' :key='item.id' :to="item.url")
        span {{ item.label }}
        v-btn(icon @click='removeItem(item)')
          v-icon close
    v-spacer
    v-menu.mr-2(top center)
      template(v-slot:activator='{ on }')
        v-btn(icon v-on='on' ripple="false" small :ripple="false")
          v-icon add
          | New
      v-list
        v-list-tile(v-for='item in newItems' :key='item.id' :to="item.url" @click='addItem({ label: item.label, url: item.url })')
          v-list-tile-title  {{ item.label }}
</template>
<script>
export default {
  name: "AppTabToolbar",
  data() {
    return {
      newItems: [
        { label: "New Ticket", url: "/ticket/new-ticket" },
        { label: "New Email", url: "/ticket/new-email" },
        { label: "New Chat", url: "/ticket/new-chat" }
      ]
    };
  },
  computed: {
    tabsList() {
      return this.$store.state.openTabs.tabs;
    }
  },
  mounted: function() {
    //
  },
  methods: {
    currentTabCount() {
      return this.$store.state.openTabs.count;
    },
    addItem(tab) {
      this.$store.commit("openTabs/add", tab);
    },
    removeItem(item) {
      this.$store.state.openTabs.tabs = this.$store.state.openTabs.tabs.filter(
        e => e.id !== item.id
      );
    }
  }
};
</script>

Page that renders my data (pages/ticket/_id.vue)

<template lang="pug">
v-container(ma-0 pa-0 fluid)
  v-layout(align-space-between justify-start row fill-height)
    v-flex.listbox-max-vh
      v-card.outline-border-right(height="100vh")
        .vuebar-element(v-bar)
          v-list.listbox-max-vh.scroll-y.pt-0
            v-list-tile
              v-list-tile-content
                v-list-tile-title.caption(v-text='ticket.subject')
                br
                | {{ ticket.content }}
            v-list-tile(v-for='reply in replies' :key='reply.id')
              v-list-tile-content
                v-list-tile-title.caption(v-text='reply.author')
                | {{ reply.content }}
  v-navigation-drawer(v-model='drawerRight' app clipped fixed flat right)
    v-list(dense)
      v-list-tile(@click.stop='right = !right')
        v-list-tile-action
          v-icon exit_to_app
        v-list-tile-content
          v-list-tile-title User/System Panel
</template>
<script>
export default {
  data: function() {
    return {
      drawerRight: true,
      right: false,
      id: this.$route.params.id,
      ticket: [],
      replies: []
    }
  },
  created: function() {
    // Alias the component instance as `vm`, so that we
    // can access it inside the promise function
    const vm = this
    // Fetch our array of posts from an API
    fetch('https://tt2mn.sse.codesandbox.io/json/tickets.json')
      .then(function(response) {
        return response.json()
      })
      .then(function(data) {
        vm.ticket = data.find(ticket => ticket.uuid === vm.id)
      })
    fetch('https://tt2mn.sse.codesandbox.io/json/tickets.responses.json')
      .then(function(response) {
        return response.json()
      })
      .then(function(data) {
        vm.replies = data.filter(reply => reply.ticket_id === vm.ticket.id)
      })
  }
}
</script>

I have a feeling that my issue lies in how I am manipulating/mutating/storing the persistent data for the tabs in my store(s).

store/openTabs.js

export const state = () => ({
  count: 1,
  tabs: [{ id: 0, label: "Ticket List", url: "/tickets" }]
});

export const mutations = {
  add(state, payload) {
    state.tabs.push({
      id: state.count++,
      label: payload.label,
      url: payload.url
    });
  },
  remove(state, payload) {
    state.tabs.splice(state.tabs.indexOf(payload), 1);
  }
};

export const actions = {
  add(state, payload) {
    state.tabs.push(payload);
  }
};

I’ve created/added a mixin plugin.

plugins/mixinTabsManager.js

import Vue from 'vue'

Vue.mixin({
  data: () => ({
    tabCount: 0
  }),
  methods: {
    listTabs() {
      return this.$store.state.openTabs.tabs
    },
    addToTabs(tab) {
      // this.$store.commit('openTabs/add')
      this.$store.commit('openTabs/add', {
        id: this.tabCount++,
        label: tab.label,
        url: tab.url
      })
    },
    countItUp(tab) {
      this.$store.commit('counter/increment')
    }
  }
})

I uploaded a copy of what I’m doing via codesandbox, but unfortunately it doesn’t want to load. 502 bad gateway :frowning:. New to using codesandbox as well and am still learning the quirks that comes with it as well.

I understand that I have a lot to learn and have a long way to go before I am going to be anywhere near competent with the framework. Hopefully this will also help anyone else that encounters this issue while attempting to utilize a similar setup.

Anyways, hopefully just by viewing the code, you can direct me to what/where I need to learn how to fix the issue and what the issue may be.

Also if anyone know of any good articles/examples/screencasts/tutorials that cover the concepts that I am trying to implement, please let me know as I would be most grateful!

Link to my codesandbox: https://codesandbox.io/s/vuejs-tabs-dynamic-tt2mn

Thanks!

Hello, please use syntax highlighting when posting code. It makes it much easier to read through lots of code.

As for your issue, in honesty I haven’t read through all your code, but I couldn’t see anywhere in your tab component (or parent of that) where you are watching for the data to change.

Basically, when you re-use the same component on the same url structure, but only change a parameter of the url (e.g. /ticket/1 to /ticket/2) the component won’t re-render (because it’s already mounted) and therefore won’t call the hooks you use to load the data.

Therefore you need to either use an in-component route guard to run the method that updates your data or use a watcher in your component to watch the route param.

It’s also better to pass route params as props to a component.

So, your component will then look something like:

export default {
  props: {
    id: {
      type: String,
      required: true
    }
  },
  
  methods: {
    fetchTabData (id) {
      // move logic from your created hook to a method
    }
  },
  
  // Will only run if the route updates
  beforeRouteUpdate (to, from, next) { 
    this.fetchTabData(to.params.id);
    next();
  },
  
  created () {
    // Runs when component initially mounts
    this.fetchTabData(this.id);
  }
}

Thank you for your response. I’ve updated my initial response with sytax highlighting (Do’h!). Thank you for the info, from what I’ve been able to gather so far, it will definitely help me understand what I’m attempting to accomplish more. And I’ll be able to learn something too!!