Skip to content

Instantly share code, notes, and snippets.

@wangziling
Last active May 25, 2023 10:16
Show Gist options
  • Save wangziling/dcbbd492bee1d5748d91aabcc16a0dd2 to your computer and use it in GitHub Desktop.
Save wangziling/dcbbd492bee1d5748d91aabcc16a0dd2 to your computer and use it in GitHub Desktop.
Render vNode in vue2 .vue template files.
/* eslint-disable no-unused-vars */
import { Component, Prop, Vue } from 'vue-property-decorator';
import { CreateElement, VNode, VueConstructor } from 'vue';
import { FunctionalComponentOptions } from 'vue/types/options';
import _ from 'lodash';
export function randomString(len?: number): string {
len = len || 32;
const $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
const maxPos = $chars.length;
let pwd = '';
for (let i = 0; i < len; i++) {
pwd += $chars.charAt(Math.floor(Math.random() * maxPos));
}
return pwd;
}
/**
* Clone a vnode.
* https://stackoverflow.com/a/51066092
* @param vnode {VNode}
* @param createElement {Function}
* @param options {*}
* @return {VNode}
*/
export function cloneVNode (
vnode: VNode,
createElement: CreateElement,
options?: Partial<{
tryUseReactiveMode: boolean;
autoMarkAsCloned: boolean;
autoRemoveLifeCycleHooks: boolean;
useRandomKey: boolean;
}>
): VNode {
const clonedChildren = vnode.children && vnode
.children
.map(vnode => cloneVNode(vnode, createElement));
const cloned = createElement(vnode.tag, vnode.data, clonedChildren);
cloned.text = vnode.text;
cloned.isComment = vnode.isComment;
cloned.componentOptions = vnode.componentOptions;
cloned.elm = vnode.elm;
cloned.context = vnode.context;
cloned.ns = vnode.ns;
cloned.isStatic = vnode.isStatic;
cloned.key = vnode.key;
cloned.parent = vnode.parent;
if (_.get(options, 'autoMarkAsCloned')) {
_.set(cloned, 'isCloned', true);
}
// Random key. To mark the vNode different.
if (_.get(options, 'useRandomKey')) {
cloned.key = randomString(5);
}
// Magic!
if (_.get(options, 'tryUseReactiveMode')) {
// Make the props reactive.
const reactiveProps = _.get(vnode.componentInstance, '$props');
if (reactiveProps) {
_.set(cloned, 'componentOptions.propsData', reactiveProps);
}
}
// Maybe we need to set it as true as default.
if (_.get(options, 'autoRemoveLifeCycleHooks')) {
const listeners = _.get(cloned, 'componentOptions.listeners');
if (listeners) {
// These hooks may trigger multiple times.
const lifeCycleHookNameRegExp = /^hook:.+$/;
_.set(
cloned,
'componentOptions.listeners',
_.omitBy(listeners, function (v, k) {
return lifeCycleHookNameRegExp.test(k);
})
);
}
}
return cloned;
}
const removeSlotAssociation = function (v: VNode) {
if (v && v.data) {
v.data.slot = undefined;
}
return v;
};
const markAsKeepAlive = function (v: VNode) {
if (v && v.data) {
v.data.keepAlive = false;
}
return v;
};
const setPropsReactive = function (v: VNode) {
const $props = _.get(v, 'componentInstance.$props');
if ($props && !_.has(_.get(v, 'componentOptions.propsData'), '__ob__')) {
_.set(v, 'componentOptions.propsData', $props);
}
return v;
};
const judgeIsVNodeLike = function (vnode: any) {
return !!vnode && _.has(vnode, 'componentOptions');
};
const defaultCloneModeOptions: Parameters<typeof cloneVNode>[2] = {
tryUseReactiveMode: true,
autoRemoveLifeCycleHooks: true,
autoMarkAsCloned: true,
// No need random key,
// Try to use vue 'reuse' logic.
useRandomKey: false
};
@Component
export class VnodeInTemplateSlot extends Vue {
/* Prototype render */
private render () {
return this.$scopedSlots.default!(this.$attrs);
}
}
@Component
export class VnodeInTemplate extends Vue {
/* Data START */
/* Data END */
/* Prop START */
@Prop({
required: true,
default: undefined
})
vnode!: VNode | VNode[] | string;
@Prop({
required: false,
default: undefined
})
cloneModeOptions!: Parameters<typeof cloneVNode>[2];
@Prop({
required: false,
default: 'div'
})
multipleVnodeRootTag!: string;
/* Prop END */
/* Methods START */
/* Methods END */
/* LifeCycle START */
private render (h: CreateElement) {
if (judgeIsVNodeLike(this.vnode) || Array.isArray(this.vnode)) {
const vnodes = ([] as VNode[]).concat(this.vnode as Exclude<typeof this.vnode, string>).map(v => {
return <VnodeInTemplateSlot
scopedSlots={
{
default: () => {
markAsKeepAlive(v);
removeSlotAssociation(v);
return cloneVNode(v, this.$createElement, this.cloneModeOptions ?? defaultCloneModeOptions);
}
}
}
/>;
});
if (vnodes.length > 1) {
return h(this.multipleVnodeRootTag, this.$attrs, vnodes);
}
return vnodes;
}
return this.vnode;
}
/* LifeCycle END */
}
export function VnodeFunctionalRendererGenerator () {
return {
functional: true,
render (createElement, { props }) {
if (judgeIsVNodeLike(props.vnode) || Array.isArray(props.vnode)) {
const vnodes = ([] as VNode[]).concat(props.vnode).map(v => {
markAsKeepAlive(v);
removeSlotAssociation(v);
return cloneVNode(v, createElement, props.cloneModeOptions ?? defaultCloneModeOptions);
});
if (vnodes.length > 1) {
return createElement(props.multipleVnodeRootTag ?? 'div', vnodes);
}
return vnodes;
}
return props.vnode;
}
} as FunctionalComponentOptions<{
vnode: VNode | VNode[],
cloneModeOptions: Parameters<typeof cloneVNode>[2];
multipleVnodeRootTag: string
}>;
}
export const VnodeFunctionalRenderer = VnodeFunctionalRendererGenerator();
@wangziling
Copy link
Author

wangziling commented May 25, 2023

<template>
  <div class="some-class">
    <vnode-in-template :vnode="vnode" />
  </div>
</template>
<script lang="ts">
import { VnodeInTemplate } from './vnode-in-template.tsx';

export default {
  name: 'Something',
  components: {
    VnodeInTemplate 
  },
  // Data or prop. Use data for demo.
  data () {
    return {
      vnode: null;
    }
  },
  created () {
    const reactiveObj = Vue.observable({ num: 1 });
    this.vnode = this.$createElement('div', null, [reactiveObj .num]);
    setInterval(() => {
      reactiveObj.num += 2;
    }, 1000);
  }
};
<script>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment