Spotlight: jQuery replaceText

Spotlight: jQuery replaceText

Tutorial Details
  • Topic: JavaScript
  • Difficulty: Intermediate
  • Estimated completion time: 20 minutes

Final Product What You'll Be Creating

Every other week, we’ll take an ultra focused look at an interesting and useful effect, plugin, hack, library or even a nifty technology. We’ll then attempt to either deconstruct the code or create a fun little project with it.

Today, we’re going to take a look at the excellent replaceText jQuery plugin. Interested? Let’s get started after the jump.


A Word from the Author

As web developers, we have access to a staggering amount of pre-built code, be it a tiny snippet or a full fledged framework. Unless you’re doing something incredibly specific, chances are, there’s already something prebuilt for you to leverage. Unfortunately, a lot of these stellar offerings languish in anonymity, specially to the non-hardcore crowd.

This series seeks to rectify this issue by introducing some truly well written, useful code — be it a plugin, effect or a technology to the reader. Further, if it’s small enough, we’ll attempt to deconstruct the code and understand how it does it voodoo. If it’s much larger, we’ll attempt to create a mini project with it to learn the ropes and hopefully, understand how make use of it in the real world.


Introducing replaceText

replaceText Blog post

We’re kicking off things by focusing on Ben Alman’s excellent replaceText plugin. Here is some quick info:

  • Type: Plugin
  • Technology: JavaScript [Built on the jQuery library]
  • Author: Ben Alman
  • Function: Unobtrusive, concise way to replace textual content

The Problem

Replacing content in your page sounds extremely simple. After all, the native JavaScript method replace seems to do the same thing. If you’re feeling particularly lazy, jQuery makes replacing the entire content of the container obscenely easy too.

// Using just replace
$("#container").text().replace(/text/g,'replacement text')

// Replacing the *entire* content of the container
var lazyFool ="entire content with text replaced externally";
$("#container").html(lazyFool);

As the saying goes, just because you can do it doesn’t really mean you should do. Both these methods are generally shunned [outside of edge cases] because they break a bunch of things whilst doing what they do.

The main issue with these approaches is that they flatten the DOM structure effectively screwing up every non-text node the container holds. If you manage to replace the html itself, using innerHTML or jQuery’s html, you’ll still unhook every event handler attached to any of its children, which is a complete deal breaker. This is the primary problem this plugin looks to solve.


The Solution

The best way to deal with the situation, and the way the plugin handles it, is to work with and modify text nodes exclusively.

Text nodes appear in the DOM just like regular nodes except that they can’t contain childnodes. The text they hold can be obtained using either the nodeValue or data property.

By working with text nodes, we can make a lot of the complexities involved with the process. We’ll essentially need to loop through the nodes, test whether it’s a text node and if yes, proceed to manipulate it intelligently to avoid issues.

We’ll be reviewing the source code of the plugin itself so you can understand how the plugin implements this concept in detail.


Usage

Like most well written jQuery plugins, this is extremely easy to use. It uses the following syntax:

$(container).replaceText(text, replacement);

For example, if you need to replace all occurrences of the word ‘val’ with ‘value’, for instance, you’ll need to instantiate the plugin like so:

 $("#container").replaceText( "val", "value" );

Yep, it’s really that simple. The plugin takes care of everything for you.

If you’re the kind that goes amok with regular expressions, you can do that too!

 $("#container").replaceText( /(val)/gi, "value" );
 

You need not worry about replacing content in an element’s attributes, the plugin is quite clever.


Deconstructing the Source

Since the plugin is made of only 25 lines of code, when stripped of comments and such, we’ll do a quick run through of the source explaining which snippet does what and for which purpose.

Here’s the source, for your reference. We’ll go over each part in detail below.

  $.fn.replaceText = function( search, replace, text_only ) {
    return this.each(function(){
      var node = this.firstChild,
        val,
        new_val,
        remove = [];
      if ( node ) {
        do {
          if ( node.nodeType === 3 ) {
            val = node.nodeValue;
            new_val = val.replace( search, replace );
            if ( new_val !== val ) {
              if ( !text_only && /</.test( new_val ) ) {
                $(node).before( new_val );
                remove.push( node );
              } else {
                node.nodeValue = new_val;
              }
            }
          }
        } while ( node = node.nextSibling );
      }
      remove.length && $(remove).remove();
    });
  }; 
  

