Spotlight: jQuery replaceText

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.

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.

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.

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!