[Vuejs]-Watch props update in a child created programmatically

0👍

Update:

There’s an easier way to achieve this:

  • create a <div>
  • append it to your $refs.container
  • create a new Vue instance and .$mount() it in the div
  • set the div instance’s data to whatever you want to bind dynamically, getting values from the parent
  • provide the props to the mounted component from the div’s data, through render function
methods: {
  addComponent() {
    const div = document.createElement("div");
    this.$refs.container.appendChild(div);
    new Vue({
      components: { Test },
      data: this.$data,
      render: h => h("test", {
        props: {
          message: this.msg
        }
      })
    }).$mount(div);
  }
}

Important note: this in this.$data refers the parent (the component which has the addComponent method), while this inside render refers new Vue()‘s instance. So, the chain of reactivity is: parent.$data > new Vue().$data > new Vue().render => Test.props. I had numerous attempts at bypassing the new Vue() step and passing a Test component directly, but haven’t found a way yet. I’m pretty sure it’s possible, though, although the solution above achieves it in practice, because the <div> in which new Vue() renders gets replaced by its template, which is the Test component. So, in practice, Test is a direct ancestor of $refs.container. But in reality, it passes through an extra instance of Vue, used for binding.

Obviously, if you don’t want to add a new child component to the container each time the method is called, you can ditch the div placeholder and simply .$mount(this.$refs.container), but by doing so you will replace the existing child each subsequent time you call the method.

See it working here: https://codesandbox.io/s/nifty-dhawan-9ed2l?file=/src/components/HelloWorld.vue

However, unlike the method below, you can’t override data props of the child with values from parent dynamically. But, if you think about it, that’s the way data should work, so just use props for whatever you want bound.


Initial answer:

Here’s a function I’ve used over multiple projects, mostly for creating programmatic components for mapbox popups and markers, but also useful for creating components without actually adding them to DOM, for various purposes.

import Vue from "vue";
// import store from "./store";

export function addProgrammaticComponent(parent, component, dataFn, componentOptions) {
  const ComponentClass = Vue.extend(component);
  const initData = dataFn() || {};
  const data = {};
  const propsData = {};
  const propKeys = Object.keys(ComponentClass.options.props || {});

  Object.keys(initData).forEach(key => {
    if (propKeys.includes(key)) {
      propsData[key] = initData[key];
    } else {
      data[key] = initData[key];
    }
  });

  const instance = new ComponentClass({
    // store,
    data,
    propsData,
    ...componentOptions
  });

  instance.$mount(document.createElement("div"));

  const dataSetter = data => {
    Object.keys(data).forEach(key => {
      instance[key] = data[key];
    });
  };

  const unwatch = parent.$watch(dataFn || {}, dataSetter);

  return {
    instance,
    update: () => dataSetter(dataFn ? dataFn() : {}),
    dispose: () => {
      unwatch();
      instance.$destroy();
    }
  };
}

componentOptions is to provide any custom (one-off) functionality to the new instance (i.e.: mounted(), watchers, computed, store, you name it…).

I’ve set up a demo here: https://codesandbox.io/s/gifted-mestorf-297xx?file=/src/components/HelloWorld.vue

Notice I’m not doing the appendChild in the function purposefully, as in some cases I want to use the instance without adding it to DOM. The regular usage is:

const component = addProgrammaticComponent(this, SomeComponent, dataFn);
this.$el.appendChild(component.instance.$el);

Depending on what your dynamic component does, you might want to call .dispose() on it in parent’s beforeDestroy(). If you don’t, beforeDestroy() on child never gets called.


Probably the coolest part about it all is you don’t actually need to append the child to the parent’s DOM (it can be placed anywhere in DOM and the child will still respond to any changes of the parent, like it would if it was an actual descendant). Their “link” is programmatic, through dataFn.

Obviously, this opens the door to a bunch of potential problems, especially around destroying the parent without destroying the child. So you need be very careful and thorough about this type of cleanup. You either register each dynamic component into a property of the parent and .dispose() all of them in the parent’s beforeDestroy() or give them a particular selector and sweep the entire DOM clean before destroying the parent.

Another interesting note is that in Vue 3 all of the above will no longer be necessary, as most of the core Vue functionality (reactivity, computed, hooks, listeners) is now exposed and reusable as is, so you won’t have to $mount a component in order to have access to its “magic”.

Leave a comment