Finding Elements page

Learn how to create a basic jQuery constructor function that can find elements in the page and manipulate them.

Overview

We will learn to:

  • Find elements in the document
  • Create the $ function
  • Create text/html/val functions
  • Find elements from an element and create collection.find
  • Create a find function

Video

Slides

Setup

Run the following example in CodePen:

<div id="qunit"></div>
<div id="qunit-fixture"></div>
<link rel="stylesheet" href="//code.jquery.com/qunit/qunit-1.12.0.css" />
<script src="//code.jquery.com/qunit/qunit-1.12.0.js"></script>
<script src="//bitovi.github.io/academy/static/scripts/jquery-test.js"></script>
<script type="module">
  (function () {
    $ = function (selector) {};

    $.extend = function (target, object) {
      for (var prop in object) {
        if (object.hasOwnProperty(prop)) {
          target[prop] = object[prop];
        }
      }
      return target;
    };

    // Static methods
    $.extend($, {
      isArray: function (obj) {
        return Object.prototype.toString.call(obj) === "[object Array]";
      },
      isArrayLike: function (obj) {
        return (
          obj &&
          typeof obj === "object" &&
          (obj.length === 0 ||
            (typeof obj.length === "number" &&
              obj.length > 0 &&
              obj.length - 1 in obj))
        );
      },
      each: function (collection, cb) {
        if ($.isArrayLike(collection)) {
          for (var i = 0; i < collection.length; i++) {
            if (cb.call(this, i, collection[i]) === false) {
              break;
            }
          }
        } else {
          for (var prop in collection) {
            if (collection.hasOwnProperty(prop)) {
              if (cb.call(this, prop, collection[prop]) === false) {
                break;
              }
            }
          }
        }
        return collection;
      },
      makeArray: function (arr) {
        if ($.isArray(arr)) {
          return arr;
        }
        var array = [];
        $.each(arr, function (i, item) {
          array[i] = item;
        });
        return array;
      },
      proxy: function (fn, context) {
        return function () {
          return fn.apply(context, arguments);
        };
      },
    });

    $.extend($.prototype, {
      html: function (newHtml) {},
      val: function (newVal) {},
      text: function (newText) {},
      find: function (selector) {},
    });
  })();
</script>

Each exercise builds on the previous exercise. There is a completed solution at the end of this page.

Exercise: new $(selector) -> collection

The problem

The $ function creates instances of a jQuery collection of elements. We will change $() to be able to work without new later. But for now, $ will be treated as a constructor function. It should return an instance of $ with:

  • A length property equaling the number of elements matched by the selector.
  • Enumerated properties (ex: "0", "1", "2", etc) whose value is a matched element.

Click to see test code

QUnit.test("new $(selector)", function () {
  document.getElementById("qunit-fixture").innerHTML = `
        <ul id="contacts">
            <li class="contact"/>
            <li class="contact"/>
        </ul>`;

  var $contacts = new $("#contacts li.contact");
  equal($contacts.length, 2, "There are 2 contacts");

  equal($contacts[0].nodeName.toLowerCase(), "li", "got an li");
  equal($contacts[1].className, "contact", "got a .contact");

  ok($contacts instanceof $, "$ is an instance of my_jquery");
});

What you need to know

  • document.querySelectorAll returns a NodeList of elements matching the selector.

    <ul class="dogs">
      <li>fido</li>
      <li>rover</li>
    </ul>
    <ul class="cats">
      <li>sparkles</li>
    </ul>
    <script type="module">
      var elements = document.querySelectorAll(".dogs li");
      console.log(elements); // logs [<li>, <li>]
    </script>
    
  • The [] member operator can create enumerated properties:

    var obj = {};
    obj["0"] = "x";
    obj["1"] = "y";
    
  • Array.prototype.push implementation looks like:

    Array.prototype.push = function () {
      var length = this.length || 0;
      for (var i = 0; i < arguments.length; i++) {
        this[length + i] = arguments[i];
      }
      this.length = length + arguments.length;
    };
    

The solution

Click to see the solution

$ = function (selector) {
  var elements = document.querySelectorAll(selector);
  [].push.apply(this, elements);
};

Exercise: collection.html( [newHtml] )

The problem

