Conditionally rendering parent element, keep inner html - vue.js

Is there any built-in way to go about conditionally showing a parent element?
To illustrate:
<a v-show-but-keep-inner="someCondition">
<span>This is always rendered no matter what</span>
</a

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.

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/vue#2.6.14/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

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.

Without Vuejs or other framework context, solely DOM.
You can't remove a DOM element without removing its children too.
What you could do is get the children of a DOM element and replace them with the parent or something similar like that.
With Vuejs you may be able to hide this functionality behind a directive or component, but I think this would be overcomplicating what you want to achieve.
If you want your anchor not to be clickable in certain cases, you could do something like v-on:click.prevent="yourCondition && xxx()". On top of that you could use css classes to hide the fact that it's still an anchor v-bind:class="{ fakeAnchor: yourCondition}".
Though the simplest solution may be to just duplicate your html.
<a v-show="someCondition">
<span>This is always rendered no matter what</span>
</a>
<span v-show="!someCondition">This is always rendered no matter what</span>
The best solution depends on what your case is. If the real inner content will be much larger it might not be okay to duplicate that. If that's the case you could encapsulate that in another vue component.

Maybe this approach helps you (using 'is'):
<template lang="pug">
component(is=someCondition?"v-show-but-keep-inner":"my-another-component")
v-form
v-layout
v-flex
v-btn(#click="doThat")
</template>
This way parent component changes depending on 'someCondition' and children are the same for both conditions.

Related

Vue/Nuxt Component updates and rerenders without any data changing

I have two components, Carousel.vue and Showcase.vue. I'm testing them both in a page like this:
<template>
<main>
<app-showcase
before-focus-class="app-showcase__element--before-focus"
after-focus-class="app-showcase__element--after-focus"
>
<div class="test-showcase" v-for="n in 10" :key="n">
<img
class="u-image-cover-center"
:src="`https://picsum.photos/1000/1000?random=${n}`"
alt=""
/>
<div>Showcase numero {{ n }}</div>
</div>
</app-showcase>
<div class="u-layout--main u-margin-vertical--4">
<div>
<app-button #click="changeRequestTest(4)">Test Request 4</app-button>
<app-button #click="changeRequestTest(10)">Test Request 10</app-button>
<app-carousel
content-class="u-spread-horizontal--main"
center
:request-element="requestTest"
scroll-by="index"
#index-change="onIndexChange"
>
<template #header>
<h4>Placeholder images</h4>
<div>
Carousel heading h4, showing item number {{ index + 1 }}.
</div>
</template>
<template #default>
<img
:src="`https://picsum.photos/100/80?random=${n}`"
:data-carousel-item-name="n === 10 ? 'giovanni-rana' : ''"
alt=""
v-for="n in 20"
:key="n"
/>
</template>
</app-carousel>
</div>
</div>
</main>
</template>
<script>
export default {
data() {
return {
n1: 20,
n2: 20,
isAnimationOver: false,
index: 0,
requestTest: null,
};
},
methods: {
changeRequestTest(n) {
this.requestTest = n;
},
onIndexChange(e) {
this.requestTest = e;
},
logTest(msg = "hello bro") {
console.log(msg);
},
logElement(e) {
console.log(e);
},
},
created() {
this.requestTest = this.$route.query.hero;
},
};
</script>
Both components use a parameter called index, which basically registers the position (in the children array) of the element that is being focused/shown/highlighted.
...
data() {
return {
index: 0,
showBackButton: true,
showForwardButton: true,
lockScroll: false,
};
},
...
Carousel actually has a prop, requestElement, that allows the carousel to be scrolled to a specific element (via number or element name), and is used in combination with the event "index-change" to update the value in the parent component.
Now that's the problem: every time requestElement updates (there is a watcher in Carousel.vue to follow that), the Showcase component rerenders.
That's a huge problem because the Showcase component applies specific classes on the mounted hook, and on rerender, all that classes are lost. I solved the problem by calling my class-applying-methods in the update hook, but I don't particularly like this performance-wise.
Basically, if I remove any reference to requestElement, everything works as intended, but as soon as that value changes, the showcase rerenders completely. I tried to change props/params names, to use dedicated functions instead of inline scripts, nothing works. No common value is shared, neither via props or vuex, so this makes it even weirder.
Any idea why this happens?
EDIT: I tried the solution from this question, as expected no change was detected in Showcase component, but it still gets updated when requestElement is changed in Carousel.

How to use vuetify's v-img inside v-html

I would like to use <v-img> of vuetify inside v-html to replace the standard <img>, i.e.
<div v-html="'<v-img src="some_image_url" />'">
I understand that v-html is meant for only standard HTML component, and I would like to know if there is anyway to use a custom component (such as <v-img>) inside v-html.
The example below uses some cheap and cheerful RegExps to do the parsing, nothing I would use in production code. My focus was on how to avoid using v-html rather than finding a reliable way to parse out the <img> tags.
The key thing I'm trying to demonstrate is how you can parse the text into chunks and then iterate over the chunks in the template to create v-img components. I've used a dummy component for v-img but the principle would be exactly the same for the real thing.
new Vue({
el: '#app',
components: {
vImg: {
template: '<strong>[<slot/>]</strong>'
}
},
data () {
return {
text: 'Something something <img src="somepath"> and <img src="otherpath">'
}
},
computed: {
chunks () {
const re = /(<img\s[^>]*>)/g
const text = this.text
const parts = text.split(re).filter(part => part)
return parts.map(part => {
if (part.match(re)) {
const matchSrc = part.match(/\ssrc="([^"]*)"/)
return {
src: matchSrc && matchSrc[1]
}
}
return part
})
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<template v-for="chunk in chunks">
<template v-if="typeof chunk === 'string'">{{ chunk }}</template>
<v-img v-else>{{ chunk.src }}</v-img>
</template>
</div>
I managed to solve it with v-runtime-template, which can be found here:
https://github.com/alexjoverm/v-runtime-template
Cheers!

Scoping of HTML element id in Vue component

Is there some built-in scoping mechanism for Vue component in the sense that value of id attribute of html element inside Vue component be uniquely defined without programmer's efforts to do it?
In the following code, I create two components and hope each behaves independently to each other. So, ideally if I click on each button, each is required to print out "foo" but actually not because value of ids are duplicated.
<!DOCTYPE html>
<head>
<title></title>
</head>
<body>
<div id="app">
<my-comp></my-comp>
<my-comp></my-comp>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://unpkg.com/vue"></script>
<script>
Vue.component('my-comp', {
template: `
<div>
<button id="btn" #click="onClick">Click me</button>
<div id="pid"></div>
</div>
`,
methods: {
onClick(e) {
$('#pid').text("foo");
}
},
});
const vm = new Vue({
el: '#app',
data: () => ({}),
methods: {}
});
</script>
</body>
</html>
Don't use id in vue components unless you are passing a unique value for it using props. You should very rarely ever actually need to get a reference to an element in vue and if you do find you need to then you should be using refs.
In your case you can just use a property and template binding to handle things for you:
Vue.component('my-comp', {
template: `
<div>
<button #click="onClick">Click me</button>
<div>{{ text }}</div>
</div>
`,
data() {
text: ''
},
methods: {
onClick(e) {
this.text = 'foo'
},
},
})
It looks like the vue-uniq-ids package is what you're looking for.
It is a trend to use components. Components are cool, they are small,
obvious, easy to use and modular. Untill it comes to the id property.
Some HTML tag attributes requires using an id property, like
label[for], input[form] and many of aria-* attributes. And the problem
with the id is that it is not modular. If several id properties on the
page will has the same value they can affect each other.
VueUniqIds helps you to get rid of this problem. It provides the set
of id-related directives which value is automatically modified by
adding unique string while keeping the attrbitue easy to read.

Getting ref of component in async v-for

I have a list of items that don't get created until after an async call happens. I need to be able to get the getBoundingClientRect() of the first (or any) of the created items.
Take this code for instance:
<template>
<div v-if="loaded">
<div ref="myItems">
<div v-for="item in items">
<div>{{ item.name }}</div>
</div>
</div>
</div>
<div v-else>
Loading...
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
items: []
}
},
created() {
axios.get('/url/with/some/data.json').then((response) => {
this.items = response.data;
this.loaded = true;
}, (error) => {
console.log('unable to load items');
});
},
mounted() {
// $refs is empty here
console.log(this.$refs);
// this.$refs.myItems is undefined
}
};
</script>
So, I'm trying to access the myItems ref in the mounted() method, but the this.$refs is empty {} at this point. So, therefore, I tried using a component watch, and various other methods to determine when I can read the ref value, but have been unsuccessful.
Anyone able to lead me in the right direction?
As always, thanks again!!
UPDATES
Added a this.$watch in the mounted() method and the $refs still come back as {}. I then added the updated() method to the code, then was able to access $refs there and it seemed to work. But, I don't know if this is the correct solution?
How does vuejs normally handle something like dynamically moving a div to an on-screen position based on async data? This is similar to what I'm trying to do, grab an element on screen once it has been rendered first (if it even should be rendered at all based on the async data), then access it to do something with it (move it to a position)?
Instead of doing on this.$refs.myItems during mounted, you can do it after the axios promise returns the the response.
you also update items and loaded, sou if you want to use watch, you can use those
A little late, maybe it helps someone.
The problem is, you're using v-if, which means the element with ref="myItems" doesn't exist yet. In your code this only happens when Axios resolves i.e. this.loaded.
A better approach would be to use a v-show.
<template>
<div>
<div v-show="loaded">
<div ref="myItems">
<div v-if="loaded">
<div v-for="item in items">
<div>{{ item.name }}</div>
</div>
</div>
</div>
</div>
<div v-show="!loaded">
Loading...
</div>
</div>
</template>
The difference is that an element with v-show will always be rendered and remain in the DOM; v-show only toggles the display CSS property of the element.
https://v2.vuejs.org/v2/guide/conditional.html#v-show

Custom Vue directive to omit tag but render tag's contents?

I'd like to create a custom Vue directive to omit the tag but render the tag's contents when the directive is true.
So for example, if the data for my vue instance is defined as
data:{
omitIt: true
}
And if the markup looks like this:
<div v-omit="omitIt" class="someClass">
Hello world!
</div>
When omitIt is set to false as it is above, I'd like the following rendered into the dom:
<div class="someClass">
Hello world!
</div>
But when omitIt is true I'd like only the following rendered into the dom:
Hello world!
I initially tried to solve this by doing the following (admittedly not a custom vue directive):
<template v-if="!omitIt">
<div class="someClass">
</template>
Hello world!
<template v-if="!omitIt">
</div>
</template>
The above isn't pretty but I thought perhaps it would work. But alas what gets rendered into the dom when omitIt is false is:
<div class="someClass"></div>
Hello world!
Any suggestions on how to achieve the results I'm looking for?
I thought #Nit's answer was a great and simple one and upvoted it, but it does have one flaw: a slot may not be a root element so the component will fail when the wrapper needs to be omitted. This is because slots can contain more than one element and if the slot does contain more than one, there could end up being more than one root element, which is not allowed.
I have a partial solution that renders just the first element in the slot if the component should not wrap.
Vue.component("wrapper", {
props:{
nowrap: {type: Boolean, default: false}
},
render(h){
// This will *only* render the *first* element contained in
// the default slot if `nowrap` is set. This is because a component
// *must* have a single root element
if (this.nowrap) return this.$slots.default[0]
// Otherwise, wrap the contents in a DIV and render the contents
return h('div', this.$slots.default)
}
})
Here is an example of it working.
console.clear()
Vue.component("wrapper", {
props:{
nowrap: {type: Boolean, default: false}
},
render(h){
// Log a warning if content is being omitted
const omissionMessage = "Wrapper component contains more than one root node with nowrap specified. Only the first node will be rendered."
if (this.$slots.default.length > 1 && this.nowrap)
console.warn(omissionMessage)
// This will *only* render the *first* element contained in
// the default slot if `nowrap` is set. This is because a component
// *must* have a single root element
if (this.nowrap) return this.$slots.default[0]
// Otherwise, wrap the contents in a DIV and render the contents
return h('div', this.$slots.default)
}
})
new Vue({
el: "#app"
})
.someClass{
color: blue
}
<script src="https://unpkg.com/vue#2.4.2"></script>
<div id="app">
<wrapper class="someClass">Hello World</wrapper>
<wrapper nowrap>No wrap, single root</wrapper> <br>
<wrapper nowrap>
No wrap, two roots. Paragraph is ommitted.
<p>Some other content</p>
</wrapper>
</div>
A couple notes: The component will always wrap unless you add nowrap as an attribute. Also, notice the class is added to the wrapped container without specifying it as a prop. This is because Vue automatically renders attributes that aren't specified as props on the root element of a component, unless you tell it not to.
This answer is wrong, slots cannot be used in this manner. Please see Bert's answer instead.
The easiest solution would be to create a wrapper component with slots for this purpose, passing the omitting argument as a prop.
The content distribution part becomes rather straightforward.
In the wrapper component template:
<slot v-if="omitIt"></slot>
<div v-else>
<slot></slot>
</div>
Wherever you want to use the wrapper:
<wrapper v-bind:omitIt="omitIt">
// Content
</wrapper>