My class:
var indexedDBInitInterval; // clearInterval Doesnt work when done from within class for some reason..
var coumter = 1;
class IndexedDBWrapper {
constructor() {
this._db = undefined
this._dbInitInterval = undefined
this.dbInitRequest = indexedDB.open("someDB", 1)
this.dbInitRequest.onerror = (event) => { this.dbInitRequestOnError(event) }
this.dbInitRequest.onsuccess = (event) => { this.dbInitRequestOnSuccess(event) }
this.dbInitRequest.onupgradeneeded = (event) => { this.dbInitRequestOnUpgradedeNeeded(event) }
}
isDBInitalized() {
return new Promise((resolve) => {
indexedDBInitInterval = setInterval(() => {
log(this.dbInitRequest)
log(this.dbInitRequest.readyState)
log(indexedDBInitInterval)
coumter = coumter + 1
if (this.dbInitRequest.readyState == "done") {
log("mutants")
log(coumter)
log(clearInterval(indexedDBInitInterval))
resolve()
}
}, 300)
})
}
dbInitRequestOnError(event) {
log("Error initializing IndexedDB: ")
log(event)
}
And calling with:
indexedDBWrapper.isDBInitalized().then(() => {
Neither clearInterval or resolve gets fired, even tho log("mutants") gets fired.
What a puzzle..
You'd want to make indexedDBInitInterval a variable within isDBInitialized. Otherwise if you call the function multiple (times or even on multiple objects), they would interfere with each other. The same can be said for coumter, although that might just be a debug variable.
Does indexedDBWrapper.isDBInitalized().then(() => console.log('OK')) print OK? I can understan the clearInterval failing if the wrong indexedDBInitInterval is used, but mutants being logged should indicate that resolve() also gets called, unless an error happens in between.
Related
Part of a small project I'm working on is the user being able to add tags to items, similarly to StackOveflow. These tags are stored in a database, so obviously I need to call an API to fetch matching tags. To prevent my API from being hit too often, I want to add a debounce method, however none of the examples I've found seem to work. Even lodash's debounce method doesn't work.
I'm currently trying this debounce method in Vue3;
const debounce = (fn: Function, delay: number): Function => {
let timeout: NodeJS.Timeout;
return (...args: any): void => {
clearTimeout(timeout);
timeout = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
};
onMounted(() => {
debounce(() => console.log('hello'), 500);
});
The debounce method itself is called just fine, but the provided callback isn't. Same goes for lodash's debounce method, the method itself is called but whatever method I pass isn't.
Am I missing something obvious?
Edit: I was indeed missing something obvious. The actual use case was this method (no API call yet, wanted to get the debounce method working first);
const handleTagKeyUp = (event: KeyboardEvent): void => {
filteredTags.value = [];
const value = inputTags.value.trim();
if (event.code === 'Enter') {
addTag(value);
return;
}
if (value.length < 3) {
return;
}
const selectedTagNames = selectedTags.value.map((t: Tag) => t.name.toLowerCase());
filteredTags.value = props.tags.filter((t: Tag) => t.name.toLowerCase().includes(value) && ! selectedTagNames.includes(t.name.toLowerCase()));
};
which is called whenever the keyup event is fired. Simply changing it to
const handleTagKeyUp = (event: KeyboardEvent): void => {
filteredTags.value = [];
const value = inputTags.value.trim();
if (event.code === 'Enter') {
addTag(value);
return;
}
if (value.length < 3) {
return;
}
findTags(value);
};
const findTags = debounce((value: string) => {
const selectedTagNames = selectedTags.value.map((t: Tag) => t.name.toLowerCase());
filteredTags.value = props.tags.filter((t: Tag) => t.name.toLowerCase().includes(value) && ! selectedTagNames.includes(t.name.toLowerCase()));
}, 500);
fixed the issue.
This isn't how debounce is used. Think about it: how would debounce respond to multiple calls to onMounted if a new debounce is created every time onMounted is called?
debounce returns a function that must be called, and calls to that returned function are denounced:
// In some scope. I don't know Vue.js
const debounced = _.debounce(() => console.log('hello'), 500);
onMounted(() => {
debounced()
});
Here is a very simplified reproduction of the issue I am facing:
window.onload=function(){
touchme = document.getElementById('click');
function mains(){
touchme.innerHTML = "clicked " + Math.random().toString();
}
function process() {
touchme.dataset.clicked = "false";
mains();
touchme.addEventListener("click", () => {
touchme.dataset.clicked = "true";
}, {once : true});
let clickPromise = new Promise((resolve, reject) => {
var timer = setTimeout(() => {
if(touchme.dataset.clicked == "true"){
resolve();
}
else{
reject();
}
}, 1500);
});
clickPromise.then(() => {
process();
});
clickPromise.catch(() => {
alert("game over");
});
}
process();
}
This bit of HTML has been used <body><button id="click">Click</button></body>
What I basically want is:
if I click the button, mains() will run immediately
if I don't, setTimeout() will wait 1.5secs for me. Within this time if I click, setTimeout() resets and mains() gets executed
if I still don't click, alert("game over") is executed. The "self callback loop" breaks
However, the mains() function isn't being run immediately whenever I am clicking, instead it is waiting for the timeout, causing a delay of 1.5s after every click.
Naturally, the solution is to use clearTimeout(), however, I can't seem to wrap my head around where to use it. Adding it inside the function argument of event listener causes the timeout to run independently of the click event and just makes it reject the promise 1.5s later, notwithstanding my button clicks. I also tried calling the function itself inside the event listener function, which doesn't work. Adding it inside an if(touchme.dataset.clicked == "true") outside the setTimeout() and inside the promise wouldn't work, as my initial value is false, so it just checks the initial state.
You really don't need to use promises for this, just a simple handler function will make it a lot easier to clear and reset the timeout:
let lost = false;
function timeoutHandler() {
alert("game over");
lost = true;
}
let timeout = setTimeout(timeoutHandler, 1500);
document.getElementById('click').addEventListener('click', () => {
if (lost) return;
clearTimeout(timeout);
timeout = setTimeout(timeoutHandler, 1500);
});
<button id="click">Click</button>
This makes a good use case for Promise.race:
async function main() {
while (await game()) {}
}
async function game() {
let clickPromise = new Promise(res =>
document.querySelector('button').onclick = () => res(true))
let timerPromise = new Promise(res =>
setTimeout(() => res(false), 1500))
let ok = await Promise.race([clickPromise, timerPromise])
document.querySelector('button').textContent = ok
? 'keep clicking ' + Math.random()
: 'game over'
return ok
}
window.onload = main
<button>click</button>
I wanted to directly call a function (like interrupt handler) when a certain condition is met. I didn't want to using "polling" for that as it increases time complexity.
count = 1
p = new Promise((resolve, reject)=>{
if(count == 2){
resolve("hello")
}
});
p.then((msg)=>{
console.log(msg)
})
console.log("1 now");
count = 2;
I expected console.log(msg) to run when count=2 but this is not the case. It turned out that the promise is still "pending". What is the reason this happens? And how do I implement my question.
You can use a Proxy to listen variable changes.
const count = new Proxy({
value: 0
}, {
set(target, prop, val) {
// set value
target[prop] = val;
console.log(`count is ${val}`);
// stop condition
if (val == 2) {
console.log(`count is 2(trigger stop condition)`);
}
}
});
// wait 2 seconds and change count.value to 1
setTimeout(() => count.value = 1, 2000);
// wait 2 seconds and change count.value to 2
// it triggers the stop condition
setTimeout(() => count.value = 2, 2000);
console.log("Waiting for changes ...");
reference: Listen to js variable change
Proxy is one of the solutions for this. But I post another approach for your case.
You can define a custom class or object, and work with that class. Also you register your custom listener for it, and do whatever.
This is a sample of my code. Maybe it will give you some ideas for your solution.
class MyObject {
constructor(value, func) {
this._value = value;
this._listener = func;
}
get value() {
return this._value;
}
set value(newValue) {
this._listener(newValue);
this._value = newValue;
}
}
function customListener(changedValue) {
console.log(`New Value Detected: ${changedValue}`);
}
const count = new MyObject(1, customListener);
count.value = 2;
The issue you're having is that the code inside the Promise resolves synchronously. It seems like you are assuming Promises are by default async, but that is a common async misconception. So, the code
if(count == 2){
resolve("hello")
}
resolves synchronously (that is, right after you declare count to be 1) so the Promise will never be resolved. If you want to asynchronously check for a condition without using libraries, you can use setInterval:
function checkForCondition(count, time){
return new Promise(resolve => {
const interval = setInterval(() => {
if (count == 2){
resolve("The count is 2!");
}
} , time);
});
}
If you call this function, the callback inside setInterval will be placed on the event loop every x ms, where x is equal to the time parameter.
Let's say there is a code in place 2
var place2IsReady = true;
In place 1 I need to implement the logic below :
Once place2IsReady value was changed (to true) then display alert('ready!');
Notes:
place2IsReady variable is not available in the scope of place 1.
the code from place 1 gets executed before place 2 gets executed (or there is a race condition).
Solution 1
I believe I can use window.place2IsReady instead and use setTimeout/setInterval in place 1 until I get window.place2IsReady === true.
Any better options? Using Listeners? On the variable change?
P.S. I need to track only first possible change of place2IsReady.
Is there a better way? Thank you.
You can create a listener for the variable change using setTimeout, something like:
let place2IsReady = false;
setReadyListener();
// testing wait 2 seconds to set place2IsReady to true
// so: an alert should occur after 2 seconds
setTimeout(() => place2IsReady = true, 2000);
function setReadyListener() {
const readyListener = () => {
if (place2IsReady) {
return alert("Ready!");
}
return setTimeout(readyListener, 250);
};
readyListener();
}
A more generic listener 'factory' could be:
let place2IsReady = false;
let fromObj = {
place2IsReady: "busy",
done() { this.place2IsReady = "done"; },
};
const listen = changeListenerFactory();
listen(
() => place2IsReady,
() => console.log("place2IsReady") );
listen(
() => fromObj.place2IsReady === "done",
() => console.log("formObj.place2IsReady done!") );
console.log("Listening...");
// test change variables with listeners
setTimeout(() => place2IsReady = true, 1000);
setTimeout(() => fromObj.done(), 3000);
function changeListenerFactory() {
const readyListener = (condition, callback, delay) => {
if (!condition || typeof condition !== "function") { return true; }
if (condition()) {
return callback();
}
setTimeout(() => readyListener(condition, callback, delay), delay);
};
return (condition, callback = () => {}, delay = 250) =>
readyListener(condition, callback, delay);
}
Or maybe using a Proxy (with a set trap) works for you
const readyState = new Proxy({ ready: false }, {
set (target, prop, val) {
console.log(`readyState.ready changed from ${target[prop]} to ${val}`);
target[prop] = val;
}
});
console.log("Waiting for changes ...");
setTimeout(() => readyState.ready = true, 2000);
Assuming you can replace place2IsReady with an object:
place2IsReady = {
state: false,
set ready(value) {
this.state = value
state && place_1_call()
},
get ready() {
return state
}
}
place_1_call = () => {
alert('ready')
}
place2IsReady.ready = true
I'm trying to add a canvas over another canvas – how can I make this function wait to start until the first canvas is created?
function PaintObject(brush) {
this.started = false;
// get handle of the main canvas, as a DOM object, not as a jQuery Object. Context is unfortunately not yet
// available in jquery canvas wrapper object.
var mainCanvas = $("#" + brush).get(0);
// Check if everything is ok
if (!mainCanvas) {alert("canvas undefined, does not seem to be supported by your browser");}
if (!mainCanvas.getContext) {alert('Error: canvas.getContext() undefined !');}
// Get the context for drawing in the canvas
var mainContext = mainCanvas.getContext('2d');
if (!mainContext) {alert("could not get the context for the main canvas");}
this.getMainCanvas = function () {
return mainCanvas;
}
this.getMainContext = function () {
return mainContext;
}
// Prepare a second canvas on top of the previous one, kind of second "layer" that we will use
// in order to draw elastic objects like a line, a rectangle or an ellipse we adjust using the mouse
// and that follows mouse movements
var frontCanvas = document.createElement('canvas');
frontCanvas.id = 'canvasFront';
// Add the temporary canvas as a second child of the mainCanvas parent.
mainCanvas.parentNode.appendChild(frontCanvas);
if (!frontCanvas) {
alert("frontCanvas null");
}
if (!frontCanvas.getContext) {
alert('Error: no frontCanvas.getContext!');
}
var frontContext = frontCanvas.getContext('2d');
if (!frontContext) {
alert("no TempContext null");
}
this.getFrontCanvas = function () {
return frontCanvas;
}
this.getFrontContext = function () {
return frontContext;
}
If you have access to the code that creates the canvas - simply call the function right there after the canvas is created.
If you have no access to that code (eg. If it is a 3rd party code such as google maps) then what you could do is test for the existence in an interval:
var checkExist = setInterval(function() {
if ($('#the-canvas').length) {
console.log("Exists!");
clearInterval(checkExist);
}
}, 100); // check every 100ms
But note - many times 3rd party code has an option to activate your code (by callback or event triggering) when it finishes to load. That may be where you can put your function. The interval solution is really a bad solution and should be used only if nothing else works.
Depending on which browser you need to support, there's the option of MutationObserver.
EDIT: All major browsers support MutationObserver now.
Something along the lines of this should do the trick:
// callback executed when canvas was found
function handleCanvas(canvas) { ... }
// set up the mutation observer
var observer = new MutationObserver(function (mutations, me) {
// `mutations` is an array of mutations that occurred
// `me` is the MutationObserver instance
var canvas = document.getElementById('my-canvas');
if (canvas) {
handleCanvas(canvas);
me.disconnect(); // stop observing
return;
}
});
// start observing
observer.observe(document, {
childList: true,
subtree: true
});
N.B. I haven't tested this code myself, but that's the general idea.
You can easily extend this to only search the part of the DOM that changed. For that, use the mutations argument, it's an array of MutationRecord objects.
This will only work with modern browsers but I find it easier to just use a then so please test first but:
ES5
function rafAsync() {
return new Promise(resolve => {
requestAnimationFrame(resolve); //faster than set time out
});
}
function checkElement(selector) {
if (document.querySelector(selector) === null) {
return rafAsync().then(() => checkElement(selector));
} else {
return Promise.resolve(true);
}
}
ES6
async function checkElement(selector) {
const querySelector = null;
while (querySelector === null) {
await rafAsync();
querySelector = document.querySelector(selector);
}
return querySelector;
}
Usage
checkElement('body') //use whichever selector you want
.then((element) => {
console.info(element);
//Do whatever you want now the element is there
});
A more modern approach to waiting for elements:
while(!document.querySelector(".my-selector")) {
await new Promise(r => setTimeout(r, 500));
}
// now the element is loaded
Note that this code would need to be wrapped in an async function.
Here's a minor improvement over Jamie Hutber's answer
const checkElement = async selector => {
while ( document.querySelector(selector) === null) {
await new Promise( resolve => requestAnimationFrame(resolve) )
}
return document.querySelector(selector);
};
To use:
checkElement('.myElement').then((selector) => {
console.log(selector);
});
If you want a generic solution using MutationObserver you can use this function
// MIT Licensed
// Author: jwilson8767
/**
* Waits for an element satisfying selector to exist, then resolves promise with the element.
* Useful for resolving race conditions.
*
* #param selector
* #returns {Promise}
*/
export function elementReady(selector) {
return new Promise((resolve, reject) => {
const el = document.querySelector(selector);
if (el) {resolve(el);}
new MutationObserver((mutationRecords, observer) => {
// Query for elements matching the specified selector
Array.from(document.querySelectorAll(selector)).forEach((element) => {
resolve(element);
//Once we have resolved we don't need the observer anymore.
observer.disconnect();
});
})
.observe(document.documentElement, {
childList: true,
subtree: true
});
});
}
Source: https://gist.github.com/jwilson8767/db379026efcbd932f64382db4b02853e
Example how to use it
elementReady('#someWidget').then((someWidget)=>{someWidget.remove();});
Note: MutationObserver has a great browser support; https://caniuse.com/#feat=mutationobserver
Et voilà ! :)
Is better to relay in requestAnimationFrame than in a setTimeout. this is my solution in es6 modules and using Promises.
es6, modules and promises:
// onElementReady.js
const onElementReady = $element => (
new Promise((resolve) => {
const waitForElement = () => {
if ($element) {
resolve($element);
} else {
window.requestAnimationFrame(waitForElement);
}
};
waitForElement();
})
);
export default onElementReady;
// in your app
import onElementReady from './onElementReady';
const $someElement = document.querySelector('.some-className');
onElementReady($someElement)
.then(() => {
// your element is ready
}
plain js and promises:
var onElementReady = function($element) {
return new Promise((resolve) => {
var waitForElement = function() {
if ($element) {
resolve($element);
} else {
window.requestAnimationFrame(waitForElement);
}
};
waitForElement();
})
};
var $someElement = document.querySelector('.some-className');
onElementReady($someElement)
.then(() => {
// your element is ready
});
Here is a solution using observables.
waitForElementToAppear(elementId) {
return Observable.create(function(observer) {
var el_ref;
var f = () => {
el_ref = document.getElementById(elementId);
if (el_ref) {
observer.next(el_ref);
observer.complete();
return;
}
window.requestAnimationFrame(f);
};
f();
});
}
Now you can write
waitForElementToAppear(elementId).subscribe(el_ref => doSomethingWith(el_ref);
You can check if the dom already exists by setting a timeout until it is already rendered in the dom.
var panelMainWrapper = document.getElementById('panelMainWrapper');
setTimeout(function waitPanelMainWrapper() {
if (document.body.contains(panelMainWrapper)) {
$("#panelMainWrapper").html(data).fadeIn("fast");
} else {
setTimeout(waitPanelMainWrapper, 10);
}
}, 10);
Another variation of Iftah
var counter = 10;
var checkExist = setInterval(function() {
console.log(counter);
counter--
if ($('#the-canvas').length || counter === 0) {
console.log("by bye!");
clearInterval(checkExist);
}
}, 200);
Just in case the element is never shown, so we don't check infinitely.
A pure promise based JavaScript approach, you can tell for many milliseconds to wait.
const waitElementFor = function(query, ms = 3000) { // 3000 === 3 seconds
return new Promise((resolve) => {
var waited = 0;
var el = null;
var wi = setInterval(function() {
el = document.querySelector(query);
if (waited >= ms || el) {
clearInterval(wi);
if(el) {
resolve(el);
} else {
resolve(null);
}
}
waited += 10;
}, 10);
});
}
To use the function, simply use the following code in an asynchronous function.
var element = await waitElementFor('#elementID');
Snippet:
const waitElementFor = function(query, ms = 3000) { // 3000 === 3 seconds
return new Promise((resolve) => {
var waited = 0;
var el = null;
var wi = setInterval(function() {
el = document.querySelector(query);
if (waited >= ms || el) {
clearInterval(wi);
if(el) {
resolve(el);
} else {
resolve(null);
}
}
waited += 10;
}, 10);
});
}
async function snippetTestAyncFunction(){
var element = await waitElementFor('#elementID');
console.log(element);
}
snippetTestAyncFunction();
Maybe I'm a little bit late :), but here is a nice and brief solution by chrisjhoughton, which allows to perform a callback function when the wait is over.
https://gist.github.com/chrisjhoughton/7890303
var waitForEl = function(selector, callback) {
if (jQuery(selector).length) {
callback();
} else {
setTimeout(function() {
waitForEl(selector, callback);
}, 100);
}
};
waitForEl(selector, function() {
// work the magic
});
If you need to pass parameters to a callback function, you can use it this way:
waitForEl("#" + elDomId, () => callbackFunction(param1, param2));
But be careful! This solution by default can fall into a trap of an infinite loop.
Several improvements of the topicstarter's suggestion are also provided in The GitHub thread.
Enjoy!
This is for those of you who are running code in the Chrome console and not just hard-coded into the html.
user993683 above offered code that will work in your console code. His/her code is as follows:
while(!document.querySelector(".my-selector")) {
await new Promise(r => setTimeout(r, 500));
}
// now the element is loaded
He/she added that it "needs to be inside an async function." And if you are using code in Chrome's console then in fact you DON'T need to wrap it in a function. It will work just as written. You only need to place it in your code at the place right before you try to access the element to make sure it exists.
The only caveat is that it won't work on elements that are only sometimes present under other circumstances. Otherwise it will loop indefinitely if the element never downloads and you'll have to close the browser to stop the wait. Only use it for elements which you are certain will be present.
My company's form page has a dozen or more fields to fill out for each case number. And I have hundreds of case numbers in the script array every day. The elements do not all load simultaneously when changing the iFrame SRC and "onload" does not work in Chrome console scripts. So this method is a god-send to me and it saves me at least 45 minutes every day over the old generic async wait 10 seconds here or 30 seconds there due to fluctuating load times.
The only change I made is "getElementById" instead of the general "querySelector" because all of the elements I need have ID's.
while(!document.getElementById("myFrame").contentWindow.document.getElementById('someDocID')) {
await new Promise(r => setTimeout(r, 500));
}
// After completing the wait above it is now safe to access the element
document.getElementById("myFrame").contentWindow.document.getElementById('someDocID'
).innerText = "Smith, John R";
// and now click the submit button then change the SRC to a fresh form, and use
//*emphasized text* the code again to wait for it to fully load
I apologize to the monitors, but I added this as an answer because after several months of research on console scripts and waiting for elements to load, user993683's remark about a function finally made me realize that console scripts do not require a function for this code. My goal here is only to save other consoler script users the same learning curve that I went through.
Just use setTimeOut with recursion:
waitUntilElementIsPresent(callback: () => void): void {
if (!this.methodToCheckIfElementIsPresent()) {
setTimeout(() => this.waitUntilElementIsPresent(callback), 500);
return;
}
callback();
}
Usage:
this.waitUntilElementIsPresent(() => console.log('Element is present!'));
You can limit amount of attempts, so an error will be thrown when the element is not present after the limit:
waitUntilElementIsPresent(callback: () => void, attempt: number = 0): void {
const maxAttempts = 10;
if (!this.methodToCheckIfElementIsPresent()) {
attempt++;
setTimeout(() => this.waitUntilElementIsPresent(callback, attempt), 500);
return;
} else if (attempt >= maxAttempts) {
return;
}
callback();
}