Implement a classnames function
classNames
is popular Javascript package used to conditionally apply and join CSS classnames
together.
1. Basic implementation
We assume the possible values are strings, boolean, arrays and objects. We must also remove duplicates in classnames.
function classNames(...classes) { const result = new Set();
function classNamesImpl(...classesArray) { classesArray.forEach((cls) => { // if not truthy we don't want to include them if (!cls) { return; }
// for string and numbers if (typeof cls === 'string' || typeof cls === 'number') { result.add(cls.toString()); return; }
// for arrays if (Array.isArray(cls)) { cls.forEach((classValue) => { classNamesImpl(classValue); }); return; }
// objects for (const key in cls) { // to avoid iterating over inherited keys if (Object.hasOwn(cls, key)) { const value = cls[key]; // we only want truthy values for object if (value) { // if truthy value and not nested we simply want the key if (typeof value !== 'object') { classNamesImpl(key); } else { // recursively get the keys from the nested object classNamesImpl(value); } } } } }); }
classNamesImpl(...classes);
return Array.from(new Set(result)).join(' ');}
// Usage
// yields : "red yellow 1"console.log(classNames('red', 'yellow', 'yellow', null, false, 1, undefined));
// yields : "red yellow purple magenta"console.log( classNames(['red', 'yellow'], { green: false, colors: { purple: true, voilet: { magenta: true } } }));
2. Toggle classes implementation
In the above implementation, we only considered cases to add classes (as object keys) whemever they had a truthy value. In this implementation we can remove classes from the final result when same class with a falsy value appears on the object.
An example to demonstrate the idea:
import classNames from './src/classNames.js';
const output = classNames('foo', 'bar', { foo: false });
console.log('output', output); // yields "bar" and not "foo bar"
Most of the implementation for this variation remains exactly same as above, so I will highlight only the new changes which needs to be made to accomodate the new case.
function classNames(...classes) { const result = new Set();
function classNamesImpl(...classesArray) { classesArray.forEach((cls) => { // if not truthy we don't want to include them if (!cls) { return; }
// for string and numbers if (typeof cls === 'string' || typeof cls === 'number') { result.add(cls.toString()); return; }
// for arrays if (Array.isArray(cls)) { cls.forEach((classValue) => { classNamesImpl(classValue); }); return; }
// objects for (const key in cls) { // to avoid iterating over inherited keys if (Object.hasOwn(cls, key)) { const value = cls[key]; // add to result when truthy if (value) { // if truthy value and not nested we simply want the key if (typeof value !== 'object') { classNamesImpl(key); } else { // recursively get the keys from the nested object classNamesImpl(value); } } // remove if already consider when falsy else { if (result.has(key)) { result.delete(key); } } } } }); }
classNamesImpl(...classes);
return Array.from(new Set(result)).join(' ');}
// Usage
// yields : "red yellow 1"console.log(classNames('red', 'yellow', 'yellow', null, false, 1, undefined));
// yields : "red purple magenta"console.log( classNames(['red', 'yellow'], { green: false, colors: { purple: true, voilet: { magenta: true, // should remove yellow yellow: false } } }));
Further Reading
I strongly encourage you to explore and tackle additional questions in my Recursion Questions for Frontend Interviews blog series.
By doing so, you can enhance your understanding and proficiency with recursion, preparing you to excel in your upcoming frontend interviews.
Wishing you best. Happy Interviewing 🫡.