Is it possible to get reference to comment element/block by JavaScript?

This sounds a little crazy, but I'm wondering whether possible to get reference to comment element so that I can dynamically replace it other content with JavaScript.

<html>
<head>
</head>
<body>
<div id="header"></div>
<div id="content"></div>
<!-- sidebar place holder: some id-->
</body>
</html>

In above page, can I get reference to the comment block and replace it with some content in local storage?

I know that I can have a div place holder. Just wondering whether it applies to comment block. Thanks.

Answers:

Answer
var findComments = function(el) {
    var arr = [];
    for(var i = 0; i < el.childNodes.length; i++) {
        var node = el.childNodes[i];
        if(node.nodeType === 8) {
            arr.push(node);
        } else {
            arr.push.apply(arr, findComments(node));
        }
    }
    return arr;
};

var commentNodes = findComments(document);

// whatever you were going to do with the comment...
console.log(commentNodes[0].nodeValue);
Answer

It seems there are legitimate (performance) concerns about using comments as placeholders - for one, there's no CSS selector that can match comment nodes, so you won't be able to query them with e.g. document.querySelectorAll(), which makes it both complex and slow to locate comment elements.

My question then was, is there another element I can place inline, that doesn't have any visible side-effects? I've seen some people using the <meta> tag, but I looked into that, and using that in <body> isn't valid markup.

So I settled on the <script> tag.

Use a custom type attribute, so it won't actually get executed as a script, and use data-attributes for any initialization data required by the script that's going to initialize your placeholders.

For example:

<script type="placeholder/foo" data-stuff="whatevs"></script>

Then simply query those tags - e.g.:

document.querySelectorAll('script[type="placeholder/foo"]')

Then replace them as needed - here's a plain DOM example.

Note that placeholder in this example isn't any defined "real" thing - you should replace that with e.g. vendor-name to make sure your type doesn't collide with anything "real".

Answer

Building off of hyperslug's answer, you can make it go faster by using a stack instead of function recursion. As shown in this jsPerf, function recursion is 42% slower on my Chrome 36 on Windows and 71% with IE11 in IE8 compatibility mode. It appears to run about 20% slower in IE11 in edge mode but faster in all other cases tested.

function getComments(context) {
    var foundComments = [];
    var elementPath = [context];
    while (elementPath.length > 0) {
        var el = elementPath.pop();
        for (var i = 0; i < el.childNodes.length; i++) {
            var node = el.childNodes[i];
            if (node.nodeType === Node.COMMENT_NODE) {
                foundComments.push(node);
            } else {
                elementPath.push(node);
            }
        }
    }

    return foundComments;
}

Or as done in TypeScript:

public static getComments(context: any): Comment[] {
    const foundComments = [];
    const elementPath = [context];
    while (elementPath.length > 0) {
        const el = elementPath.pop();
        for (let i = 0; i < el.childNodes.length; i++) {
            const node = el.childNodes[i];
            if (node.nodeType === Node.COMMENT_NODE) {
                foundComments.push(node);
            } else {
                elementPath.push(node);
            }
        }
    }

    return foundComments;
}
Answer

There is an API for document nodes traversal: Document#createNodeIterator():

var nodeIterator = document.createNodeIterator(
    document.body,
    NodeFilter.SHOW_COMMENT,    
    { acceptNode: function(node) { return NodeFilter.FILTER_ACCEPT; } }
);

// Replace all comment nodes with a div
while(nodeIterator.nextNode()){
    var commentNode = nodeIterator.referenceNode;
    var id = (commentNode.textContent.split(":")[1] || "").trim();
    var div = document.createElement("div");
    div.id = id;
    commentNode.parentNode.replaceChild(div, commentNode);
}
#header,
#content,
#some_id{
  margin: 1em 0;
  padding: 0.2em;
  border: 2px grey solid;
}

#header::after,
#content::after,
#some_id::after{
  content: "DIV with ID=" attr(id);
}
<html>
<head>
</head>
<body>
<div id="header"></div>
<div id="content"></div>
<!-- sidebar place holder: some_id -->
</body>
</html>


Edit: use a NodeIterator instead of a TreeWalker

Answer

If you use jQuery, you can do the following to get all comment nodes

comments = $('*').contents().filter(function(){ return this.nodeType===8; })

If you only want the comments nodes of the body, use

comments = $('body').find('*').contents().filter(function(){
     return this.nodeType===8;
 })

If you want the comment strings as an array you can then use map:

comment_strings = comments.map(function(){return this.nodeValue;})
Answer

This is an old question, but here's my two cents on DOM "placeholders" IMO a comment element is perfect for the job (valid html, not visible, and not misleading in any way). However, traversing the dom looking for comments is not necessary if you build your code the other way around.

I would suggest using the following method:

  1. Mark the places you want to "control" with markup of your choice (e.g a div element with a specific class)

    <div class="placeholder"></div>
    <div class="placeholder"></div>
    <div class="placeholder"></div>
    <div class="placeholder"></div>
    <div class="placeholder"></div>
    
  2. Find the placeholders the usual way (querySelector/classSelector etc)

var placeholders = document.querySelectorAll('placeholder');

  1. Replace them with comments and keep reference of those comments:

var refArray = [];

[...placeholders].forEach(function(placeholder){ var comment = document.createComment('this is a placeholder'); refArray.push( placeholder.parentNode.replaceChild(comment, placeholder) ); });

at this stage your rendered markup should look like this:

<!-- this is a placeholder -->
<!-- this is a placeholder -->
<!-- this is a placeholder -->
<!-- this is a placeholder -->
<!-- this is a placeholder -->
  1. Now you can access each of those comments directly with your built refArray and do whatevere it is you wanna do... for example:

replace the second comment with a headline

let headline = document.createElement('h1'); headline.innerText = "I am a headline!"; refArray[1].parentNode.replaceChild(headline,refArray[1]);

Answer

If you just want to get an array of all comments from a document or part of a document, then this is the most efficient way I've found to do that in modern JavaScript.

function getComments (root) {

    var treeWalker = document.createTreeWalker(
        root,
        NodeFilter.SHOW_COMMENT,
        {
            "acceptNode": function acceptNode (node) {
                return NodeFilter.FILTER_ACCEPT;
            }
        }
    );

    // skip the first node which is the node specified in the `root`
    var currentNode = treeWalker.nextNode();

    var nodeList = [];
    while (currentNode) {

        nodeList.push(currentNode);
        currentNode = treeWalker.nextNode();

    }

    return nodeList;

}

I am getting over 50,000 operations per second in Chrome 80 and the stack and recursion methods both get less than 5,000 operations per second in Chrome 80. I had tens of thousands of complex documents to process in node.js and this worked the best for me.

https://jsperf.com/getcomments/6

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.