s0up
s0up

Reputation: 422

How to render component after inserting its tag as raw html? Vue 3

So i have a vue instance with a globally registered component in a legacy project. Since it's legacy, it mainly uses jsrender to render html after some ajax requests.

But even if the component is already registered in the vue instance, vue does not render the new component instance after that data fetch, when component gets inserted as raw html <component-a></component-a>.

How do I make vue render this?

I need this to work to be able to gradually inject VueJS into the legacy project

Further explanation:

I have this html blocks (jsrender templates) like below on the page from the initial page request

<script id="some-name-tmpl" type="text/x-jsrender">
        {{ if var1 == 0 }}
        <div> {{ variable }} etc...</div>
        {{/if}}
        <my-vue-component></my-vue-component> // I need this to get rendered as a component
</script>

They get rendered by jsrender library when some data come from the backend and in JS some render methods of jsrender got called along with the data, so I get something like this:

<div> x etc...</div>
<my-vue-component></my-vue-component>

But the component (<my-vue-component></my-vue-component>) gets inserted just as raw html, though it's inside of the #app element on which vuejs instance is mounted, but it gets inserted after the vue instance is already initialized, so Vue does not render this component

So I need to force Vue to scan the page html again as on mount() to render this component. Or I will have to migrate all the templates and data from jsrender lib to Vue in once and it's a lot of work

Upvotes: 5

Views: 14476

Answers (3)

matan11
matan11

Reputation: 21

You can use dynamic components in conjunction with the built-in is attribute as a template. This will let vue render raw html along with rendering vue components.

<component :is="{ template: `${html}` }" />

then in your computed, return whatever html block you want that has the globally mounted vue component:

computed: {
  html() {
    return `<div> x etc...</div><my-vue-component></my-vue-component>`
  }
}

using this method you will need to import:

import { createApp } from 'vue/dist/vue.esm-bundler'

This gives you the ability to use runtime template compilation in your app.

Upvotes: 2

Xinchao
Xinchao

Reputation: 3543

If I understand it right, essentially, you are trying to either render a component whose template is not known statically, or a component whose definition is not known statically. In other words, you want the template, or the component itself to be the "data" so that it can be fetched remotely/constructed dynamically at runtime.

This is achievable using dynamic component (the <component> tag). The magic is the second bullet point in the document:

the value passed to :is can contain either:

  • the name string of a registered component, OR
  • the actual imported component object

Except that you will not import the component, you just create it on the fly. Vue component, surprising to most of people, is actually pretty simple - it is just a plain js object (in other words, the component definition is merely a spec).

In your case, one way of doing it is to define a DynamicComponent to accept remote template as a string property (let's call it layout), and this component can then create an anonymous component on the fly based on the given layout.

In the below example, two existing components are registered globally: DynamicComponent and MyComponent. The app is not rendering MyComponent at the moment. You can type your jsrender-rendered layout as string into the text area, say

<my-component msg="hello world">
    <button>I can even accept slot</button>
</my-component>

and click the button Render It

const App = {
  template: `
    <div style="display: flex; flex-flow: column">
      <h1>App</h1>
      <textarea v-model="text"/>
      <button @click="renderIt"> Render It </button>
      <dynamic-component :layout="layout" />
    </div>`,

  data() {
    return { title: "App", text: "", layout: "" };
  },
  
  methods: {
    renderIt() {
      this.layout = this.text;
    }
  }
}

const MyComponent = {
  props: ["msg"],

  template: `
   <h1>Hello from the MyComponent.vue</h1>
   <div>{{ msg }}</div>
   <slot></slot>
  `,
};


const DynamicComponent = {
  props: ["layout"],

  template: `<component :is="definition" v-if="!!layout" />`,

  computed: {
   definition() {
     return { template: this.layout };
   },
  }
};

const app = Vue.createApp(App);
app.component("my-component", MyComponent);
app.component("dynamic-component", DynamicComponent);
app.mount("#app");
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

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

Obviously you still might have refactor your code a little bit, e.g. you probably need to figure out a way to know when jsrender has completed its part of the job. Even if there might not be a way to directly ask jsrender to render into a string, you can always just grab the innerHTML/outterHTML and forward it to the DynamicComponent.

Alternatively, Vue 3 root app can be mounted with dom template. E.g. it can take the DOM subtree rooted at the designated mounting point as template. This actually feels more ideal for your case, but might involve a bigger refactoring as you will break your entire application into multiple Vue apps so that you can imperatively mount a new Vue app instance when needed.

Upvotes: 2

Navid Shad
Navid Shad

Reputation: 1016

As I understand you need to render some Vue components after ajax calls and it seems they are out of Vue scope. There are 3 solutions for working with dynamic components, third one may be a solution for your case becuase it works out of Vue.

  1. v-html directive, it's prebuilt feature that you can render HTML string content under an dom element.
<div v-html="htmlStringConent" />
  1. Dynamic component, it's prebuilt component of Vue that accepts one tag name or a selector of a registered component to render it.
<component :is="href ? 'a' : 'registeredComp'"></component>
  1. Web component, this is a feature of modern browsers for creating reusable custom elements — with their functionality encapsulated away from the rest of your code. Simply you should define your Vue compoents in a way browser could render it like a native dom element. then it works in jsrenderer scope automatically.
import { defineCustomElement } from 'vue'

const MyVueElement = defineCustomElement({
  // normal Vue component options here
  props: {},
  emits: {},
  template: `...`,

  // defineCustomElement only: CSS to be injected into shadow root
  styles: [`/* inlined css */`]
})

// Register the custom element.
// After registration, all `<component-a>` tags
// on the page will be upgraded.
customElements.define('component-a', MyVueElement)

then you can use your custom element anywhere the document tree

<component-a></component-a>

Hope it will help you to resolve your problem.

Upvotes: 4

Related Questions