Course: JavaScript

Progress (0%)

  1. Foundation

  2. Numbers

  3. Strings

  4. Conditions

  5. Loops

  6. Arrays

  7. Functions

  8. Objects

  9. Exceptions

  10. HTML DOM

  11. CSSOM

  12. Events

  13. Drag and Drop

  14. opt Touch Events

  15. Misc

  16. Project: Analog Clock

Exercise: Redefining insertAdjacentElement()

Exercise 43 Average

Prerequisites for the exercise

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

Objective

Redefine the insertAdjacentElement() method of the Element interface, manually in JavaScript.

Description

In the chapter HTML DOM — Elements, we learnt about the insertAdjacentElement() method of the Element interface.

It allows us to insert an element node inside another element node, either before the element, or after the element, or inside the element as its first child, or inside the element as its last child.

If your understanding of insertAdjacentElement() is a bit rusty, then you should consider learning about it first in the chapter HTML DOM — Elements.

So in this exercise, you have to redefine insertAdjacentElement() as a method of the Element interface, manually in JavaScript.

That is, it must behave exactly like the native insertAdjacentElement() method, as provided by the DOM API via its Element interface.

Although the browser support of insertAdjacentElement() is pretty amazing, we go on to redefine it only for the sake of better understanding the DOM API and JavaScript, in general. A lot of concepts will be put at test while redefining this method in JavaScript.

View Solution

New file

Inside the directory you created for this course on JavaScript, create a new folder called Exercise-43-Redefining-insertAdjacentElement() and put the .html solution files for this exercise within it.

Solution

First, let's gather some implementation details of the method before coding even a single word.

The method requires two arguments: the first one, let's call its position, is meant to be a string specifying the position where to add the new element, while the second one, let's call it element, is the element node to add.

If position is not a string, or not amongst the four valid values 'beforebegin', 'afterbegin', 'beforeend' and 'afterend', an error is to be thrown.

Moreover, if element is not an Element, an error is to be thrown in this case as well.

As for the actual operation of the method, depending on the given position, element is added to the corresponding position adjacent to the calling element node.

Alright, now we could go on and craft up some conditionals to check for the aforementioned conditions in the same order they're written. However, we won't do so, and for good reason.

If we first check if position is a string and then check if it is amongst the four values, then nonetheless, later on, we'd still have to repeat these checks to determine the exact value of position and take a consequent action.

In other words, we'd have to repat stuff, which is not desirable at all.

So for now, we'll ignore the check for an invalid position, and rather proceed with a check for an invalid element argument. That seems reasonable because we won't have to check for element again later on.

The idea is to check if element is an instance of Element (i.e directly or by means of inheritance) and to throw an error if it ain't.

This is accomplished below:

Element.prototype.insertAdjacentElement = function(position, element) {
   if (!(element instanceof Element)) {
      throw new TypeError('Invalid second argument provided. It must implement the Element interface.');
   }
}

The error message can be any string of text, as long as you make sure that it easily describes the error.

Great so far.

Now the next step is to check position, right?

Absolutely yes. But we'll take a different approach here.

Instead of checking right away whether position is valid or not, we'll check it against 'beforebegin', then against 'afterbegin', then against 'beforeend', and then finally against 'afterend'.

If any of these values does match position, we'll take the corresponding node-insertion action right away.

On the other hand, if none of them matches, it means that the given position is invalid. Hence, in this last case, we'll throw an error.

Any guesses on how to implement this?

Well, we could use a set of if and else if statements (four, to be precise) to check position against the four values 'beforebegin', 'afterbegin', 'beforeend', and 'afterend', and then an else statement to deal with the invalid-value case and throw an error.

But there is a more suitable conditional construct in JavaScript for such a scenario — a scenario where we want to match a given variable against a couple of values (using ===) and even lay out a fallback set of statements.

Recall anything?

Well, we want the switch statement.

The code below lays out checks for all these four cases and even a default, fallback case, throwing an error:

