How do I smoothly reduce a divs height to 0 in VueJS? - vue.js

I've tried and failed for hours now. The way I've done it is in onMounted() I first div = document.GetElementByID. Then, div.style.height = '0';. However, since this is being run from inside onMounted(), it doesn't animate or transition it, even though I have all the necessary Tailwind classes for it to do so (note that it successfully resizes the div to 0 height, but there's no animation or transition). I even tried to put the div.style.height = '0' into a seperate function outside onMounted() which didn't work. The only thing that animates or transitions is opacity. And only that. Why? Why not height or anything else? What do I have to do to make it work?
<template>
<div v-if="props.state == 2" class="w-full bg-[#E7685D] text-black text-2xl overflow-hidden duration-500 ease-in-out" id="alert">
<p class="text-center p-2">text</p>
</div>
</template>
<script>
import { onMounted } from '#vue/runtime-core'
onMounted(() => {
const alert = document.getElementById('alert');
setTimeout(() => { alert.style.height = '0%', 5000 })
})
const props = defineProps({
state: String
})
</script>

Maybe that's not animating because of your wrong syntax for setTimeout function. setTimeout(() => { alert.style.height = '0%', 5000 }). You've to define milliseconds out of the function brackets {...} in case you want a 5 seconds delay before execution of the setTimeout function. Given below is the right syntax for the setTimeout function.
Syntax: setTimeout(function, milliseconds, param1, param2, ...)
setTimeout(() => {
alert.style.height = '0%'
}, 5000)
Edited:
I'm not sure! Maybe adding the transition/animation classes in the setTimeout will do the trick.
setTimeout(() => {
alert.classList.add("duration-500", "ease-in-out")
alert.style.height = '0%'
}, 5000)
If it didn't work, try moving lines up/down in the setTimeout function.

Related

How to wait for click button to continue function

I have a function with a popup dialog window with button. I want to wait with executing the rest of the function until the button is clicked. I tried it with Promise and AddEventListener, but cannot find out why is it not working. Could someone help me? (I use Vue3 and Quasar)
I have an error for "const confirm" - Object is possibly 'null'.ts(2531)
Thank you for any advices.
Here is a part of my template:
<q-dialog persistent v-model="phoneDialogBank">
<q-card>
<q-card-section>
<div class="items" v-for="formField in dynamicDialogFieldsVisible" :key="formField?.Key">
<dynamic-form-field :form-field="formField"></dynamic-form-field>
</div>
<div class="row items-center justify-center">
<q-btn color="primary" class="confirm-phone-dialog" v-close-popup>
{{ t('signing-table.phoneDialogBank.ok') }}
</q-btn>
</div>
</q-card-section>
</q-card>
</q-dialog>
Here is my function:
async function dynamicDialogBeforeSubmit() {
const params = getParamsFromAdvisorDeviceBoolean();
if (params && params.hasOwnProperty('dialogBeforeSubmit') && params.dialogBeforeSubmit) {
phoneDialogBank.value = true;
const confirm = document.querySelector('.confirm-phone-dialog');
const waitForButtonClick = new Promise((resolve) => { confirm.addEventListener('click', resolve); });
await waitForButtonClick.then(() => {
dynamicDialogSave();
});
return;
}
dynamicDialogSave();
}
That error is because your button is within a dialog that renders conditionally. So if there is no dialog, that DOM element does not exist. I'm curious why you are not using some of the great features of Vue? Like putting the click handler on the element and using refs to target the DOM instead of using query selectors and event listeners.
<q-btn color="primary" class="confirm-phone-dialog" #click.prevent="dynamicDialogBeforeSubmit" v-close-popup>
{{ t('signing-table.phoneDialogBank.ok') }}
</q-btn>
Your function is a bit mixed up between async and Promises. async / await is just syntactic sugar for a Promise. The way you have it written now, you are wrapping a Promise within another Promise.
async function dynamicDialogBeforeSubmit() {
const params = getParamsFromAdvisorDeviceBoolean();
if (params && params.hasOwnProperty('dialogBeforeSubmit') && params.dialogBeforeSubmit) {
phoneDialogBank.value = true;
await dynamicDialogSave();
} else {
console.log('There was a problem')
}
}
I should note that you will likely need to pass an object to dynamicDialogSave(someObject) for it to actually save something. I assume this, not knowing what that function actually looks like.
Event listeners are not asynchronous and you wouldn't want to wrap them in a promise anyways. If you wish to write it as you have, it would be best to declare the click listener as a separate function that fires onMounted, then call it from any async function. You should also detach native event listeners onUnmounted to avoid memory leaks with Vue.

