How can I use transition on individual list elements in Vue.js? - vue.js

My understanding of transitions in vue.js is that you use <transition>
to animate between individual elements and <transition-group> to animate a whole list.
It seems as though if you wanted to animate a transition within a list item you'd use <transition> within the list. e.g. something like this:
<span v-for="item in items">
<transition>
<div>
Transition from this...
</div>
<div>
...to this.
</div>
</transition>
</span>
Yet, when I make that change the animation doesn't work. Is this an expected behavior?
Update: after some tinkering, I have found that my original hypothesis was correct. I was just doing something else wrong. But it's worth noting for anyone else who comes across this problem.
You can use <transition> inside a list if you want to animate individual components of the list.

You use transition groups to transition all children in the same way.
In addition, try setting the transition group before your v-for
new Vue({
el: "#app",
data: {
items : [
{message: 'sometext', id: 1},
{message: 'sometext', id: 2},
{message: 'sometext', id: 3}
],
id : 3
},
methods: {
addItem(){
this.id++
this.items.push({message: 'sometext', id: this.id});
},
enter(){
console.log('transition enter called');
}
}
})
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div id="container">
<button #click="addItem()">Add Item</button>
<transition-group :name="'fade'" v-on:enter="enter">
<span v-for="item in items" v-bind:key="item.id">
{{item.message}}
</span>
</transition-group>
</div>
</div>

I am not exactly sure what you are trying to do with your example.
If you would like to transition a list
<transition-group name="fade" tag="span">
<div v-for="item in items" v-bind:key="item">
{{ item }}
</div>
</transition-group>
If you would like to transition between two items.
<transition name="fade">
<div v-show="whatever === true">
Transition from this...
</div>
</transition>
<transition name="fade">
<div v-show="whatever === false">
...to this.
</div>
</transition>

Related

Can I create a conditional v-for loop to loop on the same div?

I'm curious is there is a way to have a v-for loop with a conditional statement inside of it so I can reduce redundancies in my program. I have a div tag that needs to loop on the tagfitlers object if the tag_filters object does not exist, otherwise, I need it to loop on the tag_fitlers object.
This is a snippet of my current loop:
<div v-else class="text-left mt-2 border" v-for="(filter, index) in tagfilters" :key="index">
<span v-for="(f, i) in filter" :key="i">
<div class="d-flex justify-content-between align-items-center pr-3 pl-3 pt-3">
<!-- Multiselect Search with Tagging -->
<div>
<multiselect #change="onEdit(filter, 'code', f.code)" class="mb-2" v-model="f.code" placeholder="Search & Select Code Options" :custom-label="customCodesLabel" track-by="code" :options="codesArr"></multiselect>
</div>
</div>
</div>
I am hoping to do something like this:
v-for="tag_filters ? (filter, index) in tag_filters : (filter, index) in tagfilters"
Is this possible?
According to Vue.js,
Using v-if and v-for together is not recommended. See the style guide for further information.
https://v2.vuejs.org/v2/guide/conditional.html#v-if-with-v-for
You could use a computed property to get the correct list.
new Vue({
el: "#app",
data: function() {
return {
tag_filters: {
filterC: "tag_filter C.",
filterD: "tag_filter D.",
},
tagfilters: {
filterA: "tagfilter A.",
filterB: "tagfilter B."
}
}
},
computed: {
getTagFilters() {
if (Object.keys(this.tag_filters).length === 0) {
return this.tagfilters;
} else {
return this.tag_filters;
}
}
}
})
<div id="app">
<div v-for="(filter, key) of getTagFilters" :key="key">
<span>{{ filter }}</span>
</div>
<button #click="tag_filters = {}">Remove tag_filter data</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
One option is to use a computed prop, but not sure if it is enough for what you need:
get realTagFilters() {
return this.tagFilters ?? this.tag_filters;
}
And you can call it like
<div v-for="(filter, index) in realTagFilters">
Or you can try with the logical or operator, which also seems to work:
<div v-for="filter in (tag_filters || tagFilters)" :key="filter.id">

