Learning JavaScript By Building Your Own Version Of JQuery – Part 4

If you’ve been following along with all the posts in this series, you should now have your own very cutdown version of JQuery called _$. You’re now going to add two functions to that:

  1. _$(selector1).children(selector2) – Finds all direct children of the selector1, filtered by selector2
  2. _$(selector1).find(selector2) – Finds all descendants of the selector1 which match selector2

The best way to really get your head around what these JQuery functions do is to use them. So whip up the following HTML in JSBin and you can do just that:

<!DOCTYPE html>
<html>
<head>
<script src="https://code.jquery.com/jquery-3.1.0.js"></script>
  
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
  <ul id="list">
    <li id="first">1</li>
    <li>
      <ul class="special">
        <li class="special">
          2
        </li>
      </ul>
    </li>
  </ul>
</body>
</html>

You’re going to be focused on the unordered list (ul) element here. We’ll run some .find() and .children() on it with various arguments and see what the results are. In the following example you’ll just print the html for all the elements returned in the console so you can get a good look at what gets returned. You do this by doing an .each() to iterate over all the returned elements, then console.log(this.outerHTML); to output their HTML.

.children() with no arguments:

$('#list').find().each(function () {
  console.log(this.outerHTML);
});

Output:

1. "<li id=\"first\">1</li>"
2. "<li>
      <ul class=\"special\">
        <li class=\"special\">
          2
        </li>
      </ul>
    </li>"

With no selector, .children() returns us all immediate children of the ul, so two li‘s. Note the nested ul with class special is only shown since we are outputting the entire content of both lis, the second of which contains .special. It is not returned as a top level result from the query, as it is not a direct child of #list.

$('#list').find().each(function () {
  console.log(this.outerHTML);
});

Output:


This returns nothing. This is because the .find() function only returns descendants that match the given selector, and we gave no selector. This differs from .children() which doesn’t require a selector match, as it only filters on the selector. The difference is match vs filter.

To get all the descendants you can pass in a match all selector:

$('#list').find('*').each(function () {
  console.log(this.outerHTML);
});

Output:

1. "<li id=\"first\">1</li>"
2. "<li>
      <ul class=\"special\">
        <li class=\"special\">
          2
        </li>
      </ul>
    </li>"
3. "<ul class=\"special\">
        <li class=\"special\">
          2
        </li>
      </ul>"
4. "<li class=\"special\">
          2
        </li>"

Note when you get all descendants this is different than getting all children, and as such we get twice as many results. All descendants is every tag within #list, not just the top level children. So that’s children, and children’s children and children’s children’s children… You get the idea!

Try another selector match with find:

$('#list').find('.special').each(function () {
  console.log(this.outerHTML);
});

Output:

1. "<ul class=\"special\">
        <li class=\"special\">
          2
        </li>
      </ul>"
2. "<li class=\"special\">
          2
        </li>"

As expected, all tags with the class .special are returned. Now let’s see what happens when you try the same selector query with .children():

$('#list').children('.special').each(function () {
  console.log(this.outerHTML);
});

If you were expecting the same result, you’d be wrong. This returns nothing, as there are no direct children of #list with the class .special.

By now you should have a very good idea of what these functions do, and the differences between them. Saddle up, because it’s time to implement them on your _$ JQuery clone.

First here’s a reminder of the code so far, so you can paste it into JSBin:

const buildEventFunction = (parent, selector, eventName) => (handler) => {
  const nodes = document.querySelectorAll(selector);
  [...nodes].forEach(
    (node) => node.addEventListener(eventName, handler)
  );
  return parent;
};

const supportedEvents = [
  'click',
  'blur',
  'focus',
  'keypress',
  'keydown',
  'keyup',
  'dblclick',
  'hover',
  'mousedown',
  'mouseup',
  'mouseover',
  'mouseout'
];

