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

Last time you built upon the cutdown _$ object you created in Part 1 by adding the ability to add handlers for many event types in a very efficient manner. This time we’ll add our own equivalent of JQuery’s .each() function.

Use JSBin to create an HTML document as follows:

<!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>
  <form id="login-form">
    <div>
      <input type="email" placeholder="Email Address" name="email" />
    </div>
    <div>
      <input type="password" placeholder="Password" name="password" />
    </div>
     <div>
      <input
        id="rememberMe"
        type="checkbox"
        checked="checked"
        name="rememberMe"
        value="true"
      />
      <label for="rememberMe">Remember Me?</label>
    </div>
  </form>
  <div>
    <button id="login-button">Login</button>
   </div>
</body>
</html>

If you’ve done that all correctly, the output should look something like this:

Notice the button is outside of the form, this is so it doesn’t submit when clicked.

The goal is to output the form values to the console in the format { fieldName: fieldValue } when the button is clicked. The following JQuery code will do this:

$('#login-button').click(function () {
  const formData = {};
  $('#login-form input').each(function () {
    formData[this.name] = this.value;
  });
  console.log(formData);
});

When the login button is clicked, the code iterates all input elements within the form. It builds up an object in the format: { fieldName: fieldValue } and logs it to the console. When you click the button you should see something like:

There is a problem here. If you are familiar with how checkboxes work you may already know what it is. Uncheck the checkbox and resubmit. You will notice the value of remeberMe is still “true”. This is because a checkbox always just gives you it’s value attribute when you use checkbox.value, whether it’s checked or not. This means, unlike with text inputs, field.value is not going to be useful to you. You want the value to be a boolean that is true of it’s checked, otherwise false. There may be a similar problem with other input types to (selects for example). What we really need is something that will give us a useful value for the field, given the HTML element for that field.

This functionality is crying out to be it’s own self contained function. Whenever you have something that can be self contained, performs a very specific and narrow task, and has the potential to be used many times and in different places, it’s best to implement it as a function. This is good software engineering practice.

This function should do the trick:

const getUsefulInputValue = (input) =>
  (input.type === 'checkbox') ? input.checked : input.value;

The function takes one argument, which is the DOM element representing an input who’s value we want to know. If it’s a checkbox it returns input.checked which is a boolean telling us whether the checkbox is checked. If it isn’t a checkbox it will return the input’s value.

You can now integrate this into our call to .each() like so:

$('#login-button').click(function () {
  const formData = {};
  $('#login-form input').each(function () {
    formData[this.name] = getUsefulInputValue(this);
  });
  console.log(formData);
});

Now we get better output for the remeberMe checkbox when we click the button:

Right, you’ve now achieved what we wanted using JQuery. Time for you to implement your own version of .each() using _$.

The function needs to do the following:

  • Get the list of DOM nodes that match the selector passed to _$
  • Iterate the list, running the callback function passed to .each() for each node.
  • The callback must be executed in the context of the current DOM node so the value of this (aka the context) within the function contains the current DOM node.
  • If you’re unsure what the last point means, read my post on mastering the ‘this’ variable in JavaScript.

    Your each function should look something like this:

    function each (callback) {
      const nodes = [...document.querySelectorAll(selector)];
      nodes.forEach((node) => callback.call(node));
    }
    

    Note the callback.call(node). This is how you ensure that the value of this within the callback is the current node, as it calls the callback in the context of node.

    The only problem you have now is that this function uses selector, but it’s not passed in as an argument. If we define each inline within the _$ function then this won’t be a problem, as each will form a closure that contains this variable. The full code with each integrated 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',
      '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;
    }
    

    You are now ready to swap out JQuery for your own function and it will work the same:

    const getUsefulInputValue = (input) =>
      (input.type === 'checkbox') ? input.checked : input.value;
    
    _$('#login-button').click(function () {
      const formData = {};
      _$('#login-form input').each(function () {
        formData[this.name] = getUsefulInputValue(this);
      });
      console.log(formData);
    });
    

    Next time I’ll show you how to implement functionality to search within the current selector by replicating the functionality of JQuery’s .find() and .children() methods.

One comment

Leave a Reply

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