Javascript: setTimeout Closure inside nested (double) for loops - javascript

Hi I'm trying to recreate what http://austintexas.wayblazer.com/locations/austin-tx is doing for its search textbox - displaying typed character and rotating between several phrases.
My initial logic is to read the phrases from an array like so: [abc,123,XYZ] and then to use a split and forEach to separate the string into characters and create a typing effect using setTimeout.
However, when I attempt this logic, I seem to have trouble getting the desired effect. I've been reading through a lot of these questions on SO, but few of them address issues with nested for loops.
Desired results:
a b c (in a typing effect character by character)
clear textbox
1 2 3 (in a typing effect character by character)
clear textbox
X Y Z
Actual results:
abc123XYZ (abc shows up together at once, followed by 123, then XYZ)
$(document).ready(function() {
$('input#123').attr("placeholder", "");
var phrases = ['abc', '123', 'XYZ'];
for (var i = 0; i < phrases.length; i++) {
$('input#123').attr("placeholder", "");
(function(ind) {
var sentence = phrases[ind];
sentence.split("").forEach(function(char, index) {
setTimeout(function() {
$('input#123').attr("placeholder", $('input#123').attr("placeholder") + char);
}, ind * 500);
});
})(i);
}
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<input id="123" />
Is there an elegant solution for what I want to achieve, and hopefully some explanation to enlighten me would be very much appreciated!

The great thing with javascript is closures, so you can create function closures, and push onto an array to act like a stack.. You can then pop these function off inside a setInterval..
$(document).ready(function(){
var input = $('input#123'),
phrases = ['Stack Overflow','Is the place you come','To get help with coding stuff'],
typedelay = 140,
waitops = 5,
cmds = [],
ph = input.attr.bind(input, 'placeholder');
function clear() {
ph('');
}
function addLetter(a) {
return function () {
ph(ph() + a);
}
}
function doLoop() {
cmds = [];
for (var i=0; i<phrases.length;i++) {
cmds.push(clear);
var sentence = phrases[i];
sentence.split("").forEach(function(char,index) {
cmds.push(addLetter(char));
});
//at the end of each sentence, there is a pause. lets do some no ops,.. 5*300 ms
for (var nn = 0; nn < waitops; nn ++) cmds.push(0);
}
//lets make this thing go on forever.. :)
cmds.push(doLoop);
}
doLoop();
var icheck = setInterval(function () {
//is there a cmd waiting, pop it and run it.
var cmd = cmds.shift();
if (cmd) { cmd(); }
}, typedelay);
input.focus(function () {
clearInterval(icheck);
cmds = [];
ph('What your want');
});
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<input id="123" />

Here's a recursive function that shifts the array and stops when array has no length
typePhrases(['Message #1','Another message','And the last one'], 300)
function typePhrases(phrases, speed){
// shift array to get word/phrase
var word = phrases.length && phrases.shift(), $input= $('#test').attr('placeholder','');
// don't do anything if no phrase/word
if(word){
word.split('').forEach(function(char, i){
setTimeout(function(){
// use attr(attributeName, function) to update
$input.attr('placeholder', function(_, curr){
return curr + char;
});
// on last letter call function again to get next word
if(i=== word.length-1){
setTimeout(function(){
typePhrases(phrases, speed);
},speed);
}
},i*speed)
});
}
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input id="test" placeholder=''>

Related

When combining 2 strings, Dom element is not showing same results as static entered query

I'm sure I'm overlooking the most obvious, but I need to loop through a few links on the page, but some reason when I try to introduce the iterated number, and change to a string, it doesn't work. By not working, I mean when I run the function, it gives me error ' Uncaught TypeError: Cannot read property 'click' of null'. The first is an example showing it works without the loop. The 2nd is the string combo that doesn't work.
This works!
function myFunction() {
var grids = document.getElementsByClassName('gridRow')
setInterval(function(){
for(i=1;i<grids.length;i++) {
var test = '_resultsGrid__Button_1'
document.getElementById(test).click()
}
}, 2000);
}
This doesn't!
function myFunction() {
var grids = document.getElementsByClassName('gridRow')
setInterval(function() {
for(i=1;i<grids.length;i++) {
var test = '_resultsGrid__Button_' + String(i)
document.getElementById(String(test)).click()
}
}, 2000);
}
Instead of doing
for (i = 1;i < grids.length;i++) { ... }
You should be doing
for (i = 1;i <= grids.length;i++) { ... }
In your first example you were looping only while i was less than (<) the elements length, so you would loop one less time than the length; you'll want to use the less than or equal to (<=) operator instead to include that last iteration
function myFunction() {
var grids = document.getElementsByClassName('gridRow')
setInterval(function() {
for(i = 1; i <= grids.length; i++) {
var test = '_resultsGrid__Button_'+String(i)
document.getElementById(String(test)).click()
}
}, 2000);
}

setTimeout and recursive IFFEs, nested!? How to calculate the delay.

This problem has really reminded me where I am regarding JS skills... :(
I feel I'm right on the cusp but I'm struggling to understand what to do conceptually, it's not so much a syntax issue as I feel I almost cracked it.
What I need
I'm trying to console.log a series of strings, one letter at a time.
There needs to be a delay between each LETTER outputted, say 300ms.
There must then be a delay between each STRING outputted, say 2000ms.
There are 2 strings in example array, but solution must support a dynamic number of strings.
My Current Code (can be pasted into a console)
var stringList = ['first test','second test'],
stringListLen = stringList.length;
for(var i = 0; i < stringListLen; i++){
// begin with first string and read it's length for iterations
var stringNumber = 0;
var currentString = stringList[stringNumber];
var currentStringLen = currentString.length;
// just see the word for clarification at this point in code
console.log(currentString);
(function (i) {
setTimeout(function () {
for (var j = 0; j < currentStringLen; j++) {
(function (j) {
setTimeout(function () {
// THE MAGIC HAPPENS HERE
console.log(j, currentString.charAt(j));
// End of string, so read next string, reset letter count
if(j === currentStringLen - 1){
stringNumber++;
currentString = stringList[stringNumber];
j = 0;
}
}, 300 * j); // each letter * specified delay
})(j);
};
}, currentStringLen * 300 * i); // letter * delay * letters in word
})(i);
}
The Issue
THE GOOD: I am successfully getting the short delay between letters outputted, and my check to switch to a new word and reset the letter counter when we get to the end of the first word is working...
THE BAD: I can't get the wait between the two words to work. I have tried a few ideas and have just got myself so confused I don't know if my approach is correct now.
THE UGLY: The final letter of the last term is also not outputting, and that is just totally unexpected.
What I've tried.
Okay, I've tried simply changing the "currentStringLen * 300 * i" elements to various combinations that seemed logical but had no effect better or worse. Ultimately I think I am trying to calculate "wait the number of letters in current string times 300 (the letter delay) * " <---- STRIKETHROUGH...
I actually don't know what I'm calculating and that's the issue.
I now think I want to split this into TWO functions, not two nested ones. One to READ AND PASS IN a string to another function that JUST outputs the letters with a short delay, then once it gets to the last letter it calls the first function asking for the next word. BUT then I'm still going to need to recurse for the number of strings in the array which creates the same issue...
Am I missing anything fundamental here people?
Is this roughly what you had in mind?
function printLetters(stringList) {
var wordIndex = 0,
letterIndex = 0;
printNext();
function printNext() {
var word = stringList[wordIndex];
var letter = word.charAt(letterIndex);
console.log(letter);
++letterIndex;
if (letterIndex === word.length) {
letterIndex = 0;
++wordIndex;
if (wordIndex < stringList.length) {
setTimeout(printNext, 2000);
}
return;
}
setTimeout(printNext, 300);
}
}
printLetters(['first test', 'second test']);
Here there's only ever one setTimeout running at once and a new one is being set as required with the appropriate time.
While I don't recommend having multiple timers running at once, it can be done. Something like this:
function printLetters(stringList) {
var letterCount = 0,
startTime = Date.now();
stringList.forEach(function(word, wordCount) {
word.split('').forEach(function(letter) {
setTimeout(function() {
console.log(letter, Date.now() - startTime);
}, wordCount * 1700 + (letterCount * 300));
++letterCount;
});
});
}
printLetters(['first test', 'second test']);
Here I've included the time delta in the logging to give a better sense of what is going on when. The gap between strings is 2000 but the constant in the code is 1700 because there's already 300 being added.
I would do a quite different approach. Rather than doing a bunch of precalculated timeouts and their associated closures, i would do just one timeout at a time, using recursion to then move on to the next timeout:
function delayShow(words) {
if (!words || words.length === 0) {
return;
} else if (words[0].length === 0) {
words.shift()
setTimeout(() => delayShow(words), 2000);
} else {
console.log(words[0].charAt(0));
words[0] = words[0].substr(1);
setTimeout(() => delayShow(words), 300);
}
}
delayShow(['first test','second test']);
You can use a condition inside the loop, and when you're on the last iteration of the strings characters, you call the recursive function again, inside a timeout, to iterate over the next string in the array etc.
var stringList = ['first test', 'second test'];
(function rec(j) {
var chars = stringList[j].split('');
chars.forEach(function(char, i) {
setTimeout(function() {
console.log(char);
// if it's the last iteration && there are more strings in the array
if ((i == (chars.length - 1)) && (j < stringList.length - 1)) {
setTimeout( function() {
rec(++j); // play it again
}, 2000);
}
}, i * 300);
});
})(0);
Even something as trivial as setTimeout's, is a good example of using async / await. So I've included an example below.
As you will see, the code is much easier to follow. With the added advantage of not creating multiple setTimeout's running concurrently,.
It also really helps make things much easier to change things, eg. If I asked you to alter the code below so that SPACE takes less time than other letters, to make it flow more naturally, it wouldn't take a lot of thought to work out what to change.
var
stringList = ['first test','second test'];
async function delay(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms);
});
}
async function run() {
let wordpos = 0;
let wordElem = document.querySelector('.words');
while (true) {
let word = stringList[wordpos];
let text = '';
for (var letterpos = 0; letterpos < word.length; letterpos ++ ) {
let letter = word[letterpos];
text = text + letter;
wordElem.innerText = text;
await delay(300);
}
await delay(2000);
wordpos = (wordpos + 1) % stringList.length;
}
}
run();
<h1 class="words">?</h1>

How to go through an unknown number of quotes randomly without repeating

I have a small snippet of Javascript where I rotate through a list of quotes in order from beginning to end.
However, I want to randomly go through the list (instead of in order), without repeating until all of the quotes are iterated through, and then start around with a random quote again. How would I go about doing this?
$(function(){
var quotes = $('#quotes').children('.rotate-quote');
firstQuo = quotes.filter(':first');
lastQuo = quotes.filter(':last');
quotes.first().show();
setInterval(function(){
if($(lastQuo).is(':visible')) {
var nextElem = $(firstQuo);
} else {
var nextElem = $(quotes).filter(':visible').next();
}
$(quotes).filter(':visible').fadeOut(300);
if($(lastQuo).is(':visible')) {
setTimeout(function() {
$(firstQuo).fadeIn(300);
}, 600);
} else {
setTimeout(function() {
$(nextElem).fadeIn(600);
}, 600);
}
}, 10000);
});
Here's a possible solution with demo:
var $container = $('div'),
quotes = $('quote').hide().toArray(),
delay = 500;
function shuffle(arr) {
return arr.map(function(v){ return [v,Math.random()]; })
.sort().map(function(v){ return v[0]; });
}
function loop() {
$(shuffle(quotes)).each(function(i,el) {
setTimeout(function(){ $(el).appendTo($container).show(); }, i*delay);
});
}
function start() {
function begin(){ $(quotes).hide(); loop(); }
setInterval(begin, quotes.length * delay);
begin();
}
start();
Demo: http://jsbin.com/agihix/1/edit
Edit: I turned this into a little plugin, grab it here https://gist.github.com/elclanrs/5610886
Just to show the Fisher Yates (not my code):
function fisherYates ( myArray ) {
var i = myArray.length, j, temp;
if ( i === 0 ) return false;
while ( --i ) {
j = Math.floor( Math.random() * ( i + 1 ) );
temp = myArray[i];
myArray[i] = myArray[j];
myArray[j] = temp;
}
return myArray;
}
So get your quotes into an array, run it through that function, then do loop through the array, when you get to the end, run it through that function again, etc.
The following copies the quote array, then randomly slices one from it each time. wWen there are no entries left, it starts again.
var randomQuote = (function() {
var quotes = ['quote 0','quote 1','quote 2'];
var quoteCopy = [];
return function () {
if (!quoteCopy.length) {
quoteCopy = quotes.slice();
}
return quoteCopy.splice(Math.random()*quoteCopy.length | 0, 1)[0];
}
}());
I don't think your algorithm as stated is good. Suppose you have 100 quotes and then you make a first random presentation of the full set.
At the end, as stated in your description, will start again from scratch and so it's possible that quote 101 will be the same quote as 100.
I think that something better (more uniform) would be
Randomly shuffle the n quotes. This is done only once at the beginning.
Pick the first quote and display it.
Remove the quote from the list and insert it in a random position between n-w and n where w is a parameter (e.g. n/2).
Repeat from 2 for each quote you need.
The number w will modulate how much random you want the sequence to be... the smaller and the more uniform the delay will be after a quote is presented.
With this approach there will be no "distribution glitch" and the average delay after which a quote is presented next time will not depend on the current position.

JavaScript: Improving the performance of a FOR Loop to stop the browser locking up?

I have an array of objects, I loop through this array one object at a time and I make a couple of checks to see if each object in that array meets certain criteria, if that object meets this criteria I then copy a property of this object in to an array (that property also contains another object).
for(var v = 0; features.length > v; v++){
var feature = features[v];
//If the feature is on the map then we want to add it
if(feature.onScreen()){
dataArray.push(feature.attributes);
}
}
Now for some reason if this array of objects is big (5000+) in size this operation becomes very expensive and the browser just locks up for a second or two (Some times more).
I cant go in to much more info of what the code does but I was wondering given this loop, what would be the best way to give the browser a break lets say every 500 iterations and so it doesn't lock up and then continue on etc.
Thanks
Not sure... but what about putting that code into a function, then breaking out and recalling the function (say every xMs)?
eg,
var LastPos = 0;
function DoLoop()
{
for(var v = LastPos; features.length > v; v++){
var feature = features[v];
//If the feature is on the map then we want to add it
if(feature.onScreen()){
dataArray.push(feature.attributes);
}
if(v > 500)
{
LastPos = v;
break;
}
}
setTimeout('DoLoop()', 10);
}
setTimeOut() can solve this.
var maxStatement = 1000, currentIndex=0, timeoutVar = ''
var sLimit = features.length
function multiStepLoop(){
for (var i=currentIndex; i<currentIndex+maxStatement; i++){
//---DO YOUR STUFFS HERE
}
var a = sLimit-i;
currentIndex += maxStatement;
if (maxStatement >= a){ maxStatement=a }
if (a<=0){
callBackFunction() //-- function to call when your peocess finish.
}else{
timeoutVar = setTimeout('multiStepLoop()',1)
}
}
multiStepLoop();
The drawback is that you need to cover all you want to do after this process complete into a function, then run it like a callBack_Function.
Note : Jus need to set the time for setTimeout to 1 millisecond & the browser won't display the waiting-cursor.
Change the structure of the loop to :
var index = 0;
function myfunc() {
while(features.length > index){
var feature = features[v];
//If the feature is on the map then we want to add it
if(feature.onScreen()){
dataArray.push(feature.attributes);
}
index++;
//You can do this
var waitingTime = 0;
If (index% 500=0) waitingTime=100; //or 10, 20...
//Or just with a short interval
var waitingTime = 10;
setTimeOut('myfunc', waitingTime);
}
}
Or with a parameter : [I prefer this one]
function myfunc(index) {
if(!index) {
index=0;
}
while(features.length > index){
var feature = features[v];
//If the feature is on the map then we want to add it
if(feature.onScreen()){
dataArray.push(feature.attributes);
}
//You can do this
var waitingTime = 0;
If (index% 500=0) waitingTime=100; //or 10, 20...
//Or just with a short interval
var waitingTime = 10;
setTimeOut('myfunc', waitingTime);
}
}
[EDIT]
Change setTimeOut calls...
And in your code, when you call the function, don't give the param, to initialize the index!
What if you did something with a little burst and a little recursion:
Fiddle Code
Main Function
What happens is a given index is passed with a burst amount of intervals (i.e. 500), upon ending the for loop at the given number of burst iterations, it recalls itself with the same burst number. The function ends when the end of the features array is completed.
var transferFeatures = function (index, burst) {
for (var z = 0; z <= burst; z++) {
if (index === features.length) {
return;
}
var feature = features[index];
if (feature.onScreen) {
dataArray.push(feature.attributes);
}
index++;
}
if (index !== features.length) {
transferFeatures(index, burst);
}
};
Testing
To simulate load, I created an array of objects using various key-value pairs (most importantly, your attributes inner object):
//To simulate a random boolean
var randomBoolean = function () {
return Math.random() <= 0.5;
};
//A random integer
var getRand = function () {
return (Math.floor(Math.random() * 10).toString());
};
// Create a bunch of dummy objects
var randomFeatures = function (arr, i) {
for (var p = 0; p < i; p++) {
arr.push({
onScreen: randomBoolean(),
attributes: {
width: getRand(),
height: getRand(),
someAtt: "I'm just an attribute",
coolKidsRideBikes: true,
foo: "bar",
bar: "baz"
}
});
}
};
Granted, it's not the same onScreen() test you will be using, but either way, it evaluates to a boolean value. I think if you apply this concept with your code, you could have amazing results.
Everything in the Fiddle I linked too at the top is called like this:
randomFeatures(features, 5000);
console.log(features.length);
transferFeatures(0,500);
console.log(dataArray.length);
Load Testing
I simulated 5000000 (5 million) random objects being pushed onto features with a burst of 1000 and the script completed in around 3.29 seconds.

Javascript character limit counter function for text input

I've made a counter with javascript that shows a user how characters are remaining (from a set limit) for some text input or text area. Here's the code:
<script type="text/javascript">
function CountRemaining()
{
var limit = 1000;
var count = document.getElementById('press-form-body').value.length;
document.getElementById('counter').innerHTML = ((limit-count) + " characters left");
var timer = setTimeout("CountRemaining()",50);
}
</script>
My abomination above works fine but my problem is that I need to use this multiple times and making a separate function for every time I need it would be impractical to say the least.
I tried this and it didn't work:
<script type="text/javascript">
function CountRemaining(string, targetcounter, limit)
{
var count = document.getElementById(string).value.length;
document.getElementById(targetcounter).innerHTML = ((limit-count) + " characters left");
var timer = setTimeout("CountRemaining()",50);
}
I then figured I put the wrong statement for the timer so I changed it to this but still didn't work:
var timer = setTimeout("CountRemaining(string, targetcounter, limit)",50);
I'm lost. Any help would be highly appreciated. Thank you!
I think a better idea would be to use the "onchange" event for those types of elements.
Basically as soon as the text area / text input loses focus and is changed, you can bind a function to count how many characters are left.
document.getElementById('press-form-body').onchange = function() {
// your stuff (double check this to make sure the "this" value is right
// use this as an example
document.getElementById(targetcounter).innerHTML = this.value.length - 1000
}
Another solution would be to use the "key" events to listen to any keypress in the inputs.
document.getElementById('press-form-body').onkeypress = function() {
// your stuff (double check this to make sure the "this" value is right
// use this as an example
document.getElementById(targetcounter).innerHTML = this.value.length - 1000
}
function limittxt()
{
var tval = document.getElementById('press-form-body').value;
tlength = tval.length;
set = 100;
remain = parseInt(set - tlength);
document.getElementById('counter').innerHTML = remain + " characters left";
if (remain <= 0) {
document.getElementById('press-form-body').value = tval.substring(0, tlength - Math.abs(remain)))
}
}
An call this function in the input element like the following :
<input type='text' onkeypress='limittxt()' onkeyup='limittxt()' onkeydown='limittxt()'>
I suppose the error is with following line:
var timer = setTimeout("CountRemaining(string, targetcounter, limit)",50);
Here i think it should come like:
var str = "CountRemaining(" + string + "," + targetcounter + "," + limit + ")";
var timer = setTimeout(str,50);
If you want to proceed along the lines of using the timer to run the function at regular intervals, then you would need code similar to the following (hat-tip to #Sameera Thilakasiri for the inspiration):
function CountRemaining(string, targetcounter, limit){
var count = document.getElementById(string).value.length;
document.getElementById(targetcounter).innerHTML = ((limit-count) + " characters left");
}
setInterval(function() {
// call the function for each of the inputs on the page you need a counter for
CountRemaining('press-form-body', 'counter', 1000);
// etc
}, 50);
However, I believe #amchang87's approach is better overall, so I recommend you go with that if possible.
Tracking the number of characters left is always a little difficult. A good event to use is keyup or keypress, but that doesn't cover text that is dragged and dropped into the element, so people end up using a timer.
If you have many elements to monitor, consider putting them into an array, then call the timer at each interval and check all of the elements. Be careful with performance though, running the function every 50 ms may sap quite a bit of browser performance so try to keep the processing to an absolute minimum.
That means caching whatever you can and keep the logic simple.
Edit
The run and stop methods below could be used to start the timer when particular elements get focus, then stop it when they lose focus. That way you aren't hogging resources when not required.
/Edit
var keyCountCheck = (function() {
var elementArray, timerRef;
return {
// Initialise once
init: function() {
var input, inputs;
// Initialise elementArray if hasn't been done already
// If adding and removing elements, create new aray
// instead each time.
if (!elementArray) {
elementArray = [];
inputs = document.getElementsByTagName('input');
for (var i=0, iLen=inputs.length; i<iLen; i++) {
input = inputs[i];
if (input.type == 'text') {
elementArray.push(input);
}
}
}
timerRef = window.setInterval(keyCountCheck.run, 50);
},
// Run timer
run: function() {
// If setInterval not running, start it
if (!timerRef) {
keyCountCheck.init();
}
var el;
for (var i=0, iLen=elementArray.length; i<iLen; i++) {
checkLength(elementArray[i]);
}
},
// In case there is a reason to stop this thing.
stop: function() {
if (timerRef) {
window.clearTimeout(timerRef);
timerRef = null;
}
}
};
}());
window.onload = keyCountCheck.init;
function checkLength(el) {
// Character limit can be set as a data- attribute or
// class or various other ways. This is the simple way
var limit = 10;
var msgEl = document.getElementById(el.id + '_limitMsg');
if (msgEl) {
msgEl.innerHTML = (limit - el.value.length) + ' characters left. ' + (new Date());
}
}
Some supporting HTML to play with:
<input id="i0" value="1"><span id="i0_limitMsg"></span>
<br>
<input id="i1" value="2"><span id="i1_limitMsg"></span>
<br>
<button onclick="keyCountCheck.stop()">stop</button>
<button onclick="keyCountCheck.run()">run</button>
setInterval(
function CountRemaining(string, targetcounter, limit){
var count = document.getElementById(string).value.length;
document.getElementById(targetcounter).innerHTML = ((limit-count) + " characters left");
},50
);
Tryout this way.
Basic concept of this solution,
var f = function() {function_name(arg1); };
setTimeout(f, msec);

Categories

Resources