Skip to content

Commit

Permalink
bugfix
Browse files Browse the repository at this point in the history
  • Loading branch information
maphel committed Sep 3, 2023
1 parent e14e7d0 commit 3f41306
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 122 deletions.
130 changes: 47 additions & 83 deletions src/classnames/index.test.ts
Original file line number Diff line number Diff line change
@@ -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('');
});
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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');
});
});
89 changes: 52 additions & 37 deletions src/classnames/index.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,64 @@
export type ClassValue = string | undefined | null | boolean;
export type ClassMap = Record<string, boolean | string | number>;
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());
}
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export {classNames, ClassValue, ClassMap} from "./classnames"
export {classNames, ClassValue, ClassMap, ClassValueArray} from "./classnames"
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "ES2021",
"target": "es5",
"outDir": "./dist",
"skipLibCheck": false,
"strictNullChecks": false,
Expand Down

0 comments on commit 3f41306

Please sign in to comment.