DEV Community

Alexander Nenashev
Alexander Nenashev

Posted on • Edited on

Conditional wrap component in Vue 3 pt.3

Previously we observed 2 options to create a conditional wrap component in Vue 3. Now it's time for the most complex one which exploits vnodes in a deeper way.

On the client side it's pretty ordinary with the wrapper provided in a wrapper slot and the wrapped content in the default one. But we do some magic inside the wrap component:

  1. We collect all leaf vnodes in the wrapper slot's vnode tree
  2. We inject our content into the leaves like they'd support a default slot. For that we should set shapeFlag of the leaves as ShapeFlags.ARRAY_CHILDREN | ShapeFlags.ELEMENT.

See on Vue SFC Playground

import { ShapeFlags } from "@vue/shared";
import { cloneVNode} from "vue";

const findLeaves = (vnode, cb) => 
  vnode.children?.length ? 
    vnode.children.forEach(vnode => findLeaves(vnode, cb)) : 
    cb(vnode)
;

export default function Wrap({isWrapped}, {slots}){

  if (!isWrapped) return slots.default();

  const wrapper = slots.wrapper().map(vnode => cloneVNode(vnode));
  findLeaves({children: wrapper}, vnode => {
    vnode.shapeFlag = ShapeFlags.ARRAY_CHILDREN | ShapeFlags.ELEMENT;
    (vnode.children ??= []).push(...slots.default());
  });

  return wrapper;
}

Wrap.props = { isWrapped: Boolean };  
Enter fullscreen mode Exit fullscreen mode

Usage:

  <wrap :is-wrapped>
    <template #wrapper>
      <div class="wrapper">
        <div class="inner-wrapper"></div>
      </div>
    </template>
    <p>
      I'm wrapped
    </p>
  </wrap>
Enter fullscreen mode Exit fullscreen mode

As you see the DX is pretty good, with full ability to define our wrapper with any nesting fully inside a template.

Now you have a choice to select a wrap component from 3 options or probably combine them into one ultimate component.

Top comments (2)

Collapse
 
slavco86 profile image
SlavCo Zute • Edited

Great article!
I am trying to convert the above to TS but am struggling to properly type slots in the functional component. Vue official docs seem to have left out typing slots in functional component completely.

Actually, I made it TS compliant... to some degree at least:

// eslint-disable-next-line vue/prefer-import-from-vue
import { ShapeFlags } from '@vue/shared';
import { cloneVNode, FunctionalComponent, VNode } from 'vue';
type Props = { active?: boolean };

const findLeaves = (vnode: Partial<VNode>, cb: (vnode: Partial<VNode>) => void): void =>
  vnode.children?.length && Array.isArray(vnode.children)
    ? vnode.children.forEach((vnode) => findLeaves(vnode as VNode, cb))
    : cb(vnode);

const ConditionalWrapper: FunctionalComponent<
  Props,
  {},
  { default: () => VNode[]; wrapper: () => VNode[] }
> = ({ active = false }, { slots }) => {
  if (!active) return slots.default();

  const wrapper = slots.wrapper().map((vnode) => cloneVNode(vnode));

  findLeaves({ children: wrapper }, (vnode) => {
    vnode.shapeFlag = ShapeFlags.ARRAY_CHILDREN | ShapeFlags.ELEMENT;
    if (vnode.children && Array.isArray(vnode.children)) {
      vnode.children.push(...slots.default());
    } else {
      vnode.children = [];

      vnode.children.push(...slots.default());
    }
  });

  return wrapper;
};
ConditionalWrapper.props = {
  active: {
    type: Boolean
  }
};
export default ConditionalWrapper;

Enter fullscreen mode Exit fullscreen mode

However, I found another issue - the above approach doesn't deal with recursive (nested) usage of the conditional wrapper.
Here's a link, which demonstrates the problem.

For context, the current published solution deals very well with conditional wrapping of an element and even accounts for complex nested structures to be used as "wrapper". But, what if the complex nested structure needs to have the conditional wrapping behaviour in an off itself?
In the published solution the below is a single whole wrapper:

<div class="wrapper">
  <div class="inner-wrapper"/>
</div>
Enter fullscreen mode Exit fullscreen mode

What if we need to use .wrapper, wrapping the .inner-wrapper only when a condition B is true. Otherwise, we only use .inner-wrapper to wrap the p element in the published example?

Upon further investigation, seems like the above example doesn't work when trying to use any Vue component as "wrapper"

Collapse
 
alexander-nenashev profile image
Alexander Nenashev

I'm currently working with VUE/TS, will try to investigate