Created
July 16, 2022 08:24
-
-
Save IvanAdmaers/fb7050062b0e98d97364c220e2325eab to your computer and use it in GitHub Desktop.
classNames
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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