completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Build a jQuery-style library

Build a safe chainable $ library with an IIFE, prototypes, plugins, delegated events, and modern DOM alternatives you can compare on a real page.

By the end you can
  • 01
    Keep code privateUse an IIFE and strict mode so the library's helpers stay private and only $ becomes a global.
  • 02
    Build a small chainable libraryWrite a $ function that needs no new, share methods through one prototype, and make setters return this.
  • 03
    Add to libraries and read themAdd plugins with $.fn, keep text safe, and read a real library's code step by step.

What this library is

jQuery is a JavaScript library from 2006. A library is a file of ready-made code that you add to your page. With jQuery you wrote $(".item") to find elements. Then you called a method such as addClass, and it changed all of them at once.

Back then, browsers did not agree with each other. The same job needed different code in different browsers:

  • Finding elements with a CSS selector worked differently.
  • Listening for clicks and key presses worked differently.
  • Asking a server for data (called Ajax) needed special code for each browser.
  • Common page changes took many lines of code.

jQuery hid those differences behind one short set of methods. That is why so many sites used it.

Today browsers agree much more, and they have these tools built in. But building a small jQuery-style library is still a great way to practise ideas from earlier lessons:

The main idea

A jQuery-style mini library is a function, usually named $. You give it a CSS selector, and it returns a wrapper: an object that holds the matching elements. The wrapper has methods that change those elements, read values from them, and listen for events. A method that changes things gives back the same wrapper, so you can call one method right after another.

Real-life analogyA wrapper is a tray of cups

Picture a waiter in a café. She carries several cups on one tray. She takes the tray to the coffee machine, and the machine fills every cup. Then she walks to the milk jug with the same tray in her hands. The café doesn't need one coffee machine per tray. One machine serves every tray.

In real life: The tray holds several cups
In JavaScript: The wrapper holds several elements
In real life: One coffee machine fills the cups on any tray
In JavaScript: One shared method works on any wrapper
In real life: After filling, you still hold the same tray
In JavaScript: A method that changes things returns this, the same wrapper
In real life: The café can add a new machine
In JavaScript: A plugin adds a new method through $.fn

Keep the library's helpers private

FULL SOURCE

Old libraries were added with a plain <script> tag, before JavaScript had modules. Any name a script made at its top level became a global: a name that every script on the page shares. Two scripts could then fight over the same name.

To avoid that, the library puts all its code inside an IIFE. IIFE is short for "Immediately Invoked Function Expression". It is a function that runs right where it is written. Names made inside it stay inside it. The IIFE here does three more things:

  • It takes window and document as parameters. So the first line shows which browser objects the library uses.
  • It turns on "use strict". In strict mode, a typo that would quietly make a new global throws an error instead.
  • It makes exactly one global on purpose, on the last line: window.$ = $.
