Skip to content

Instantly share code, notes, and snippets.

@IvanAdmaers
Created July 16, 2022 08:24
Show Gist options
  • Save IvanAdmaers/fb7050062b0e98d97364c220e2325eab to your computer and use it in GitHub Desktop.
Save IvanAdmaers/fb7050062b0e98d97364c220e2325eab to your computer and use it in GitHub Desktop.
classNames
const isArray = (item) => Array.isArray(item);
const isObject = (item) =>
item !== null && isArray(item) === false && typeof item === 'object';
const isFunction = (item) => typeof item === 'function';
const isBoolean = (item) => typeof item === 'boolean';
const hasOwnPrototypeOfToString = (object) =>
object.toString !== Object.prototype.toString;
const classNames = (...classes) => {
let resultClassName = '';
const handlePrimitive = (item, asKey) => {
const isItemBoolean = isBoolean(item);
if (!item || isItemBoolean === true) {
return;
}
resultClassName += `${asKey !== undefined ? asKey : item} `;
};
const handleObject = (object) => {
const entries = Object.entries(object);
/* maybe throw it out */
const hasObjectOwnPrototypeOfToString = hasOwnPrototypeOfToString(object);
/**/
entries.forEach(([key, value]) => {
if (!value) {
return;
}
const isValueFunction = isFunction(value);
const isToString = key === 'toString';
if (isValueFunction === true && isToString === false) {
const functionResult = value();
handlePrimitive(functionResult, key);
return;
}
if (isToString === false) {
resultClassName += `${key} `;
}
});
/* maybe throw it out */
if (hasObjectOwnPrototypeOfToString === true) {
const toStringValue = object.toString();
handlePrimitive(toStringValue);
}
/**/
};
const handleArray = (array) => {
array.forEach((item) => {
if (!item || typeof item === 'boolean') {
return;
}
const isItemArray = isArray(item);
const isItemObject = isObject(item);
if (isItemArray === true) {
return handleArray(item);
}
if (isItemObject === true) {
return handleObject(item);
}
resultClassName += `${item} `;
});
};
for (let i = 0; i < classes.length; i += 1) {
const classNameItem = classes[i];
const isItemObject = isObject(classNameItem);
const isItemArray = isArray(classNameItem);
if (isItemObject === true) {
handleObject(classNameItem);
continue;
}
if (isItemArray === true) {
handleArray(classNameItem);
continue;
}
handlePrimitive(classNameItem);
}
return resultClassName.trim();
};
export default classNames;
// eslint-disable-next-line
import classNames from './classNames';
describe('classNames', () => {
it('should ignore primitive boolean values', () => {
expect(classNames(true, false)).toBe('');
});
it('keeps object keys with truthy values', () => {
expect(
classNames({
a: true,
b: false,
c: 0,
d: null,
e: undefined,
f: 1,
}),
).toBe('a f');
});
it('joins arrays of class names and ignore falsy values', () => {
expect(classNames('a', 0, null, undefined, true, 1, 'b')).toBe('a 1 b');
});
it('supports heterogenous arguments', () => {
expect(classNames({ a: true }, 'b', 0)).toBe('a b');
});
it('should be trimmed', () => {
expect(classNames('', 'b', {}, '')).toBe('b');
});
it('returns an empty string for an empty configuration', () => {
expect(classNames({})).toBe('');
});
it('supports an array of class names', () => {
expect(classNames(['a', 'b'])).toBe('a b');
});
it('joins array arguments with string arguments', () => {
expect(classNames(['a', 'b'], 'c')).toBe('a b c');
expect(classNames('c', ['a', 'b'])).toBe('c a b');
});
it('handles multiple array arguments', () => {
expect(classNames(['a', 'b'], ['c', 'd'])).toBe('a b c d');
});
it('handles arrays that include falsy and true values', () => {
expect(classNames(['a', 0, null, undefined, false, true, 'b'])).toBe('a b');
});
it('handles arrays that include arrays', () => {
expect(classNames(['a', ['b', 'c']])).toBe('a b c');
});
it('handles arrays that include objects', () => {
expect(classNames(['a', { b: true, c: false }])).toBe('a b');
});
it('handles deep array recursion', () => {
expect(classNames(['a', ['b', ['c', { d: true }]]])).toBe('a b c d');
});
it('handles arrays that are empty', () => {
expect(classNames('a', [])).toBe('a');
});
it('handles nested arrays that have empty nested arrays', () => {
expect(classNames('a', [[]])).toBe('a');
});
it('handles all types of truthy and falsy property values as expected', () => {
expect(
classNames({
// falsy:
null: null,
emptyString: '',
noNumber: NaN,
zero: 0,
negativeZero: -0,
false: false,
// eslint-disable-next-line object-shorthand
undefined: undefined,
// truthy (literally anything else):
nonEmptyString: 'foobar',
whitespace: ' ',
function: Object.prototype.toString,
emptyObject: {},
nonEmptyObject: { a: 1, b: 2 },
emptyList: [],
nonEmptyList: [1, 2, 3],
greaterZero: 1,
}),
).toBe(
'nonEmptyString whitespace function emptyObject nonEmptyObject emptyList nonEmptyList greaterZero',
);
});
it('handles toString() method defined on object', () => {
expect(
classNames({
toString: () => 'classFromMethod',
}),
).toBe('classFromMethod');
});
it('handles toString() method defined inherited in object', function test1() {
const Class1 = function c1() {};
const Class2 = function c2() {};
Class1.prototype.toString = function propt() {
return 'classFromMethod';
};
Class2.prototype = Object.create(Class1.prototype);
expect(classNames(new Class2())).toBe('classFromMethod');
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment