Reputation: 4430
function say(str){
for(i=0;i<str.length;i++){
setTimeout(function(){
$("#text").append(str[i]);
console.log(str[i]);
},
i*200);
}
}
say("Hello World !");
Look: http://jsfiddle.net/XnKNX/
It seems like each str[i]
is undefined;
How to get it works ? I am learning, so it'll be great if somebody explain things. Thanks.
Upvotes: 1
Views: 502
Reputation: 2781
I stumbled upon this question when looking for a script to use on my website. After looking for a while I decided to write my own.
Here is the link for it on Github, hopefully it might help someone :)
A brief explanation
class Typer {
constructor(typingSpeed, content, output) {
this.typingSpeed = typingSpeed;
// Parses a NodeList to a series of chained promises
this.parseHtml(Array.from(content), output);
};
makePromise(node, output) {
if (node.nodeType == 1) // element
{
// When a new html tag is detected, append it to the document
return new Promise((resolve) => {
var tag = $(node.outerHTML.replace(node.innerHTML, ""));
tag.appendTo(output);
resolve(tag);
});
} else if (node.nodeType == 3) // text
{
// When text is detected, create a promise that appends a character
// and sleeps for a while before adding the next one, and so on...
return this.type(node, output, 0);
} else {
console.warn("Unknown node type");
}
}
parseHtml(nodes, output) {
return nodes.reduce((previous, current) => previous
.then(() => this.makePromise(current, output)
.then((output) => this.parseHtml(Array.from(current.childNodes), output))), Promise.resolve());
}
type(node, output, textPosition) {
var textIncrement = textPosition + 1;
var substring = node.data.substring(textPosition, textIncrement);
if (substring !== "") {
return new Promise(resolve => setTimeout(resolve, this.typingSpeed))
.then(() => output.append(substring))
.then(() => this.type(node, output, textIncrement));
}
return Promise.resolve(output);
}
}
Upvotes: 1
Reputation: 50905
I wouldn't suggest using a for
loop and trying to get the actual timing to line up. Recursively calling a function with setTimeout
makes more sense to me, and doesn't suffer from the closure problem you have. You can use:
function say(str) {
(function iterator(index) {
if (index < str.length) {
$("#text").append(str.charAt(index));
console.log(str.charAt(index));
setTimeout(function () {
iterator(++index);
}, 200);
}
})(0);
}
say("Hello World !");
DEMO: http://jsfiddle.net/XnKNX/6/
.charAt()
is the preferred way of getting a string's character because older IE doesn't support []
indexing.
If you wanted to keep the for
loop (for whatever reason), you could set it up like this:
var say = (function () {
var generateTyper = function (s, index) {
return function () {
$("#text").append(s.charAt(index));
console.log(s.charAt(index));
};
};
return function (str) {
for (var i = 0, j = str.length; i < j; i++) {
setTimeout(generateTyper(str, i), i * 200);
}
};
})();
say("Hello World !");
DEMO: http://jsfiddle.net/8kxFd/1/
Upvotes: 2
Reputation: 382130
That's because i
has the value of end of loop when the callback you give to setTimeout
is called.
A solution is to change it to
function say(str){
for(i=0;i<str.length;i++){
(function(i){
setTimeout(function(){
$("#text").append(str[i]);
},
i*200);
})(i);
}
}
This saves in the immediately called internal function the value of i
at iteration time.
Upvotes: 3