Why does transition-group animate opacity but not animate height? - vue.js

the code below animates the element you click on, but I want it to smoothly decrease the height to zero as well. Unfortunately, the height does not change, but everything works with opacity.
height: 0 !improtant; //does not help to solve the problem
<template>
<transition-group name="msgAnimation" tag="div">
<div v-for="(obj, i) in messages" :key="obj.key" class="wrapper">
<div class="wrapper__block" #click="messages.splice(i, 1)">
{{ obj.msg }}
</div>
</div>
</transition-group>
</template>
<style lang="css">
.msgAnimation-enter-active,
.msgAnimation-leave-active {
transition: all 5s;
}
.msgAnimation-enter,
.msgAnimation-leave-to {
height: 0;
opacity: 0;
}
.wrapper {
width: 100%;
height: 9vmin;
}
.wrapper__block {
background: green;
height: 9vmin;
width: 100%;
}
</style>
<script>
export default {
name: "HelloWorld",
data() {
return {
totalAmount: 0,
messages: [{ key: 0, msg: "Are u hacker" }],
};
},
};
</script>

Your CSS rules was declared after your animation
const example = {
data() {
return {
totalAmount: 0,
messages: [{
key: 0,
msg: "Are u hacker"
},
{
key: 1,
msg: "Are u hacker"
}
],
};
},
};
const app = new Vue(example);
app.$mount("#app");
.msgAnimation-enter-active,
.msgAnimation-leave-active {
transition: all 5s;
}
.wrapper {
width: 100%;
height: 9vmin;
overflow:hidden;
}
.wrapper__block {
background: green;
width: 100%;
}
.msgAnimation-enter,
.msgAnimation-leave-to {
opacity: 0;
height: 0;
}
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<div id="app">
<transition-group name="msgAnimation" tag="div">
<div v-for="(obj, i) in messages" :key="obj.key" class="wrapper">
<div class="wrapper__block" #click="messages.splice(i, 1)">
{{ obj.msg }}
</div>
</div>
</transition-group>
</div>

You're not limiting overflow of .wrapper, and you have a height specified in .wrapper__block. Thus, even if .wrapper goes smoothly to 0, its child will not.
Setting height: 100%; on .wrapper__block, or setting overflow: hidden on .wrapper should do the trick.
<template>
<transition-group name="msgAnimation" tag="div">
<div v-for="(obj, i) in messages" :key="obj.key" class="wrapper">
<div class="wrapper__block" #click="messages.splice(i, 1)">
{{ obj.msg }}
</div>
</div>
</transition-group>
</template>
<style lang="css">
.msgAnimation-enter-active,
.msgAnimation-leave-active {
transition: all 5s;
}
.msgAnimation-enter,
.msgAnimation-leave-to {
height: 0;
opacity: 0;
}
.wrapper {
width: 100%;
height: 9vmin;
}
.wrapper__block {
background: green;
height: 100%;
width: 100%;
}
</style>
<script>
export default {
name: "HelloWorld",
data() {
return {
totalAmount: 0,
messages: [{ key: 0, msg: "Are u hacker" }],
};
},
};
</script>
Alternatively, if you don't mind distortion during the animation, it's a lot more performant to animate transform: scaleY(0), as transform and opacity are applied at the Composition step in CSS, you prevent a lot of in-between style calculations, making your app noticeably faster whn you have several thousand messages.

Related

Unwanted animation bug in Vue.js

