loading...
Cover image for Dynamic components using VueJS

Dynamic components using VueJS

pikax profile image Carlos Rodrigues ・4 min read

First things first, if you're just starting with VueJS this might be a bit too advanced for you, I would strongly recommend read documentation on VueJS, especially components.

This is my first guide, criticism is welcome :)

Preface

Swapping components based on user inputs or even just have a form being set by a json file, is really usefull method to maintain your sanity level low throughout a big project, since usually there's hundreds components/forms/pages/etc, so making a change in code it might cause a ripple effect and break something elsewhere.

Since VueJS will handle all the dirty details of handling DOM, we can focus on solve business problems.

I will cover loading components by name, creating on-the-fly and async components.

Component "magic"

Doing dynamic wouldn't be as easy without <component v-bind:is="dynComponent"></component> check Dynamic & Async Components for more detailed information.
Basically the components will render a component, async function or by component name.

Loading components by name

Using <component/> will allow you to access global and also local components by name.

codepen

// add some different components globaly
Vue.component("test-comp0", {
  template: `<p>comp0</p>`
});

Vue.component("test-comp1", {
  template: `<p>comp1</p>`
});

Vue.component("test-comp2", {
  template: `<p>comp2</p>`
});

// sample app
new Vue({
  el: "#app",

  components: {
    // add a local component
    // check https://vuejs.org/v2/guide/components-registration.html#Component-Names
    TestComp3: {
      template: `<p>comp3 locally registered component</p>`
    }
    // you can also add some components from other files, using ES6 import or required.
  },

  data() {
    return {
      componentIndex: 0
    };
  },

  computed: {
    componentName() {
      return "test-comp" + this.componentIndex;
    }
  },

  template: `
      <div>
        Component: {{componentIndex}} <button @click="componentIndex=(++componentIndex)%4">change</button>
        <component :is="componentName"></component>
      </div>`
});

Cycle between components is useful, but in the real world you would pass some props to it.
To add props, lets change the component test-comp0 and to the app template.

Vue.component("test-comp0", {
  props: ["name"], // please use the object props for production
  template: `<p>Hello {{name}}</p>`
});

...

// add name prop
<component :is="componentName" name="pikax"></component>

This will pass the prop name to every component. To solve this we can have a computed property and bind it to the component.

// app becomes

new Vue({
  el: "#app",

  components: {
    // add a local component
    // check https://vuejs.org/v2/guide/components-registration.html#Component-Names
    TestComp3: {
      template: `<p>comp3 locally registered component</p>`
    }
    // you can also add some components from other files, using ES6 import or required.
  },

  data() {
    return {
      componentIndex: 0,
      name: "pikax"
    };
  },

  computed: {
    componentName() {
      return "test-comp" + this.componentIndex;
    },
    componentProps() {
      if (this.componentIndex == 0) {
        return {
          name: this.name
        };
      }
      return {}; // return empty object
    }
  },

  template: `
      <div>
        Component: {{componentIndex}} <button @click="componentIndex=(++componentIndex)%4">change</button>
        <component :is="componentName" v-bind="componentProps"></component>
      </div>`
});

On-the-fly components

On-the-fly components are components we just generate as we need using javascript, this shows how powerful the <component></component> is, some use case scenarios would be building widgets.

We can generate and test components based on the user input.

NOTE: be really careful with this, this can allow attackers to attack your application, please ensure the source is trusted!

codepen

new Vue({
  el: "#app",

  data() {
    return {
      componentDefinition: `{ template: "<div>Hello</div>" }`
    };
  },

  computed: {
    myComponent() {
      return eval(`(${this.componentDefinition})`);
    }
  },

  template: `<div>
    <p>Change me</p>
    <textarea v-model="componentDefinition" rows="4" cols="50"></textarea>
    <component v-if="myComponent" :is="myComponent"></component>
</div>
`
});

You can see as you change the textarea, the component should render straight away.
I don't recommend using this, but I think is a good example on how powerful <component></component> is.

Importing async Components

This is for me the most usefull use case of the component. I greatly recommend reading the (official guide)[https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components]

Vue.component(
  "async-webpack-example",
  // The `import` function returns a Promise.
  () => import("./my-async-component")
);

Real world problem

In my last project we had a problem of we wanted to collect user information, but the fields would change depending on the journey, some journey would required email, others email and phone.

The solution was to get the journey definition in a JSON file, each time the user would start an journey we would load that file and load the fields.
The strategy was to use names to load the component dynamically loading the components by name, but we ended up loading all the possible editors in Vue.Component, this worked... but loading them at the start up means the startup time and app size were much bigger than needed.

Solution

