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.