The whole library, mini-dollar.jsPop out in the code editor (opens in a new tab)JavaScript
(function (window, document) {
  "use strict";

  const previousDollar = window.$;
  const events = new WeakMap();
  const slice = Array.prototype.slice;

  function isElement(value) {
    return value && (value.nodeType === 1 || value === window || value === document);
  }

  function isListLike(value) {
    return value && typeof value !== "string" && typeof value.length === "number";
  }

  function normalize(input, context) {
    if (input == null) return [];
    if (input instanceof Collection) return input.toArray();
    if (typeof input === "string") {
      try {
        return slice.call((context || document).querySelectorAll(input));
      } catch (error) {
        throw new SyntaxError('Invalid selector "' + input + '": ' + error.message);
      }
    }
    if (isElement(input)) return [input];
    if (isListLike(input)) {
      const elements = slice.call(input);
      if (!elements.every(isElement)) {
        throw new TypeError("List input must contain only DOM elements.");
      }
      return elements;
    }
    throw new TypeError("Expected a selector string, DOM element, or array-like collection.");
  }

  function classes(names) {
    return String(names).trim().split(/\s+/).filter(Boolean);
  }

  function remember(element, record) {
    const list = events.get(element) || [];
    list.push(record);
    events.set(element, list);
  }

  function Collection(input, context) {
    const elements = normalize(input, context);
    this.length = elements.length;
    for (let index = 0; index < elements.length; index += 1) {
      this[index] = elements[index];
    }
  }

  function $(input, options) {
    if (options !== undefined && (!options || typeof options !== "object" || Array.isArray(options))) {
      throw new TypeError("Options must be an object.");
    }
    const settings = Object.assign({ root: document }, options);
    if (!settings.root || typeof settings.root.querySelectorAll !== "function") {
      throw new TypeError("options.root must support querySelectorAll().");
    }
    return new Collection(input, settings.root);
  }

  Collection.prototype.toArray = function () {
    return slice.call(this, 0, this.length);
  };

  Collection.prototype.each = function (callback) {
    for (let index = 0; index < this.length; index += 1) {
      callback.call(this[index], this[index], index);
    }
    return this;
  };

  Collection.prototype.map = function (callback) {
    return $(this.toArray().map(function (element, index) {
      return callback.call(element, element, index);
    }).filter(Boolean));
  };

  Collection.prototype.filter = function (test) {
    const predicate = typeof test === "string"
      ? function (element) { return element.matches(test); }
      : test;
    return $(this.toArray().filter(function (element, index) {
      return predicate.call(element, element, index);
    }));
  };

  Collection.prototype.find = function (selector) {
    const found = [];
    this.each(function (element) {
      found.push.apply(found, element.querySelectorAll(selector));
    });
    return $(found);
  };

  Collection.prototype.addClass = function (names) {
    return this.each(function (element) {
      element.classList.add.apply(element.classList, classes(names));
    });
  };

  Collection.prototype.removeClass = function (names) {
    return this.each(function (element) {
      element.classList.remove.apply(element.classList, classes(names));
    });
  };

  Collection.prototype.toggleClass = function (name, force) {
    return this.each(function (element) {
      element.classList.toggle(name, force);
    });
  };

  Collection.prototype.css = function (name, value) {
    if (typeof name === "string" && value === undefined) {
      return this[0] ? window.getComputedStyle(this[0])[name] : undefined;
    }
    const styles = typeof name === "object" ? name : { [name]: value };
    return this.each(function (element) {
      Object.keys(styles).forEach(function (property) {
        element.style[property] = styles[property];
      });
    });
  };

  Collection.prototype.text = function (value) {
    if (value === undefined) return this[0] ? this[0].textContent : "";
    return this.each(function (element) {
      element.textContent = value;
    });
  };

  Collection.prototype.attr = function (name, value) {
    if (value === undefined) return this[0] ? this[0].getAttribute(name) : undefined;
    return this.each(function (element) {
      if (value === null) element.removeAttribute(name);
      else element.setAttribute(name, String(value));
    });
  };

  Collection.prototype.on = function (type, selector, handler) {
    const delegated = typeof selector === "string";
    const listener = delegated ? handler : selector;
    if (typeof listener !== "function") throw new TypeError("on() expects a handler function.");
    return this.each(function (element) {
      const wrapped = function (event) {
        if (!delegated) {
          listener.call(element, event, element);
          return;
        }
        const match = event.target.closest(selector);
        if (match && element.contains(match)) listener.call(match, event, match);
      };
      remember(element, { type: type, selector: delegated ? selector : null, listener: listener, wrapped: wrapped });
      element.addEventListener(type, wrapped);
    });
  };

  Collection.prototype.off = function (type, selector, handler) {
    const delegated = typeof selector === "string";
    const listener = delegated ? handler : selector;
    return this.each(function (element) {
      const list = events.get(element) || [];
      const kept = [];
      list.forEach(function (record) {
        const sameType = !type || record.type === type;
        const sameSelector = !delegated || record.selector === selector;
        const sameListener = !listener || record.listener === listener;
        if (sameType && sameSelector && sameListener) element.removeEventListener(record.type, record.wrapped);
        else kept.push(record);
      });
      events.set(element, kept);
    });
  };

  $.fn = Collection.prototype;
  $.version = "1.0.0";
  $.extend = function (target) {
    const sources = slice.call(arguments, 1);
    if (sources.length === 0) {
      sources.push(target);
      target = $;
    }
    sources.forEach(function (source) {
      Object.keys(source || {}).forEach(function (key) {
        target[key] = source[key];
      });
    });
    return target;
  };
  $.noConflict = function () {
    if (window.$ === $) window.$ = previousDollar;
    return $;
  };

  window.$ = $;
})(window, document);

The code is long. You don't need to read it all now. Each part is explained in its own section below.

A library's public API is the list of names other people are meant to use. This library puts three of them on $ itself, so you can check them:

Check the library's public namesPop out in the code editor (opens in a new tab)JavaScript
console.log($.version);
console.log(typeof $.fn.text);
console.log(typeof $.extend);

It prints 1.0.0, then function twice:

  • $.version is the version number of the library.
  • $.fn holds the methods every wrapper can call, such as text.
  • $.extend is a function that copies extra names onto an object.

These names are a promise to users. The helpers inside the IIFE are not part of that promise.

Now look at the whole library from the outside in. The only global it makes on purpose is window.$ = $. Everything else stays inside the IIFE: events, normalize, classes, and Collection. Code outside can't reach them. This is the same privacy idea as in closure patterns, used for a browser library.

Each row of this table names a risk, what the library does about it, and why that helps.

How the mini library stays safe
What could go wrongWhat the library doesWhy it helps
A user's text could run as HTML.text(value) writes to textContent.The text is shown as plain characters, so no HTML is built and no code runs.
Two scripts could fight over one global name.Only $ becomes global, and noConflict() can give it back.Other scripts keep their names.
Changing built-in prototypes changes every array or element.Methods go on Collection.prototype, never on Array.prototype.Built-in objects work the same for every other script.
A selector could match nothing.Setters loop zero times. Getters return undefined or "".Code can run before the elements exist, without crashing.
The library never changes built-in objects

Patching means adding your own methods to a built-in prototype, such as Array.prototype. This library never does that. It adds methods only to its own Collection.prototype. It never changes Array.prototype, NodeList.prototype, or Element.prototype. So arrays, lists, and elements work the same for every other script on the page. The native prototypes lesson explains this rule in full.

One $ function, shared methods

STEP THROUGH

A factory is a function that makes objects for you, so you don't write new. In this library, $ is the factory. You write $(selector), not new Collection(selector). $ does three jobs:

  • It makes the new Collection(...) call for you.
  • It turns many kinds of input into one plain list of elements.
  • It returns a wrapper with a length and numbered slots 0, 1, and so on, like an array. An object shaped like this is called array-like.

It accepts any of these inputs:

  • a selector string, such as ".item",
  • a single element,
  • a NodeList, the list that querySelectorAll returns,
  • an array of elements,
  • another wrapper.

$ also takes an optional second argument: an options object. Its root option says where to search.

Search inside one area, and find nothingPop out in the code editor (opens in a new tab)JavaScript
const root = document.querySelector(".cards");
const cards = $(".card", { root });
console.log(cards.length);
console.log($(".missing", { root }).length);

Without options, $ searches the whole document. Here root is the .cards area, so $ searches only there:

  • Line 2 finds one .card, so line 3 prints 1.
  • Line 4 searches for .missing, finds nothing, and prints 0.

A selector that matches nothing is not an error. You simply get an empty wrapper.

$ also checks its input. When the input is wrong, it throws an error with a clear message.

Wrong input throws a clear errorPop out in the code editor (opens in a new tab)JavaScript
try {
  $(42);
} catch (error) {
  console.log(error.name);
  console.log(error.message);
}
try {
  $("li[");
} catch (error) {
  console.log(error.name);
  console.log(error.message.startsWith("Invalid selector"));
  console.log(error instanceof SyntaxError);
}

This code has two try blocks. Each one catches an error and prints parts of it:

  • $(42) passes a number. $ can't use a number, so it throws a TypeError. The message lists the inputs it accepts.
  • $("li[") passes a broken selector. $ throws a SyntaxError. Its message starts with Invalid selector, and it really is a SyntaxError, so the last two lines print true.

A clear error tells the user what went wrong, so the bug is quick to find.