This is an unwanted animation bug that seems to be normal, but I want the element to be animationed which is in <input v-model="todo"/>. The problem is each time I press on add button to add the todo name (<input v-model="todo"/>), the last element 'last' runs the animation.
P.s. I tried to keep the code as simple as possible.
Vue.createApp({
data() {
return {
todo: "",
todo_list: ['last'],
};
},
methods: {
add() {
this.todo_list.unshift(this.todo);
console.log(this.todo_list);
},
remove(index) {
this.todo_list.splice(index, 1);
},
},
}).mount('#app');
.list-enter-active,
.list-leave-active {
transition: all 200ms ease-out;
}
.list-enter-from {
transform: translateY(-20px);
}
.list-leave-to {
opacity: 0;
transform: translateX(20px);
}
article {
width: 50%;
padding: 10px;
background-color: #dddddd;
border: 1px solid #cfcfcf;
border-radius: 5px;
margin-block: 10px
}
<script src="https://unpkg.com/vue#next"></script>
<div id="app">
<input type="text" v-model="todo" /> <button #click="add">add</button>
<transition-group name="list" tag="section">
<article v-for="(todo, index) in todo_list" :key="index" #click="remove(index)">
<span>{{ todo }}</span>
</article>
</transition-group>
</div>
The reason you're experiencing this issue, is because you're using the index as a key.
If you added the elements to the end of the list, this would be fine.
But since you're adding it to the start, it will cause the issue you're seeing.
I'd suggest you make each todo an object, and add a unique identifier to each object. This can be a simple integer that you increment. Then you can use that property as the key.
Example
let id = 1;
Vue.createApp({
data() {
return {
todo: "",
todo_list: [{
id: id++,
value: 'last'
}],
};
},
methods: {
add() {
const todo = {
id: id++,
value: this.todo
}
this.todo_list.unshift(todo);
},
remove(index) {
this.todo_list.splice(index, 1);
},
},
}).mount('#app');
.list-enter-active,
.list-leave-active {
transition: all 200ms ease-out;
}
.list-enter-from {
transform: translateY(-20px);
}
.list-leave-to {
opacity: 0;
transform: translateX(20px);
}
article {
width: 50%;
padding: 10px;
background-color: #dddddd;
border: 1px solid #cfcfcf;
border-radius: 5px;
margin-block: 10px
}
<script src="https://unpkg.com/vue#next"></script>
<div id="app">
<input type="text" v-model="todo" /> <button #click="add">add</button>
<transition-group name="list" tag="section">
<article v-for="(todo, index) in todo_list" :key="todo.id" #click="remove(index)">
<span>{{ todo.value }}</span>
</article>
</transition-group>
</div>

Asynchronous task is not reactive in vuejs view