Right, let’s do a moderately high level run through of the code.

 $.fn.replaceText = function( search, replace, text_only ) {};
 

Step 1 – The generic wrapper for a jQuery plugin. The author, rightly, has refrained from adding vapid options since the functionality provided is simple enough to warrant one. The parameters should be self explanatory — text_only will be handled a bit later.

return this.each(function(){});

Step 2 - this.each makes sure the plugin behaves when the plugin is passed in a collection of elements.

var node = this.firstChild,
        val,
        new_val,
        remove = [];

Step 3 - Requisite declaration of the variables we’re going to use.

  • node holds the node’s first child element.
  • val holds the node’s current value.
  • new_val holds the updated value of the node.
  • remove is an array that will contain node that will need to be removed from the DOM. I’ll go into detail about this in a bit.
if ( node ) {}

Step 4 - We check whether the node actually exists i.e. the container that was passed in has child elements. Remember that node holds the passed element’s first child element.

do{} while ( node = node.nextSibling );

Step 5 - The loop essentially, well, loops through the child nodes finishing when the loop is at the final node.

if ( node.nodeType === 3 ) {}

Step 6 - This is the interesting part. We access the nodeType property [read-only] of the node to deduce what kind of node it is. A value of 3 implies that is a text node, so we can proceed. If it makes life easier for you, you can rewrite it like so: if ( node.nodeType == Node.TEXT_NODE ) {}.

val = node.nodeValue;
new_val = val.replace( search, replace );

Step 7 - We store the current value of the text node, first up. Next, we quickly replace instances of the keyword with the replacement with the native replace JavaScript method. The results are being stored in the variable new_val.

if ( new_val !== val ) {}

Step 8 - Proceed only if the value has changed!

if ( !text_only && /</.test( new_val ) ) {
   $(node).before( new_val );
   remove.push( node );
} 

Step 9a - Remember the text_only parameter. This comes into play here. This is used to specify whether the container should be treated as one which contains element nodes inside. The code also does a quick internal check to see whether it contains HTML content. It does so by looking for an opening tag in the contents of new_val.

If yes, the a textnode is inserted before the current node and the current node is added to the remove array to be handled later.

else {
         node.nodeValue = new_val;
        }

Step 9b – If it’s just text, directly inject the new text into the node without going through the DOM juggling hoopla.

remove.length && $(remove).remove();

Step 10 – Finally, once the loop has finished running, we quickly remove the accumulated nodes from the DOM. The reason we’re doing it after the loop has finished running is that removing a node mid-run will screw up the loop itself.


Project

The small project we’re going to build today is quite basic. Here is the list of our requirements:

  • Primary requirement: Applying a highlight effect to text that’s extracted from user input. This should be taken care of completely by the plugin.
  • Secondary requirement: Removing highlight on the fly, as required. We’ll be drumming up a tiny snippet of code to help with this. Not production ready but should do quite well for our purposes.

Note: This is more of a proof of concept than something you can just deploy untouched. Obviously, in the interest of preventing the article from becoming unweildy, I’ve skipped a number of sections that are of utmost importance for production ready code — validation for instance.

The actual focus here should be on the plugin itself and the development techniques it contains. Remember, this is more of a beta demo to showcase something cool that can be done with this plugin. Always sanitize and validate your inputs!


The Foundation: HTML and CSS

<!DOCTYPE html>  
<html lang="en-GB">  
	<head>
		<title>Deconstruction: jQuery replaceText</title>
		<link rel="stylesheet" href="style.css" />
	</head>

	<body>
    	<div id="container">
        	<h1>Deconstruction: jQuery replaceText</h1>
		<div>by Siddharth for the lovely folks at Nettuts+</div>
		
		<p>This page uses the popular replaceText plugin by Ben Alman. In this demo, we're using it to highlight arbitrary chunks of text on this page. Fill out the word, you're looking for and hit go. </p>
		
		<form id="search"><input id="keyword" type="text" /><a id="apply-highlight" href="#">Apply highlight</a><a id="remove-highlight" href="#">Remove highlight</a></form>
		<p id="haiz"> <-- Assorted text here --></div>
	<script src="js/jquery.js"></script>
	<script src="js/tapas.js"></script>

	</body>
</html>

The HTML should be pretty explanatory. All I’ve done is create a text input, two links to apply and remove the highlight as well as a paragraph containing some assorted text.

body{
	font-family: "Myriad Pro", "Lucida Grande", "Verdana", sans-serif;
	font-size: 16px;
}

p{
	margin: 20px 0 40px 0;
}
h1{
	font-size: 36px;
	padding: 0;
	margin: 7px 0;
}

h2{
	font-size: 24px;
}

#container{
	width: 900px;
	margin-left: auto;
	margin-right: auto;
	padding: 50px 0 0 0;
	position: relative;
}

#haiz { 
	padding: 20px; 
	background: #EFEFEF; 
	-moz-border-radius:15px;
	-webkit-border-radius: 15px;
	border: 1px solid #C9C9C9; 
}

#search {
	width: 600px; 
	margin: 40px auto; 
	text-align: center; 
}

#keyword { 
	width: 150px; 
	height: 30px; 
	padding: 0 10px; 
	border: 1px solid #C9C9C9; 
	-moz-border-radius:5px;
	-webkit-border-radius: 5px;
	background: #F0F0F0;
	font-size: 18px;
}

#apply-highlight, #remove-highlight { 
	padding-left: 40px; 
}

.highlight { 
	background-color: yellow;
}

Again, pretty self explanatory and quite basic. The only thing to note is the class called highlight that I’m defining. This will be applied to the text that we’ll need to highlight.

At this stage, your page should look like so:

Tutorial image

The Interaction: JavaScript

First order of the day is to quickly hook up our link with their handlers so the text is highlighted and unhighlighted appropriately.

var searchInput = $("#keyword"), 
      searchTerm, 
      searchRegex;  
$("#apply-highlight").click(highLight);
$("#remove-highlight").bind("click", function(){$("#haiz").removeHighlight();});

Should be fairly simple. I declare a few variables for later use and attach the links to their handlers. highLight and removeHighlight are extremely simple functions we’ll look at below.

function highLight() { 
   searchTerm = searchInput.val();
   searchRegex  = new RegExp(searchTerm, 'g');
   $("#haiz *").replaceText( searchRegex, ''+searchTerm+'');
}
  • I’ve chosen to create a vanilla function, and not a jQuery plugin, because I’m lazy as a pile of rocks. We start off by capturing the input box’s value.
  • Next up, we create a regular expression object using the search keyword.
  • Finally, we invoke the replaceText plugin by passing in the appropriate values. I’m choosing to directly include searchTerm in the markup for brevity.
jQuery.fn.removeHighlight = function() {
   return this.find("span.highlight").each(function() {
      with (this.parentNode) {
         replaceChild(this.firstChild, this);
      }
 })
};

A quick and dirty, hacky method to get the job done. And yes, this is a jQuery plugin since I wanted to redeem myself. The class is still hardcoded though.

I’m merely looking for every span tag with a class of highlight and replacing the entire node with the value it contains.

Before you get your pitchforks ready, remember that this is just for demonstration purposes. For your own application, you’ll need a much more sophisticated unhighlight method.


Wrapping Up

And we’re done. We took a look at an incredibly useful plugin, walked through the source code and finally finished by creating a mini project with it.