collection.html either:

  • gets the HTML contents of the first element in the set of matched elements.

    <ul class="dogs">
      <li><b>f</b>ido</li>
      <li>rover</li>
    </ul>
    <ul class="cats">
      <li>sparkles</li>
    </ul>
    <script type="module">
      import "https://unpkg.com/jquery@3/dist/jquery.js";
      var dogs = $(".dogs li");
      console.log(dogs.html()); // Logs "<b>f</b>ido"
    </script>
    
  • sets the HTML contents of each element in the set of matched elements and returns the collection.
    <ul class="dogs">
      <li>fido</li>
      <li>rover</li>
    </ul>
    <ul class="cats">
      <li>sparkles</li>
    </ul>
    <script type="module">
      import "https://unpkg.com/jquery@3/dist/jquery.js";
      var dogs = $(".dogs li");
      dogs.html("<b>rover</b>"); //-> dogs
    </script>
    

Many of jQuery’s methods either get or set depending on the number of arguments.

Click to see test code

QUnit.test("$.fn.html", function () {
  new $("#qunit-fixture").html(`
        <ul id="contacts">
            <li class="contact"></li>
            <li class="contact"></li>
        </ul>`);
  new $(".contact").html("Hi There");

  equal(
    new $(".contact").html(),
    "Hi There",
    "First contact html set correctly"
  );
  equal(
    new $(".contact:nth-child(2)").html(),
    "Hi There",
    "Second contact html set correctly"
  );
});

What you need to know

The solution

Click to see the solution

      html: function(newHtml) {
        if(arguments.length) {
          return $.each(this, function(i, element) {
            element.innerHTML = newHtml;
          });
        } else {
          return this[0].innerHTML;
        }
      },

Exercise: collection.val( [newValue] )

The problem

collection.val gets the current value of the first element in the set of matched elements or sets the value of every matched element.

<input type="text" class="first" value="first" />
<input type="text" class="second" value="second" />
<script type="module">
  import "https://unpkg.com/jquery@3/dist/jquery.js";

  console.log($(".first").val()); // Log "first"
  $(".second").val("SECOND");
</script>

Click to see test code

QUnit.test("$.fn.val", function () {
  new $("#qunit-fixture").html(`
        <input type="text" class="age"/>
        <input type="text" class="age"/>`);

  equal(new $(".age").val(), "", "Input is initially empty");

  new $(".age").val("Hi There");

  equal(new $(".age").val(), "Hi There", "First .age value set correctly");

  equal(
    new $(".age:nth-child(2)").val(),
    "Hi There",
    "Second .age value set correctly"
  );
});

What you need to know

  • The .value property can be used to read and write an input element’s value.

The solution

Click to see the solution

      val: function(newVal) {
        if(arguments.length) {
          return $.each(this, function(i, element) {
            element.value = newVal;
          });
        } else {
          return this[0].value;
        }
      },

Exercise: $(selector)

The problem

Let’s remove the need to use new when using $.

Click to see test code

QUnit.test("$(selector)", function () {
  document.getElementById("qunit-fixture").innerHTML = `
        <ul id="contacts">
            <li class="contact"/>
            <li class="contact"/>
        </ul>`;

  var $contacts = $("#contacts li.contact");
  equal($contacts.length, 2, "There are 2 contacts");

  equal($contacts[0].nodeName.toLowerCase(), "li", "got an li");
  equal($contacts[1].className, "contact", "got a .contact");

  ok($contacts instanceof $, "instanceof $ without new");
});

What you need to know

  • Use instanceof to tell if something is an instance of something else:

    const Animal = function (name) {
      this.name = name;
    };
    const sponge = new Animal("bob");
    console.log(sponge instanceof Animal); //-> true
    

The solution

Click to see the solution

$ = function (selector) {
  if (!(this instanceof $)) {
    return new $(selector);
  }
  var elements = document.querySelectorAll(selector);
  [].push.apply(this, elements);
};

Exercise: collection.text( [newText] )

The problem

collection.text get the combined text contents of each element in the set of matched elements, including their descendants, or sets the text contents of the matched elements.

<ul class="dogs">
  <li>fido</li>
  <li>rover</li>
</ul>
<script type="module">
  import "https://unpkg.com/jquery@3/dist/jquery.js";
  var $ul = $(".dogs");
  console.log($ul.text()); // Logs "\n\tfido\n\trover\n"

  var dogs = $(".dogs li");
  dogs.text("<b>rover</b>"); //-> dogs