Creating a countdown in v-for for each result

I have a loop, in which I get several result from my API call.
Each of them has an property called processing_time that shows how many seconds it has left until something happens.
How can I implement the countdown for each interval?
Here is my <progressbar> component which needs a value for initially displaying the progress (I need it to be dynamic)
<progress-bar :options="options"
:value="data.properties.processing_time"
/>
<span class="progress-bar-seconds"
>{{ data.properties.processing_time }} Seconds left
...</span>
I feel like I need to use computed but I don't exactly know how to do it.
A computed prop is probably not the best solution, since you need to change the timer value locally. I recommend using a local copy that is updated periodically:
Create a local data property (a ref).
Copy the value prop to the local data prop upon mounting.
Use setInterval to periodically decrement the value.
<template>
<div>
<span class="progress-bar-seconds">{{ seconds }} Seconds left ...</span>
</div>
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue'
export default {
props: {
value: {
type: Number,
default: 0,
required: true,
},
},
setup(props) {
const seconds = ref(0) // 1️⃣
let _timerId = null
onMounted(() => {
seconds.value = props.value // 2️⃣
if (seconds.value > 0) {
// 3️⃣
_timerId = setInterval(() => {
seconds.value--
if (seconds.value <= 0) {
clearInterval(_timerId)
}
}, 1000)
}
})
onUnmounted(() => clearInterval(_timerId))
return { seconds }
}
}
</script>
demo

Transition using this.router.push with tracking end of animation in VUE CLI

I really hope for your help! I'm a beginner, this is my first experience in creating web applications.
I work with Vue Cli, there is a lottie element that should be animated by click (I did it), then I should go to the “other page” (I did it) But, how do I implement the transition to the page only after the animation finishes? Help! You are welcome! For animation I use Anime.js
<script>
import { translate } from '../js/animate'
export default {
methods: {
go () {
translate(this.$refs.square)
this.$router.push('Comprestore')
}
}
}
</script>
/vue
<template>
<div id="animate" v-on:click = "go" ref="square">
<app-lottie></app-lottie>
</div>
</template>
<style scoped>
</style>
import anime from 'animejs'
export function translate (element) {
anime({
targets: element,
translateX: 500
})
}
You can use complete callback to wait until the animation is completed.
Now your code may looks like this:
...
go () {
translate(this.$refs.square, () => {
this.$router.push('Comprestore')
})
}
...
And
export function translate (element, callback) {
anime({
targets: element,
translateX: 500,
complete: callback
})
}
I create the example here.
In the example I also use page transition by using Vue built-in transition to transition between page. See Enter/Leave & List Transitions and Transitions in Vue Router.

Delay the rendering of a component in vue

