Generically overriding component render functions in Vue.js

Besides from using templates, Vue.js components can be set up with a render function, allowing one to programmatically determine their output in a more advanced way. Combining this feature with the HOC pattern, it is thus possible to easily create "augmented" versions of any component in an app, which can come in handy in some situations.

For the sake of example, let's picture a scenario where we have a form, and want to display each of its fields twice: the regular version, and a disabled version, displaying a recommended value based on business rules.

The resulting HOC might look something like this:

import clone from "clone";
import getDisabledVersion from "./getDisabledVersion";

export default function withRecommendedValue(originalComponent) {
const component = clone(originalComponent); // deep-clone component to avoid reference errors
const originalRender = component.render;
const recommendedValue = /* fetch it from wherever */;

component.render = function(h) {
return h("div", {}, [
getDisabledVersion(originalRender.call(this, h), recommendedValue), // disabled doppelganger
originalRender.call(this, h) // original component
]);
};

return component;
}

originalRender.call(this, h) will give us the component's rendered virtual DOM tree as a JSON object, which can then be amended by our getDisabledVersion function, overriding its value and disabled state. The recursive nature of this function implies that it will possibly deal with multiple cases:

  • if our component only contains plain DOM nodes, we will be looking for form controls (i.e. input, select, or textarea) and update their attributes directly; other DOM nodes will be checked for children to call the function upon
  • on the other hand, if our component contains other components, we will rather be interested in their props; specific value handling/transformation that was handled by our own component, if any, will also have to be done here

And this is where the magic happens:

export default function getDisabledVersion(el, value) {
if (["input", "select", "textarea"].includes(el.tag)) {
// Form control node
el.data.domProps.value = value;
el.data.attrs.disabled = true;
} else if (el.componentOptions) {
// Component
// Do dirty things to value if relevant
el.componentOptions.propsData.value = value;
el.componentOptions.propsData.disabled = true;
} else if (Array.isArray(el.children)) {
// Something else, we need to go deeper
el.children = el.children.map(child => getDisabledVersion(child, value));
}

return el;
}

Finally, to use the HOC:

Vue.component("MyComponentWithRecommendedValue", withRecommendedValue(MyComponent));

Of course, with the upcoming uprising of version 3.0 and its composition API, this pattern might soon be a thing of the past; I'm eager to see how it will make this kind of voodoo trick easier (or not). Feel free to drop knowledge about it in the comments if you have some!