Build jqueryClassManipulation, a small wrapper modeled on jQuery's class methods — .addClass(), .removeClass(), .toggleClass(), and .hasClass(). You're given a single DOM element and return an object whose methods read and write that element's classes. The three mutators return the wrapper itself, so calls chain; hasClass returns a boolean. Each mutator also accepts a space-separated list of class names, just like jQuery.
interface ClassWrapper {
addClass(names: string): ClassWrapper; // add one or more; chainable
removeClass(names: string): ClassWrapper; // remove one or more; chainable
toggleClass(names: string): ClassWrapper; // flip one or more; chainable
hasClass(name: string): boolean; // is this class present?
}
function jqueryClassManipulation(element: HTMLElement): ClassWrapper;
const el = document.createElement('div');
const $el = jqueryClassManipulation(el);
$el.addClass('active'); // el.className → 'active', returns the wrapper
$el.hasClass('active'); // → true
$el.removeClass('active'); // el.className → '', returns the wrapper
// Mutators chain, and toggle flips: 'a b' added, then 'a' toggled off, 'b' stays.
jqueryClassManipulation(el)
.addClass('a b')
.toggleClass('a')
.hasClass('b'); // → true (a was removed, b is still present)
// A space-separated list addresses several classes in one call:
jqueryClassManipulation(el).addClass('one two three'); // adds all three
addClass, removeClass, and toggleClass each return the wrapper so the next method can be called on the result. hasClass is the exception — it returns a boolean.removeClass('btn') on an element with class="btn btn-primary" removes btn and leaves btn-primary intact. Class names are whole tokens, not substrings.toggleClass(name) adds the class when it's absent and removes it when it's present.element.classList — its add, remove, toggle, and contains methods handle tokens correctly. Don't hand-edit the className string.You'll write a factory that wraps one DOM element and hands back an object whose methods add, remove, toggle, and test that element's CSS classes.
jQuery lets you write $el.addClass('active').removeClass('hidden') and have it just work — each call mutates the element and returns something you can call the next method on. We're rebuilding that for one plain DOM element: a wrapper with addClass, removeClass, toggleClass, and hasClass. Two things make it more than a one-liner. First, the three mutators have to return the wrapper so calls chain. Second, classes are tokens — 'btn' and 'btn-primary' are separate classes that happen to share letters — so you can't treat the class attribute as a plain string and search-and-replace inside it.
Think of the wrapper as a small remote control bound to one element. The element is captured once, when you call jqueryClassManipulation(element), and every button on the remote operates on that same element. Three of the buttons (addClass, removeClass, toggleClass) hand the remote back to you so you can press the next button; the fourth (hasClass) reads a light off the device and tells you on or off.
The obvious version drives the className string directly — concatenate to add, replace to remove:
function jqueryClassManipulation(element) {
return {
addClass(name) {
element.className = element.className + ' ' + name;
},
removeClass(name) {
element.className = element.className.replace(name, '');
},
toggleClass(name) {
if (element.className.includes(name)) {
element.className = element.className.replace(name, '');
} else {
element.className = element.className + ' ' + name;
}
},
hasClass(name) {
return element.className.includes(name);
},
};
}
This breaks in three ways. It treats the class attribute as a string instead of a set of tokens, so addClass('active') called twice produces 'active active' — a duplicate the real API would never create. removeClass('btn') on class="btn btn-primary" calls 'btn btn-primary'.replace('btn', ''), which deletes the first 'btn' substring it finds and leaves ' btn-primary' — the sibling class is corrupted and a stray space is left behind. And every mutator returns undefined, so the moment you try to chain — .addClass('a').removeClass('b') — the second call throws because undefined has no removeClass.
function jqueryClassManipulation(element) {
// Build the wrapper as a named const so each mutator can `return wrapper`
// (returning `this` works too, but a closure reference is unambiguous).
const wrapper = {
addClass(names) {
// Split on spaces so 'a b c' becomes three tokens. classList.add ignores
// a class that's already present, so adding twice never duplicates.
for (const name of names.split(' ')) {
if (name) element.classList.add(name);
}
return wrapper;
},
removeClass(names) {
// classList.remove matches whole tokens, so removing 'btn' leaves
// 'btn-primary' untouched. Removing an absent class is a silent no-op.
for (const name of names.split(' ')) {
if (name) element.classList.remove(name);
}
return wrapper;
},
toggleClass(names) {
// toggle adds the token if absent, removes it if present — per token,
// so 'a b' can remove one and add the other in the same call.
for (const name of names.split(' ')) {
if (name) element.classList.toggle(name);
}
return wrapper;
},
hasClass(name) {
// contains is an exact-token test; returns a boolean, ends the chain.
return element.classList.contains(name);
},
};
return wrapper;
}
module.exports = { jqueryClassManipulation };
The whole shift is to stop editing a string and let element.classList — a live, token-aware view of the class attribute — do the work. add, remove, and toggle each operate on one whole token: add is idempotent (no duplicates), remove only deletes an exact match (no substring corruption), and toggle flips presence. To support space-separated lists we split the argument and loop, guarding if (name) so an empty string from a stray double space doesn't reach classList (which throws on an empty token). And every mutator ends with return wrapper, which is what makes chaining work.
Trace jqueryClassManipulation(el).addClass('a b').toggleClass('a').hasClass('b') on a fresh <div>:
jqueryClassManipulation(el) captures el and returns wrapper..addClass('a b') splits 'a b' into ['a', 'b'], calls classList.add('a') then classList.add('b'). The class list is now ['a', 'b']. Returns wrapper..toggleClass('a') splits to ['a']. 'a' is present, so classList.toggle('a') removes it. The class list is now ['b']. Returns wrapper..hasClass('b') runs classList.contains('b'). 'b' is still in the list, so it returns true.The expression evaluates to true, and the element ends with exactly class="b".
undefined from a mutator. If addClass doesn't return wrapper, then .addClass('a').removeClass('b') throws — undefined has no removeClass. Fix: every mutator ends with return wrapper.className string by hand. className.replace('btn', '') on 'btn btn-primary' corrupts the sibling into ' -primary', and + ' ' + name duplicates an existing class. Fix: use classList, which treats classes as whole tokens.'a b' straight to classList.add adds a single token literally named "a b", which is invalid. Fix: split(' ') and loop, adding each token on its own.'a b'.split(' ') yields ['a', '', 'b'], and classList.add('') throws a SyntaxError. Fix: guard each iteration with if (name) to skip empties..toggleClass(name, state) takes a boolean second argument that forces the class on (true) or off (false) regardless of current state. element.classList.toggle(name, force) accepts that same second argument, so threading it through is a small addition.addClass/toggleClass that receives the current class string and returns the classes to apply. Supporting it means detecting typeof names === 'function' and calling it with element.className first.NodeList, looping the same per-element logic, and returning the wrapper generalizes this from one node to many.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Build jqueryClassManipulation, a small wrapper modeled on jQuery's class methods — .addClass(), .removeClass(), .toggleClass(), and .hasClass(). You're given a single DOM element and return an object whose methods read and write that element's classes. The three mutators return the wrapper itself, so calls chain; hasClass returns a boolean. Each mutator also accepts a space-separated list of class names, just like jQuery.
interface ClassWrapper {
addClass(names: string): ClassWrapper; // add one or more; chainable
removeClass(names: string): ClassWrapper; // remove one or more; chainable
toggleClass(names: string): ClassWrapper; // flip one or more; chainable
hasClass(name: string): boolean; // is this class present?
}
function jqueryClassManipulation(element: HTMLElement): ClassWrapper;
const el = document.createElement('div');
const $el = jqueryClassManipulation(el);
$el.addClass('active'); // el.className → 'active', returns the wrapper
$el.hasClass('active'); // → true
$el.removeClass('active'); // el.className → '', returns the wrapper
// Mutators chain, and toggle flips: 'a b' added, then 'a' toggled off, 'b' stays.
jqueryClassManipulation(el)
.addClass('a b')
.toggleClass('a')
.hasClass('b'); // → true (a was removed, b is still present)
// A space-separated list addresses several classes in one call:
jqueryClassManipulation(el).addClass('one two three'); // adds all three
addClass, removeClass, and toggleClass each return the wrapper so the next method can be called on the result. hasClass is the exception — it returns a boolean.removeClass('btn') on an element with class="btn btn-primary" removes btn and leaves btn-primary intact. Class names are whole tokens, not substrings.toggleClass(name) adds the class when it's absent and removes it when it's present.element.classList — its add, remove, toggle, and contains methods handle tokens correctly. Don't hand-edit the className string.You'll write a factory that wraps one DOM element and hands back an object whose methods add, remove, toggle, and test that element's CSS classes.
jQuery lets you write $el.addClass('active').removeClass('hidden') and have it just work — each call mutates the element and returns something you can call the next method on. We're rebuilding that for one plain DOM element: a wrapper with addClass, removeClass, toggleClass, and hasClass. Two things make it more than a one-liner. First, the three mutators have to return the wrapper so calls chain. Second, classes are tokens — 'btn' and 'btn-primary' are separate classes that happen to share letters — so you can't treat the class attribute as a plain string and search-and-replace inside it.
Think of the wrapper as a small remote control bound to one element. The element is captured once, when you call jqueryClassManipulation(element), and every button on the remote operates on that same element. Three of the buttons (addClass, removeClass, toggleClass) hand the remote back to you so you can press the next button; the fourth (hasClass) reads a light off the device and tells you on or off.
The obvious version drives the className string directly — concatenate to add, replace to remove:
function jqueryClassManipulation(element) {
return {
addClass(name) {
element.className = element.className + ' ' + name;
},
removeClass(name) {
element.className = element.className.replace(name, '');
},
toggleClass(name) {
if (element.className.includes(name)) {
element.className = element.className.replace(name, '');
} else {
element.className = element.className + ' ' + name;
}
},
hasClass(name) {
return element.className.includes(name);
},
};
}
This breaks in three ways. It treats the class attribute as a string instead of a set of tokens, so addClass('active') called twice produces 'active active' — a duplicate the real API would never create. removeClass('btn') on class="btn btn-primary" calls 'btn btn-primary'.replace('btn', ''), which deletes the first 'btn' substring it finds and leaves ' btn-primary' — the sibling class is corrupted and a stray space is left behind. And every mutator returns undefined, so the moment you try to chain — .addClass('a').removeClass('b') — the second call throws because undefined has no removeClass.
function jqueryClassManipulation(element) {
// Build the wrapper as a named const so each mutator can `return wrapper`
// (returning `this` works too, but a closure reference is unambiguous).
const wrapper = {
addClass(names) {
// Split on spaces so 'a b c' becomes three tokens. classList.add ignores
// a class that's already present, so adding twice never duplicates.
for (const name of names.split(' ')) {
if (name) element.classList.add(name);
}
return wrapper;
},
removeClass(names) {
// classList.remove matches whole tokens, so removing 'btn' leaves
// 'btn-primary' untouched. Removing an absent class is a silent no-op.
for (const name of names.split(' ')) {
if (name) element.classList.remove(name);
}
return wrapper;
},
toggleClass(names) {
// toggle adds the token if absent, removes it if present — per token,
// so 'a b' can remove one and add the other in the same call.
for (const name of names.split(' ')) {
if (name) element.classList.toggle(name);
}
return wrapper;
},
hasClass(name) {
// contains is an exact-token test; returns a boolean, ends the chain.
return element.classList.contains(name);
},
};
return wrapper;
}
module.exports = { jqueryClassManipulation };
The whole shift is to stop editing a string and let element.classList — a live, token-aware view of the class attribute — do the work. add, remove, and toggle each operate on one whole token: add is idempotent (no duplicates), remove only deletes an exact match (no substring corruption), and toggle flips presence. To support space-separated lists we split the argument and loop, guarding if (name) so an empty string from a stray double space doesn't reach classList (which throws on an empty token). And every mutator ends with return wrapper, which is what makes chaining work.
Trace jqueryClassManipulation(el).addClass('a b').toggleClass('a').hasClass('b') on a fresh <div>:
jqueryClassManipulation(el) captures el and returns wrapper..addClass('a b') splits 'a b' into ['a', 'b'], calls classList.add('a') then classList.add('b'). The class list is now ['a', 'b']. Returns wrapper..toggleClass('a') splits to ['a']. 'a' is present, so classList.toggle('a') removes it. The class list is now ['b']. Returns wrapper..hasClass('b') runs classList.contains('b'). 'b' is still in the list, so it returns true.The expression evaluates to true, and the element ends with exactly class="b".
undefined from a mutator. If addClass doesn't return wrapper, then .addClass('a').removeClass('b') throws — undefined has no removeClass. Fix: every mutator ends with return wrapper.className string by hand. className.replace('btn', '') on 'btn btn-primary' corrupts the sibling into ' -primary', and + ' ' + name duplicates an existing class. Fix: use classList, which treats classes as whole tokens.'a b' straight to classList.add adds a single token literally named "a b", which is invalid. Fix: split(' ') and loop, adding each token on its own.'a b'.split(' ') yields ['a', '', 'b'], and classList.add('') throws a SyntaxError. Fix: guard each iteration with if (name) to skip empties..toggleClass(name, state) takes a boolean second argument that forces the class on (true) or off (false) regardless of current state. element.classList.toggle(name, force) accepts that same second argument, so threading it through is a small addition.addClass/toggleClass that receives the current class string and returns the classes to apply. Supporting it means detecting typeof names === 'function' and calling it with element.className first.NodeList, looping the same per-element logic, and returning the wrapper generalizes this from one node to many.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.