nachocab
nachocab

Reputation: 14364

How to trigger a render after manually modifying a slot in Vue.js?

Reproducible simplified example: https://jsfiddle.net/nachocab/evc2374p/235/

More complex example: https://jsfiddle.net/nachocab/evc2374p/125/ (pressing the right arrow twice (doesn't work), then left, then right (it works). More details

I know this is not the Vue way, but I need to modify a string of HTML that I receive from a server (it represents a slide deck with slides). I traverse the DOM tree and change data-hidden to true for a few elements and I would like to trigger an update in the slide component.

I've tried several things, including changing the component key, emitting an event and calling forceUpdate, but it doesn't work.

Template:

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


<template id="wrapper">
  <slide-deck>
    <slide>
      <p data-hidden="true">Hello</p>
    </slide>
  </slide-deck>
</template>

JS:

Vue.prototype.$eventBus = new Vue();
Vue.component('slide-deck', {
  created() {
    window.addEventListener('keydown', this.handleKeydown);
  },
  destroyed() {
    window.removeEventListener('keydown', this.handleKeydown);
  },
  methods: {
    handleKeydown(e) {
      this.$slots.default[0].componentOptions.children[0].data.attrs['data-hidden']="false"

      this.$eventBus.$emit('renderSlide')
    },
  },

    render(h) {
    console.log('render deck')
    return h('div',{}, this.$slots.default)
  }
})

Vue.component('slide', {
    created() {
      this.$eventBus.$on('renderSlide', () => {
        this.$forceUpdate()
      })
    },
    render(h) {
    console.log('render slide', this.$slots.default[0].data.attrs['data-hidden'])
    return h('div',{}, this.$slots.default)
  }
})


Vue.component('wrapper', {
    template: '#wrapper',
});

new Vue({
  el: '#app'
});

Upvotes: 1

Views: 1503

Answers (1)

Hammerbot
Hammerbot

Reputation: 16324

I think it's because you are updating the wrong property on child nodes.

You are using

child.data.attrs['data-hidden']="false"

But you should manipulate the dom element directly using:

child.elm.dataset['hidden'] = false

That being said, you have other problems. You trigger your updateVisibilities method into the created hook where the DOM has not been created yet. I suggest you to use the mounted hook instead.

And finally, because you trigger your updateVisibilities method after a data update, you should also wait into this method for the DOM to be updated using the Vue nextTick helper:

updateVisibilities() { 
  this.$nextTick(() => {
    const validElements = this.currentSlide.componentOptions.children.filter(child => child.tag)
    validElements.forEach(child => {
      console.log(child, child.elm)
      child.elm.dataset['hidden'] = child.elm.dataset['order'] > this.currentFragmentIndex
      console.log('data-order', child.elm.dataset['order'], 'data-hidden', child.elm.dataset['hidden'])
    })
  })
}

You also don't need any $forceUpdate in your case.

Here are your two fiddles fixed:

I hope this helps!

EDIT: And here is the full updated code:

Vue.component('slide-deck', {
  data() {
    return {
      currentSlideIndex: 0,
      currentFragmentIndex: 0,
      numFragmentsPerSlide: [2,2]
    }
  },
  computed: {
    slideComponents() {
      return this.$slots.default.filter(slot => slot.tag)
    },
    currentSlide() {
      return this.slideComponents[this.currentSlideIndex]
    }
  },

  mounted () {
    window.addEventListener('keydown', this.handleKeydown);
    this.updateVisibilities();
  },
  destroyed() {
    window.removeEventListener('keydown', this.handleKeydown);
  },
  methods: {
    handleKeydown(e) {
      if (e.code === 'ArrowRight') {
        this.increaseFragmentOrSlide()
      } else if (e.code === 'ArrowLeft') {
        this.decreaseFragmentOrSlide()
      }
    },

    increaseFragmentOrSlide() {
      if (this.currentFragmentIndex < this.numFragmentsPerSlide[this.currentSlideIndex] - 1) {
        this.currentFragmentIndex +=1
        console.log('increased fragment index:', this.currentFragmentIndex)
      } else if (this.currentSlideIndex < this.numFragmentsPerSlide.length - 1){
        this.currentSlideIndex += 1
        this.currentFragmentIndex = 0
        console.log('increased slide index:', this.currentSlideIndex)
      }
      this.updateVisibilities()
    },

    decreaseFragmentOrSlide() {
      if (this.currentFragmentIndex > 0) {
        this.currentFragmentIndex -=1
        console.log('decreased fragment:', this.currentFragmentIndex)
      } else if (this.currentSlideIndex > 0) {
        this.currentSlideIndex -= 1
        this.currentFragmentIndex = 0
        console.log('decreased slide:', this.currentSlideIndex)
      }
      this.updateVisibilities()
    },

    updateVisibilities() {
      // this.$forceUpdate() // hack
      this.$nextTick(() => {
        const validElements = this.currentSlide.componentOptions.children.filter(child => child.tag)
        validElements.forEach(child => {
          console.log(child, child.elm)
          child.elm.dataset['hidden'] = child.elm.dataset['order'] > this.currentFragmentIndex
          console.log('data-order', child.elm.dataset['order'], 'data-hidden', child.elm.dataset['hidden'])
        })
      })
    }
  },

  render(h) {
    return h('div',{}, [this.currentSlide])
  }
})

Vue.component('slide', {
  render(h) {
    return h('div',{}, this.$slots.default)
  }
})


Vue.component('wrapper', {
  template: '#wrapper',
});

new Vue({
  el: '#app'
});

Upvotes: 2

Related Questions