Course: JavaScript

Progress (0%)

Exercise: Text Content

Exercise 40 Average

Prerequisites for the exercise

  1. HTML DOM — The Node Interface
  2. HTML DOM — Basics
  3. All previous chapters

Objective

Manually define the textContent property on the Node interface.

Description

As we saw in the previous HTML DOM — The Node Interface chapter, the textContent accessor property of the Node interface returns back the textual content of a given node.

Its value depends on the type of the underlying node:

  • For text and comment nodes, it's just the nodeValue property the node.
  • For element nodes, it's the concatenation of the textContent of each of its children excluding comment nodes.

In this exercise, you have to redefine textContent on the Node interface based on the discussion above.

You don't need to implement textContent in a way such that it takes into account all the different types of nodes. Rather, you only need to consider text, comment and element nodes.

That's it.

View Solution

New file

Inside the directory you created for this course on JavaScript, create a new folder called Exercise-40-Text-Content and put the .html solution files for this exercise within it.

Solution

The textContent property is an accessor property of the Node interface that can be get as well as set.

Likewise, first let's set up the code that defines it as one:

Object.defineProperty(Node.prototype, 'textContent', {
   get: function() {},
   set: function(value) {}
});

Now, let's deal with the getter function of this property.

The idea is that if the calling node is a text node or a comment node, textContent should just evaluate down to the node's nodeValue property.

This is accomplished below:

Object.defineProperty(Node.prototype, 'textContent', {
   get: function() {
      var nodeType = this.nodeType;

      if (nodeType === Node.TEXT_NODE || nodeType === Node.COMMENT_NODE) {
         return this.nodeValue;
      }
   },

   set: function(value) {}
});

Otherwise, if the node is an element node, textContent should evaluate down to the concatenation of the textContent of each of its children, excluding comment nodes. Obviously, no one wants to see comments appearing in the textual content of an element.

This hints us at a for loop iterating over all the childNodes of the calling element node and then concatenating its textContent value with an accumulator string variable if it's not a comment node.

In the code below, we solve this very case:

Object.defineProperty(Node.prototype, 'textContent', {
   get: function() {
      var nodeType = this.nodeType;

      if (nodeType === Node.TEXT_NODE || nodeType === Node.COMMENT_NODE) {
         return this.nodeValue;
      }

      else if (nodeType === Node.ELEMENT_NODE) {
         var text = '';
         var childNodes = this.childNodes;

         for (var i = 0, len = childNodes.length; i < len; i++) {
            if (childNodes[i].nodeType !== Node.COMMENT_NODE) {
               text += childNodes[i].textContent;
            }
         }
         return text;
      }
   },

   set: function(value) {}
});

And this completes the getter function. Now over to the setter function.

When textContent is set, if the calling node is a text node or a comment node, it's nodeValue property is modified. Changing nodeValue automatically triggers the browser's mutation algorithms if the need be (i.e. updating the user interface if a text node is visible on the screen).

Let's get done with this first:

Object.defineProperty(Node.prototype, 'textContent', {
   get: function() { /* ... */ },

   set: function(value) {
      var nodeType = this.nodeType;

      if (nodeType === Node.TEXT_NODE || nodeType === Node.COMMENT_NODE) {
         this.nodeValue = value;
      }
   }
});

So far, so good.

On the other hand, if the calling node instance is an element node, setting textContent effectively removes all of its children and in turn adds just one single text node. Note that a text node is added only if it's non-empty (i.e. not equal to '').

This gives us the following code:

Object.defineProperty(Node.prototype, 'textContent', {
   get: function() { /* ... */ },

   set: function(value) {
      var nodeType = this.nodeType;

      if (nodeType === Node.TEXT_NODE || nodeType === Node.COMMENT_NODE) {
         this.nodeValue = value;
      }

      else if (nodeType === Node.ELEMENT_NODE) {
         // Remove all children.
         while (this.firstChild) {
            this.removeChild(this.firstChild);
         }

         if (value !== '') {
            this.appendChild(document.createTextNode(value));
         }
      }
   }
});

Let's now test this textContent property on a variety of nodes in a document to see whether it really works the way the native textContent property works.

Consider the following HTML document:

<div id="main">
   <p>A paragraph</p>
   <!--A comment-->
</div>

First, we'll test the native textContent property (without putting the JavaScript code above in place):

var mainElement = document.getElementById('main')
undefined
mainElement.textContent
'\n   A paragraph\n   \n'
mainElement.childNodes[0].textContent
'\n   '
mainElement.childNodes[1].textContent
'A paragraph'
mainElement.childNodes[2].textContent
'\n   '
mainElement.childNodes[3].textContent
'A comment'
mainElement.childNodes[4].textContent
'\n'

Now, let's put our JavaScript code in action and test the manual textContent property:

var mainElement = document.getElementById('main')
undefined
mainElement.textContent
'\n   A paragraph\n   \n'
mainElement.childNodes[0].textContent
'\n   '
mainElement.childNodes[1].textContent
'A paragraph'
mainElement.childNodes[2].textContent
'\n   '
mainElement.childNodes[3].textContent
'A comment'
mainElement.childNodes[4].textContent
'\n'

Voila! Our textContent works exactly like the native textContent property.

And with this, we've completed our exercise.

"I created Codeguage to save you from falling into the same learning conundrums that I fell into."

— Bilal Adnan, Founder of Codeguage