back to series

Implement a classnames function

classNames is popular Javascript package used to conditionally apply and join CSS classnames together.

Variation: Basic implementation

1. Basic implementation

We assume the possible values are strings, boolean, arrays and objects. We must also remove duplicates in classnames.

./src/classNames.js
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
}
}
})
);
Variation: Toggle classes

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:

./src/utils.js
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.

./src/classNames.js
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 🫡.