Creating Vue.js Component Instances Programmatically

Avatar of Kushagra Gour
Kushagra Gour on

This article assumes basic understanding of Vue.js framework and how to create components in it. If you are new to Vue, then this CSS-Tricks series is a good place to start.

I have been on a Vue.js project that required the ability to create components programmatically. By programmatically, I mean you create and insert the components completely from JavaScript, without writing anything in the template. This article aims to illustrate how different aspects of using components in a template, such as instantiation, props passing, slots, mounting, translate to JavaScript code.

Normally if you are working with the recommended Single File Component style, you would have a Button component like so:

<template>
 <button :class="type"><slot /></button>
</template>
<script>
 export default {
   name: 'Button',
   props: [ 'type' ],
 }
</script>

To use it in another component, all you have to do is import the Button component and insert its tag in the template:

<template>
 <Button type="primary">Click me!</Button>
</template>
<script>
 import Button from 'Button.vue'
 export default {
   name: 'App',
   components: { Button }
 }
</script>

In my case, I don’t know which component to insert in the template and also where to insert it. This information is only available at runtime. So I needed a way to dynamically create component instance for any passed component and insert it in the DOM, during runtime.

Creating the Instance

The very first idea I had to create a dynamic instance of a given component was to pass it to new and it would give me the actual instance. But if you notice carefully the script block in any of the above component code, they are all exporting a simple object, and not a class (constructor function). If I do this:

import Button from 'Button.vue'
var instance = new Button()

…it fails. We need a class. Or, in simple terms, a constructor function. The way to do that is to pass the component object to Vue.extend to create a subclass of the Vue constructor. Now we can create an instance out of it with the new keyword:

import Button from 'Button.vue'
import Vue from 'vue'
var ComponentClass = Vue.extend(Button)
var instance = new ComponentClass()

Hooray! Step 1 cleared! Now we need to insert it in the DOM.

Inserting in DOM

Every Vue instance has a method called $mount on it which mounts the component instance on the element you pass to it (i.e. it replaces the passed element with the component instance). This is not a behavior I wanted. I wanted to insert my component instances inside some DOM element. There is a way to do that too. From the official docs:

If elementOrSelector argument is not provided, the template will be rendered as an off-document element, and you will have to use native DOM API to insert it into the document yourself.

That’s what I did:

import Button from 'Button.vue'
import Vue from 'vue'
var ComponentClass = Vue.extend(Button)
var instance = new ComponentClass()
instance.$mount() // pass nothing
this.$refs.container.appendChild(instance.$el)

There are a couple of things to note in the code above.

First, $refs is the recommended way to get reference to a DOM element in Vue.js. You specify an attribute on the DOM element you want to reference (<div ref="container"></div> in this case) and then that element is available on the set key on component’s $refs property.

Second, to get the native DOM element reference from a Vue component instance, you can use the $el property.

Passing Props to the Instance

Next, I had to pass some props to my Button instance. Specifically, the type prop. The Vue constructor function accepts an options object that we can use to pass in and initialize related things. For passing props, there is a key called propsData which we can use, like so:

import Button from 'Button.vue'
import Vue from 'vue'
var ComponentClass = Vue.extend(Button)
var instance = new ComponentClass({
    propsData: { type: 'primary' }
})
instance.$mount() // pass nothing
this.$refs.container.appendChild(instance.$el)

We are almost done, with one final remaining bit. With the normal template method, we used button like: <Button>Click me!</Button>;. We simply wrote the inner text between the tags and it rendered inside the final button tag with the help of slot. But now how do we pass it?

Setting the Slot

If you have used slots in Vue.js, you might be aware that the slots are accessible on any instance on the $slots property. And if named slots are not used, then slots are available on $slots.default as an array. That is the exact key we’ll modify on the instance to set our button’s inner text. Remember, this needs to be done before mounting the instance. Note that in our case we just put a simple string in the slot because that is all we required. But you can pass more complex DOM to it in form for Virtual nodes or VNode using the createElement function. You can read about creating Virtual nodes in the Vue.js documentation. Thanks to a Vue core team member, Rahul, for suggesting this technique.

import Button from 'Button.vue'
import Vue from 'vue'
var ComponentClass = Vue.extend(Button)
var instance = new ComponentClass({
    propsData: { type: 'primary' }
})
instance.$slots.default = [ 'Click me!' ]
instance.$mount() // pass nothing
this.$refs.container.appendChild(instance.$el)

Final Demo

View Demo

Hope you liked this article. Follow me on Twitter where I share more articles and side projects of mine.