</script>

Click to see test code

QUnit.test("$.fn.text", function () {
  $("#qunit-fixture").html("Hi <span>there</span>.");

  equal($("#qunit-fixture").text(), "Hi there.", "The text is right");

  $("#qunit-fixture span").text("<input/>");

  equal($("#qunit-fixture input").length, 0, "there’s no input");

  equal(
    $("#qunit-fixture span").text(),
    "<input/>",
    "The text is what we sent"
  );
});

What you need to know

  • textContent can get and set the text content of an element.

For an extra challenge, don’t use textContent and recursively collect the textContent. For that, you’ll need to know:

  • Use document.createTextNode to create a text node.

  • Text nodes have a nodeType of 3. You can use Node.TEXT_NODE to get this value.

  • Read nodeValue to get the value of a Text node.

  • Use appendChild to add a text node to another element.

    var textNode = document.createTextNode("My favorite element is <script>");
    
    console.log(textNode.nodeType === Node.TEXT_NODE);
    // logs true
    
    console.log(textNode.nodeValue);
    // logs "My favorite element is <script>"
    
    document.body.appendChild(textNode);
    

The solution

Click to see the solution

      text: function(newText) {
        if (arguments.length) {
          return $.each(this, function(i, element) {
            element.textContent = newText;
          });
        } else {
          return this[0].textContent;
        }
      },

Click to see the extra challenge solution

var getText = function (childNodes) {
  var text = "";
  $.each(childNodes, function (i, child) {
    if (child.nodeType === 3) {
      text += child.nodeValue;
    } else {
      text += getText(child.childNodes);
    }
  });
  return text;
};
      text: function(newText) {
        if (arguments.length) {
          this.html("");
          return $.each(this, function(i, element) {
            const text = document.createTextNode(newText);
            element.appendChild( text );
          });
        } else {
          return getText(this[0]);
        }
      },

Exercise: collection.find( selector )

The problem

collection.find gets the descendants of each element in the current set of matched elements filtered by a selector.

<ul class="level-1">
  <li class="item-i">I</li>
  <li class="item-ii">
    II
    <ul class="level-2">
      <li class="item-a">A</li>
      <li class="item-b">
        B
        <ul class="level-3">
          <li class="item-1">1</li>
          <li class="item-2">2</li>
          <li class="item-3">3</li>
        </ul>
      </li>
      <li class="item-c">C</li>
    </ul>
  </li>
  <li class="item-iii">III</li>
</ul>
<script type="module">
  import "https://unpkg.com/jquery@3/dist/jquery.js";

  $("li.item-ii").find("li").css("border", "solid 1px red");
</script>

Click to see test code

QUnit.test("$.fn.find", function () {
  var $ul = $("#qunit-fixture")
    .html("<ul><li/><li/></ul><ul><li/><li/></ul>")
    .find("ul");

  equal($ul.length, 2, "got 2 uls");
  equal($ul.find("li").length, 4, "got four lis");
});

What you need to know

  • Element.querySelectorAll can be used to find all elements that match a selector who are descendants of the element on which it was called

  • The spread syntax can be used to push multiple items to an array.

    const letters = ["a", "b"];
    const lettersToAdd = ["x", "y"];
    letters.push(...lettersToAdd);
    
    console.log(letters);
    // logs ["a","b","x","y"]
    

This can be done with Array.prototype.push.apply in older browsers.

  • You need to make $() accept an array of nodes similar to how jQuery does:

    <div id="first">First</div>
    <span id="second">Second</div>
    <script type="module">
    import "https://unpkg.com/jquery@3/dist/jquery.js";
    
    $([first, second])
      .css( "border", "solid 1px red" );
    </script>
    

The solution

Click to see the solution

$ = function (selector) {
  if (!(this instanceof $)) {
    return new $(selector);
  }
  var elements;
  if (typeof selector === "string") {
    elements = document.querySelectorAll(selector);
  } else if ($.isArrayLike(selector)) {
    elements = selector;
  }
  [].push.apply(this, elements);
};
      find: function(selector) {
        var elements = [];
        $.each(this, function(i, element){
            var result = element.querySelectorAll(selector);
            elements.push(...result);
            // Or elements.push.apply(elements, result);
        })
        return new $(elements);
      }

