Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I add a Vue component within some HTML to another Vue component?

I have a giant, dynamic HTML string that I'm loading into a div within a Vue component. The HTML string is essentially the content from a WYSIWYG editor. Originally, I was just using v-html for this, and it was fine.

However, there are now cases where I need to replace part of the HTML string with an actual Vue component, and I'm not sure of the best way to do that.

As an example, I might have some markup in the HTML string that looks like the following:

||map:23||

And what I want to do is replace that with a Vue component like the following:

<map-component :id="23"></map-component>

I tried doing the string conversion ahead of time in Laravel and then just using v-html in the Vue component to inject the content, but that doesn't seem to load the Vue component.

I then tried using a slot for the HTML content, and that does work, but it has the nasty side effect of showing a bunch of unformatted HTML content on the screen for a second or two before Vue is able to properly render it.

So my question is: Is there another (more elegant) way to do this? I was thinking that after the Vue component loads with the HTML content, I could somehow find the, for example, ||map:23|| instances in the markup and then dynamically replace them with the correct Vue component, but if that's possible, I don't know how; I couldn't find anything in the Vue docs.

Does anyone know if this is possible? Thank you.

like image 387
HartleySan Avatar asked Jul 30 '19 18:07

HartleySan


2 Answers

You can use Vue.compile to compile a template string (that can include vue components).

Then you can combine this with a component that has a render() method, to just render the template:

// this component will dynamically compile the html
// and use it as template for this component.
Vue.component("dynamic-html", {
  props: ["html"],
  computed: {
    template() {
      if(this.html)
        return Vue.compile(this.html).render;
      return null;
    }
  },
  render() {
    if(this.template)
      return this.template();
    return null;
  }
});

This allows you to render arbirary template strings, which can also contain vue components:

<dynamic-html html="<some-component></some-component>">
</dynamic-html>

Additionally, you can also use this to pass down props / event handlers to components within your string:

<!-- Passing down props -->
<dynamic-html
    html='<some-component :prop="$attrs.myprop"></some-component>'
    :myprop="12"
></dynamic-html>

<!-- passing down events -->
<dynamic-html
    html='<some-component @click="$emit('foo', $event)"></some-component>'
    @foo="doSomething"
></dynamic-html>

(you need to use $attrs though to access the props, because they're not in the props definition of the dynamic-html component)

Full code example:

// this component will dynamically compile the html
// into a vue component
Vue.component("dynamic-html", {
  props: ["html"],
  computed: {
    template() {
      if(this.html)
        return Vue.compile(this.html).render;
      return null;
    }
  },
  render() {
    if(this.template)
      return this.template();
    return null;
  }
});

Vue.component("red-bold-text", {
  props: ["text"],
  template: '<span class="red">{{text}}</span>'
});

new Vue({
  el: '#root',
  data: {
    html: null,
    myBoundVar: "this is bound from the parent component"
  },
  mounted() {
    // get the html from somewhere...
    setTimeout(() => {
      this.html = `
        <div>
          WELCOME!
          <red-bold-text text="awesome text"></red-bold-text>
          <red-bold-text :text="$attrs.bound"></red-bold-text>
          <button @click="$emit('buttonclick', $event)">CLICK ME</button>
        </div>
      `;
    }, 1000);
  },
  methods: {
    onClick(ev) {
      console.log("You clicked me!");
    }
  }
});
.red { color: red; font-weight: bold; margin: 6px; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="root">
  <div>This will load dynamically:</div>
  <dynamic-html :html="html" :bound="myBoundVar" @buttonclick="onClick"></dynamic-html>
</div>
like image 50
Turtlefight Avatar answered Sep 19 '22 16:09

Turtlefight


Turtlefight's answer is very helpful and complete, but for anyone looking for a quick, simple answer, use the literal component component with :is as follows to inject HTML content containing Vue components into a dynamic component:

// htmlStrWithVueComponents is a large string containing HTML and Vue components.
<component
    :is="{
        template: `<div>${htmlStrWithVueComponents}</div>`
    }"
>
</component>

Here're a couple of sources that describe this technique:

  • https://jannerantala.com/tutorials/vue-replace-text-with-component-with-props/
  • https://jsfiddle.net/Herteby/5kucj6ht/

Edit: It's worth noting that component :is is fairly limited in what you can do. You can't make very complex template strings or added mounted methods, etc.

For my particular use case, because I needed some of this more complex stuff, I ended up going with the following, which is kind of a hybrid between the simpler answer above and Turtlefight's answer:

// This code goes within the parent component's mounted method or wherever is makes sense:
Vue.component('component-name-here', {
    // Can add much more complex template strings here.
    template: `
        <div class="someClass">
            ${content}
        </div>
    `,
    // Can add lifecycle hooks, methods, computed properties, etc.
    mounted: () => {
        // Code here
    }
});

const res = Vue.compile(`
    <component-name-here>
    </component-name-here>
`);

new Vue({
    render: res.render,
    staticRenderFns: res.staticRenderFns
}).$mount('dom-selector-for-dom-element-to-be-replaced-by-vue-component');
like image 41
HartleySan Avatar answered Sep 20 '22 16:09

HartleySan