Stanniebeer
Stanniebeer

Reputation: 89

Vue keep alive component when moving to other array

I'm trying to keep a component alive when moving the bound item object to a different data array. Because it gets moved, the default keep-alive tag doesn't work.

I need this to improve loading time when dynamic components in my app use external libraries.

Simplified example: (https://jsfiddle.net/eywraw8t/24419/)

HTML:

<div id="app">
  <div v-for="list in lists">
    <h1>{{ list.title }}</h1>
    <ul>
      <draggable v-model="list.items" :options="{group: 'list-items'}">
        <list-item 
           v-for="item in list.items" 
           :key="item.key" 
           :content="item.content">
        </list-item>
      </draggable>
    </ul>
  </div>
</div>

JS:

Vue.component('list-item', {
  props: {
    content: {
        required: true
    }
  },
  mounted () {
    document.body.insertAdjacentHTML('beforeend', 'Mounted! ');
  },
  template: '<li>{{ content }}</li>'
})

new Vue({
  el: "#app",
  data: {
    lists: [
        {
        title: 'List 1',
        items: [
            { key: 'item1', content: 'Item 1' },
          { key: 'item2', content: 'Item 2' },
          { key: 'item3', content: 'Item 3' }
        ]
      },
      {
        title: 'List 2',
        items: [
            { key: 'item4', content: 'Item 4' },
          { key: 'item5', content: 'Item 5' },
          { key: 'item6', content: 'Item 6' }
        ]
      }
    ]
  }
})

Upvotes: 3

Views: 1752

Answers (2)

Richard Matsen
Richard Matsen

Reputation: 23483

If the problem is just one of caching expensive html build, you can do it by removing the list-item component from the template and building them ahead of time in app.mounted().

How well this works in your real-world scenario depends on the nature of item.content and it's lifecycle.

console.clear()
const ListItem = Vue.component('list-item', {
  props: {
    content: {
      required: true
    }
  },
  mounted () {
    document.body.insertAdjacentHTML('beforeend', 'Mounted! ');
  },
  template: '<li>{{ content }}</li>'
})

new Vue({
  el: "#app",
  methods: {
    getHtml(content) {
      const li = new ListItem({propsData: {content}});
      li.$mount()
      return li.$el.outerHTML
    }
  },
  mounted () {
    this.lists.forEach(list => {
      list.items.forEach(item => {
        const cacheHtml = this.getHtml(item.content)
        Vue.set( item, 'cacheHtml', cacheHtml )
      })
    })
  },
  data: {
    lists: [
    	{
      	title: 'List 1',
        items: [
        	{ key: 'item1', content: 'Item 1' },
          { key: 'item2', content: 'Item 2' },
          { key: 'item3', content: 'Item 3' }
        ]
      },
      {
      	title: 'List 2',
        items: [
        	{ key: 'item4', content: 'Item 4' },
          { key: 'item5', content: 'Item 5' },
          { key: 'item6', content: 'Item 6' }
        ]
      }
    ]
  }
})
ul {
  margin-bottom: 20px;
}

li:hover {
  color: blue;
  cursor: move;
}

h1 {
  font-size: 20px;
  font-weight: bold;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.6.0/Sortable.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Vue.Draggable/15.0.0/vuedraggable.min.js"></script>

<div id="app">
  <div v-for="list in lists">
    <h1>{{ list.title }}</h1>
    <ul>
      <draggable v-model="list.items" :options="{group: 'list-items'}">
        <div v-for="item in list.items" :key="item.key">
          <li v-html="item.cacheHtml"></li>   
        </div>
      </draggable>
    </ul>
  </div>
</div>

Reactive item.content

To keep reactivity when item.content changes, you will need a little more code.

  • add a copy of the item.content to the cache
  • add a method to fetch the cached html with refresh if content has changed.

(You may be able to do this a little more elegantly with a parameterized computed property).

To simulate an item.content change, I've added a setTimeout to mounted().

console.clear()
const ListItem = Vue.component('list-item', {
  props: {
    content: {
      required: true
    }
  },
  mounted () {
    document.body.insertAdjacentHTML('beforeend', 'Mounted! ');
  },
  template: '<li>{{ content }}</li>'
})

new Vue({
  el: "#app",
  methods: {
    getHtml(content) {
      const li = new ListItem({
        propsData: { content }
      });
      li.$mount()
      return li.$el.outerHTML
    },
    cacheHtml(item) {
      if (item.cache && item.cache.content === item.content) {
        return item.cache.html
      } else {
        const html = this.getHtml(item.content)
        const cache = {content: item.content, html} 
        Vue.set(item, 'cache', cache)
      }
    }
  },
  mounted () {
    this.lists.forEach(list => {
      list.items.forEach(item => {
        this.cacheHtml(item)
      })
    })
    setTimeout(() => 
      Vue.set( this.lists[0].items[0], 'content', 'changed' )
    ,2000)      
  },
  data: {
    lists: [
    	{
      	title: 'List 1',
        items: [
        	{ key: 'item1', content: 'Item 1' },
          { key: 'item2', content: 'Item 2' },
          { key: 'item3', content: 'Item 3' }
        ]
      },
      {
      	title: 'List 2',
        items: [
        	{ key: 'item4', content: 'Item 4' },
          { key: 'item5', content: 'Item 5' },
          { key: 'item6', content: 'Item 6' }
        ]
      }
    ]
  }
})
ul {
  margin-bottom: 20px;
}

li:hover {
  color: blue;
  cursor: move;
}

h1 {
  font-size: 20px;
  font-weight: bold;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.6.0/Sortable.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Vue.Draggable/15.0.0/vuedraggable.min.js"></script>

<div id="app">
  <div v-for="list in lists">
    <h1>{{ list.title }}</h1>
    <ul>
      <draggable v-model="list.items" :options="{group: 'list-items'}">
        <div v-for="item in list.items" :key="item.key">
          <li v-html="cacheHtml(item)"></li>   
        </div>
      </draggable>
    </ul>
  </div>
</div>

Upvotes: 3

Ben Croughs
Ben Croughs

Reputation: 2644

i looked into your problem and i think i might found a solution, i cannot do it in js fiddle but i'll try and explain it:

in your js fiddle the mounted is hooked in your list-item component, so indeed every time that state changed (when dragging), the event is triggered.

i create a setup with a main templated component (componentX), with a mounted function, and then created a seperated list-item component

in my sample you will see the mounted twice at the start, that is normal since we have 2 lists! but then when you start to drag and drop you will not get additional mounted events

you can download the solution in a zip from:

http://www.bc3.eu/download/test-vue.zip

it is a vue cli project, so you can just npm run dev to start a local server

Upvotes: 1

Related Questions