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

In part one you built a basic copycat JQuery function (_$) with the ability to add handlers for click events. In this post you’ll expand upon this to allow _$ to be used to add handlers for multiple types of event.

The code so far should look like so:

function _$ (selector) {
  const actions = {
     click: (handler) => {
       const nodes = document.querySelectorAll(selector);
       [...nodes].forEach((node) => node.addEventListener('click', handler));
       return actions;
     }
  };
  return actions;
}

You won’t be implementing the entire JQuery events API (although if you wish to, feel free), but I’ve chosen a handful:

  • click (done)
  • blur
  • focus
  • keypress
  • keydown
  • keyup
  • dblclick
  • hover
  • mousedown
  • mouseup
  • mouseover
  • mouseout

A naive way to implement blur would be:

function _$ (selector) {
  const actions = {
     click: (handler) => {
       const nodes = document.querySelectorAll(selector);
       [...nodes].forEach((node) => node.addEventListener('click', handler));
       return actions;
     },
     blur: (handler) => {
      const nodes = document.querySelectorAll(selector);
      [...nodes].forEach((node) => node.addEventListener('blur', handler));
      return actions;
     }
  };
  return actions;
}

_$('button')
  .blur(function () { console.log('blur', this); })
  .click(() => alert('clicked'));

I say naive because if you did it this way, you’d have to write out about half a dozen lines of code once per event type, with each time the code being almost identical to the last. Any time you have a situation where you find yourself copy / pasting the same block of code multiple times and making small changes for the few parts that are variable, you’ve almost certainly fucked up. If there’s one thing software excels at, it’s automating tasks which are similar each time but have a few variable inputs. In these cases we let the software do the repetitive work so we don’t have to. The solution is to extract the repetitive code block into a reusable function.

You do this using a two stage process. First, copy the code you wish to reuse into the body of a new function:

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

Here we’ve simply moved the body of the click function outside of the _$ function into it’s own function called buildEventFunction. Step two is to identify the parts of the function body which need to be variable. These are all the things that will change each time. You then need to replace these with variables which will be arguments to the function. We can identify the following variables:

  • selector: This is already a variable, but before you had access to the selector variable that was passed into the _$ function. Since you are definingĀ buildEventFunction outside of _$, you no longer have access to this, so need to pass selector in as a function argument to buildEventFunction.
  • eventName: The name of the event to listen for. This is hardcoded in as ‘click’ at the moment.
  • parent: The parent object to return. You currently have return actions;. This has the same problem as the selector variable, we no longer have access to it, so it needs to be passed in as an argument. I think parent is a good choice of name.

If you add in these new arguments, you’ll be left with the following function:

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

This isn’t quite right though. Can you see the problem here? Each event function needs to be a function which takes just an event handler, after all you call it as such: _$(‘a’).click(handlerFunction). People using $_ won’t be passing in parent, eventName and selector with the call to click. The solution is that what we actually need buildEventFunction to do is take in the parent, eventName and selector arguments, and return a function which takes an event handler and applies it as required.

In JavaScript functions can be used as variables. This means they can be passed as arguments to functions (we do this when we pass in a click handler as the handler argument), and they can also be returned from functions. Functions which have functions as arguments or as the return type are called higher order functions. Fancy name, but that’s all it means.

If you make buildEventFunction return the function to add the event handler, it will look like this:

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

Don’t let the arrow function syntax confuse you here, all it means is:

// outer: args for outer function
// inner: args for inner function
const myFunctionWhichReturnsAFunction = (outer) => (inner) => {
  // This function body will be the function body of the function
  // which is returned
}
[/script]

If you're still scratching your head, here's the code using standard functions instead of arrow functions. This should make things very clear:


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

Now you have a function capable of building functions that allow event handlers to be added to selectors. Next you need to use this to add a function to the object which _$ returns for each event type we wish to handle. I think it’s best to start with a list of all these event types:

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

If you’re sharp you will have noticed that ‘hover’ is missing from the list. This is deliberate. The following code won’t work:

node.addEventListener('hover', handler)

This is because there’s no such browser event ‘hover’. JQuery just adds this as an alias for mouseover & mouseout. It also does not fit our buildEventFunction since hover can take two callbacks: one for mouseover and one for mouseout. We will add hover as a special case later.

For the events that do fit this way of doing things, you need to iterate the event list and add a function for each to the actions object. Your code should look like this:

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',
  'mousedown',
  'mouseup',
  'mouseover',
  'mouseout'
];

function _$ (selector) {
  const actions = {};
  for (let eventName of supportedEvents) {
    actions[eventName] = buildEventFunction(
      actions,
      selector,
      eventName
    );
  }
}

The code in $_ now creates a new object. It then iterates through each item in the supportedEvents array using the for .. in syntax. For each eventName in the array it adds a function using the buildEventFunction function.

To test that you’ve got this working correctly, you can do the following:

_$('a')
  .mousover(function () { console.log('mouse over'); })
  .click(() => alert('clicked'));

Now when you mouseover any links you’ll get ‘mouse over’ logged in the console. If you click a link you’ll get an alert popup.

Finally you need to implement hover. In JQuery, .hover() takes either one or two arguments. If one argument is passed, this is used as a handler for a mouseover event on matching elements. If there is a second argument, this will be used as a handler for the mouseout event.

Here’s how you can add the hover function to _$:

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);
    }
  }
  return actions;
}

You can then use the hover function as follows:

_$('a')
  .hover(
    function () { console.log('mouse over'); },
    function () { console.log('mouse out'); }
);

Now when you hover over any links you should get ‘mouse over’ in the console, then when you move the mouse off them you should get ‘mouse out’.

That concludes this post. If you are unsure of anything, or need more detail in a certain area, do post a comment, I will respond with help.

Next time you’ll enhance your cut down version of JQuery by adding a feature which allows users to iterate over all the elements of a selector. I’ll show you a practical application of this as you’ll use it to read all of the values in a form.

One comment

Leave a Reply

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