Step through the factory below. Watch it make two wrappers, then compare their methods.

Step through the $ factory
Step 0 of 4Ready
Your turn: follow the blue line

Step through how $ makes wrappers that hold elements but share one set of methods.

Running in
  1. script
Next: line 1
const b = $(document.querySelectorAll(".item"));console.log(a.length);console.log(a.addClass === b.addClass);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
$ wraps a selector, an element, or a listPop out in the code editor (opens in a new tab)JavaScript
const items = $(".item");
const button = $(document.querySelector(".save"));
const fromList = $(document.querySelectorAll(".item"));
console.log(items.length);
console.log(button.length);
console.log(fromList[0].textContent.trim());

It prints 2, 1, and Alpha. The selector found two items. The single button became a wrapper of one. The NodeList kept its order, so slot 0 holds the Alpha item.

The methods live on the prototype: one shared object that every wrapper can reach. When you ask a wrapper for addClass, JavaScript doesn't find it on the wrapper itself. So it looks in the prototype and finds it there.

If a page makes 100 wrappers, there is still only one addClass function. You can check that:

Two wrappers share one addClassPop out in the code editor (opens in a new tab)JavaScript
const a = $(".item");
const b = $(".save");
console.log(a.addClass === b.addClass);

It prints true. Both wrappers point to the very same function.

Two wrappers. Wrapper a, made from the .item selector, holds two li elements. Wrapper b, made from the .save selector, holds one button. Both point to the same prototype, which holds one addClass function.0: <li>1: <li>a$(".item")0: <button>b$(".save")addClassprototypeshared by a and b
Each wrapper keeps only its own elements. Both find addClass in one shared prototype, so a.addClass === b.addClass is true.
How real jQuery skipped new

Real jQuery used a hidden constructor called init behind $. A constructor is a function you call with new to build an object. jQuery then set jQuery.fn.init.prototype = jQuery.fn. So objects built by init still found every method in jQuery.fn, and users never wrote new. This lesson uses the simpler return new Collection(...), so the idea is easier to see.

The classic init pattern, shortenedJavaScript
function Dollar(selector) {
  return new Dollar.fn.init(selector);
}
Dollar.fn = Dollar.prototype = {
  constructor: Dollar,
  addClass(name) {
    this[0].classList.add(name);
    return this;
  },
};
Dollar.fn.init = function (selector) {
  this[0] = document.querySelector(selector);
  this.length = this[0] ? 1 : 0;
};
Dollar.fn.init.prototype = Dollar.fn;

Line 2 calls the hidden init with new. Line 11 is init itself. Line 15 points init's prototype at Dollar.fn, so every object it builds shares the same methods.

Calling methods one after another

INTERACTIVE

A chain is several method calls joined by dots, such as $(".item").addClass("active").text("Hi"). You read it from left to right. It works because of one rule:

  • A setter is a method that changes something. It does its work, then returns this, the wrapper it was called on. The next method in the chain runs on that same wrapper.
  • A getter is a method that reads something. text(), attr(name), and css(name) return a value from the first element. A value is not a wrapper, so the chain ends there.

Step through a chain below. Watch each setter hand back the same wrapper.

Step through a chain
Step 0 of 4Ready
Your turn: follow the blue line

Follow $(".item").addClass("active").text("Hi"): make the wrapper, call two setters, and get the same wrapper back each time.

Running in
  1. script
Next: line 1
const result = items  .addClass("active")  .text("Hi");console.log(result === items);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
A chain of three setters, then three readsPop out in the code editor (opens in a new tab)JavaScript
$(".item")
  .addClass("active")
  .attr("data-state", "ready")
  .text("Hi");
console.log($(".item").attr("class"));
console.log($(".item").attr("data-state"));
console.log($(".item").text());

Lines 1 to 4 are one chain with three setters. Lines 5 to 7 read the results back. It prints item active, ready, and Hi.

Notice that text does two jobs. text("Hi") writes. text() reads. Using one name for two jobs is called overloading. It keeps the library short. But you can't tell what a call returns until you count its arguments. A modern API design might use two names instead, such as getText and setText.

