Widget based website with dynamic Vue.js components

by Vincent Will

In this post I will explain how to generate a webpage using dynamic Vue.js components. This could be the foundation of a CMS based on Vue.

The finished project can be found here

Project Setup

First of all install the vue-cliif you havent already. To do so open a console and type

npm install -g @vue/cli

Afterwards create a new project by entering

vue create widget-app

You will be prompted to pick a preset. For this project just use default.
When the installation finished, go into the new project directory and run the Vue app.

cd widget-app

npm run serve

The app should now be available at http://localhost:8080/

First of all, to keep it simple, remove everything in the template of src/components/HelloWorld.vue exept the <h1>.

<template>
    <h1>{{ msg }}</h1>
</template>

Then remove everything in src/App.vue template inside of app div. Then add ref="main" to the div.

<template>
    <div id="app" ref="main">

    </div>
</template>

Dynamic Component creation

First of all we want to create the HelloWorld component dynamically. Therefor update the code in the <script>block inside of your App.vue

import Vue from 'vue'
import HelloWorld from './components/HelloWorld.vue'

export default {
name: 'app',
components: {
    HelloWorld
},
methods: {
    init: function() {
    const ComponentClass = Vue.extend( HelloWorld );
    const instance = new ComponentClass({
        propsData: { msg: 'This is a dynamic headline' }
    });
    instance.$mount();
    this.$refs['main'].appendChild(instance.$el);
    }
},
mounted() {
    this.init();
}

Here we added import Vue from 'vue' in the first line. Then we addedmethods and mounted().

mounted gets called when the app has mounted and then proceeds to call the function init inside of our methods.

Because components do not export a class, we need to pass the component to Vue.extendto be able to create an instance of the component with the new keyword.

When creating the instance, we can provide the properties with propsData

To mount the instance, we have to call instance.$mount();. As a last step, we are then able to attach the instance of the component to the DOM. Therefor we call this.$refs['main']to get the <div> where we want to insert our component. Then we call appendChildwith instance.$el where $el is used to get the native DOM element reference.

If you refresh localhost:8080, the dynamic headline should now be visible. Now let's make it possible to choose the component based on a string.

Replace const ComponentClass = Vue.extend( HelloWorld ); with this two lines:

const component = this.$options.components['HelloWorld'];
const ComponentClass = Vue.extend(component)

The code should still work the same, but now we are able to use variables to choose the component we want to generate.

Next let's extract our method for the dynamic creation of components into a separate function.

init: function() {
    this.createComponent('HelloWorld', { msg: 'This is a dynamic headline' }, 'main');
},
createComponent: function(componentName, propsData, domRef) {
    const component = this.$options.components[componentName];
    const ComponentClass = Vue.extend(component);
    const instance = new ComponentClass({
        propsData // shorthand for propsData: propsData
    });
    instance.$mount();
    this.$refs[domRef].appendChild(instance.$el);
}

As you can see, we now have a function called createComponent, which receives the name of the component to render, the properties of the component and the DOM reference, where the component will be inserted.

Now I'll add an object array after our imports, which defines how widgets are initialized. In a real application this would probably be data, which is fetched from a database.

const widgets = [
    {
        'component': 'HelloWorld',
        'dom': 'main',
        'props': { msg: 'This is a dynamic headline'}
    },
    {
        'component': 'HelloWorld',
        'dom': 'main',
        'props': { msg: 'This is a second headline'}
    }
]

Afterwards we can iterate through this array in our init function to render all the Widgets:

init: function() {
    for (const widget of widgets) {
        this.createComponent(widget.component, widget.props, widget.dom);
    }
}

You should now be able to see both headlines defined in our widgets array.

For the last step i'll change the data a little bit to make the widgets reusable.

const page = {
    'main': [0, 0, 1]
    }
    const widgets = [
    {
        'id': 0,
        'component': 'HelloWorld',
        'props': { msg: 'This is a dynamic headline' }
    },
    {
        'id': 1,
        'component': 'HelloWorld',
        'props': { msg: 'This is a second headline' }
    }
]

and update the init function to loop through the page object as well as the widgetIds inside of each DOM reference inside of the page object.

init: function() {
    for (let domRef in page) {
        for (let widgetId of page[domRef]) {
        const widget = widgets.find(widget => widget.id === widgetId);
        this.createComponent(widget.component, widget.props, domRef);
        }
    }
},	

As a final result you should see the first widget rendered two times and the second widget one time.

Conclusion

A working example of the code can be found here: https://codesandbox.io/s/01x2xyq7kv

Dynamic rendering of components is pretty straightforward in Vue.js.
This pattern can be easily extended for usage on multiple pages and multiple components. It is possible to dynamically reuse components with different properties and to append them multiple times to different DOM elements.

Cheers,
Vincent