0

I'm trying to print to a div one character at a time. It works, however, it runs both lines at the same time so that all I get is a jumbled mess.

How can I make the commands run one after the other?

function print(str){
    var arr = str.split("");
    var i = 0;
    function write(){
        setTimeout(function(){
            if(i < arr.length){
                var cont = $(".content").html();
                cont = cont.replace("_","");
                $(".content").html(cont + arr[i] + "_");
                i++;
                write();
            }
        },30);
    }
    write();
}

var str = [
    "I am the egg man",
    "I am the walrus"
];

for(x in str){
    print(str[x];
}

jsFiddle: http://jsfiddle.net/PscNC/1/

Will
  • 37
  • 5
  • Similar http://stackoverflow.com/questions/6074833/load-and-execute-javascript-code-synchronously – RAJ Aug 03 '14 at 04:52
  • 1
    You have two asynchronous functions and you're starting them both at the same time. If you want the first one to complete before you start the second one, then you need a notification from the first one when it's done so you can then start the second one. – jfriend00 Aug 03 '14 at 04:54
  • btw, for-in loops in Javascript are NOT intended to be used as a for-each loop and the order the keys are iterated is not guaranteed by the standard. Use `for(var i=0; i – hugomg Aug 03 '14 at 05:24

3 Answers3

1

You have two asynchronous functions that you start one right after the other so they run in parallel. If you want them to run serially, then you have to create some sort of notification when the first one is done so you then can trigger the start of the next one and so on. This can be done a number of ways (I show three ways below). You can use a callback, you can use promises and you can avoid having to sequence the async operations at all.

Method #1 - Completion Callback

Here's adding a callback to your print function and then use that to trigger the next string to go and then changing your iteration of strings to use the callback:

Working demo: http://jsfiddle.net/jfriend00/Lyu5V/

$(function() {
    function print(str, fn) {
        var i = 0;
        var items = $(".content");

        function write() {
            setTimeout(function() {
                if (i < str.length) {
                    items.html(items.html().replace("_", "") + str.charAt(i) + "_");
                    i++;
                    write();
                } else {
                    fn();
                }
            }, 100);
        }
        write();
    }

    var data = [
            "I am the egg man...",
            "I am the walrus"
        ];

    var i = 0;
    function next() {
        if (i < data.length) {
            print(data[i++], next);
        }
    }
    next();
});

FYI, there's really no reason to split your string into an array. You can access the individual characters of the string with the .charAt(index) method.


Method #2 - Promises - use .then() to sequence operations

And, here's a version of your code using promises instead of passing the callback:

Working demo: http://jsfiddle.net/jfriend00/97UtX/

$(function() {
    function print(str) {
        var i = 0, items = $(".content"), def = $.Deferred();

        function write() {
            setTimeout(function() {
                if (i < str.length) {
                    items.html(items.html().replace("_", "") + str.charAt(i) + "_");
                    i++;
                    write();
                } else {
                    def.resolve();
                }
            }, 100);
        }
        write();
        return def.promise();
    }

    var data = [
            "I am the egg man..",
            "I am the walrus"
    ];

    data.reduce(function(p, item) {
        return p.then(function() {
            return print(item);
        });
    }, $.Deferred().resolve());

});

Method #3 - Avoid sequencing by combining data into one single operation

And, if you want to simplify/DRY it up a bit, you can do this which avoids having to sequence the successive operations by just turning it into one longer operation and I made a few simplifications to your code:

Working demo: http://jsfiddle.net/jfriend00/TL8pP/

$(function() {
    function print(str) {
        var i = 0, items = $(".content");

        function write() {
            setTimeout(function() {
                if (i < str.length) {
                    items.html(items.html().replace("_", "") + str.charAt(i) + "_");
                    i++;
                    write();
                }
            }, 100);
        }
        write();
    }

    var data = [
            "I am the egg man..",
            "I am the walrus"
    ];
    print(data.join(""));

});
jfriend00
  • 683,504
  • 96
  • 985
  • 979
1

This is based on jfriend's answer, but it uses primitives with promises rather than promises at a high level. I believe this makes for cleaner code.

First, let's write a function that represents a delay with promises:

function delay(ms){ // generic delay function
     var d = $.Deferred();
     setTimeout(d.resolve, ms);
     return d;
}

Next, let's use promises to their fullest

var delay100 = delay.bind(null, 100); // a 100 ms delay

function write(el, str, initial) { // write a single word
    return [].reduce.call(str, function (prev, cur) { // reduce is generic
        return prev.then(delay100).then(function (letter) {
            initial += cur;
            el.text(initial + "_");
        });
    }, $.when());
}
data.reduce(function (p, item) {
    return p.then(function () { // when the last action is done, write the next
        return write($(".content"), item, ""); // might want to cache this
    });
}, $.ready.promise()); // we don't need `$(function(){})` 

Here is a fiddle illustrating this solution: http://jsfiddle.net/feq89/

Just for fun, here is an ES6 solution without jQuery:

var delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));

var write = (el, str, initial) => 
    [].reduce.call(str, (prev, cur) =>
        prev.then(() => delay(100)).then(() => {
          initial += cur;
          el.textContent = initial + "_";
        });
    }, Promise.resolve());

var content = document.querySelector(".content");
data.reduce((p, item) => p.then(() => write(content, item, "")));
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • If you want to write the sentences one after the other rather than replace the last one with the new one - all you have to do is to pass the parameter in `p.then` to the `initial` value and start the `data.reduce` with an empty `""`. – Benjamin Gruenbaum Aug 03 '14 at 07:12
  • Without checking, as far as I remember - No, it is not and there is discussion about documenting it officially into the API It has been there since forever :) – Benjamin Gruenbaum Aug 04 '14 at 07:55
  • Aye, that's my understanding too. – Roamer-1888 Aug 04 '14 at 08:06
  • BTW - a terminal "_" can be avoided by building the string in an array and outputting with `.join('_')`. Also gives GC less to do. – Roamer-1888 Aug 04 '14 at 08:08
  • @Roamer-1888 I thought the terminal `_` caret was supposed to be there (like when you type in a word processor). It's possible to `.then` the reduce call and remove it manually. At least in `v8` this code is rather slow (because of the `.reduce`, string property access and so on) anyway as I'm assuming you're not doing this more than a 1000 times concurrently. I'm not sure how your suggestion with `.join` would work, mind making a fiddle? – Benjamin Gruenbaum Aug 04 '14 at 08:22
  • Doh! Benjamin, you are so right - it's a *teletype emulator*. There's less justification, but my principle of building the string in an array then outputting with `.join()` can still be applied, only now it's `.join('') + '_'`. [Fiddle](http://jsfiddle.net/j4Bq8/) – Roamer-1888 Aug 04 '14 at 10:35
  • By `.join`ing it each time, aren't you creating that intermediate string anyway? – Benjamin Gruenbaum Aug 04 '14 at 10:39
  • Yes but it's written straight to the DOM, without being assigned in Javascript. Hence, I think, less work for GC in sweeping up expired strings; GC was minor point anyway - as I said, less justification now you've corrected me on the intended functionality. – Roamer-1888 Aug 04 '14 at 11:41
0

bobef is right.

Add another argument to print, which is a callback. And you should call the print method inside another recursive method instead a loop.

function print(str, _cb){
    var arr = str.split("");
    var i = 0;
    function write(){
        setTimeout(function(){
            if(i < arr.length){
                var cont = $(".content").html();
                cont = cont.replace("_","");
                $(".content").html(cont + arr[i] + "_");
                i++;
                write();
            } else {
                _cb();
            }
        },30);
    }
    write();
}

var str = [
    "I am the egg man",
    "I am the walrus"
];

var j = 0,
    callback = function () {
        if(j < str.length){
            print (str[j++], callback);
        }
    };

callback();
Callebe
  • 1,087
  • 7
  • 18