Sunday, May 15, 2011

jQuery find/replace text without destroying DOM elements

I found this question on StackOverflow on how to replace text without destroying event handlers, DOM nodes and such. He just wanted to replace a name inside a block of HTML. The name could be inside of an LI, A (link), p or div element, etc. Here is the example HTML markup provided:
<div id="test">
    <h1>An article about John</h1>
    <p>The first paragraph is about John.</p>
    <p>The second paragraph contains a <a href="#">link to John's CV</a>.</p>
    <div class="comments">
        <h2>Comments to John's article</h2>
        <ul>
            <li>Some user asks John a question.</li>
            <li>John responds.</li>
        </ul>
    </div>
</div>
You couldn't simply grab the html() and replace the text because it the result replaces all of the elements inside of "#test" and thus breaks all previously attached functions (click, hover, etc.) or any entered form data inside of inputs (demo):
$('#test').html(function(i, v) {
    return v.replace(/John/g, 'Peter');    
});
Instead you'd have to step through each text node, find the name, then replace it - it's quite a chunk of code. But I think I found a quick and dirty solution (demo) that works on the HTML markup above:
$('#test :not(:has(*))').text(function(i, v) {
  return v.replace(/John/g, 'Peter');    
});
What this does is find all elements that don't have children, and replace the text inside.

To understand what the selector is doing, first imagine the selector like this: "#test *:not(:has(*))". So reading it out in plain language would sound like this: Find the element with an ID of test, then find all elements within it (the first asterisk "*"), then find the opposite ":not()" of all elements that have children ":has(*)", meaning elements that don't have children. Then get the text and replace it.

But this quick and dirty method does have an important limitation which is that it would ignore text inside an element that has child elements. Here is an example of what I mean where the link is treated as a child element and thus the name is not replaced (nothing would change):
<div id="test">
    <p>Visit John's <a href="blog.html">blog</a></p>
</div>
So, if you do have markup like as it is above, then just wrap the name in a span (demo):
<div id="test">
    <p>Visit <span>John's</span> <a href="blog.html">blog</a></p>
</div>
If you don't or can't wrap the name, then you can't use the quick and dirty method I posted above. You'll have to use this textWalk plugin by PatrickDW (a demo, and see his answer for full details) or check out Bobince's answer in this question for a similar method of walking through the text nodes.