Unwanted animation bug in Vue.js - 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>

Related

Why does transition-group animate opacity but not animate height?

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.

How to animate todo moving from one list to another with Vue.js?

I am trying to do this svelte example todo moving animation with Vue.js.
Below you can find what I have done so far. Just click on the todo to see.
new Vue({
el: "#app",
data: {
items: [
{ id: 1, name: 'John', done: false },
{ id: 2, name: 'Jane', done: false },
{ id: 3, name: 'Jade', done: true },
{ id: 4, name: 'George', done: true },
]
},
computed: {
done () {
return this.items.filter(i => i.done)
},
undone () {
return this.items.filter(i => !i.done)
}
},
methods: {
toggle: function(todo){
todo.done = !todo.done
}
}
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
height: 500px;
transition: all 0.2s;
}
.todos {
display: grid;
grid-template-columns: 1fr 1fr;
}
.todo {
border: 1px solid #ccc;
}
.todo.undone {
grid-column: 2 /span 1;
}
.todo.done {
grid-column: 1 /span 1;
background: blue;
color: white;
}
.flip-list-move {
transition: all 1s ease-in-out;
}
.header-wrapper {
display: grid;
grid-auto-flow: column;
}
.header, .todo {
display: grid;
grid-template-columns: repeat(3, 1fr);
padding: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div class="header-wrapper">
<div class="header">
<span>Name</span>
<span>Age</span>
<span>Gender</span>
</div>
<div class="header">
<span>Name</span>
<span>Age</span>
<span>Gender</span>
</div>
</div>
<transition-group name="flip-list" tag="div" class="todos">
<div class="todo done" v-for="item of done" :key="item.id" #click="toggle(item)">
<span>{{item.name}}</span>
<span>26</span>
<span>Male</span>
</div>
<div class="todo undone" v-for="item of undone" :key="item.id" #click="toggle(item)">
<span>{{item.name}}</span>
<span>20</span>
<span>Male</span>
</div>
</transition-group>
</div>
In order to animate the todo move from one list to another, I used CSS grid but I can't find a way to distinguish todos (left and right) without having a grid cell which is empty.
I would appreciate if there is a better way to achieve the example in svelte docs or a way to omit the empty cells.
Even though it seemed easy in the beginning, it's a bit tricky.
You can target the first element by tracking the index in the v-for loop. Index 0 is always going to be the first element. And give it the following style:
grid-row-start: 1;
EDIT DEMO:
new Vue({
el: "#app",
data: {
items: [
{ id: 1, name: 'John', done: false },
{ id: 2, name: 'Jane', done: false },
{ id: 3, name: 'Jade', done: true },
{ id: 4, name: 'George', done: true },
]
},
computed: {
done () {
return this.items.filter(i => i.done)
},
undone () {
return this.items.filter(i => !i.done)
}
},
methods: {
toggle: function(todo){
todo.done = !todo.done
}
}
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
height: 500px;
transition: all 0.2s;
}
.todos {
display: grid;
grid-template-columns: 1fr 1fr;
}
.todo {
border: 1px solid #ccc;
}
.todo.undone {
grid-column: 2 /span 1;
}
.todo.done {
grid-column: 1 /span 1;
background: blue;
color: white;
}
.first-right {
grid-row-start: 1;
}
.flip-list-move {
transition: all 1s ease-in-out;
}
.header-wrapper {
display: grid;
grid-auto-flow: column;
}
.header, .todo {
display: grid;
grid-template-columns: repeat(3, 1fr);
padding: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div class="header-wrapper">
<div class="header">
<span>Name</span>
<span>Age</span>
<span>Gender</span>
</div>
<div class="header">
<span>Name</span>
<span>Age</span>
<span>Gender</span>
</div>
</div>
<transition-group name="flip-list" tag="div" class="todos">
<div class="todo done" v-for="item of done" :key="item.id" #click="toggle(item)">
<span>{{item.name}}</span>
<span>26</span>
<span>Male</span>
</div>
<div :class="['todo', 'undone', { 'first-right': index === 0 }]" v-for="(item, index) of undone" :key="item.id" #click="toggle(item)">
<span>{{item.name}}</span>
<span>20</span>
<span>Male</span>
</div>
</transition-group>
</div>
Adding grid-row-start to the first undone element doesn't works if there are more than 6 items in array.
As a solution, I used the index of v-for loop to add to every undone todo the corresponding grid-row-start.
index starts at 0 so we have to make index + 1
<div
class="todo undone"
v-for="(item, index) of undone"
:key="item.id"
:style="{'grid-row': index + 1}" // => HERE we guarantee no gaps are present in undone list`
#click="toggle(item)"
>
<span>{{item.name}}</span>
<span>20</span>
<span>Male</span>
</div>
You can find the working example on this codesandbox

#click on parent div not firing when pressing child button

I have made a custom number input box component and want it to activate on click. The component consists of 3 elements, two buttons (for decreasing and increasing the number value), and an input, where the number is displayed and can manually be changed. The problem is that the #click of the parent div (.numberField) only fires when clicking the input box, not when the buttons are clicked.
Because the input box seemed to be working I have tried changing the button elements to input[type=button] elements, but that failed.
I have checked when #click of the child elements (the two buttons and the input) fire, and confirmed that all of them behave in the same way (they don't fire on the initial click that sets :disabled="false" on each of them)
My Vue version is 3.7.0 if that matters
<template>
<div class="numberField" #click="fieldClicked">
<button #click="stepDown()" :disabled="disabled" class="left">
−
</button>
<input
v-model="value"
type="number"
:max="max"
:min="min"
:step="step"
:disabled="disabled">
<button #click="stepUp()" :disabled="disabled" class="right">
&plus;
</button>
</div>
</template>
<script>
export default {
name: 'NumberField',
props: {
defaultValue: Number,
max: Number,
min: Number,
step: Number,
disabled: Boolean,
},
data() {
return {
value: this.defaultValue,
};
},
watch: {
value() {
this.value = Math.min(this.max, Math.max(this.min, this.value));
},
},
methods: {
stepDown() {
this.value -= this.step;
},
stepUp() {
this.value += this.step;
},
fieldClicked(event) {
// #click.stop will stop all clicks from propagating; we will only stop clicks when the field is active
if (!this.disabled) event.stopPropagation()
},
},
};
</script>
<style scoped>
.numberField {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
height: 3em;
width: 8em;
}
.left {
border-top-left-radius: 0.2em;
border-bottom-left-radius: 0.2em;
}
.right {
border-top-right-radius: 0.2em;
border-bottom-right-radius: 0.2em;
}
input, button {
padding: 0;
border-radius: 0;
min-width: 0;
height: 100%;
width: 33%;
min-height: 0;
font-size: 1rem;
text-align: center;
}
</style>
This is a summary of how I'm using this component:
<div class="item" v-for="item in items" :key="item.id" #click="toggleItem(item.id)" :class="{ active: activeItems[item.id] }">
<h1>{{ item.name }}, some other elements irrelevant are here too</h1>
<NumberField
:defaultValue="item.amount"
:max="item.amount"
:min="1"
:step="1"
:disabled="!activeItems[item.id]"></NumberField>
</div>
toggleItem(id) toggles the boolean value of activeItems[item.id]. The NumberField is disabled when the item is inactive.
My expectation would be that clicking on any of the child elements of .numberField would fire the #click of .numberField, which (only if the item is inactive) then gets propagated to the #click of .item, but this only seems to be the case when clicking the input[type=number].
I would appreciate any help, I'm absolutely lost!
A <button> with a disabled attribute set will not fire click events. If the event doesn't fire on the <button> then it won't propagate to the <div> either.
In your case a simple workaround would be to put pointer-events: none on your buttons so that the button is skipped altogether. The <div> will just receive the click directly, as though the button wasn't even there.
const NumberField = {
name: 'NumberField',
template: `
<div class="numberField" #click="fieldClicked">
<button #click="stepDown()" :disabled="disabled" class="left">
−
</button>
<input
v-model="value"
type="number"
:max="max"
:min="min"
:step="step"
:disabled="disabled">
<button #click="stepUp()" :disabled="disabled" class="right">
&plus;
</button>
</div>
`,
props: {
defaultValue: Number,
max: Number,
min: Number,
step: Number,
disabled: Boolean,
},
data() {
return {
value: this.defaultValue,
};
},
watch: {
value() {
this.value = Math.min(this.max, Math.max(this.min, this.value));
},
},
methods: {
stepDown() {
this.value -= this.step;
},
stepUp() {
this.value += this.step;
},
fieldClicked(event) {
console.log('here')
// #click.stop will stop all clicks from propagating; we will only stop clicks when the field is active
if (!this.disabled) event.stopPropagation()
},
},
};
new Vue({
el: '#app',
components: {
NumberField
}
})
.numberField {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
height: 3em;
width: 8em;
}
.left {
border-top-left-radius: 0.2em;
border-bottom-left-radius: 0.2em;
}
.right {
border-top-right-radius: 0.2em;
border-bottom-right-radius: 0.2em;
}
input, button {
padding: 0;
border-radius: 0;
min-width: 0;
height: 100%;
width: 33%;
min-height: 0;
font-size: 1rem;
text-align: center;
}
button[disabled] {
pointer-events: none;
}
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<number-field :defaultValue="7" :step="1" :min="0" :max="10" disabled></number-field>
</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: ...

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