I've created a component in vue which wraps a vue-apexchart donut graph. As soon as the page loads and this component is loaded, the vue-apexchart animates and displays a small graph.
Now I would like to instantiate multiple of these components from a dataset side by side. Instead of the components to all load an animate at the same time, I would like a small rendering delay to give it an overall nice effect. Something like this would be nice:
<donut :items="series1"></donut>
<donut :items="series2" delay=1500></donut>
The vue-apexchart doesent support initialization delays, and as far as I can see there isn't any vue-specific official solution to delay the rendering of components.
I've tried to put a setTimeout in any of the component hooks to stall the initialization,
I´ve also tried to inject the all the graph DOM in the template element on a v-html tag in a setTimeout, but apexchart doesent notice this new dom content, and vue doesent notice the html bindings either.
I´ve created this fiddle which loads two instances of a graph:
https://jsfiddle.net/4f2zkq5c/7/
Any creative suggestions?
There are several ways you can do this, and it depends on whether you can actually modify the <animated-component> logic yourself:
1. Use VueJS's built-in <transition-group> to handle list rendering
VueJS comes with a very handy support for transitions that you can use to sequentially show your <animated-component>. You will need to use a custom animation library (like VelocityJS) and simply store the delay in the element's dataset, e.g. v-bind:data-delay="500". VueJS docs has a very good example on how to introduce staggered transitions for <transition-group>, and the example below is largely adapted from it.
You then use the beforeAppear and appear hooks to set the opacity of the individual children of the <transition-group>.
Vue.component('animated-component', {
template: '#animatedComponentTemplate',
props: {
data: {
required: true
}
}
});
new Vue({
el: '#app',
data: {
dataset: {
first: 'Hello world',
second: 'Foo bar',
third: 'Lorem ipsum'
}
},
methods: {
beforeAppear: function(el) {
el.style.opacity = 0;
},
appear: function(el, done) {
var delay = +el.dataset.delay;
setTimeout(function() {
Velocity(
el, {
opacity: 1
}, {
complete: done
}
)
}, delay)
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<transition-group name="fade" v-on:before-appear="beforeAppear" v-on:appear="appear">
<animated-component v-bind:data="dataset.first" v-bind:key="0"> </animated-component>
<animated-component v-bind:data="dataset.second" v-bind:key="1" v-bind:data-delay="500"> </animated-component>
<animated-component v-bind:data="dataset.third" v-bind:key="2" v-bind:data-delay="1000"> </animated-component>
</transition-group>
</div>
<script type="text/x-template" id="animatedComponentTemplate">
<div>
<h1>Animated Component</h1>
{{ data }}
</div>
</script>
2. Let <animated-component> handle its own rendering
In this example, you simply pass the a number to the delay property (remember to use v-bind:delay="<number>" so that you pass a number and not a string). Then, in the <animated-component>'s mounted lifecycle hook, you use a timer to toggle the visibility of the component itself.
The technique on how you want to show the initially hidden component is up to you, but here I simply apply an initial opacity of 0 and then transition it after a setTimeout.
Vue.component('animated-component', {
template: '#animatedComponentTemplate',
props: {
data: {
required: true
},
delay: {
type: Number,
default: 0
}
},
data: function() {
return {
isVisible: false
};
},
computed: {
styleObject: function() {
return {
opacity: this.isVisible ? 1 : 0
};
}
},
mounted: function() {
var that = this;
window.setTimeout(function() {
that.isVisible = true;
}, that.delay);
}
});
new Vue({
el: '#app',
data: {
dataset: {
first: 'Hello world',
second: 'Foo bar',
third: 'Lorem ipsum'
}
}
});
.animated-component {
transition: opacity 0.25s ease-in-out;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<animated-component v-bind:data="dataset.first"> </animated-component>
<animated-component v-bind:data="dataset.second" v-bind:delay="500"> </animated-component>
<animated-component v-bind:data="dataset.third" v-bind:delay="1000"> </animated-component>
</div>
<script type="text/x-template" id="animatedComponentTemplate">
<div class="animated-component" v-bind:style="styleObject">
<h1>Animated Component, delay: {{ delay }}</h1>
{{ data }}
</div>
</script>
If you have the possibility to reformat your data, you can build an array of series objects, add a show: true/false property and iterate it:
//template
<div v-for="serie in series">
<donut :items="serie.data" v-if="serie.show"></donut>
</div>
//script
data: function() {
return {
series: [
{ data: [44, 55, 41, 17, 15], show: false },
{ data: [10, 20, 30], show: false },
]
}
}
Now you can create a setTimeout function which will change the serie.show to true by incrementing the delay based on the serie index.
Then add the function on the mounted hook:
methods: {
delayedShow (serie, idx) {
let delay = 1500 * idx
setTimeout(() => {
serie.show = true
}, delay)
}
},
mounted () {
this.series.forEach((serie, idx) => {
this.delayedShow(serie, idx)
})
}
Live example
Faced the same problem with ApexCharts Pie Charts being redrawn rapidly in sequence due to data being pulled from a pinia store mutating too quickly for the chart to keep up, leading to ugly errors in the console.
I resolved the issue by using a boolean ref in a v-if="showChart" on the component and then using a setTimeout to trigger a delayed drawing of the chart:
import { ref } from "vue";
import useStore from "#/store/myChartStore";
const store = useStore();
const showChart = ref(false);
store.$subscribe((mutation, state) =>{
showChart.value = false;
setTimeout(()=> {
showChart.value = true;
}
, 100);
});
If you're not using a store, you may find another way to watch the initial availability of the chart data and then delay the rendering using that same approach.

React Motion: simple chaining

I'm using react, mobx and I need to animate things.
I want to do a simple animation chaining: when the first animation ends, the second starts.
As use case think of a section templates: every time the user change the section, the current section fade out and then the next section fade in.
Any hints?
Regardless react-motion or any other library, you can always use onAnimationEnd animation event listener .
With this method you are able to detect when css animation ends on related html element and execute method function.
Then you can choose what to do , for example using setState():
animation1 = () => {
this.setState({
animation1: true
})
}
animation2 = () => {
this.setState({
animation2: true
})
}
animation3 = () => {
this.setState({
animation3: true
})
}
render() {
return (
<div>
<div onAnimationEnd={this.animation1}>
First animation
</div>
<div onAnimationEnd={this.animation2}>
Second animation
</div>
<div onAnimationEnd={this.animation3}>
Third animation
</div>
</div>
)
}