Left: calling text with the value Hi writes the text and returns the same wrapper. Right: calling text with no value reads the text and returns the string Hi.$(".item").text("Hi")0: <li>1: <li>wrapperreturns this, the same wrapper$(".item").text()"Hi"returns the first item's text
With a value, text("Hi") writes and hands back the wrapper, so you can call another method. With no value, text() reads and hands back the text, so the chain stops there.
One method name, two jobsPop out in the code editor (opens in a new tab)JavaScript
console.log($(".item").css("color"));
$(".item").css("color", "crimson");
$(".item").css({ backgroundColor: "lemonchiffon" });
console.log($(".item").attr("data-name"));
$(".item").attr("data-name", "renamed");

Line 1 passes one argument, so css reads the color. Lines 2 and 3 write: one with a name and a value, one with an object. Line 4 reads data-name, and line 5 writes it.

Run a tiny $ library on a real list
Playground sourcePop out in the code editor (opens in a new tab)JavaScript
const items = $(".item");items.addClass("active");items.text("Safe text");items.find("span").addClass("label");$.fn.flash = function () {  return this.addClass("highlighted");};items.flash();
The real listonly this box
Try it yourself

Reset builds a real list. Press one button at a time and watch the list change.

This runs the library on a real list in its own box. The $ here only searches inside this box, so the rest of the page is not touched. Try typing HTML into the text field: it shows up as plain text.

Changing elements and listening for clicks

The methods that change elements are short on purpose. Each one hands the work to a tool the browser already has:

  • addClass, removeClass, and toggleClass use classList, the element's list of CSS classes.
  • css writes style properties, such as color.
  • attr reads and writes attributes, such as data-name.
  • find searches inside each wrapped element.

These build on modifying the DOM and styles and classes.

The text(value) setter writes to textContent, not innerHTML. The two are different:

  • textContent shows a string as plain text.
  • innerHTML reads a string as HTML and builds elements from it.

So with text, a string like <img src=x onerror=alert(1)> shows up as those characters on the page. No image is made, and no code runs. This matters for text that came from a user. The later XSS lesson explains why turning a user's text into HTML is dangerous.

Delegation means one listener on a parent element handles events for its children. It works because a click on a child also reaches its parents. This is called bubbling. The library's on(type, selector, handler) uses delegation:

  • It adds one listener to the parent.
  • When a click arrives, it calls event.target.closest(selector). closest starts at the clicked element and walks up through its parents until it finds a match.
  • If it finds one, it calls your handler with this set to the matching child.
One listener on the list hears a click on BetaPop out in the code editor (opens in a new tab)JavaScript
const list = $(".todo-list");
let heard = "";
function rememberClick(event) {
  heard = this.dataset.name;
}
list.on("click", ".item", rememberClick);
document.querySelector('[data-name="Beta"]').click();
list.off("click", ".item", rememberClick);
console.log(heard);

Line 6 adds one listener to the list. Line 7 clicks the Beta item. Line 8 removes the listener. Inside rememberClick, this is the Beta item, so heard becomes "Beta". Line 9 prints Beta.

A list, ul.todo-list, holds one listener called rememberClick and two items, Alpha and Beta. A click on Beta goes up to the listener. The listener finds Beta and the code prints Beta.ul.todo-listone listenerrememberClickAlphaBetaclickBetaprinted
The click on Beta travels up to the list, where the one listener waits. closest(".item") finds Beta, so heard becomes "Beta".

How does off on line 8 remove the right listener? on doesn't add your handler directly. It adds a small function of its own around it. So on saves that function in a private WeakMap, a map from each element to its saved listeners. off looks there and removes the exact function. This is the same idea as the delegation lesson, packed into one library method.

on also checks that you gave it a function:

A missing handler throws right awayPop out in the code editor (opens in a new tab)JavaScript
try {
  $(".item").on("click", ".item", null);
} catch (error) {
  console.log(error.name);
  console.log(error.message);
}

Passing null instead of a function throws a TypeError with the message on() expects a handler function. It is better to fail now than to add a listener that can never run.

Adding your own methods with plugins

EXTEND

