jQuery's signature move is the chain: $('.card').addClass('open').text('Hi').attr('role', 'dialog') reads like a sentence and touches every matched element. The trick behind it is almost embarrassingly small — every mutating method just returns the wrapper (this) instead of undefined, so the next call has something to run on. This "fluent interface" pattern shows up far beyond the DOM (query builders, test assertions, streams), and building a mini jQuery is the clearest way to internalise it.
Implement domMethodChaining(target, root) returning a wrapper over a set of elements with chainable methods.
function domMethodChaining(target, root = document) {
return wrapper; // { addClass, text, attr, css, on, each, length, ... }
}
const $ = domMethodChaining;
$(node).addClass('box').text('hello').attr('id', 'x'); // all applied, in order
$('.item', container).addClass('on'); // applies to EVERY match
$(node).text(); // no arg -> getter, returns text
target is a selector (queried in root), a single Node, or a NodeList. Methods act on all wrapped elements.return this) is what makes calls chain.text()/attr(name)/html()/css(prop) with no value returns the value from the first element instead of the wrapper.undefined.We'll normalise the target into an array of elements, then expose methods that each loop the array and return the wrapper — with dual-purpose accessors that read from the first element when called with no argument.
A jQuery-style wrapper is two ideas. First, it holds a set of elements (from a selector, a node, or a node list), and its mutating methods apply to every element. Second, those methods return the wrapper itself, so the next .method() has something to run on — that's the chain. Accessor methods like text() do double duty: with an argument they set (and keep the chain), without one they get from the first element (and end it).
Wrap the target into an array once. Every setter is elements.forEach(apply); return this. Every getter is return elements[0] ? read(elements[0]) : undefined. The accessor methods just branch on whether they got a value.
The obvious version operates on one element and returns the result of each operation:
function domNaive(node) {
return {
addClass(name) { return node.classList.add(name); }, // returns undefined!
text(value) { node.textContent = value; },
};
}
domNaive(el).addClass('a').text('hi'); // TypeError: cannot read 'text' of undefined
classList.add returns undefined, so the chain dies after the first call. It also only wraps a single node and can't get values. The fixes: hold a set, return this from every mutator, and make accessors bidirectional.
function domMethodChaining(target, root = document) {
const elements = resolve(target, root);
const wrapper = {
elements,
get length() {
return elements.length;
},
addClass(name) { elements.forEach((el) => el.classList.add(name)); return wrapper; },
removeClass(name) { elements.forEach((el) => el.classList.remove(name)); return wrapper; },
toggleClass(name) { elements.forEach((el) => el.classList.toggle(name)); return wrapper; },
text(value) {
if (value === undefined) return elements[0] && elements[0].textContent;
elements.forEach((el) => { el.textContent = value; });
return wrapper;
},
html(value) {
if (value === undefined) return elements[0] && elements[0].innerHTML;
elements.forEach((el) => { el.innerHTML = value; });
return wrapper;
},
attr(name, value) {
if (value === undefined) return elements[0] ? elements[0].getAttribute(name) : undefined;
elements.forEach((el) => el.setAttribute(name, value));
return wrapper;
},
css(prop, value) {
if (value === undefined) return elements[0] && elements[0].style[prop];
elements.forEach((el) => { el.style[prop] = value; });
return wrapper;
},
on(event, handler) { elements.forEach((el) => el.addEventListener(event, handler)); return wrapper; },
each(fn) { elements.forEach((el, i) => fn(el, i)); return wrapper; },
};
return wrapper;
}
function resolve(target, root) {
if (typeof target === 'string') return Array.from(root.querySelectorAll(target));
if (target && target.nodeType) return [target]; // a single Node
if (target && typeof target.length === 'number') return Array.from(target); // NodeList/array
return [];
}
module.exports = { domMethodChaining };
The three ideas made concrete: resolve normalises any target into an array; every mutator ends in return wrapper (so the chain never breaks); and each accessor branches on value === undefined — read the first element or write all of them. An empty set makes setters harmless no-ops and getters return undefined.
domMethodChaining(node).addClass('box').text('hi').attr('id', 'x'):
resolve(node) → [node]; wrapper.elements = [node]..addClass('box') → node.classList.add('box'); returns wrapper..text('hi') → value given, so node.textContent = 'hi'; returns wrapper..attr('id','x') → value given, node.setAttribute('id','x'); returns wrapper.wrapper, so all three mutations applied in order.Contrast domMethodChaining(node).text() — no argument, so it returns node.textContent (a string), and the chain stops there by design.
classList.add()/setAttribute() return undefined, killing the chain. Return the wrapper explicitly.value === undefined, not falsiness, or text('') (a valid clear) would be treated as a get.elements[0] &&/a length check; setters forEach over an empty array harmlessly.this vs a captured wrapper — returning this works with normal method calls, but capturing const wrapper = {...}; return wrapper is robust even if a method is detached and re-invoked.find, parent, closest, filter return new wrappers over a different set, composing the chain across the tree.get(i) / toArray — escape hatches back to raw elements when you need the underlying node.return-a-wrapper idea, but each link is fresh.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
jQuery's signature move is the chain: $('.card').addClass('open').text('Hi').attr('role', 'dialog') reads like a sentence and touches every matched element. The trick behind it is almost embarrassingly small — every mutating method just returns the wrapper (this) instead of undefined, so the next call has something to run on. This "fluent interface" pattern shows up far beyond the DOM (query builders, test assertions, streams), and building a mini jQuery is the clearest way to internalise it.
Implement domMethodChaining(target, root) returning a wrapper over a set of elements with chainable methods.
function domMethodChaining(target, root = document) {
return wrapper; // { addClass, text, attr, css, on, each, length, ... }
}
const $ = domMethodChaining;
$(node).addClass('box').text('hello').attr('id', 'x'); // all applied, in order
$('.item', container).addClass('on'); // applies to EVERY match
$(node).text(); // no arg -> getter, returns text
target is a selector (queried in root), a single Node, or a NodeList. Methods act on all wrapped elements.return this) is what makes calls chain.text()/attr(name)/html()/css(prop) with no value returns the value from the first element instead of the wrapper.undefined.We'll normalise the target into an array of elements, then expose methods that each loop the array and return the wrapper — with dual-purpose accessors that read from the first element when called with no argument.
A jQuery-style wrapper is two ideas. First, it holds a set of elements (from a selector, a node, or a node list), and its mutating methods apply to every element. Second, those methods return the wrapper itself, so the next .method() has something to run on — that's the chain. Accessor methods like text() do double duty: with an argument they set (and keep the chain), without one they get from the first element (and end it).
Wrap the target into an array once. Every setter is elements.forEach(apply); return this. Every getter is return elements[0] ? read(elements[0]) : undefined. The accessor methods just branch on whether they got a value.
The obvious version operates on one element and returns the result of each operation:
function domNaive(node) {
return {
addClass(name) { return node.classList.add(name); }, // returns undefined!
text(value) { node.textContent = value; },
};
}
domNaive(el).addClass('a').text('hi'); // TypeError: cannot read 'text' of undefined
classList.add returns undefined, so the chain dies after the first call. It also only wraps a single node and can't get values. The fixes: hold a set, return this from every mutator, and make accessors bidirectional.
function domMethodChaining(target, root = document) {
const elements = resolve(target, root);
const wrapper = {
elements,
get length() {
return elements.length;
},
addClass(name) { elements.forEach((el) => el.classList.add(name)); return wrapper; },
removeClass(name) { elements.forEach((el) => el.classList.remove(name)); return wrapper; },
toggleClass(name) { elements.forEach((el) => el.classList.toggle(name)); return wrapper; },
text(value) {
if (value === undefined) return elements[0] && elements[0].textContent;
elements.forEach((el) => { el.textContent = value; });
return wrapper;
},
html(value) {
if (value === undefined) return elements[0] && elements[0].innerHTML;
elements.forEach((el) => { el.innerHTML = value; });
return wrapper;
},
attr(name, value) {
if (value === undefined) return elements[0] ? elements[0].getAttribute(name) : undefined;
elements.forEach((el) => el.setAttribute(name, value));
return wrapper;
},
css(prop, value) {
if (value === undefined) return elements[0] && elements[0].style[prop];
elements.forEach((el) => { el.style[prop] = value; });
return wrapper;
},
on(event, handler) { elements.forEach((el) => el.addEventListener(event, handler)); return wrapper; },
each(fn) { elements.forEach((el, i) => fn(el, i)); return wrapper; },
};
return wrapper;
}
function resolve(target, root) {
if (typeof target === 'string') return Array.from(root.querySelectorAll(target));
if (target && target.nodeType) return [target]; // a single Node
if (target && typeof target.length === 'number') return Array.from(target); // NodeList/array
return [];
}
module.exports = { domMethodChaining };
The three ideas made concrete: resolve normalises any target into an array; every mutator ends in return wrapper (so the chain never breaks); and each accessor branches on value === undefined — read the first element or write all of them. An empty set makes setters harmless no-ops and getters return undefined.
domMethodChaining(node).addClass('box').text('hi').attr('id', 'x'):
resolve(node) → [node]; wrapper.elements = [node]..addClass('box') → node.classList.add('box'); returns wrapper..text('hi') → value given, so node.textContent = 'hi'; returns wrapper..attr('id','x') → value given, node.setAttribute('id','x'); returns wrapper.wrapper, so all three mutations applied in order.Contrast domMethodChaining(node).text() — no argument, so it returns node.textContent (a string), and the chain stops there by design.
classList.add()/setAttribute() return undefined, killing the chain. Return the wrapper explicitly.value === undefined, not falsiness, or text('') (a valid clear) would be treated as a get.elements[0] &&/a length check; setters forEach over an empty array harmlessly.this vs a captured wrapper — returning this works with normal method calls, but capturing const wrapper = {...}; return wrapper is robust even if a method is detached and re-invoked.find, parent, closest, filter return new wrappers over a different set, composing the chain across the tree.get(i) / toArray — escape hatches back to raw elements when you need the underlying node.return-a-wrapper idea, but each link is fresh.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.