javascripthtmlinnerhtml

How to append characters to innerHTML one-by-one, while ensuring that HTML tags are correctly interpreted?


As part of a larger script, I've been trying to make a page that would take a block of text from another function and "type" it out onto the screen:

function typeOut(page,nChar){
  var txt = document.getElementById("text");
  if (nChar < page.length){
    txt.innerHTML = txt.innerHTML + page[nChar];
    setTimeout(function () { typeOut(page, nChar + 1); }, 20);
  }
}

typeOut('Hello, <b>world</b>!', 0)
<div id="text">

This basically works the way I want it to, but if the block of text I pass it has any HTML tags in it (like <a href> links), those show up as plain-text instead of being interpreted. Is there any way to get around that and force it to display the HTML elements correctly?


Solution

  • The problem is that you will create invalid HTML in the process, which the browser will try to correct. So apparently when you add < or >, it will automatically encode that character to not break the structure.

    A proper solution would not work literally with every character of the text, but would process the HTML element by element. I.e. whenever you encounter an element in the source HTML, you would clone the element and add it to target element. Then you would process its text nodes character by character.

    Here is a solution I hacked together (meaning, it can probably be improved a lot):

    function typeOut(html, target) {
        var d = document.createElement('div');
        d.innerHTML = html;
        var source = d.firstChild;
        var i = 0;
    
        (function process() {
            if (source) {
                if (source.nodeType === 3) { // process text node
                    if (i === 0) { // create new text node
                        target = target.appendChild(document.createTextNode(''));
                        target.nodeValue = source.nodeValue.charAt(i++);
                    // stop and continue to next node
                    } else if (i === source.nodeValue.length) { 
                        if (source.nextSibling) {
                            source = source.nextSibling;
                            target = target.parentNode;
                        }
                        else {
                            source = source.parentNode.nextSibling;
                            target = target.parentNode.parentNode;
                        }
                        i = 0;
                    } else { // add to text node
                        target.nodeValue += source.nodeValue.charAt(i++);
                    }
                } else if (source.nodeType === 1) { // clone element node 
                    var clone = source.cloneNode();
                    clone.innerHTML = '';
                    target.appendChild(clone);
                    if (source.firstChild) {
                        source = source.firstChild;
                        target = clone;
                    } else { 
                        source = source.nextSibling;
                    }
                }
                setTimeout(process, 20);
            }
        }());
    }
    

    DEMO