4
votes

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?

2
If you're using a slot, it means you are passing in HTML when using the component's tag in the consuming parent's template. Simply pass the HTML to the parent component, and have it injected into the component.Terry
@Terry "Simply pass the HTML to the parent component, and have it injected into the component". Sorry, I'm new to Vue. I know how to do this when creating a component, but I'm not sure how to pass or inject to an existing components slot. Also, to be clear, there's only one component, no parent or children.Goose
try to return the html from the server with a computed property, and use a wrapper with v-htmlaf costa
@afcosta I tried but it did not work, see edit in question for exactly how I tried to apply it.Goose
if you just want to put your HTML to the default slot, try <component><template v-html="yourHtml"></template></component>Sphinx

2 Answers

7
votes

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>
4
votes

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>