Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to update a slot in Vue.JS

I have a Vue component simplified below.

Here is the template

<template>
    <slot></slot>
</template>

The slot may contain HTML, which is why I decided to use a slot rather than a prop which I would simply bind to. I'd like to keep it that way.

I have a method that gets new HTML from the server. I'd like to use this new HTML to update the slot. I'm not sure if slots are reactive and how I can accomplish this.

I can view the default slot using this.$slots.default[0], but I don't know how to update it with a string of HTML content. Simply assigning the string to the element is obviously incorrect, to .innerHtml does not work because it isn't an available function, and to .text doesn't work. I assume that even though the text element exists on the slot object, the element properties take precedence.

Per suggestion in comments, I've tried this along with a computer property.

<span v-html="messageContent"><slot></slot></span>

But now the problem is that it overwrites the slot passed to me.

How can I reactively update a slot with new HTML in Vue.JS?

like image 600
Goose Avatar asked May 08 '18 20:05

Goose


People also ask

How do I use Vue JS slots?

Using the slot is very easy, we should just write the <slot> component (which is a reserved word in Vue. js) inside the child component's template, where we want to receive data. A child component uses slot.

Do props update Vue?

As long as you're updating a reactive property (props, computed props, and anything in data ), Vue knows to watch for when it changes. All we have to do is update count , and Vue detects this change. It then re-renders our app with the new value!

Does Vue automatically update?

One of Vue's best features is reactivity. You change a variable, and Vue automatically updates what is rendered to the page. However, not everything in Vue is reactive by default.


2 Answers

I think your issue comes from a misunderstanding of how <slot> inherently works in VueJS. Slots are used to interweave content from a consuming parent component into a child component. See it as a HTML equivalent of v-bind:prop. When you use v-bind:prop on a component, you are effectively passing data into a child component. This is the same as slots.

Without any concrete example or code from your end, this answer is at best just guess-work. I assume that your parent component is a VueJS app itself, and the child component is the one that holds the <slot> element.

<!-- Parent template -->
<div id="app">
    <custom-component>
        <!-- content here -->   
    </custom-component>
</div>

<!-- Custom component template -->
<template>
    <slot></slot>
</template>

In this case, the app has a default ground state where it passes static HTML to the child component:

<!-- Parent template -->
<div id="app">
    <custom-component>
        <!-- Markup to be interweaved into custom component -->
        <p>Lorem ipsum dolor sit amet.</p>
    </custom-component>
</div>

<!-- Custom component template -->
<template>
    <slot></slot>
</template>

Then, when an event is fired, you want to replace that ground-state markup with new incoming markup. This can be done by storing the incoming HTML in the data attribute, and simply using v-html to conditionally render it. Let's say we want to store the incoming markup in app's vm.$data.customHTML:

data: {
    customHTML: null
}

Then your template will look like this:

<!-- Parent template -->
<div id="app">
    <custom-component>
        <div v-if="customHTML" v-html="customHTML"></div>
        <div v-else>
            <p>Lorem ipsum dolor sit amet.</p>
        </div>
    </custom-component>
</div>

<!-- Custom component template -->
<template>
    <slot></slot>
</template>

Note that in contrast to the code you have tried, the differences are that:

  • It is the parent component (i.e. the consuming component) that is responsible for dictating what kind of markup to pass to the child
  • The child component is as dumb as it gets: it simply receives markup and renders it in the <slot> element

See proof-of-concept below:

var customComponent = Vue.component('custom-component', {
  template: '#custom-component-template'
});

new Vue({
  el: '#app',
  data: {
    customHTML: null
  },
  components: {
    customComponent: customComponent
  },
  methods: {
    updateSlot: function() {
      this.customHTML = '<p>Foo bar baz</p>';
    }
  }
});
.custom-component {
  background-color: yellow;
  border: 1px solid #000;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>

<div id="app">
  <h1>I am the app</h1>
  <button type="button" @click="updateSlot">Click me to update slot content</button>
  <custom-component>
    <div v-if="customHTML" v-html="customHTML">
    </div>
    <div v-else>
      <p>Lorem ipsum dolor sit amet.</p>
    </div>
  </custom-component>
</div>

<!-- custom-component template -->
<script type="text/template" id="custom-component-template">
  <div class="custom-component">
    <h2>I am a custom component</h2>
    <!-- slot receives markup set in <custom-component> -->
    <slot></slot>
  </div>
</script>
like image 103
Terry Avatar answered Oct 13 '22 09:10

Terry


Below is my solution though I don't like this opinion (load html into slot directly in current component level) because it breaks the rules for the slot. And I think you should do like this way (<component><template v-html="yourHtml"></template></component>), it will be better because Slot will focus on its job as Vue designed.

The key is this.$slots.default must be one VNode, so I used extend() and $mount() to get the _vnode.

Vue.config.productionTip = false

Vue.component('child', {
  template: '<div><slot></slot><a style="color:green">Child</a></div>',
  mounted: function(){
    setTimeout(()=>{
      let slotBuilder = Vue.extend({
        // use your html instead
        template: '<div><a style="color:red">slot in child</a></div>',
      })
      let slotInstance = new slotBuilder()
      this.$slots.default = slotInstance.$mount()._vnode
      this.$forceUpdate()
    }, 2000)
  }
})

new Vue({
  el: '#app',
  data() {
    return {
      test: ''
    }
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<child><h1>Test</h1></child>
</div>
like image 35
Sphinx Avatar answered Oct 13 '22 11:10

Sphinx