Engineering Publication · Syed Omar Ibn Feroj← Back to portfolio
jQ
Engineering Notes · Open Knowledge Repository
jQuery — Engineering
Notes & the Way Off It
A working reference for jQuery in codebases that still run it — selectors, traversal, events and Ajax done correctly — paired with the native-DOM equivalent for every pattern, so you can maintain it today and migrate it tomorrow.
26
Chapters
3.x
Baseline
Living
Document
Free
License
Ch. 01
Setup & ready
Load it, then wait for the DOM.
1.1 — The ready gate
JavaScript
// run after the DOM is parsed $(function () { // safe to query/manipulate here }); // equivalent: $(document).ready(fn)
$(fn) waits for DOM-ready, not full load (images/CSS) — that's what you want for setup. If the script tag is already at the end of <body> with defer, you often don't even need it.Ch. 02
Selectors
CSS selectors plus a few jQuery-only extensions.
2.1 — Select like CSS
JavaScript
$("#id"); $(".cls"); $("ul > li"); $("input[type=email]"); $("li:first"); $(":checked"); $(":visible"); // jQuery extensions
jQuery-only pseudo-selectors (
:visible, :first, :contains) can't use the browser's fast native querySelectorAll path — jQuery filters in JS. On large pages prefer standard CSS selectors.Ch. 03
The jQuery Object
A wrapped, array-like set — not a DOM node.
3.1 — Wrapped set vs raw node
JavaScript
const $els = $(".item"); // jQuery set (maybe 0..n) $els.length; // how many matched $els[0]; // underlying DOM node $els.get(0); // same, bounds-safe $(domNode); // wrap a node back up
A selector that matches nothing returns an empty set, not
null — methods silently no-op. "Nothing happens" almost always means $(sel).length === 0; check it before assuming the method is broken.Ch. 04
Traversal
Walk the tree from a set.
4.1 — Up, down, sideways
JavaScript
$(".x").closest(".card"); // nearest ancestor (incl self) $(".x").find("a"); // descendants $(".x").parent().children(); $(".x").next(); $(".x").siblings(); $(".x").filter(".active").end(); // end() pops the chain
closest() (not parent()) is what you want in delegated handlers — it finds the meaningful ancestor regardless of how deeply the click landed.Ch. 05
Chaining
Most methods return the set — fluent by design.
5.1 — One statement, many ops
JavaScript
$(".card") .addClass("on") .css("opacity", 1) .find(".title").text("Hi").end() .fadeIn(200);
Getters (
.text(), .val(), .attr(name) with no value) break the chain — they return a value, not the set. Only setters are chainable.Ch. 06
Content
html, text, val — and the XSS line.
6.1 — Read/write content
JavaScript
$("#out").text(userInput); // safe — escaped $("#out").html(trustedMarkup); // parses HTML — danger if untrusted $("#name").val(); // form value (get)
.html(x) with any user-controlled x is an XSS vector — identical risk to innerHTML. Use .text() for user data; reserve .html() for markup you generated.Ch. 07
Attributes & props
The attr/prop distinction that confuses everyone.
7.1 — attr is the HTML, prop is the live state
JavaScript
$("#c").attr("checked"); // the initial HTML attribute $("#c").prop("checked"); // the CURRENT checked state (bool)
For
checked, disabled, selected, value use .prop() — .attr() returns the original HTML and won't reflect user interaction. Using attr("checked") for a live checkbox is a classic bug.Ch. 08
Classes & CSS
toggleClass, and why inline css() is a smell.
8.1 — Prefer classes to inline styles
JavaScript
$(".x").addClass("active").removeClass("loading"); $(".x").toggleClass("open", isOpen); $(".x").hasClass("active"); $(".x").css({ color: "red" }); // last resort
Toggle a class and keep styling in the stylesheet. Scattering
.css() calls makes state un-inspectable and impossible to theme; a single class flips dozens of declarations.Ch. 09
Dimensions
width vs innerWidth vs outerWidth.
9.1 — The box variants
JavaScript
$("#b").width(); // content box $("#b").innerWidth(); // + padding $("#b").outerWidth(true); // + padding + border + margin $("#b").offset(); // position vs document
.width() of a hidden element (display:none) is 0 — there's no layout to measure. Measure after it's visible, or temporarily make it visible off-screen.Ch. 10
Creating Elements
Build and insert in one expression.
10.1 — Construct, configure, place
JavaScript
const $li = $("<li>", { "class": "item", text: name }); $("#list").append($li); // inside, end $target.prepend(x); $target.before(x); $target.after(x);
The
$("<li>", { props }) form sets text safely (escaped) — building elements this way avoids the string-concatenation XSS you'd get from $("<li>" + name + "</li>").Ch. 11
Removing Elements
remove vs detach vs empty.
11.1 — Three different removals
JavaScript
$(".x").remove(); // gone + its data/handlers freed $(".x").detach(); // removed but KEEPS data/handlers (re-insert later) $(".x").empty(); // keep .x, drop its children
Use
detach() only if you'll re-insert and need the handlers; otherwise remove() so jQuery cleans up bound data/events. Manually $el[0].remove() via native API skips that cleanup → potential leak.Ch. 12
.data()
A subtle store that isn't quite the data-* attribute.
12.1 — Initial read vs the cache
JavaScript
// <div data-id="42"> $("#d").data("id"); // 42 (number — type-coerced!) $("#d").data("id", 99); // updates the jQuery cache only $("#d").attr("data-id"); // still "42" — attribute unchanged
.data() reads data-* once into an internal cache and type-coerces ("42" → 42, "true" → true). Writing with .data() does NOT update the DOM attribute. Mixing .data() and .attr("data-…") on the same key is a reliable source of confusion.Ch. 13
Binding Events
on/off — and never inline.
13.1 — on() is the only one you need
JavaScript
$("#btn").on("click", handler); $("#btn").off("click", handler); $("#f").on("submit", (e) => { e.preventDefault(); });
The old
.click(), .bind(), .live(), .delegate() helpers are all just .on() underneath and several are removed in jQuery 3+. Standardise on .on()/.off().Ch. 14
Delegation
One handler for current and future elements.
14.1 — Bind to a stable parent
JavaScript
$("#list").on("click", "li.item", function (e) { // `this` is the matched li, even if added later $(this).toggleClass("selected"); });
Binding directly to
$(".item") attaches N handlers and misses elements added afterwards. Delegated binding (parent.on(evt, selector, fn)) is one handler that covers dynamic content — the correct default for lists.Ch. 15
The Event Object
Normalised across browsers.
15.1 — Stop, prevent, data
JavaScript
$("a").on("click", (e) => { e.preventDefault(); // stop the default action e.stopPropagation(); // stop bubbling e.target; e.currentTarget; e.which; });
return false; in a jQuery handler does BOTH preventDefault and stopPropagation — often more than intended (it can break delegation/analytics). Call the specific one you mean.Ch. 16
Effects
fade/slide/animate — and the queue.
16.1 — Built-ins and custom animate
JavaScript
$(".x").fadeIn(200).delay(500).fadeOut(200); $(".x").animate({ left: "+=100" }, 300, fn); $(".x").stop(true, true); // clear queue, jump to end
Effects queue per element — rapid hover can stack dozens of animations ("animation jank" on menus). Call
.stop(true, true) before starting a new one, or prefer CSS transitions for hover/UI state.Ch. 17
Ajax
$.ajax and the shorthands.
17.1 — Request, handle, fail
JavaScript
$.ajax({ url: "/api", method: "POST", data: payload }) .done(res => render(res)) .fail((xhr) => show(xhr.status)) .always(() => spinner.hide());
In greenfield code prefer the native
fetch — but in a jQuery codebase, keep one consistent style. $.get/$.post/$.getJSON are thin wrappers over $.ajax; contentType/processData trip people up on file uploads.Ch. 18
Deferred & Promises
jqXHR is thenable — but not spec-perfect.
18.1 — Interop with native promises
JavaScript
const p = Promise.resolve($.get("/a")); // adopt into a real Promise const data = await $.getJSON("/b"); // awaitable (3.x) $.when($.get("/a"), $.get("/b")).done(...); // like Promise.all
jQuery Deferred predates A+ promises and has subtle differences (multiple resolve args, no automatic exception → rejection in old versions). When mixing with
async/await, wrap in Promise.resolve() to get standard semantics.Ch. 19
Forms
Serialise and submit.
19.1 — serialize / serializeArray
JavaScript
const qs = $("#f").serialize(); // "a=1&b=2" const fd = new FormData($("#f")[0]); // for file uploads
.serialize() skips disabled fields, unchecked boxes, and file inputs. For uploads use native FormData on the raw form node ($form[0]) and let jQuery send it with processData:false, contentType:false.Ch. 20
Utilities & $.each
The static helpers.
20.1 — Iterate, map, extend
JavaScript
$.each(arr, (i, v) => { /* note: index FIRST */ }); $("li").each(function (i) { /* this = node */ }); $.extend({}, defaults, options); // shallow merge
$.each(arr, fn) passes (index, value) — the opposite order of native Array.forEach(value, index). Swapping them is an everyday bug; native forEach/map are usually clearer now.Ch. 21
Plugins
Extending $.fn the correct way.
21.1 — A well-behaved plugin
JavaScript
$.fn.highlight = function (opts) { const o = $.extend({ color: "yellow" }, opts); return this.each(function () { $(this).css("background", o.color); }); // return this → keep chainable };
A plugin must
return this.each(...) so it stays chainable and operates on every matched element. Merge options over defaults with $.extend; never mutate the passed options object.Ch. 22
noConflict
Sharing the page with other libraries.
22.1 — Release the $
JavaScript
var jq = $.noConflict(); jq(function ($) { // $ is jQuery again, only inside here });
Use
noConflict() when another library also owns $ (or two jQuery versions coexist during a migration). The IIFE-with-$-parameter pattern keeps plugin code readable while being safe globally.Ch. 23
Performance
The cheap wins in a jQuery codebase.
23.1 — Cache, batch, delegate
JavaScript
const $list = $("#list"); // cache — don't re-query in a loop const html = rows.map(r => `<li>${r}</li>`).join(""); $list.html(html); // one DOM write, not N appends
Re-running
$(selector) inside a loop and append-ing per iteration are the two classic jQuery slowdowns. Cache the set once; build markup as a string and write it in a single operation; delegate events.Ch. 24
jQuery → Native Map
Every common call has a one-line modern equivalent.
24.1 — The translation table
| jQuery | Native |
|---|---|
$(sel) | document.querySelectorAll(sel) |
$el.addClass(c) | el.classList.add(c) |
$el.attr(a, v) | el.setAttribute(a, v) |
$el.on(e, fn) | el.addEventListener(e, fn) |
$el.closest(s) | el.closest(s) |
$el.css(p, v) | el.style.setProperty(p, v) |
$.ajax | fetch() |
$(fn) | defer script / DOMContentLoaded |
Ch. 25
Migration Strategy
Off jQuery without a rewrite.
25.1 — Incremental removal
- Stop adding new jQuery; write new code with native APIs
- Replace
$.ajaxwithfetchfirst — biggest bundle win, lowest risk - Swap leaf utilities (
addClass,on,closest) file by file - Remove jQuery plugins or replace with framework components
- Drop the jQuery
<script>only when grep finds zero$(
The win isn't ideology — it's ~30 KB gzipped and one fewer dependency to keep patched. Migrate where it's cheap (Ajax, class/attr toggling); don't rewrite a working complex plugin just to remove a symbol.
Ch. 26
Common Pitfalls
The recurring jQuery bugs.
26.1 — The list
attr("checked")instead ofprop("checked")- Binding events before DOM-ready / to not-yet-existing elements
- Direct binding instead of delegation for dynamic lists
$.eachargument order (index first).html(userInput)→ XSS.data()write not reflecting in the DOM attribute- Re-querying selectors in loops; per-item
append return falsedoing more than intended
REF
jQuery Cheatsheet
The 12 calls that are 90% of real jQuery code.
The essentials
JavaScript
$(fn) // DOM ready $("sel") // select .find() / .closest() / .filter() .text() / .html() / .val() .addClass() / .toggleClass() .attr() / .prop() .on(evt, sel, fn) // delegated .append() / .remove() $.ajax(...).done().fail() $("<li>", { text }) // safe create