Element.prototype.insertAdjacentElement = function(position, element) {
   if (!(element instanceof Element)) {
      throw new TypeError('Invalid second argument provided. It must implement the Element interface.');
   }

   switch (position) {
      case 'beforebegin':
         // Code to go here.
         break;

      case 'afterbegin':
         // Code to go here.
         break;

      case 'beforeend':
         // Code to go here.
         break;

      case 'afterend':
         // Code to go here.
         break;

      default:
         throw new SyntaxError("Invalid first argument provided. It must be one of the four values: 'beforebegin', 'afterbegin', 'beforeend', 'afterend'.");
   }
}

Now, it's time for the most important thing, i.e. to write the statements, for each case block, that perform the desired insertion of the given element.

And this just requires knowlegde of the insertBefore() method of the Element interface and the parentNode property of the Node interface. That's it.

The code below completely defines insertAdjacentElement():

Element.prototype.insertAdjacentElement = function(position, element) {
   if (!(element instanceof Element)) {
      throw new TypeError('Invalid second argument provided. It must implement the Element interface.');
   }

   switch (position) {
      case 'beforebegin':
         // Insert element before this node (i.e. the calling element node),
         // as its previous sibling.
         this.parentNode.insertBefore(element, this);
         break;

      case 'afterbegin':
         // Insert element as the first child of this node.
         this.insertBefore(element, this.childNodes[0]);
         break;

      case 'beforeend':
         // Insert element as the last child of this node.
         this.insertBefore(element, null);
         break;

      case 'afterend':
         // Insert element after this node, as its next sibling.
         this.parentNode.insertBefore(element, this.nextSibling);
         break;

      default:
         throw new SyntaxError("Invalid first argument provided. It must be one of the four values: 'beforebegin', 'afterbegin', 'beforeend', 'afterend'.");
   }
}

Perfect!

It's time to test this implementation.

Recall the following code from the chapter HTML DOM — Elements while we were learning about the insertAdjacentElement() method:

<div id="main" style="border: 1px solid red;">
   <h1>A heading</h1>
</div>
function getElementNode(textContent) {
   var paraElement = document.createElement('p');
   paraElement.textContent = textContent;
   return paraElement;
}

var mainElement = document.getElementById('main');

mainElement.insertAdjacentElement('beforebegin', getElementNode('beforebegin position'));
mainElement.insertAdjacentElement('afterbegin', getElementNode('afterbegin position'));
mainElement.insertAdjacentElement('beforeend', getElementNode('beforeend position'));
mainElement.insertAdjacentElement('afterend', getElementNode('afterend position'));

Live Example

Let's now execute this same code with our redefined version of insertAdjacentElement():

Element.prototype.insertAdjacentElement = function(position, element) {
/* ... */
} function getElementNode(textContent) { var paraElement = document.createElement('p'); paraElement.textContent = textContent; return paraElement; } var mainElement = document.getElementById('main'); mainElement.insertAdjacentElement('beforebegin', getElementNode('beforebegin position')); mainElement.insertAdjacentElement('afterbegin', getElementNode('afterbegin position')); mainElement.insertAdjacentElement('beforeend', getElementNode('beforeend position')); mainElement.insertAdjacentElement('afterend', getElementNode('afterend position'));

Live Example

Just to be 100% sure that it's our implementation that's being put in action, we'll add a simple console.log() statement inside the insertAdjacentElement() definition to make a log indicating that our redefined method is executing:

Element.prototype.insertAdjacentElement = function(position, element) {
console.log('Method executing');
/* ... */
} function getElementNode(textContent) { var paraElement = document.createElement('p'); paraElement.textContent = textContent; return paraElement; } var mainElement = document.getElementById('main'); mainElement.insertAdjacentElement('beforebegin', getElementNode('beforebegin position')); mainElement.insertAdjacentElement('afterbegin', getElementNode('afterbegin position')); mainElement.insertAdjacentElement('beforeend', getElementNode('beforeend position')); mainElement.insertAdjacentElement('afterend', getElementNode('afterend position'));

In the link below, open up the console and see any logs there. If there is one, this confirms that it's our implementation that's being invoked in lines 14 - 17.

Live Example

Guess what? We really do witness a console log in the link below, which confirms the execution of our implementation.

And this completes this exercise.