Velo by Wix: Promise Queue
In this post, we look at concurrent hide/show animation behavior in Velo APIs, when one of the animation effects can't start because a previous one isn't finished yet.
What is the issue?
Let's suppose that we have to animate an image by a mouse event. For example, we want to create a hover effect that combines mouse in/out events.
Our solution is quite simple. We have an image with two event listeners for onMouse{In/Out}
events and a vector image that can be shown or hidden.
It's the next code snippet:
As you can see above, the animation has a duration of 300 ms. What happens if we move the cursor in/out of the image faster than 300 ms?
Yes, there is an issue. The next animation in the queue doesn't run if the previous one is ongoing at the moment.
Why does it happen?
Let's visualize a timeline of the animation's execution. For example, we have three events, in -> out -> in
. The first animation starts at 0 and will finish at 300 ms. The second animation starts at 200 ms but will be skipped because this element is animating at this moment. The third one starts at 300 ms, which will successfully run because the first one has finished, the second one skipped, so the element can animate again.
How can we fix it?
We have to wait for the animation's end before running a new one. For this, we create a queue. We push each animation request to this queue, where animations will be called one by one.
Create a queue
Create a queue.js
file in the public
folder. In this file, we will implement the queue logic.
First, we need to implement a mechanism for adding actions to the queue.
public/queue.js
export const createQueue = () => {
// Action list
const actions = [];
return (action) => {
// Adds action to the end of the list
actions.push(action);
};
};
Turn your attention, we don't run animation when the mouse event is fired. Instead, we wrap it in a function and push it to the array.
Let's upgrade the code on the page to see how it works.
HOME Page (code)
import { createQueue } from 'public/queue.js';
$w.onReady(() => {
// Initializing queue
const queue = createQueue();
const fadeOptions = {
duration: 300,
};
$w('#imageParrot')
.onMouseIn(() => {
// Add actions to queue
queue(() => $w('#vectorHat').show('fade', fadeOptions));
})
.onMouseOut(() => {
// Add actions to queue
queue(() => $w('#vectorHat').hide('fade', fadeOptions));
});
});
Great, we have a list of actions. The next step is to run the queue.
It will be an auxiliary function to help start the queue.
public/queue.js
export const createQueue = () => {
const actions = [];
const runQueue = () => {
// Check: are we have any actions in queue
if (actions.length > 0) {
// Removes the first action from the queue
// and returns that removed action
const action = actions.shift();
// Waits the promise
action().then(() => {
// When the Promise resolves
// then it runs the queue to the next action
runQueue();
});
}
};
return (action) => {
actions.push(action);
// Runs the queue when adding a new action
runQueue();
};
};
The runQueue()
function is recursive, running itself after the promise has been resolved. Additionally, we trigger runQueue()
by adding a new action. To ensure it runs only once at the start of the queue, we must limit the trigger.
Furthermore, we have added a flag to close the runQueue()
if the queue is active.
public/queue.js
export const createQueue = () => {
// Flag
let isActive = false;
const actions = [];
const runQueue = () => {
// Check: if the queue is running
if (isActive) {
// Stop this call
return;
}
if (actions.length > 0) {
const action = actions.shift();
// Before: closes the queue
isActive = true;
action().then(() => {
// After: opens the queue
isActive = false;
runQueue();
});
}
};
return (action) => {
actions.push(action);
runQueue();
};
};
When a new action is added to the list, we check if the queue is active. If the queue is not active, we run it. If the queue is active, we do nothing.
Queue length
The last thing we need is a control of the queue length. We can create a lot of animation actions that could lead to a blinking effect.
The algorithm is simple. If the queue has a maximum length, then we remove the last action before adding a new one.
Let's set a maximum length by default as one. I think it covers 99% of use cases.
public/queue.js
// By default, the queue has one action
export const createQueue = (maxLength = 1) => {
let isActive = false;
const actions = [];
const runQueue = () => {…};
return (action) => {
// Check: if the queue has max length
if (actions.length >= maxLength) {
// Removes the last action from the queue
// before adding a new one
actions.pop();
}
actions.push(action);
runQueue();
};
};
That's it! I hope it could be helpful to your projects. Thanks for reading.
Code Snippets
Here is the complete code snippet, including JSDoc types.
public/queue.js
/**
* Create a promise queue
*
* @typedef {() => Promise<unknown>} Action
*
* @param {number} [maxLength] - max count actions in the queue
* @returns {(action: Action) => void}
*/
export const createQueue = (maxLength = 1) => {
/** @type {boolean} */
let isActive = false;
/** @type {Action[]} */
const actions = [];
const runQueue = () => {
if (isActive) {
return;
}
if (actions.length > 0) {
const action = actions.shift();
isActive = true;
action().then(() => {
isActive = false;
runQueue();
});
}
};
return (action) => {
if (actions.length >= maxLength) {
actions.pop();
}
actions.push(action);
runQueue();
};
};
Example of using:
HOME Page (code)
import { createQueue } from 'public/queue.js';
$w.onReady(() => {
const queue = createQueue();
const fadeOptions = {
duration: 300,
};
$w('#imageParrot')
.onMouseIn(() => {
queue(() => $w('#vectorHat').show('fade', fadeOptions));
})
.onMouseOut(() => {
queue(() => $w('#vectorHat').hide('fade', fadeOptions));
});
});