A plugin is extra code that adds a method to a library without editing the library's file. Teams use plugins to add the methods they need.

In jQuery, $.fn is another name for the wrapper prototype. This library does the same, with $.fn = Collection.prototype. Put a function on $.fn, and every wrapper can call it. If the plugin changes things, it is a setter, so it must return this. Otherwise the next method in a chain fails.

A highlight pluginPop out in the code editor (opens in a new tab)JavaScript
$.fn.highlight = function (color = "gold") {
  return this.css("backgroundColor", color).addClass("highlighted");
};
$(".item").highlight("gold").text("Plugin");
console.log($(".item").attr("class"));
console.log($(".item").text());

Lines 1 to 3 add highlight to $.fn. Line 2 returns what addClass returns, which is the wrapper. So line 4 can chain .text("Plugin") after it. It prints item highlighted and Plugin.

A tag that says $.fn points to the prototype cart. The cart holds addClass, which was already there, and highlight, the new plugin.$.fnanother namealready therenew pluginaddClasshighlightprototype
$.fn is another name for the wrapper prototype. Putting highlight there gives it to every wrapper, next to the library's own methods.

Plugins often take options. A plugin's defaults are the values it uses when the caller gives none. Copy the defaults into a new object first, then put the caller's options on top. That way one caller can't change the defaults for everyone after them.

Merge options without changing the defaultsPop out in the code editor (opens in a new tab)JavaScript
$.fn.markReady = function (options = {}) {
  const settings = Object.assign({}, $.fn.markReady.defaults, options);
  return this.addClass(settings.className);
};
$.fn.markReady.defaults = { className: "ready" };
const options = Object.freeze({ className: "active" });
$(".item").markReady(options);
console.log($(".item").attr("class"));
console.log(options.className);
console.log($.fn.markReady.defaults.className);

Line 2 uses Object.assign. It copies the defaults into a new empty object, {}, then copies the caller's options over them. It prints item active, active, and ready:

  • The caller asked for active, so the items get that class.
  • The caller's own options object is unchanged.
  • The plugin's default is still ready.

This copy is shallow: it copies only the top-level properties. An object nested inside the options would still be shared, so it needs its own copy.

Some names belong on $ itself, not on each wrapper. $.extend copies properties for that:

  • $.extend(target, source) copies the properties of source onto target.
  • $.extend(source), with one argument, copies them onto $.
Copy names onto $ or onto a new objectPop out in the code editor (opens in a new tab)JavaScript
$.extend({ libraryName: "Mini" });
const settings = $.extend({}, { root: ".todo-list" });
console.log($.libraryName);
console.log(settings.root);

Line 1 adds libraryName to $. Line 2 copies root onto a new empty object. It prints Mini and .todo-list. Like Object.assign, this copy is shallow.

Some pages load two scripts that both want the name $. noConflict() solves that. It gives window.$ back to whatever it was before. It also returns the library, so you can keep it under another name.

Give $ back, keep the libraryPop out in the code editor (opens in a new tab)JavaScript
const localDollar = $.noConflict();
console.log(window.$ === undefined ? "released" : "still global");
window.$ = localDollar;
console.log(localDollar(".item").length);

Line 1 gives $ back. Nothing had $ before, so window.$ is now undefined and line 2 prints released. Line 3 puts the library back for the next examples. Line 4 uses the saved name and prints 2.

Checklist for a good plugin
  • Use a regular function when the plugin needs this. An arrow function doesn't get its own this.
  • Return this from a plugin that changes things.
  • Keep the plugin's data private, in a closure or a WeakMap, not in new globals.
  • Write down whether the plugin reads, writes, or does both.
Version numbers tell users what changed

A version such as 1.0.0 has three numbers: major, minor, and patch. This system is called semantic versioning.

  • A patch release, such as 1.0.1, fixes a bug. Documented behavior stays the same.
  • A minor release, such as 1.1.0, adds a new method or option. Old code keeps working.
  • A major release, such as 2.0.0, changes or removes documented behavior, such as what a setter returns.

Before you call a change "breaking", ask: could users depend on the old behavior? Your tests and examples count as part of what you promised.

