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.
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')
'\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')
'\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.