Siddharth is Siddharth on Codecanyon
Tags: jQuery
Note: Want to add some source code? Type <pre><code> before it and </code></pre> after it. Find out more
  • Ian

    Doesn’t seem to work for some words in Chrome. I tried to find “by” and it came up with nothing. Very nice though!

    • http://tomaszslazok.pl Nexik

      Good article.

      On Chrome 8 everything works.

    • http://www.ssiddharth.com Siddharth
      Author

      Not sure what’s wrong either. Tried it on Chrome 8 and it seemed to work well.

  • http://www.wordimpressed.com Devin Walker

    Awesome use of jQuery

  • Keith

    Tried the demo in Chrome 9 and Firefox 3.6 – works really well, except searching for a period, “.”, or a string of periods, “…”, yields some very strange results.

    Thanks for the great article!

  • http://www.movieviews.be Dennis

    That was just what I needed. Thank you very much.
    I never thought in this direction to solve my problem !

  • http://jonweb.co.uk Jon Cousins

    Type .*/* into the text box and add highlight. Crazy stuff happens/

  • http://jonweb.co.uk Jon Cousins

    Type .*/* into the text box and add highlight. Crazy stuff happens.

    • Ali Baba

      Also try just . and just $. Great code but need fix bugs not for production

      • http://www.subcityart.com Mike

        I must agree this is not ready for production! Too many bugs. Typing () completely crashes the web page and results in some very unwanted affects. Try being your average smart ass when programming, you may yield some pretty interesting results and bugs!

        -Mike

      • http://www.ssiddharth.com Siddharth
        Author

        See my comment below.

  • James

    You should escape the search string before building the RegExp.
    A way to do this is shown in http://simonwillison.net/2006/Jan/20/escape/.
    All this strange results are caused by not escaping the input.
    Remember to sanitize your inputs!

  • http://www.ssiddharth.com Siddharth
    Author

    Hey guys! Quick note. As I mention in the article, this is more of a proof of concept than something you can just deploy untouched. Obviously, in the interest of preventing the article from becoming unweildy, I’ve skipped on the validation part.

    You’ll need to filter out symbols and other assorted characters in your live code. Just because I’m not doing it here doesn’t mean that I’m implying that you shouldn’t either. Remember, this is more of a beta demo than a showcase of pristine code. The actual focus here should be on the plugin itself and the development techniques it contains.

    Thanks for reading!

    • http://www.ssiddharth.com Siddharth
      Author

      I’ve added a quick disclaimer to the post to make sure readers remember to sanitize and validate their inputs in their code. For now, as a showcase, I’m going to leave it as it is.

  • http://distance-calculator.co.za/ distance calculator

    very handy tutorial, thanks :)

  • http://www.wtgif.com Ricardo

    thanks for the tutorial … is there any way to count how many words are going to change?

    Thanks

  • Eugene

    Great scripts and I tried it.

    But I noticed one thing that when your search string contains dollar sign, for example ‘$keyword$’ or ‘$keyword2$’, it doesn’t work at all.

    Any idea with that and how can we fix that?

    Thanks.

  • http://stackoverflow.com/questions/2349138/jquery-find-and-replace-text-without-element-id Steve

    This is great information. Thanks for posting this. As I was snooping around on the web, I came across this:

    $(“*”).contents().each(function() {
    if(this.nodeType == 3)
    this.nodeValue = this.nodeValue.replace(“old”, “new”);
    });

    from this site – http://stackoverflow.com/questions/2349138/jquery-find-and-replace-text-without-element-id

    I tested it and it seems to work. However, will something like this cause the issue of flattening the DOM structure and messing up all event handlers?
    Thanks.

  • Steve

    I forgot to mention that I was trying to hardcode the find/replace text instead of it being dynamic like this tutorial shows.
    But I played it safe and used this plugin…
    http://benalman.com/projects/jquery-replacetext-plugin/

  • BGM

    Hello! I put this in my javascript, but it fails on the two jquery lines. I have other jquery that works just fine. If I comment out the two lines, the alert will fire. If I allow the two lines, the alert fails. Now, I have cut and pasted the minified plugin into my main javascript file.

    What do I do?

    $(‘body *’).replaceText(“#sectiontitle#”, “”);
    $(‘body *’).replaceText(“#/sectiontitle#”, “”);
    alert(“bob”);