Using a mix of Async, (Dynamic)[https://vuejs.org/v2/guide/components-dynamic-async.html] components and Webpack.

// returning equivalent of webpack : import(name)
const getComponent = async path => {
  /* I recomend having an switch with the possible components you will load, this
   *   will allow you only load specific components.
   */
  if (path == 1) {
    return async () => {
      template: `<p>component 0</p>`;
    };
  } else {
    return async () => {
      template: `<p>${path}</p>`;
    };
  }
};

Vue.component("component-fallback", {
  template: `<div>This is not the component you're looking for</div>`
});

new Vue({
  el: "#app",
  data() {
    return {
      componentIndex: 0,
      component: "component-fallback"
    };
  },

  methods: {
    changeComponent() {
      const newIndex = ++this.componentIndex;
      this.loadComponent(newIndex);
    },

    // returns the component
    loadComponent(name) {
      const componentFunc = getComponent(name)
        .then(x => {
          this.component = x;
        })
        .catch(e => {
          this.component = "component-fallback";
        });
    }
  },

  template: `
        <div>
            Component: {{componentIndex}} <button @click="changeComponent">change</button>
            <component :is="component"></component>
        </div>
        `
});

End

Hope my first article is useful for you, I find fascinating how powerful and flexible <component></component> is.

If you have any more use cases for <component></component> let me know on the comments.

This story was first publish at medium.com

Posted on by:

pikax profile

Carlos Rodrigues

@pikax

Passionate about new tech. VueJS lover <3

Discussion

markdown guide
 

Hello!
Thanks for your article.
I still have one question:
When you do

<component v-bind:is="something"></component>

If "something" contains a bad value (non-existing component), it doesn't throws any error.
How can we handle this case?

To give you a better background:

We have an SPA, and the user can modify the location hash to access articles:

http://mysite.com/index.html#super-article

This will load the async component "super-article", and show it via the "component" tag. I'd like to handle the case when a user enters a bad value (this is not usual, but anyway... Want to handle this bad case).

Do you know how to do that? (I don't like the "white list" solution...)

 

Hi Lain,

You welcome :)

It will depend if the something is an lazy loaded component or component definition object.

In your case I will reckon you fetch the article from the database and then render, in that case if the response of the api is empty or 404 you can render a fallback component.

 

Thank you for your quick response!

I was hoping for something like <div v-except-nonexists> or something like that.

In my case, I have no way to trigger any 404 error, because each article's component is registered using it's metadata (name, tags, ...), and then it's loaded lazily only when my <component> tag asks for it. But Vue does nothing at all if the given component does not exists.

I'll keep my whitelist solution because the solution you provides implies to rewrite the whole loading system: I register a (lazy) component from each article, keep track of all registered articles, and if the article referenced by the location hash has never been registered, I show an error with v-if/v-else:

  <component v-if="loaded_articles[curent_page]" :is="curent_page"></component>
  <div v-else class="article article-content">
    <previous-button></previous-button>
    <h3>{{curent_page}}</h3>
    <p>{{$t("articles.article_not_found")}}</p>
  </div>

This method prevents any additional request to the server...

It if you loading through webpack, it's simple, because import will throw an exception.

What do you mean by:

Vue does nothing at all if the given component does not exists.

You should be able to add that condition to v-if, no?

Are you registering the components using Vue.component()?

No, I don't use webpack.

Yes, I use Vue.component(name, promise) to register my components.

When my Vue instance is mounted, I set the language, and it triggers the loading of corresponding articles' components. Here is a simplified version of my code:

    function load_article(article, header, err_code) {
        // here, "article" is the raw html of my article
        // We'll insert it into the article's template
        return {
            data: {// some data...
            },
            template: `a template ${article} that encapsulate each article`
        }
    }
    function loader (header) {
        return (resolve, reject) => {
            vm.make_promise({
                url: `/article/${lang}/${header.name}.html`
            })
            .then((data) => {
                resolve(load_article(data, header, 200))
            })
            .catch((data) => {
                resolve(load_article(data, header))
            });
        }
    }
    var lang = vm.lang;
    headers = articles_headers();
    var loaded_articles = {};
    for(var i in headers) {
        header = headers[i];
        Vue.component(`${lang}-${header.name}`, loader(header));
        loaded_articles[`${lang}-${header.name}`] = true;
    }
    return loaded_articles;

And then, I have the code I wrote in my previous comment, with "curent_page" being the name of the article.

But, if we provide an article name that does not exists (and so, a component that is not registered), Vue shows a warning in the console, and that's all: [Vue warn]: Unknown custom element ....

Perhaps there's a way to tell Vue to call a given function when this happens?

you should be able to check if the component is register by accessing Vue.options.components

 

How does this work when your components are in separate vue files? We don't want the component to be custom written by users but we still need to dynamically load the components (by name) via the JSON file.

So our API returns back a list of components to load (these would likely be part of the Webpack result) but that list is dynamic based on the API.

 

If you have a component definition, you can have a component that loads the component from the API, I would use eval to run the javascript (using the same logic as in On-the-fly components Example).

Just be careful, using eval or new Function is usually not a good approach since it will run an possible unsecure javascript and slow.

 

Thanks Carlos - security is definitely a concern - I don't want to be loading javascript on the fly.

I think the problem is that my components aren't being registered globally properly.

I thought by loading the components in the parent component, they would be available to the lower level components but they aren't.

So in my page component, I'm doing the import (but not using the component). Shouldn't that component be available to the child?

--- EDITED ---
I'm using Quasar as our framework and they don't use a main.js but a plug-in approach. So that's where I have to go.

Thanks

I think the problem is that my components aren't being registered globally properly.

You can use Vue.component() to register globally.

Never used Quasar, if you can create a link on codesandbox with an sample on what you want to do, I can have a look and try to help.

No need - I've got it working now - I'm fairly new to vue but your examples set me on the right path.

quasar lets you build separate plug-ins.

I'll post an article when I've got it working smoothly.