Default the loading image is true after complete the upload loading image is false, but after update the loading object no effect in view, always show the loading bar.
Where is my mistake, please help anyone,
Note: also try by this.$nextTick() function, same output;
in console the update we got, but no effect in view
Vue.config.devtools=false;
Vue.config.productionTip = false;
new Vue({
el:"#app",
data: {
isloadingImage: [],
property:{
images:[]
}
},
methods: {
addFiles() {
this.$refs.files.click();
},
handleFilesUpload() {
let uploadedFiles = this.$refs.files.files;
let maxLength = uploadedFiles.length <= 4 ? uploadedFiles.length : 4;
for (let i = 0; i < maxLength; i++) {
uploadedFiles[i].url = URL.createObjectURL(uploadedFiles[i]);
this.property.images.push(uploadedFiles[i]);
}
this.uploadImages();
},
removeFile(key) {
this.property.images.splice(key, 1);
delete this.isloadingImage[key];
},
async uploadImages(){
this.property.images.forEach((value, key) => {
if (!this.isloadingImage[key]) {
this.isloadingImage[key] = true;
let myFormData = new FormData();
myFormData.append('title', value);
axios.post('http://localhost:800/uploadimage',
myFormData).then(response => {
this.isloadingImage[key] = false;
}).catch(error=> {
this.isloadingImage[key] = false;
console.log(this.isloadingImage);
})
}
});
}
}
});
.small-image {
max-height: 200px;
max-width: 200px;
}
.post-image button {
padding: 0 5px;
}
.post-image-preview {
max-height: 105px;
}
.post-image .caption {
max-width: 198px;
height: 27px;
}
.lds-facebook {
display: inline-block;
position: relative;
width: 64px;
height: 50px;
}
.lds-facebook div {
display: inline-block;
position: absolute;
left: 6px;
width: 10px;
background: #bfbebe;
animation: lds-facebook 1.2s cubic-bezier(0, 0.5, 0.5, 1) infinite;
}
.lds-facebook div:nth-child(1) {
left: 6px;
animation-delay: -0.24s;
}
.lds-facebook div:nth-child(2) {
left: 26px;
animation-delay: -0.12s;
}
.lds-facebook div:nth-child(3) {
left: 45px;
animation-delay: 0s;
}
#keyframes lds-facebook {
0% {
top: 6px;
height: 51px;
}
50%, 100% {
top: 19px;
height: 26px;
}
}
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<div class="field-title"><h5>Pictures</h5></div>
<div class="form-group post-image">
<div class="col-md-12">
<div class="upload-btn-wrapper">
<button class="add-photo" v-on:click="addFiles()"><i
class="fas fa-camera"></i></button>
<input type="file" multiple id="file" ref="files"
v-on:change="handleFilesUpload()">
</div>
<div class="brows-image-text"><p>You can upload up to<br>4 pictures per
listing</p></div>
</div>
<div class="row">
<div v-for="(file, key) in property.images" class="col-md-3">
<div class="lds-facebook" v-if="isloadingImage[key]">
<div></div>
<div></div>
<div></div>
</div>
<div v-else>
<button v-on:click="removeFile( key )" type="button">
<i class="fas fa-times text-danger"></i>
</button>
<img :src="file.url" class="small-image post-image-preview">
</div>
</div>
</div>
</div>
</div>
See rule #2 here https://vuejs.org/2016/02/06/common-gotchas/#Why-isn%E2%80%99t-the-DOM-updating
You update isloadingImage array's values using its keys. In such a case for the change to be reactive, you need to replace your whole array after the fact.
Example:
axios.post('http://localhost:800/uploadimage', myFormData)
.then(response => {
this.isloadingImage[key] = false;
this.isloadingImage = this.isloadingImage.slice(0);
// ^^^ this line
}).catch(error=> {
console.log('error', key, error)
this.isloadingImage[key] = false;
this.isloadingImage = this.isloadingImage.slice(0);
// ^^^ and line
});
Use data as function, not as object like you do.
new Vue({
el:"#app",
data () {
return {
imageIsLoading: ...

Fix animated images on slide

I'm trying to make an animation when changing a slide. The old slide disappears with the animation, and the new one shows up with the animation. My animation is alternating. Help. Thank you all !!
var app = new Vue({
el: '#app',
data() {
return {
selectedIndex: 0,
message: "Test work vue",
isOpenSlide: true,
startVal: 0,
decimals: 0,
duration: 2.5,
options: {
useEasing: true,
useGrouping: true,
separator: ',',
decimal: '.',
prefix: '',
suffix: ''
},
items: [
{
title: 'Center of osteopatia and rehabilitation',
url_img: 'https://i.imgur.com/gQp3VSW.jpg',
info_block: [
{
incremental: '800',
description: 'Increasing the number of transactions from organic search results'
},
{
incremental: '240',
description: 'Raising your revenue'
}
]
},
{
title: 'SLide 2',
url_img: 'https://newevolutiondesigns.com/images/freebies/space-wallpaper-5.jpg',
info_block: [
{
incremental: '140',
description: 'Increasing the numb organic search results'
},
{
incremental: '790',
description: 'Raising your revenue'
}
]
},
{
title: ' SLIDE 3',
url_img: 'https://www.planwallpaper.com/static/images/4433836-space-wallpapers.jpg',
info_block: [
{
incremental: '110',
description: 'Increasing the number of trans'
},
{
incremental: '99',
description: 'Raising your revenue'
}
]
}
]
}
},
methods: {
select(index) {
this.selectedIndex = index
},
index_dotnav: function (index) {
this.selectedIndex = index
},
open() {
this.isOpenSlide = true;
},
close() {
this.isOpenSlide = false;
},
toggle() {
if (this.isOpenSlide) {
this.close();
} else {
this.open();
}
},
ChangeSlider() {
setTimeout(() => {
if (++this.selectedIndex === this.items.length) {
this.selectedIndex = 0;
}
this.toggle();
this.ChangeSlider()
}, 5000)
},
callback(instance) {
instance.start();
}
},
mounted() {
this.ChangeSlider();
}
})
.slide-leave-active,
.slide-enter-active {
transition: 1s;
}
.slide-enter {
transform: translate(100%, 0);
}
.slide-leave-to {
transform: translate(-100%, 0);
}
ul {
padding-left: 0;
margin: 0;
}
.img-block,
section > *,
.uk-slideshow,
.uk-slideshow > ul {
height: 100vh !important;
}
.information-slide .uk-container {
position: absolute;
z-index: 1;
top: 0;
bottom: 0;
right: 0;
left: 0;
margin: auto 0;
display: flex;
flex-direction: column;
justify-content: center;
color: #fff;
}
.slideshow > div.dotnav-block {
top: 50%;
left: 95%;
z-index: 2;
}
.slideshow > div.dotnav-block li a {
background: #fff;
}
.slideshow > div.dotnav-block li.active a {
width: 13px;
height: 13px;
}
.slideshow > div.dotnav-block ul {
align-items: center;
}
.slideshow .slideshow-items > li {
display: none;
}
.slideshow .slideshow-items > li.active {
display: block;
position: relative;
}
.slideshow .slideshow-items > li img {
height: 100%;
width: auto;
object-fit: cover;
}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-rc.14/css/uikit.min.css"/>
<body>
<div id="app">
<section>
<div class="uk-child-width-1-2" uk-grid>
<div>
<h1>{{ message }}</h1>
</div>
<div>
<div id="slideshow">
<div class="slideshow">
<ul class="slideshow-items">
<li v-for="(item,index) in items" :class="{'active':index===selectedIndex}"
v-on:click="select(index)">
<div class="information-slide">
<transition name="slide">
<div class="img-block" v-show="isOpenSlide">
<img v-bind:src="item.url_img" alt="">
</div>
</transition>
<div class="uk-container">
<div class="title title-1">{{item.title}}</div>
<div class="info-block">
<div class="info" v-for="(iblock,ind) in item.info_block">
<div class="incremental">
<span>+</span>
<!--<app-count-up-->
<!--:startVal="startVal"-->
<!--:endVal="iblock.incremental"-->
<!--:decimals="decimals"-->
<!--:duration="duration"-->
<!--:options="options"-->
<!--:callback="onReady"></app-count-up>-->
<span>%</span>
</div>
<div class="description descr-1">{{iblock.description}}</div>
</div>
</div>
</div>
</div>
</li>
</ul>
<div class="dotnav-block uk-position-bottom-center uk-position-small">
<ul class="uk-dotnav uk-dotnav-vertical">
<li :class="{'active':index===selectedIndex}" v-for="(item,index) in items"
v-on:click="index_dotnav(index)">
Item {{index}}</li>
</ul>
</div>
</div>
</div>
</template>
</div>
</div>
</section>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.17/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-rc.14/js/uikit.min.js"></script>

How can I bind the animation duration in Vue.js?

What I am trying to build (and not use an existing solution) is an indeterminate loader with the following template:
<template>
<div class="slider">
<div class="line" :style="getLineStyle"></div>
<div class="subline inc"></div>
<div class="subline dec"></div>
</div>
</template>
I then use a getter to add the styles for the div with the line class (which works fine).
#Prop({ default: "hsl(0, 0%, 90%)" }) private backColor!: string;
...
public get getLineStyle(): any {
return {
"background-color": "black",
position: "absolute",
opacity: "0.4",
background: this.backColor,
width: "150%",
height: "100%"
};
}
I also have the following CSS:
<style lang="scss" scoped>
.slider {
position: relative;
height: 2px;
overflow-x: hidden;
}
.subline {
position: absolute;
background: #4a8df8;
height: 100%;
}
.inc {
animation: increase 2s infinite;
}
.dec {
animation: decrease 2s 0.5s infinite;
}
#keyframes increase {
from {
left: -5%;
width: 5%;
}
to {
left: 130%;
width: 100%;
}
}
#keyframes decrease {
from {
left: -80%;
width: 80%;
}
to {
left: 110%;
width: 10%;
}
}
</style>
What I want to do is turn the .inc and .dec classes to property getters as well so that I can bind the animation duration (currently set to 2s) to a property.
I initially tried modifying the template to:
<template>
<div class="slider">
<div class="line" :style="getLineStyle"></div>
<div class="subline inc" :style="getAnimateIncreaseStyle"></div>
<div class="subline dec" :style="getAnimateDecreaseStyle"></div>
</div>
</template>
With the following getters:
public get getAnimateIncreaseStyle() {
return {
animation: "increase 2s infinite"
};
}
public get getAnimateDecreaseStyle() {
return {
animation: "decrease 2s 0.5s infinite"
};
}
Only to realise that animations cannot work when added inline.
I cannot think of any other way of doing this. Any ideas?
This is how i bind the animation duration on my progress bar in vue 3, as the previous response. It's necessary removes the scope on style
<template>
...
<div v-if="duration > 0" class="w-full bg-gray-200 -mb-1">
<div
:class="`progress-bar h-0.5 progress-bar-${themeColor}`"
:style="animation"
></div>
</div>
...
</template>
<script lang="ts">
...
props: {
duration: {
type: Number,
default: 10,
},
themeColor: {
type: String,
required: false,
default: "blue",
},
},
computed: {
animation(): string {
return `animation: ${this.duration}s linear theme-color, ${this.duration}s ease-out enter forwards`;
},
},
...
<script>
<style lang="scss">
.progress-bar {
#apply bg-gray-300;
transform-origin: right;
&.progress-bar-blue {
#apply bg-gradient-to-r from-blue-600 to-blue-300;
}
&.progress-bar-green {
#apply bg-gradient-to-r from-green-600 to-green-300;
}
&.progress-bar-yellow {
#apply bg-gradient-to-r from-yellow-600 to-yellow-300;
}
&.progress-bar-red {
#apply bg-gradient-to-r from-red-500 to-red-300;
}
}
#keyframes theme-color {
100% {
background-position: 0%;
}
0% {
background-position: 100%;
}
}
#keyframes enter {
100% {
transform: scaleX(0);
}
0% {
transform: scaleX(1);
}
}
</style>