function _$ (selector) {
  const actions = {};
  for (let eventName of supportedEvents) {
    actions[eventName] = buildEventFunction(
      actions,
      selector,
      eventName
    );
  }
  actions.hover = (mouseOverHandler, mouseOutHandler) => {
    buildEventFunction(actions, selector, 'mouseover')(mouseOverHandler);
    if (mouseOutHandler) {
      buildEventFunction(actions, selector, 'mouseout')(mouseOutHandler);
    }
  }
  actions.each = function (callback) {
    const nodes = [...document.querySelectorAll(selector)];
    nodes.forEach((node) => callback.call(node));
  }
  return actions;
}

The clue as to how to implement these functions really rests on working out what they return. Since you can call .each() on them you can determine they must return the same type of object that _$ returns. If _$(‘#list’) returns an object representing everything that matches the #list selector, then _$(‘#list’).children() returns the same type of object, only for the children of #list. From this you can deduce that both our functions, .find() and snip].children()[/snip] should return the result of calling _$ with some value.

This makes the current implementation of _$ problematic. At the moment, it can only take a string representing a selector as an argument. You need some way of getting a _$ result representing the children of the current selector, but there’s no way of generating a selector string for this.

The solution to this is found in the way jQuery itself works. This:

$('a').each(function () {
  console.log(this.innerHTML);
});

Will give the exact same result as:

$(...document.getElementsByTagName('a')).each(function () {
  console.log(this.innerHTML);
});

It turns out as well as being able to pass in a selector string, jQuery can also take an array of DOM nodes, or in this case a NodeList (an array like object containing DOM nodes). Can you see the solution to our problem here?

If your _$ function could also take an array of nodes instead of just a selector string, then children() could just find all child nodes that match the filter and return the result of passing that array of nodes to _$. You would then have an object loaded with the correct nodes, on which you could call .each() or .click() etc. A similar thing could be done for .find().

You need to modify your _$ so it can take an array of DOM nodes, or a selector string. You can do this by checking if the selector is a string. If it is, look up the selector and store it in a NodeList. If not, assume it’s already a NodeList. You can then use this NodeList in the existing .each() and event handler functions, rather than previously where you used querySelectorAll() in these functions to look up a NodeList based on the string selector. Don’t be lazy! Try making these changes yourself in a JSBin before checking out the solution below.

Here’s what your new code should look like. I have included comments to draw your attention to all the changes.

// Change: This function just takes a NodeList or array of nodes now
// and iterates them instead of looking up a selector
const buildEventFunction = (parent, elems, eventName) => (handler) => {
  elems.forEach(
    (node) => node.addEventListener(eventName, handler)
  );
  return parent;
};

const supportedEvents = [
  'click',
  'blur',
  'focus',
  'keypress',
  'keydown',
  'keyup',
  'dblclick',
  'hover',
  'mousedown',
  'mouseup',
  'mouseover',
  'mouseout'
];

function _$ (selector = '') {
  // Change: Get a NodeList if the argument is a string
  // Otherwise you assume it's already a NodeList or array of nodes
  let elems;
  if (typeof selector === 'string') {
     elems = document.querySelectorAll(selector);
  } else {
    elems = selector;
  }
  const actions = {};
  for (let eventName of supportedEvents) {
    actions[eventName] = buildEventFunction(
      actions,
      elems, // Change: You pass in a NodeList / array of nodes instead of selector
      eventName
    );
  }
  actions.hover = (mouseOverHandler, mouseOutHandler) => {
    // Change: You pass in a NodeList / array of nodes instead of selector
    buildEventFunction(actions, elems, 'mouseover')(mouseOverHandler);
    if (mouseOutHandler) {
      // Change: You pass in a NodeList / array of nodes instead of selector
      buildEventFunction(actions, elems, 'mouseout')(mouseOutHandler);
    }
  }
  actions.each = function (callback) {
    // Change: You pass in a NodeList / array of nodes instead of selector
    elems.forEach((node) => callback.call(node));
  }
  return actions;
}

I was going to walk you through the implementations of both .find() and .children() in this post, but laying the groundwork has taken a lot longer than I anticipated. Since I am lazy I don’t want to overload you with information, I’ve decided to leave that for the final post. Why don’t you challenge yourself by trying to do the functions yourself before then? You learn code by writing it, after all.

Leave a Reply

Your email address will not be published. Required fields are marked *