How to read a library's source code

Real libraries are much bigger than this one. But you can read them the same way. Don't start by reading every helper from top to bottom. Follow these steps instead:

  1. Start: find where the code begins, such as the exported function or the IIFE.
  2. Public API: list the methods users are meant to call, such as $.fn.text.
  3. One call: follow one real call from start to end, such as $(".item").addClass("active").text("Hi").
  4. Tests: search the tests for the method name. They show what the method should do.
  5. History: when a line looks strange, use git blame or the commit history to see why it was written.
Take a tour of this library's code

Ask Tuto for the source tour. It walks through this library in order: the IIFE, the $ factory, the shared methods, the saved event listeners, the plugin slot $.fn, and noConflict. You can follow the same path in any open-source library.

Good docs show a call the way a user will write it:

A documented example a user can copyPop out in the code editor (opens in a new tab)JavaScript
/** Select matching items inside one root element. */
const list = $(".item", { root: document.querySelector(".todo-list") });
list.addClass("ready");
console.log(list.length);
console.log(list.attr("class"));

Line 1 is a short comment that says what the call does. Line 2 calls $ exactly as a user would, inside the .todo-list root. It prints 2 and item ready. Run examples like this in your tests too. Then you notice as soon as the docs stop matching the code.

What people use today

COMPARE

Old libraries were not silly. They solved real problems. But browsers got better, and many jQuery jobs are now built in. Today:

  • Small widgets often use the browser's own DOM methods directly.
  • Larger apps often use a framework.
  • Shared helpers live in ES modules. A module exports names, and other files import them, so no global is needed.

Each row of this table asks one question. The columns answer it for the mini $ library, the built-in DOM methods, and ES modules.

The mini $ library compared with built-in DOM methods and ES modules
QuestionMini $ libraryBuilt-in DOM methodsES modules
What is the main idea?A wrapper object holds elements and has methods you can chain.You call methods on elements and lists directly, such as classList.add.You export named functions and import them where you need them.
What becomes global?Only $. The helpers stay private inside the IIFE.Nothing new. You use document, elements, and other built-in objects.Nothing. Each file imports what it uses by name.
How do you add a feature?Put a method on $.fn, or copy a helper onto $ with $.extend.Write a plain function. Never add methods to built-in prototypes.Export another function, or build it from smaller modules.
When is it a good fit?For learning, for old pages, and for small extras on a page.For most work with the page today.For larger apps split across many files.
The same jobs with built-in DOM methodsPop out in the code editor (opens in a new tab)JavaScript
document.querySelectorAll(".item").forEach((element) => {
  element.classList.add("active");
  element.textContent = "Hi";
});
document.querySelector(".todo-list")?.addEventListener("click", (event) => {
  const item = event.target.closest(".item");
  if (item) console.log(item.dataset.name);
});

Line 1 finds every item and loops over them. Lines 2 and 3 add the class and set the text. Lines 5 and 6 add one listener to the list and use closest to find the clicked item.

The same helpers as module exportsJavaScript
export function selectAll(selector, root = document) {
  return [...root.querySelectorAll(selector)];
}

export function addClass(elements, name) {
  for (const element of elements) element.classList.add(name);
  return elements;
}

Each helper is a named export. A file that needs one imports it by name, so there is no global $.

Which approach does this code use?
  • `$.fn.highlight = function () { ... }`
  • `$(".item").addClass("active").text("Hi")`
  • `event.target.closest(".item")`
  • `element.classList.add("active")`
  • `export function addClass(elements, name) { ... }`
  • `document.querySelectorAll(".item")`
Try it yourself
0 of 6 correct

Sort each card: is it part of the mini $ library, a built-in DOM method, or the ES module style?

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common misconceptions

  • "An IIFE makes code secure." It only keeps names private. It doesn't make risky page changes or network calls safe.
  • "Array-like means it is an array." The wrapper has numbered slots and a length, but it is the library's own object with its own prototype. Array.isArray says false for it.
  • "Every method should chain." Getters return values. Setters usually return this.
  • "Plugins are always safe." A plugin can still make globals, turn a user's text into HTML, or break chains by forgetting to return this.
  • "jQuery is old, so its code teaches nothing." Its code still teaches API design, working across browsers, testing, and moving to new tools.
  • "Small helpers should go on built-in prototypes." Use your own wrapper or module functions instead.

