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>
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 }`)
})
})
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)
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>
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;
}
index.css
#app {
margin: 0 auto;
margin-top: 3rem;
padding: 1rem;
max-width: 50rem;
}
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)
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)
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)
}
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
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
}
}
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)
}
}
text
Setting the text of an element is very easy; just set .textContent
.
js/jquery.js
text(value) {
this.node.textContent = value
}
append & prepend
We're going to save append
and prepend
for later, after we implement aNodeCollection
class.
Testing the Node Class
Node
s 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"))
}
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)
})
}
}
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)
})
}
}
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
}
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)
}
...
}
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")
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:
- callback function (should fire when page loads)
- HTML element
- HTML string
- 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])
}
}
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
}
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 NodeList
s, 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
}
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)
try this: