DEV Community

loading...
Cover image for Making a (very) simple jQuery Clone

Making a (very) simple jQuery Clone

Nathan Pham
Mathematician, designer, farmer, student. Engineer would be stretching the titles a bit.
・7 min read

Disclaimer

My only experience with jQuery is stealing borrowing code off of other people's Github repositories and talking about it with some friends. I have never used jQuery in any of my projects (if I did I forgot). That said, let's dream up an interface that uses the iconic dollar sign as a selector function.

What We're Making

Our "jQuery" will have the barebones - methods to attach event listeners, manage CSS, loop through nodes, etc. Honestly this project is relatively useless considering (1) if you wanted to use jQuery you'd use it for all of the bloated but necessary functionality (2) vanilla JS offers similar methods anyways.

Rather than making a fully featured jQuery clone, the goal of this project was to gain more familiarity with ES6 and beyond (spread, classes).

If you're ever stuck or confused, you can always view the code on github.

Button App

The app we're going to be building with our fake jQuery is going to be... a button with a counter. At this point, it's a classic.

index.html

<div id="app"></div>
Enter fullscreen mode Exit fullscreen mode

index.js

$(() => {
  let count = 0
  const app = $("#app")
  const h1 = $("h1")

  app.append($("<button>count: 0</button><p>^ button up above!</p>"))

  const button = $("button")
  button.css({
    backgroundColor: "red",
    borderRadius: "0.5rem",
    fontSize: "1.25rem",
    padding: "0.5rem",
    cursor: "pointer",
    outline: "none",
    border: "none",
    color: "#fff"
  })
  button.on("click", () => {
    button.text(`count: ${ ++count }`)
  })
})
Enter fullscreen mode Exit fullscreen mode

If you tried running js/index.js, you're going to get an error that $ is undefined. In the next few sections, we'll work on implementing a fake version of jQuery.

Folder Structure

index.html
css/
    globals.css
    index.css
js/
    jquery.js
    index.js (fill it in with the demo button app)
Enter fullscreen mode Exit fullscreen mode

HTML Skeleton

Before we go any further, let's quickly set up some HTML we can interact with later on. The CSS files are purely optional; we'll focus on the JavaScript part.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width" />
  <title>jquery-clone</title>
  <link href="/css/globals.css" rel="stylesheet" type="text/css" />
  <link href="/css/index.css" rel="stylesheet" type="text/css" />
  <script src="/js/jquery.js"></script>
  <script src="/js/index.js"></script>
</head>
<body>
  <div id="app">
  </div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

CSS Presets

The CSS files reset the box-sizing to make the elements appear more predictable (stylistically) and added a margin around the #app element to make the website more appealing. As mentioned, CSS isn't necessary for this project.

globals.css

html, body {
  height: 100%;
  width: 100%;
  padding: 0;
  margin: 0;
  box-sizing: border-box;
}

*, ::before, ::after {
  box-sizing: inherit;
}
Enter fullscreen mode Exit fullscreen mode

index.css

#app {
  margin: 0 auto;
  margin-top: 3rem;
  padding: 1rem;
  max-width: 50rem;
}
Enter fullscreen mode Exit fullscreen mode

Fake jQuery

Our jQuery won't contain even half as much as the functionality, community, and code quality as the original. But first, let's define $.

const $ = (q) => document.querySelectorAll(q)
Enter fullscreen mode Exit fullscreen mode

That's basically jQuery in one line, but we're going to need to add a bit more to account for the functions like .css and .text.

Node Class

Instead of directly assigning functions to an HTML object returned from document.querySelectorAll, we're going to make a class.

js/jquery.js

class Node {
  constructor(node) {
    this.node = node // node should be an HTMLElement
  }
  prepend() {}
  append() {}
  text() {}
  css() {}
  on() {}
}

const div = document.createElement("div")
const exampleNode = new Node(div)
Enter fullscreen mode Exit fullscreen mode

on

The on method in Node is very simple. It should accept two parameters - the type of event, and a callback.

js/jquery.js

on(type, callback) {
  document.addEventListener(type, callback)
}
Enter fullscreen mode Exit fullscreen mode

css

CSS is a bit more complicated. As far as I know, the .css function in jQuery has three purposes: to set one style, to set multiple styles, and to retrieve the computed style. The usage would look something like this:

const button = $("button")
button.css("font-size", "20px") // sets font-size to 20xpx
button.css({
  fontFamily: "Verdana",
  color: "red"
}) // sets multiple CSS attributes
button.css("font-family") // retrieves font-family, Verdana
Enter fullscreen mode Exit fullscreen mode

js/jquery.js

css(property, value) {
  if(typeof property == "string") {
    if(!value) {
      // no value means retrieve computed styles
    }
    else {
      // set the element.style.property to value
    }
  }
  else {
    // assume property is an object like {} and assign element.style.key to its respective value
  }
}
Enter fullscreen mode Exit fullscreen mode

We have the basic layout of what .css looks like, we just need to fill it in. While I could easily retrieve the style of an element with this.node.style.property, I opted to use getComputedStyles just in case the style wasn't explicitly set.

js/jquery.js

css(property, value) {
  if(typeof property == "string") {
    if(!value) {
      let styles = window.getComputedStyle(this.node)
      return styles.getPropertyValue(property)
    }
    else {
      this.node.style[property] = value
    }
  }
  else {
    Object.assign(this.node.style, property)
  }
}
Enter fullscreen mode Exit fullscreen mode

text

Setting the text of an element is very easy; just set .textContent.

js/jquery.js

text(value) {
  this.node.textContent = value
}
Enter fullscreen mode Exit fullscreen mode

append & prepend

We're going to save append and prepend for later, after we implement aNodeCollection class.

Testing the Node Class