Each row of this table names an idea, what people often think it is, and what it really is.

Ideas in this lesson that are easy to mix up
IdeaWhat people often thinkWhat it really is
$.fnIt is $ itself.It is the prototype that holds the methods every wrapper can call.
$.extendIt is a plugin.It is a helper that copies properties onto $ or onto another object.
One method that reads and writesIt always chains.Reading returns a value. Writing returns the wrapper.
DelegationIt adds a listener to every child.It adds one listener to a parent, which finds the matching child.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upProve methods are shared

Run the code. Do the two wrappers have the very same addClass function? Type true or false.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const a = $(".item");
const b = $(".save");
console.log(a.addClass === b.addClass);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeAdd hide and show methods

    These two methods should work in a chain, like $(".item").hide().show(). What should both methods return?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    $.fn.hide = function () {
      return this.css("display", "none");
    };
    $.fn.show = function () {
      return this.css("display", "");
    };
    $(".item").hide().show();

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeWrite a tiny plugin

      Where should you put a method so that every wrapper can call it? Type the name of that place.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      // Replace this object with the real extension point.
      const extensionPoint = {};
      extensionPoint.highlight = function () {
        return this.addClass("highlighted");
      };

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFix a method that breaks chaining

        This method adds a class, but the next call in the chain crashes. Which line fixes it?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        $.fn.mark = function () {
          this.addClass("marked");
        };
        
        // Uncomment after you fix the method:
        // $(".item").mark().text("Broken");

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeMake text safe

          Which DOM property shows a user's text as plain text, instead of reading it as HTML?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          $.fn.text = function (value) {
            return this.each(function (element) {
              // Which property should receive value?
            });
          };

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            8 QUESTIONS
            Mini library quiz · 8 questionsScore: first tries count
            1. Question 1 of 8Why did jQuery become popular in the first place?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does the IIFE keep private?

              Choose an answer to see the explanation.

            3. Question 3 of 8What does this print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const a = $(".item");
              const b = $(".save");
              console.log(a.addClass === b.addClass);

              Choose an answer to see the explanation.

            4. Question 4 of 8Why can setter methods chain? What does this print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const items = $(".item");
              console.log(items.addClass("active") === items);

              Choose an answer to see the explanation.

            5. Question 5 of 8Why is text(value) safer than an html(value) method for a user's text? What does this print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              $(".item").text("<strong>Hi</strong>");
              console.log(document.querySelector(".item").children.length);

              Choose an answer to see the explanation.

            6. Question 6 of 8What does the delegation example print after clicking Beta?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const list = $(".todo-list");
              let heard = "";
              function rememberClick(event) {
                heard = this.dataset.name;
              }
              list.on("click", ".item", rememberClick);
              document.querySelector('[data-name="Beta"]').click();
              list.off("click", ".item", rememberClick);
              console.log(heard);

              Choose an answer to see the explanation.

            7. Question 7 of 8Where should a plugin put a method that every wrapper can call?

              Choose an answer to see the explanation.

            8. Question 8 of 8In a new app today, what would you usually use instead of a global $?

              Choose an answer to see the explanation.

            Key takeaways

            • An IIFE keeps the library's helpers private and makes just one global on purpose: $.
            • The $ factory hides new. It accepts a selector, an element, a NodeList, an array, or another wrapper.
            • Methods live on one shared prototype. Setters return this; getters return values.
            • $.fn adds methods for every wrapper, $.extend copies names onto $, and noConflict gives the name $ back.
            • Today, built-in DOM methods and ES modules do most of these jobs. Reading old library code still teaches good API design.

            Final definition: a mini DOM library is a $ function that wraps the elements you select, shares its methods through one prototype, lets setters chain, and gives clear places to add new methods.

            Next: build a tiny reactive UI with the same DOM and API design ideas.

            CompleteFrontend Clear concepts. Working examples.