Can't get Vue.js transition work without a warning message

I am using Vue.js transitions to fade elements in/out based on conditional rendering.
First, I am transitioning a group of components. This is working perfectly!
<div>
<transition-group name="component-fade" mode="out-in">
<component-one key="1" v-show="foo === 'one'" :type="type"/>
<component-one key="2" v-show="foo === 'two'" :type="type"/>
<component-one key="3" v-show="foo === 'three'" :type="type"/>
</transition-group>
</div>
Each component <component-one.../> is identical. I am rendering a bunch of <div> elements:
// component-one.vue
<template>
<div>
<div id="div-one">
<transition name="fade" mode="out-in">
<div key="one" v-if="foo === 'bar'">
<h3>My First Div</h3>
...
</div>
<div key="two" v-if="foo === 'bazz'">
<h3>My Second Div</h3>
...
</div>
<div key="three" v-if="foo === 'other'">
<h3>My Third Div</h3>
...
</div>
...
</transition>
</div>
</div>
</template>
The functionally works great. The components fade in/out nicely as to the div elements. However, I am getting a warning from vue:
[Vue warn]: can only be used on a single element. Use for lists.
This makes sense as I am transitioning a group of div elements. However, if I use <transition-group> the mode of out-in is no longer working. As I toggle through my div elements, they snap in/out of position as they are fading in/out. I have tried every combination -- including using v-if or v-show to see if that made a difference.
How can I use the transitions I have (that work) but not generate the warning?
`Adding "keys" on each item.
try this.
new Vue({
el: "#app",
data: {
foo: "bar"
},
methods: {
}
})
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<button #click="foo = 'bar'">show bar</button>
<button #click="foo = 'bazz'">show bazz</button>
<button #click="foo = 'other'">show other</button>
<transition name="fade" mode="out-in">
<div key="one" v-if="foo === 'bar'">
<h3>bar</h3>
</div>
<div key="two" v-if="foo === 'bazz'">
<h3>bazz</h3>
</div>
<div key="three" v-if="foo === 'other'">
<h3>other</h3>
</div>
</transition>
</div>
I believe that the transition element is supposed to wrap just one element and that the reason v-show isn't working is because v-show doesn't actually add/remove anything from the DOM, it just toggles the visibility attribute. If you use any logic that could theoretically show more than one at the same time, you'll get a warning. So what I think you'd really want is for your transition element to wrap each of the individual component-one elements. v-show should take care of the individual element's visibility. Basically, you need the final markup to look like this:
<transition name="fade" mode="out-in">
<div key="one" v-if="foo === 'bar'">
<h3>My First Div</h3>
...`enter code here`
</div>
</transition>
<transition name="fade" mode="out-in">
<div key="two" v-if="foo === 'bar'">
<h3>My Second Div</h3>
...
</div>
</transition>
Try doing either:
A v-for on the transition component that loops through a list of all the items you'd like to render. Each transition should have its own component-one child as it loops through the list.
<transition v-for="(item, index) in items" name="fade" mode="out-in">
<component-one :key="index" v-show="foo === item" :type="type"/>
</transition>
Wrapping the contents of component-one inside a transition. That would include the transition inside the component-one element. Then you can render component-one elements without any transition elements as a parent.
component-one
<transition name="fade" mode="out-in">
<div>
<!-- Component One contents... -->
</div>
</transition>
main
<div>
<component-one key="1" v-show="foo === 'one'" :type="type"/>
<component-one key="2" v-show="foo === 'two'" :type="type"/>
<component-one key="3" v-show="foo === 'three'" :type="type"/>
</div>
The above answers are in fact correct and should be considered by anyone who comes across this thread. I also wanted to add what I found was the underlying issue.
I was using <transition-group> on my parent element(s) that I was trying to transition between. Each component that I was transitioning to had multiple div's underneath. This is what was bubbling up (for lack of better words) that was looking for a <transition-group>.
TL;DR:
The error was coming from the child component(s) that needed to make use of <transition-group>.
Seems obvious now, but hope can save other folks some time.

Nesting a slot in a slot for vue

Update: Here's a simplified version of what I'm trying to achieve here (from the threaded conversation below):
Accept Component A - Accept Component B - Accept a condition - if
condition is true : wrap Component B with Component A [and render]- else only
render component B.
I'm interested in creating a component that renders a wrapper conditionally. I figured a theoretical approach like this would probably be best**:**
<template>
<div>
<slot v-if="wrapIf" name="wrapper">
<slot name="content"></slot>
</slot>
<slot v-else name="content"></slot>
</div>
</template>
<script>
export default {
props: {
wrapIf: Boolean,
}
}
</script>
Then when we implement, it would look something like this:
...
<wrapper-if :wrap-if="!!link">
<a :href="link" slot="wrapper"><slot></slot></a>
<template slot="content">
content
</template>
</wrapper-if>
The idea being that, in this case, if there is a link, then let's wrap the content with the wrapper slot (which can be any component/element). If there isn't, then let's just render the content without the wrapped link. Pretty simple logic, but it seems that I'm misunderstanding some basic vue functionality because this particular example does not work.
What is wrong with my code or is there some kind of native api that already achieves this or perhaps a dependency that does this sort of thing already?
The output should look like this:
wrapIf === true
<a href="some.link">
content
</a>
wrapIf === false
content
Just focus on the content itself, and let the component worry about whether or not to wrap the default or named content slot.
If you need the wrapper to be dynamic, a dynamic component should solve that. I've updated my solution accordingly. So if you need the wrapper to be a label element, just set the tag property to it, and so on and so forth.
const WrapperIf = Vue.extend({
template: `
<div>
<component :is="tag" v-if="wrapIf" class="wrapper">
<slot name="content"></slot>
</component>
<slot v-else name="content"></slot>
</div>
`,
props: ['wrapIf', 'tag']
});
new Vue({
el: '#app',
data() {
return {
link: 'https://stackoverflow.com/company',
tagList: ['p', 'label'],
tag: 'p',
wrap: true
}
},
components: {
WrapperIf
}
})
.wrapper {
display: block;
padding: 10px;
}
p.wrapper {
background-color: lightgray;
}
label.wrapper {
background-color: lavender;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<wrapper-if :wrap-if="wrap" :tag="tag">
<a :href="link" slot="content">
content
</a>
</wrapper-if>
<div>
Change wrapper type:
<select v-model="tag">
<option v-for="tag in tagList">{{tag}}</option>
</select>
</div>
<button #click="wrap = !wrap">Toggle wrapper</button>
</div>

Transition on class change property

I currently have a vue component that I use to select drinks to be ordered.
You can increment or decrement the amount of drinks per type.
There are 2 buttons (Increment and decrement), the decrement button needs to be hidden when the amount of ordered drinks for that type is equal to 0. This can easily be accomplished by using :class="drink.amount > 0 ? 'visible':'invisible'" using the tailwind classes which sets visibility:hidden css property. When using this method it looks like:
Now I tried to implement css animations (sliding the button underneath the drink block to hide it and later hide from dom). I want to implement this using the vue transition component since this is heavily used throughout the application.
Now I have the problem that the vue transition component only works with a limited amount of vue functions among which v-if and v-show. v-if removes the html from the dom. v-show sets the property display:none both of these functions have the effect of shifting the buttons:
I would like to known how I can use the vue transition component and get the requested aligned buttons as I got without the animation.
<div v-for="drink in drinks" class="w-full flex">
<div class="mx-auto flex">
<transition name="transition-slide-right">
<div class="bg-white w-16 h-16 flex justify-center items-center mt-12"
v-show="drink.amount">
<p>-</p>
</div>
</transition>
<div class="bg-brand w-32 h-24 flex justify-center items-center my-8 z-30">
<p>{{drink.name}} ({{drink.amount}})</p>
</div>
<div class="bg-white w-16 h-16 flex justify-center items-center mt-12">
<p>+</p>
</div>
</div>
</div>
And accompanying script.
<script>
export default {
name: "CreateTransaction",
data: function() {
return {
drinks: [
{name: 'Cola', price: '1.0', amount: 1},
{name: 'Sinas', price: '1.0', amount: 0}
],
}
}
}
</script>
Finally the css:
.transition-slide-right-enter-active, .transition-slide-right-leave-active {
transition: transform .5s;
}
.transition-slide-right-enter, .transition-slide-right-leave-to {
transform: translateX(100%);
}
As a current workaround I removed the transition component, added the transition-slide-right-enter-active class and if the count == 0 I conditionally add the transition-slide-right-enter class. This does not hide the element but hiding it is not required since I is moved underneath the center block.
you can try the following code
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.16/dist/vue.js"></script>
<div class="row" id="todo-list-example">
<div v-for="(drink,index) in drinks" class="w-full flex">
<transition name="transition-slide-right"><button v-show="drink.amount" type="button" class="btn btn-danger" v-on:click="click1(index)">-</button></transition>
<a class="btn btn-success" >{{drink.name}} ({{drink.amount}})</a>
<button type="button" class="btn btn-danger" v-on:click="click2(index)">+</button>
</div>
</div>
<style>
.w-full{margin:10px 0px;}
.transition-slide-right-enter-active, .transition-slide-right-leave-active {
transition: transform .5s;
}
.transition-slide-right-enter, .transition-slide-right-leave-to {
transform: translateX(100%);
}
</style>
<script>
new Vue({
el: '#todo-list-example',
data(){
return {
drinks: [
{name: 'Cola', price: '1.0', amount: 1},
{name: 'Sinas', price: '1.0', amount: 1}
],
}
},
methods:{
click1:function(index){
this.drinks[index].amount=this.drinks[index].amount-1;
},
click2:function(index){
this.drinks[index].amount=this.drinks[index].amount+1;
}
}
})
</script>

Vue 2 Transition not working

I have no idea where my code went wrong. It should be a simple transition. When I click the button the message shows correctly, but just that the there is no fade transition happening at all.
<template>
<div>
<transition name="fade">
<message v-show="showMessage" class="tr pop-up-message">
<p slot="header">This is Header</p>
<span slot="body">This is Body</span>
</message>
</transition>
<div v-if="!showMessage" class="block" #click.prevent="showMessage = true">
<a class="button is-primary">Primary</a>
</div>
<div v-else-if="showMessage" class="block" #click.prevent="showMessage = false">
<a class="button is-primary">Primary</a>
</div>
</div>
</template>
<script>
import message from './Message.vue'
export default {
components:{
'message': message,
},
data(){
return{
showMessage: false
}
},
}
</script>
Have you added these CSS as well:
.fade-enter-active, .fade-leave-active {
transition: opacity .5s
}
.fade-enter, .fade-leave-to /* .fade-leave-active in <2.1.8 */ {
opacity: 0
}
I have tried to reproduce your code here with above CSS which works.
I had two <p> tags right next to each other like below
<p v-if="!isControlling">Take control of the camera by clicking</p>
<p v-else>Press <kbd>Esc</kbd> to exit camera. <kbd>W</kbd> <kbd>A</kbd> <kbd>S</kbd> <kbd>D</kbd> <kbd>Space</kbd> <kbd>Shift</kbd> to move. Mouse to look.</p>
and they just didn't work. It looked like Vue was reusing the same <p> tag in the markup to render both. Adding a key to each made the transition work (thanks #Mark). The fix should look something like below
<p key="asd" v-if="!isControlling">Take control of the camera by clicking</p>
<p key="asf" v-else>Press <kbd>Esc</kbd> to exit camera. <kbd>W</kbd> <kbd>A</kbd> <kbd>S</kbd> <kbd>D</kbd> <kbd>Space</kbd> <kbd>Shift</kbd> to move. Mouse to look.</p>