Nodes accept one parameter for an HTMLElement. The easiest way to test what we currently have is to pass in an element we create with document.createElement.

js/index.js

// we'll implement $(() => { [Document is Ready] }) soon
window.onload = () => {
  let button = document.createElement("button")
  document.body.appendChild(button)

  button = new Node(button)
  button.text("Hello There")
  button.css("padding", "1rem")
  button.on("click", () => console.log("I've been clicked"))
}
Enter fullscreen mode Exit fullscreen mode

We're just testing if the class functions properly, so you can delete the contents of js/index.js once you get it working.

NodeCollection Class

All of the nodes we create will be housed in a NodeCollection class. If only one node is given to a NodeCollection, it will just return the node back. Using a NodeCollection also allows us to loop through the current nodes and implement .each.

js/jquery.js

class NodeCollection {
  constructor(nodes) {
    this.nodes = nodes
    return this.nodes.length <= 1 ? this.nodes.shift() : this
  }
  each(callback) {
    this.nodes.forEach((node, index) => {
      callback(node, index)
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

I'll also add a utility method (using static) that determines if an element is a NodeCollection or not, which will help us when we implement new Node().prepend and new Node().append.

js/jquery.js

class NodeCollection {
  constructor(nodes) {
    this.nodes = nodes
    return this.nodes.length <= 1 ? this.nodes.shift() : this
  }
  static isCollection(nodes) {
    return nodes.constructor.name == "NodeCollection"
  }
  each(callback) {
    this.nodes.forEach((node, index) => {
      callback(node, index)
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

Testing the NodeCollection Class

NodeCollection takes an array of Nodes.

js/index.js

window.onload = () => {
  const collection = new NodeCollection([
    new Node(document.createElement("button")),
    new Node(document.createElement("button"))
  ])

  collection.each((node, i) => {
    // we'd be able to access node.css and node.text in here
    console.log(i)
  })

  console.log(NodeCollection.isCollection(collection)) // prints true
}
Enter fullscreen mode Exit fullscreen mode

append & prepend

With NodeCollection in place, we can implement the .append and .prepend functions in the Node class. Append and prepend should detect if you are trying to add a collection or node, which is why I added the isCollection function earlier first. I used a simple ternary operator to check between the two options.

js/jquery.js

class Node {
  constructor(node) {
    this.node = node
  }
  ... 
  prepend(nodes) {
    NodeCollection.isCollection(nodes)
      ? nodes.each((nodeClass) => this.node.prepend(nodeClass.node))
      : this.node.prepend(nodes.node)
  }
  append(nodes) {
    NodeCollection.isCollection(nodes)
      ? nodes.each((nodeClass) => this.node.append(nodeClass.node))
      : this.node.append(nodes.node)
  }
  ... 
}
Enter fullscreen mode Exit fullscreen mode

A lot of new programmers don't know what a ternary operator is, but it's essentially a condensed if/else statement.

/*
condition
  ? run if condition true
  : run if condition false
*/
true ? console.log("it was true") : console.log("this will never run")
Enter fullscreen mode Exit fullscreen mode

Back to the $

Now that we've implemented the main classes, we can deal with the $. $ should be able to take different kinds of arguments, not just CSS selectors that are passed into document.querySelectorAll. Here are some use cases I covered:

  1. callback function (should fire when page loads)
  2. HTML element
  3. HTML string
  4. string (assume string is a selector, pass into document.querySelectorAll)

$ will only return a NodeCollection or a Node, depending on how many elements are selected. The callback function option won't return anything since we're just waiting for the page to load.

js/jquery

const $ = (query) => {
  if(typeof query == "function") {
    // wait for page to load
    document.addEventListener("DOMContentLoaded", query)
  }
  else if(/<[a-z/][\s\S]*>/i.test(query)) {
    // string contains some kind of HTML, parse it
    return generateCollection(parse(query))
  }
  else if(typeof query == "string") {
    // string is a selector, so retrieve it with querySelectorall
    return generateCollection(document.querySelectorAll(query))
  }
  else if(query.tagName) {
    // you could check the constructor.name for HTMLElement but elements will always have a tagName (like "button" or "a")
    return generateCollection([query]) 
  }
}
Enter fullscreen mode Exit fullscreen mode

We're not quite done yet; we just need to write generateCollection and parse.

Parse

While it would be a fun project to actually parse HTML (either with tokens or Regex), the browser provides a much easier alternative.

js/jquery.js

const parse = (string) => {
  let div = document.createElement("div")
  div.innerHTML = string
  return div.childNodes
}
Enter fullscreen mode Exit fullscreen mode

The browser automatically interprets the HTML that is passed into a new element, making it a useful tool to easily convert an HTML string to real HTML elements.

generateCollection

As the name suggests, generateCollection literally creates a new NodeCollection(). However, whenever we select an HTML element, we don't actually get back an array - we get back a NodeList. While a NodeList is very similar to an array, it doesn't contain all of the methods, like .forEach.

The NodeCollection class doesn't accept NodeLists, it should have an array of Nodes. The easiest way to convert a NodeList into an array is to use the spread operator and "recombine" it back into an array (it would look like [...NodeList]). Afterwards, we can loop through the array with .map and convert everything to a Node.

js/jquery.js

const generateCollection = (nodeList) => {
  const collection = new NodeCollection(
    [...nodeList].map(node => new Node(node))
  )
  return collection
}
Enter fullscreen mode Exit fullscreen mode

Closing

There you have it! A dead simple jQuery clone under 90 lines. Obviously, there are tons of features missing, like the ability to extend the library with plugins. Regardless, making this project was definitely a fun learning experience.

Discussion (1)

Collapse
riviergrullon profile image
Rivier Grullon

try this:

const $ = document.querySelector.bind(document);
Enter fullscreen mode Exit fullscreen mode