Bonus Exercise: Eliminate duplicate code in .html, .val, and .text

The problem

Use meta programming techniques to reduce the duplicate code in the .html, .val, and .text functions.

      html: function(newHtml) {
        if(arguments.length) {
          return $.each(this, function(i, element) {
            element.innerHTML = newHtml;
          });
        } else {
          return this[0].innerHTML;
        }
      },
      val: function(newVal) {
        if(arguments.length) {
          return $.each(this, function(i, element) {
            element.value = newVal;
          });
        } else {
          return this[0].value;
        }
      },
      text: function(newText) {
        if (arguments.length) {
          return $.each(this, function(i, element) {
            element.textContent = newText;
          });
        } else {
          return this[0].textContent;
        }
      },

What you need to know

  • You can call a function that returns a function.
    var makeLogger = function (text) {
      return function () {
        console.log(text);
      };
    };
    var logMe = makeLogger("me");
    logMe(); // Logs "me"
    

The solution

Click to see the solution

function makeSimpleGetterSetter(prop) {
  return function (value) {
    if (arguments.length) {
      return $.each(this, function (i, element) {
        element[prop] = value;
      });
    } else {
      return this[0][prop];
    }
  };
}
      html: makeSimpleGetterSetter("innerHTML"),
      val: makeSimpleGetterSetter("value"),
      text: makeSimpleGetterSetter("textContent"),

Complete Solution

Click to see completed solution

<div id="qunit"></div>
<div id="qunit-fixture"></div>
<link rel="stylesheet" href="//code.jquery.com/qunit/qunit-1.12.0.css" />
<script src="//code.jquery.com/qunit/qunit-1.12.0.js"></script>
<script src="//bitovi.github.io/academy/static/scripts/jquery-test.js"></script>
<script type="module">
  (function () {
    $ = function (selector) {
      if (!(this instanceof $)) {
        return new $(selector);
      }
      var elements;
      if (typeof selector === "string") {
        elements = document.querySelectorAll(selector);
      } else if ($.isArrayLike(selector)) {
        elements = selector;
      }
      [].push.apply(this, elements);
    };

    $.extend = function (target, object) {
      for (var prop in object) {
        if (object.hasOwnProperty(prop)) {
          target[prop] = object[prop];
        }
      }
      return target;
    };

    // Static methods
    $.extend($, {
      isArray: function (obj) {
        return Object.prototype.toString.call(obj) === "[object Array]";
      },
      isArrayLike: function (obj) {
        return (
          obj &&
          typeof obj === "object" &&
          (obj.length === 0 ||
            (typeof obj.length === "number" &&
              obj.length > 0 &&
              obj.length - 1 in obj))
        );
      },
      each: function (collection, cb) {
        if ($.isArrayLike(collection)) {
          for (var i = 0; i < collection.length; i++) {
            if (cb.call(this, i, collection[i]) === false) {
              break;
            }
          }
        } else {
          for (var prop in collection) {
            if (collection.hasOwnProperty(prop)) {
              if (cb.call(this, prop, collection[prop]) === false) {
                break;
              }
            }
          }
        }
        return collection;
      },
      makeArray: function (arr) {
        if ($.isArray(arr)) {
          return arr;
        }
        var array = [];
        $.each(arr, function (i, item) {
          array[i] = item;
        });
        return array;
      },
      proxy: function (fn, context) {
        return function () {
          return fn.apply(context, arguments);
        };
      },
    });

    $.extend($.prototype, {
      html: function (newHtml) {
        if (arguments.length) {
          return $.each(this, function (i, element) {
            element.innerHTML = newHtml;
          });
        } else {
          return this[0].innerHTML;
        }
      },
      val: function (newVal) {
        if (arguments.length) {
          return $.each(this, function (i, element) {
            element.value = newVal;
          });
        } else {
          return this[0].value;
        }
      },
      text: function (newText) {
        if (arguments.length) {
          return $.each(this, function (i, element) {
            element.textContent = newText;
          });
        } else {
          return this[0].textContent;
        }
      },
      find: function (selector) {
        var elements = [];
        $.each(this, function (i, element) {
          var result = element.querySelectorAll(selector);
          elements.push(...result);
          // Or elements.push.apply(elements, result);
        });
        return new $(elements);
      },
    });
  })();
</script>