Vue Accordion with transition

I'm trying to integrate the Accordion component with a body transition, but without success :( . All is working as well except the animation.
template:
<div class="accordion">
<div class="accordion-title" #click="isOpen = !isOpen" :class="{'is-open': isOpen}">
<span>{{title}}</span>
<i class="ic ic-next"></i>
</div>
<div class="accordion-body" :class="{'is-open': isOpen}">
<div class="card">
<slot name="body"></slot>
</div>
</div>
</div>
component:
props: {
title: {
type: String,
default: 'Title'
}
},
data() {
return {
isOpen: false
}
}
And styles:
.accordion-body {
font-size: 1.3rem;
padding: 0 16px;
transition: .3s cubic-bezier(.25,.8,.5,1);
&:not(.is-open) {
display: none;
height: 0;
overflow: hidden;
}
&.is-open {
height: auto;
// display: block;
padding: 16px;
}
}
.card {
height: auto;
}
I tried to use <transition> but it doesn't work with height or display properties.
Help please!
display:none will remove your content and avoid the animation, you should trick with opacity, overflow:hidden and height, but you ll be forced to do a method for that.
For example (not tested, but inspiring):
in template:
<div class="accordion" #click="switchAccordion" :class="{'is-open': isOpen}">
<div class="accordion-title">
<span>{{title}}</span>
<i class="ic ic-next"></i>
</div>
<div class="accordion-body">
<p></p>
</div>
</div>
in component (add a method):
methods: {
switchAccordion: function (event) {
let el = event.target
this.isOpen = !this.isOpen // switch data isOpen
if(this.isOpen) {
let childEl1 = el.childNodes[1]
el.style.height = childEl1.style.height
} else {
let childEl2 = el.childNodes[2]
el.style.height = childE2.style.height // or .clientHeight + "px"
}
}
}
in style:
.accordion {
transition: all .3s cubic-bezier(.25,.8,.5,1);
}
.accordion-body {
font-size: 1.3rem;
padding: 0 16px;
opacity:0
}
.is-open .accordion-body {
opacity:0
}
In this case, your transition should work as you want.
The javascript will change the height value and transition transition: all .3s cubic-bezier(.25,.8,.5,1); will do the animation