Conditionally rendering parent element, keep inner html

I just ran into the same Problem.

Vue.js Core team member LinusBorg provides a great solution for this use case using a functional component with a custom render function:

Vue.component('with-root', {
  functional: true,
  props: ['show'],
  render(h, ctx) {
    const children = ctx.children.filter(vnode => vnode.tag) // remove unnecessary text nodes
    // console.log(children)
    if (children.length !== 1) {
      console.warn('this component accepts only one root node in its slot')
    }
    if (ctx.props.show) {
      return children[0]
    } else {
      return children[0].children
    }
  }
})

new Vue({
  el: '#app',
  data: {
    show: true
  }
})
<script src="https://unpkg.com/[email protected]/dist/vue.js"></script>

<div id="app">
  <with-root v-bind:show="show">
    <a href="#">
      <span>This is always rendered no matter what</span>
    </a>
  </with-root>
  <br>
  <button @click="show = !show">Toggle</button>
  <pre>{{$data}}</pre>
</div>

His fiddle: https://jsfiddle.net/Linusborg/w9d8ujn8/

Source: https://forum.vuejs.org/t/conditionally-render-parent-element/9324/2


I think it's a job for custom directive. I made this one as a quick POC:

Vue.directive('showButKeepInner', {
  bind(el, bindings) {
    bindings.def.wrap = function(el) {
      // Find all next siblings with data-moved and move back into el
      while (el.nextElementSibling && el.nextElementSibling.dataset.moved) {
        el.appendChild(el.nextElementSibling).removeAttribute('data-moved')
      }
      el.hidden = false
    }

    bindings.def.unwrap = function(el) {
      // Move all children of el outside and mark them with data-moved attr
      Array.from(el.children).forEach(child => {
        el.insertAdjacentElement('afterend', child).setAttribute('data-moved', true)
      })
      el.hidden = true
    }
  },

  inserted(el, bindings) {
    bindings.def[bindings.value ? 'wrap' : 'unwrap'](el)
  },

  update(el, bindings) {
    bindings.def[bindings.value ? 'wrap' : 'unwrap'](el)
  }
})

new Vue({
  el: '#app',
  data: {
    someCondition: false
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.min.js"></script>

<div id="app">
  <p>
    <button v-on:click="someCondition = !someCondition">{{ someCondition }}</button>
  </p>
  <a v-show-but-keep-inner="someCondition" href="/">
    <span>This is always rendered no matter what</span>
  </a>
</div>

For Vue v3.x, the following would work:

  <component
    :is="condition ? 'custom-component' : 'slot'"
    custom-component-prop
    ...
  >
    ...
  </component>

For Vue v2.x, a workaround is to do:

  <component
    :is="condition ? 'custom-component' : 'v-div'"
    custom-component-prop
    ...
  >
    ...
  </component>
// VDiv.vue
<template>
  <div>
    <slot></slot>
  </div>
</template>

<script>
  export default {
    inheritAttrs: false,
  }
</script>

The tradeoff is there will be an extra element like div being rendered, since Vue v2.x doesn't support fragment.


If someone happens to be using the vue-fragment (https://www.npmjs.com/package/vue-fragment) library, the following works:

<component :is="someCondition ? 'a' : 'fragment'">
   <span>This is always rendered no matter what</span>
</component>

That being said, I do not recommend to use one library just to do this. But if you already do, it can be useful.

Tags:

Vue.Js

Vuejs2