No More Posting New Topics!

If you have a question or an issue, please start a thread in our Github Discussions Forum.
This forum is closed for new threads/ topics.

Navigation

    Quasar Framework

    • Login
    • Search
    • Categories
    • Recent
    • Tags
    • Popular
    • Users
    • Groups
    • Search

    How to remove a tab-panel with keep-alive.

    Help
    3
    17
    1975
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • CWoodman
      CWoodman last edited by CWoodman

      I have q-tabs with ‘close’ buttons on them. I’ve made q-tab-panels ‘keep-alive’, so if the tab is still available but not active and user returns to the previously active tab, it will show the same state as before. But even when the tab is closed, the panel remains in memory. What’s the best way to destroy it when user closes the tab?

      Note: Tabs are managed by a store module, and each tab-panel contains a component from a large selection of possible components. So removing a tab involves removing an element from an array in the store state. It’s at that point that I need to do whatever it takes to destroy the panel component.

      1 Reply Last reply Reply Quote 0
      • qyloxe
        qyloxe last edited by

        well, let’s see some code. I assume you are using something like this:

          <q-tab-panels keep-alive :value="value" @input="inputChanged" class="fit" :swipeable="$q.platform.has.touch">
            <q-tab-panel v-for="tab in tabsList" :name="tab.id" :key="tab.id" class="fit q-pa-none">
              <component-1 :tab="tab" v-if="tab.kind==='app-tree'" :key="tab.id"></component-1>
              <component-2 :tab="tab" v-if="tab.kind==='app-dashboard'" :key="tab.id"></component-2>
              ...
              <component-99 :tab="tab" v-if="tab.kind==='something-else'" :key="tab.id"></component-99>
        
        

        In above configuration you have:

        • dynamic tabs based on tabsList computed value
        • tab close button (implemented separately - basically just removing tab from tabsList)
        • tab visual state preserved via keep-alive
        • tab app state preserved by vuex (implemented in each tab component)
        • each tab is its own component
        • each visual tab is properly “destroyed” by vue because of dynamic “v-if” and “:key”

        You can optimize this config and make it even more dynamic by using vue “is” special attribute:

        https://vuejs.org/v2/api/#is

        BUT I would suggest to firstly make it working and then deal with “is” specifics.

        CWoodman 1 Reply Last reply Reply Quote 0
        • CWoodman
          CWoodman @qyloxe last edited by

          @qyloxe Don’t have the ‘v-if’ or ‘:key’ bits. Can you explain ‘v-if="tab.kind===’ ?

          Here’s my complete code…

          <template>
            <div class="bg-white">
              <q-tabs
                dense
                no-caps
                inline-label
                align="left"
                :breakpoint="0"
                active-color="blue-grey-1"
                active-bg-color="active"
                indicator-color="red-10"
                switch-indicator
                class="tab-strip-style"
                v-model="selectedTab"
                @drop="onDrop($event)"
                @dragover="dragOver($event, null)"
                @dragleave="dragLeave($event)"
              >
                <q-tab v-for="tab in tabs" :key="tab.name"
                  :name="tab.name"
                  :id="tab.name"
                  :label="tab.name"
                  class="tab-style"
                  :draggable="draggable"
                  @dragstart="startDrag($event, tab)"
                  @dragover="dragOver($event, tab)"
                  @dragleave="dragLeave($event, tab)"
                  @drop="onDrop($event, tab)"
                  @click="onClick(tab.template)"
                  >
                  <q-btn dense flat icon="fas fa-times" size="xs"
          
                    class="q-pr-none q-ml-sm q-mb-sm"
                    @click.stop="closeTab(tab.name)"
                  />
                  <q-menu
                    touch-position
                    context-menu
                  >
                    <q-list dense style="min-width: 100px">
                      <q-item v-if="pane === 0"
                        clickable v-close-popup
                        :disable="tabs.length < 2"
                        @click="moveTab(tab.name, 'v')">
                        <q-item-section>Split right</q-item-section
                      >
                      </q-item>
                      <q-item v-if="pane === 0"
                        clickable v-close-popup
                        :disable="tabs.length < 2"
                        @click="moveTab(tab.name, 'h')"
                      >
                        <q-item-section>Split down</q-item-section>
                      </q-item>
                      <q-item v-if="pane !== 0"
                        clickable v-close-popup
                        @click="moveTab(tab.name, 'home')"
                      >
                        <q-item-section>Un-split</q-item-section>
                      </q-item>
                      <q-separator />
                      <q-item clickable v-close-popup
                        @click="closeTab(tab.name)"
                      >
                        <q-item-section>Close tab</q-item-section>
                      </q-item>
                    </q-list>
                  </q-menu>
                </q-tab>
              </q-tabs>
          
              <q-tab-panels class="overflow-hidden"
                keep-alive
                v-model="selectedTab"
              >
                <!-- overflow hidden to hide unneeded scrollbars        -->
                <q-tab-panel class="q-pa-none overflow-hidden" id="tab-panel"
                  v-for="tab in tabs" :key="tab.name"
                  :name="tab.name"
                  :style="panelHeightStyle"
                >
                    <component
                      :is="tab.template"
                      :componentProps="tab.props"
                      :splitterRatio="splitterRatio"
                      :height="panelHeight"
                    >
                    </component>
                </q-tab-panel>
              </q-tab-panels>
            </div>
          </template>
          
          qyloxe 1 Reply Last reply Reply Quote 0
          • qyloxe
            qyloxe @CWoodman last edited by

            @CWoodman said in How to remove a tab-panel with keep-alive.:

            question: “when the tab is closed, the panel remains in memory” - which memory: javascript object is not garbage collected, vue virtual dom node is still accessible or browser’s DOM node is still alive? How do you know that it really remains - is it visible, you have something on console, you can see it in the object inspector?

            @qyloxe Don’t have the ‘v-if’ or ‘:key’ bits. Can you explain ‘v-if="tab.kind===’ ?

            oh, from Vue point of view it is essentially the same as your <component :is="..." solution. It allows Vue to dynamically “destroy” virtual node. Your code has a good concept.

            Here is what I would check:

            1

            >      <q-tab v-for="tab in tabs" :key="tab.name"
            

            Is tab.name REALLY unique? Check this rigorously. And then check again.

            2

            >       <q-tab v-for="tab in tabs" :key="tab.name"
            >         ...
            >         :id="tab.name"
            

            Probably you do not need :id attribute. It could mess with vdom-dom interaction. Remove this. If you do need to access this component, please use Vues “ref” attribute:
            https://vuejs.org/v2/api/#ref

            3

            >         <q-btn dense flat icon="fas fa-times" size="xs"
            >           ...
            >           @click.stop="closeTab(tab.name)"
            

            You are removing tab from vuex tabs array in the context of the event which is fired from the DOM node of that specific tab. There are possible async-sync quirks in such situations. Make sure, that Vue vdom is updated AFTER the change to the state in Vuex. If this would be the case then $nextTick could help:
            https://vuejs.org/v2/api/#vm-nextTick

            4

            >       <q-tab-panel class="q-pa-none overflow-hidden" id="tab-panel"
            >         v-for="tab in tabs" :key="tab.name"
            >         :name="tab.name"
            

            error: Using constant id attribute (tab-panel) when the node is in v-for is an error. Id attribute should be unique in the whole document:
            https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id

            This is the first thing I would correct, there’s possibility that it will fix your “memory error”.

            Check again if tab.name is unique, this time if it is still unique DURING your Vue problematic life cycle events (beforeCreate/beforeDestroy).

            5

            >           <component
            >             :is="tab.template"
            >             :componentProps="tab.props"
            

            This is tricky. Two possible problems:

            • firstly if your dynamic component selected by tab.template has some “static” data, ie objects shared between all component instances, there is possibility for Vue to keep all of them in memory.
            • secondly - depending on your tab.props implementation, async-sync sequence when the tab is closed/destroyed, it is possible, that this code could make a javascript circular reference and prevent garbage collector from removing those objects. It is implementation specific, could be quite easily checked in object inspector.
            dobbel CWoodman 2 Replies Last reply Reply Quote 0
            • dobbel
              dobbel @qyloxe last edited by dobbel

              @qyloxe

              a javascript circular reference … could be quite easily checked in object inspector.

              How is circular reference checked easily in the inspector?

              qyloxe 1 Reply Last reply Reply Quote 0
              • CWoodman
                CWoodman @qyloxe last edited by

                @qyloxe Thanks for your input. Note the ‘id=’ and ‘overflow-hidden’ stuff is left over from attempts to get rid of unwanted scrollbars when the tabpanel is moved into a splitter pane. I can just delete those now. (Trying a different approach, but that’s another story!)

                Tabs are created by a store module that ensures names are unique.

                Not sure how to use nextTick in this case…
                The tab manager component just calls a store action to delete a tab:

                methods: {
                    ...mapActions('workTabs', ['addTab', 'removeTab', 'selectTab', 'setDragTab', 'rearrangeTab', 'addSplit', 'moveTabHome']),
                    ...mapActions('main', ['showHelp']),
                
                    closeTab (name) {
                      this.removeTab(name)
                    },
                

                And the new setup is rendered when the store state changes…

                computed: {
                  tabs: function () {
                      const allTabs = this.tabList
                      const myTabs = allTabs.filter(item => item.pane === this.pane)
                      return myTabs
                    },
                

                Like this…

                <q-tab v-for="tab in tabs" :key="tab.name"
                        :name="tab.name"
                

                Where should I put the ‘nextTick’?

                qyloxe 1 Reply Last reply Reply Quote 0
                • qyloxe
                  qyloxe @dobbel last edited by

                  @dobbel said in How to remove a tab-panel with keep-alive.:

                  @qyloxe

                  a javascript circular reference … could be quite easily checked in object inspector.

                  How is circular reference checked easily in the inspector?

                  like this for example:

                  https://developers.google.com/web/tools/chrome-devtools/memory-problems/heap-snapshots

                  1 Reply Last reply Reply Quote 0
                  • qyloxe
                    qyloxe @CWoodman last edited by

                    @CWoodman said in How to remove a tab-panel with keep-alive.:

                    Where should I put the ‘nextTick’?

                    $nextTick should be called in the context of the event - such as:

                         closeTab (name) {
                          this.$nextTick(function () {
                            // DOM is now updated
                            // `this` is bound to the current instance
                                 this.removeTab(name)
                                 })
                         },
                    

                    If this removeTab is using vuex action then it should be ok, because vuex actions are asynchronous - BUT - devil is always in the implementations, and we need to test, test and test.

                    There is a potential another problem:

                       tabs: function () {
                           const allTabs = this.tabList
                           const myTabs = allTabs.filter(item => item.pane === this.pane)
                           return myTabs
                         },
                    

                    myTabs has object references to vuex objects. Vuex objects could be modified only via commits and yet, if you change directly something in nested tab object properties (for example in tab.props) then it would break Vuex and possibly lead to circular reference. Make sure that you are testing your app in dev mode, as in this mode, vuex shows some additional errors in such situations. More here:
                    https://vuex.vuejs.org/guide/strict.html

                    Anyway, I have a similar IDE style app with dynamic tabs, and I’m also putting all those data in Vuex. Yet, I’m too afraid 🙂 to use those objects directly, just to avoid such hard situations as yours and I’m using those data only via this:
                    https://vuex-orm.org/
                    I have defined tabs model, the state changes are very clear, I can use nested subobjects safely, everything works OK, the computed properties are in most situations unnecessary, I use my app state as local DB with queries, updates, and inserts. I do recommend to use vuex-orm.

                    CWoodman 1 Reply Last reply Reply Quote 0
                    • CWoodman
                      CWoodman @qyloxe last edited by CWoodman

                      @qyloxe Thanks for the nextTick instruction. But the panel content still stays in memory when I close the tab.

                      Re: the ‘other problem’…
                      I thought that using const allTabs = this.tabList would safely protect the store state tablist from unwanted modification. Is that not the case? (Still learning javascript 🙂

                      And I’ll check out vuex-orm.

                      I really appreciate your help.

                      qyloxe 1 Reply Last reply Reply Quote 0
                      • CWoodman
                        CWoodman last edited by

                        Can a parent component destroy one of its children? Or tell the child to commit suicide?

                        qyloxe 1 Reply Last reply Reply Quote 0
                        • qyloxe
                          qyloxe @CWoodman last edited by

                          @CWoodman said in How to remove a tab-panel with keep-alive.:

                          But the panel content still stays in memory when I close the tab.

                          🙂 still don’t know what that means in your situation and how do you measure/observe that…

                          I thought that using const allTabs = this.tabList would safely protect the store state tablist from unwanted modification. Is that not the case? (Still learning javascript 🙂

                          Nope. This tutorial looks ok in our case:
                          https://javascript.info/object-copy

                          That reminds me, that if you want to check if this is really a problem with vuex nested references, then there is a simple testing method. Instead of this:

                             return myTabs
                          

                          use for testing this:

                             return JSON.parse(JSON.stringify(myTabs))
                          

                          This is the most safe method for deep cloning objects. If your code will start working after that, then it means you are messing with vuex state. If your code would still have “memory” problems, then the problem is somewhere else.

                          1 Reply Last reply Reply Quote 0
                          • qyloxe
                            qyloxe @CWoodman last edited by

                            @CWoodman said in How to remove a tab-panel with keep-alive.:

                            Can a parent component destroy one of its children? Or tell the child to commit suicide?

                            Well, there is this instance method:

                            https://vuejs.org/v2/api/#vm-destroy

                            BUT if you want to use that (at the app level), it just means, that you are doing something wrong. The visual components should be managed by Vue, the virtual dom mechanics and at the lower level, by javascript rules for object references (that’s why I ask and ask about what you exactly mean by “memory problem”). You shouldn’t manually “destroy” components, you should just declare/inform/tell that you are no longer use them or need them. That should do.

                            Bear in mind, that memory management in browsers is indeterministict, so even if you perceive memory loss, it could be garbage collected much, much later and everything is just ok. The problem is with memory deltas, ie when same objects are continuously taking heap memory without releasing it. It is observable in devtools as I said earlier.

                            From what I saw in this thread, the question is still what is that “memory loss” exactly?

                            CWoodman 1 Reply Last reply Reply Quote 0
                            • CWoodman
                              CWoodman @qyloxe last edited by

                              @qyloxe Thanks, I had forgotten to remember about object references.

                              Here are a couple of screen shots that make me think stuff is being left in memory unnecessarily…

                              First shot shows two tabs open, ‘Introduction’ and ‘Recipe: untitled’.Inactive Tab.png

                              If I now close the ‘Introduction’ tab, I still see it in the Vue devtools…
                              Tab removed.png

                              Note, if I move one of two tabs to a split pane, and then close it, I remove the split and the closed tab no longer appears in the Vue component list.

                              Here’s a view with two tabs in separate split panes…
                              Split tabs.png

                              And here the ‘StockList’ tab has been closed, automatically unsplitting the window…
                              Split removed.png

                              Seems to me, when I just close a tab in the same pane, it should get removed from the Vue component list. No?

                              qyloxe 1 Reply Last reply Reply Quote 0
                              • qyloxe
                                qyloxe @CWoodman last edited by

                                @CWoodman ach you’re saying about vue dev panel! That’s different. I’m not sure how that vue dev panel works exactly - for example it allows to change the history so maybe it just mess somehow with the app?

                                My observations:

                                1. I checked with my app, where I have also dynamic tabs: in my app, the tabs are properly removed from vue dev tools. All is ok.

                                2. Interesting, that it marks those tabs as inactive grey nodes. Don’t know what that means. Please look up in vue dev tool docs. Why they’re “inactive”?

                                3. What is exactly your method of removing tab object from tabs list in vuex? Can you show those specific functions in vuex actions and setters?

                                4. I had in the past some quirks with keep-alive mode in q-tabs, there were some github discussions even 🙂 Maybe, this is a long shot, but keep-alive could be counteintuitive.

                                5. Please override all vue lifecycle events for your tab component and just console.log in all of them to see, if this tab is REALLY destroyed/created and what is the real sequence of those events in dev mode and in production mode.

                                CWoodman 1 Reply Last reply Reply Quote 0
                                • CWoodman
                                  CWoodman @qyloxe last edited by

                                  @qyloxe I tried watching the memory used in devtools while repetitively opening and closing a tab with a fairly big set of data, and the memory seemed pretty constant - went up when the tab was added, and went down shortly after closing it. So maybe I’m worried about nothing - just got concerned by watching the Vue devtools component list with its ‘inactive’ items.

                                  My Vuex code for removing a tab is pretty big, because I have to manage up to 2 possible splitter panes as well. Plus a tab can be dragged from one splitter pane to another. I’ll share it if you really want to see it.

                                  qyloxe 1 Reply Last reply Reply Quote 0
                                  • qyloxe
                                    qyloxe @CWoodman last edited by

                                    @CWoodman said in How to remove a tab-panel with keep-alive.:

                                    @qyloxe I tried watching the memory used in devtools while repetitively opening and closing a tab with a fairly big set of data, and the memory seemed pretty constant - went up when the tab was added, and went down shortly after closing it.

                                    That’s what I thought when I asked about what measures of memory you are taking.

                                    The chrome dev tools are only reliable measure.

                                    Vue dev tools in terms of memory are not reliable, because its observations could intefere with observed objects.

                                    I’ll share it if you really want to see it.

                                    Not necessarily, no. The important part is only one line of code in vuex setter, where you remove item from list. You can debug it yourself, and check what is happening to this particular removed object. This is more advanced stuff yet very, very rewarding.

                                    So, at the end of the day, I would leave your question as resolved at this stage, and in the future, in quality assurance moment, I would put more stress tests on tab behaviours. In the meantime I would just go to build the app and consider this ride an excellent teaching moment 🙂

                                    1 Reply Last reply Reply Quote 0
                                    • CWoodman
                                      CWoodman last edited by

                                      I agree. Thanks again for your help!

                                      1 Reply Last reply Reply Quote 0
                                      • First post
                                        Last post