diff --git a/src/classnames/index.test.ts b/src/classnames/index.test.ts index f139ced..428344a 100644 --- a/src/classnames/index.test.ts +++ b/src/classnames/index.test.ts @@ -1,77 +1,6 @@ -import {classNames, isBooleanString, isString} from "./index"; +import {classNames} from "./index"; describe("classnames", () => { - describe('isString function', () => { - it('should return true for string', () => { - expect(isString('test')).toBe(true); - }); - - it('should return false for non-string', () => { - expect(isString(123)).toBe(false); - expect(isString(null)).toBe(false); - expect(isString(undefined)).toBe(false); - expect(isString(true)).toBe(false); - expect(isString({})).toBe(false); - expect(isString([])).toBe(false); - }); - }); - - describe('isBooleanString', () => { - it('should return false for true as boolean', () => { - expect(isBooleanString(true)).toBe(false); - }); - - it('should return false for false as boolean', () => { - expect(isBooleanString(false)).toBe(false); - }); - - it('should return false for null', () => { - expect(isBooleanString(null)).toBe(false); - }); - - it('should return false for undefined', () => { - expect(isBooleanString(undefined)).toBe(false); - }); - - it('should return false for number 1', () => { - expect(isBooleanString(1)).toBe(false); - }); - - it('should return false for number 0', () => { - expect(isBooleanString(0)).toBe(false); - }); - - it('should return true for "true" string', () => { - expect(isBooleanString("true")).toBe(true); - }); - - it('should return true for "1" string', () => { - expect(isBooleanString("1")).toBe(true); - }); - - it('should return false for "false" string', () => { - expect(isBooleanString("false")).toBe(false); - }); - - it('should return false for "0" string', () => { - expect(isBooleanString("0")).toBe(false); - }); - - it('should return false for arbitrary string', () => { - expect(isBooleanString("hello")).toBe(false); - }); - - // Test for objects and arrays (should return false) - it('should return false for empty object', () => { - expect(isBooleanString({})).toBe(false); - }); - - it('should return false for empty array', () => { - expect(isBooleanString([])).toBe(false); - }); - }); - - describe('classNames function', () => { it('should return an empty string if no arguments are provided', () => { expect(classNames()).toBe(''); }); @@ -85,27 +14,30 @@ describe("classnames", () => { }); it('should filter out false keys in ClassMap and include true keys', () => { - expect(classNames({ class1: true, class2: false })).toBe('class1'); + expect(classNames({class1: true, class2: false})).toBe('class1'); }); it('should filter out false keys in ClassMap and include true keys and add class3', () => { - expect(classNames({ class1: true, class2: false }, "class3")).toBe('class1 class3'); + expect(classNames({class1: true, class2: false}, "class3")).toBe('class1 class3'); }); it('should filter out false keys in ClassMap and include true keys and add class3', () => { - expect(classNames({ class1: true, class2: false }, undefined, "class3")).toBe('class1 class3'); + expect(classNames({class1: true, class2: false}, undefined, "class3")).toBe('class1 class3'); }); it('should handle a combination of all types of arguments', () => { - expect(classNames('class1', undefined, null, { class2: true, class3: false }, 'class4')).toBe('class1 class2 class4'); + expect(classNames('class1', undefined, null, { + class2: true, + class3: false + }, 'class4')).toBe('class1 class2 class4'); }); it('should ignore object arguments with non-boolean values except strings like "true" or "false" or "1" or "2"', () => { - expect(classNames({ class1: "true", class2: 123, class3: "1" } as any)).toBe('class1 class3'); + expect(classNames({class1: "true", class2: 123, class3: "1"} as any)).toBe('class1 class3'); }); it('should only include keys with boolean true values in mixed objects', () => { - expect(classNames({ class1: true, class2: "false", class3: 0 } as any)).toBe('class1'); + expect(classNames({class1: true, class2: "false", class3: 0} as any)).toBe('class1'); }); it('should ignore empty strings', () => { @@ -125,11 +57,11 @@ describe("classnames", () => { }); it('should ignore nested objects', () => { - expect(classNames({ class1: true, nested: { class2: true }} as any)).toBe('class1'); + expect(classNames({class1: true, nested: {class2: true}} as any)).toBe('class1'); }); it('should include class names with leading or trailing whitespaces as-is', () => { - expect(classNames(' class1 ', 'class2 ')).toBe(' class1 class2 '); + expect(classNames(' class1 ', 'class2 ')).toBe('class1 class2'); }); it('should include class names with array', () => { @@ -145,10 +77,42 @@ describe("classnames", () => { }); it('should filter out false keys in ClassMap and include true keys', () => { - expect(classNames({ class1: true, class2: false })).toBe('class1'); + expect(classNames({class1: true, class2: false})).toBe('class1'); }); + it('evaluate all features', () => { + expect(classNames('a', ['b', 'c'], {d: true}, ['e', {f: true}, ' g', 'h ', ' i ', {' j': true}], ' k', 'l ', ' m ')).toBe('a b c d e f g h i j k l m'); + }); - }); + it('should return empty string for no arguments', () => { + expect(classNames()).toBe(''); + }); + + it('should concatenate class names', () => { + expect(classNames('a', 'b')).toBe('a b'); + }); -}) + it('should omit falsy values', () => { + expect(classNames('a', null, false, 'b')).toBe('a b'); + }); + + it('should include truthy values from an object', () => { + expect(classNames('a', {'b': true, 'c': false}, 'd')).toBe('a b d'); + }); + + it('should work with arrays', () => { + expect(classNames(['a', 'b'])).toBe('a b'); + }); + + it('should work with nested arrays', () => { + expect(classNames(['a', ['b', 'c']])).toBe('a b c'); + }); + + it('should work with arrays containing objects', () => { + expect(classNames(['a', {'b': true}])).toBe('a b'); + }); + + it('should work with deeply nested structures', () => { + expect(classNames(['a', ['b', {'c': true}], [['d', {'e': true}]]])).toBe('a b c d e'); + }); + }); diff --git a/src/classnames/index.ts b/src/classnames/index.ts index a536571..385a59e 100644 --- a/src/classnames/index.ts +++ b/src/classnames/index.ts @@ -1,49 +1,64 @@ -export type ClassValue = string | undefined | null | boolean; -export type ClassMap = Record; +export type ClassValue = string | boolean | ClassValueArray | ClassMap; +export type ClassValueArray = ClassValue[]; +export type ClassMap = { [key: string]: string | boolean }; -export function isString(value: unknown): value is string { - return typeof value === "string"; -} +export function classNames(...args: ClassValue[]): string { + const classes: string[] = []; -export function isBooleanString(value: unknown): boolean { - return ["true", "1"].includes(value as string); -} + for (let i = 0; i < args.length; i++) { + processArg(args[i], classes); + } -export function classNames(...args: (ClassValue | ClassMap | string | string[])[]): string { - function processArg (arg: ClassValue | ClassMap | string): string[] { - if (arg === undefined || arg === null || typeof arg === "boolean") { - return []; - } + let finalClasses = ''; + let isFirstClass = true; - if (Array.isArray(arg)) { - return arg.filter(isString); + for (let i = 0; i < classes.length; i++) { + // Ignore boolean 'true' + if (classes[i] === 'true') { + continue; } - if (typeof arg === "object") { - return Object.keys(arg) - .map((key) => { - const value = arg[key]; - if (value === true) { - return key; - } - if (isString(value) && isBooleanString(value)) { - return key; - } - return null; - }) - .filter(Boolean) - .filter(isString); + // Trim each class + const trimmedClass = classes[i].trim(); + + if (trimmedClass) { + if (!isFirstClass) { + finalClasses += ' '; + } else { + isFirstClass = false; + } + finalClasses += trimmedClass; } + } - if (isString(arg)) { - return [arg]; + return finalClasses; +} + +function processArg(arg: ClassValue, classes: string[]): void { + if (arg === undefined || arg === null || arg === false) { + return; + } + + if (Array.isArray(arg)) { + for (let i = 0; i < arg.length; i++) { + processArg(arg[i], classes); } + return; + } - return []; - }; + if (typeof arg === 'object') { + for (const key in arg) { + if (Object.prototype.hasOwnProperty.call(arg, key)) { + const value = arg[key]; + if (value === true || value === 'true' || value === '1') { + classes.push(key); + } + } + } + return; + } - return args - .flatMap(processArg) - .filter(Boolean) - .join(" "); + if (typeof arg === 'string' || typeof arg === 'boolean') { + classes.push(arg.toString()); + } } diff --git a/src/index.ts b/src/index.ts index db81aa4..63e7b3f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1 @@ -export {classNames, ClassValue, ClassMap} from "./classnames" +export {classNames, ClassValue, ClassMap, ClassValueArray} from "./classnames" diff --git a/tsconfig.json b/tsconfig.json index 199c89f..c6ccf65 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,7 @@ "removeComments": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, - "target": "ES2021", + "target": "es5", "outDir": "./dist", "skipLibCheck": false, "strictNullChecks": false,