Skip to content

Commit 7aa2c45

Browse files
committed
Feat: cx.multiple() for declare multiple classes #291
1 parent e9ebbb2 commit 7aa2c45

File tree

2 files changed

+81
-1
lines changed

2 files changed

+81
-1
lines changed

packages/css/src/classname/cx.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { clsx } from "clsx";
2+
import type { ClassMultipleInput, ClassMultipleResult } from "./types.js";
23

34
/**
45
* Conditionally join class names into a single string
@@ -26,7 +27,23 @@ import { clsx } from "clsx";
2627
* cx('foo', [1 && 'bar', { baz: false }], ['hello', ['world']], 'cya');
2728
* // => 'foo bar hello world cya'
2829
*/
29-
export const cx = clsx;
30+
export const cx = Object.assign(clsx, {
31+
multiple: cxMultiple
32+
});
33+
34+
function cxMultiple<T extends ClassMultipleInput>(
35+
map: T
36+
): ClassMultipleResult<T> {
37+
const result = {} as ClassMultipleResult<T>;
38+
39+
for (const key in map) {
40+
if (Object.prototype.hasOwnProperty.call(map, key)) {
41+
result[key] = clsx(map[key]);
42+
}
43+
}
44+
45+
return result;
46+
}
3047

3148
// == Tests ====================================================================
3249
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -98,6 +115,12 @@ if (import.meta.vitest) {
98115
expect(cx("foo", 42, "bar")).toBe("foo 42 bar");
99116
});
100117

118+
it.skip("handles bigint inputs", () => {
119+
// NOTE: CLSX currently does not support BigInt, so these tests are skipped
120+
expect(cx(BigInt(123))).toBe("123");
121+
expect(cx("foo", BigInt(42), "bar")).toBe("foo 42 bar");
122+
});
123+
101124
it("filters falsy values correctly", () => {
102125
expect(cx(null)).toBe("");
103126
expect(cx(undefined)).toBe("");
@@ -127,4 +150,55 @@ if (import.meta.vitest) {
127150
assertType<string>(cx(123));
128151
});
129152
});
153+
154+
describe.concurrent("cx.multiple()", () => {
155+
it("processes a map of class values", () => {
156+
const result = cx.multiple({
157+
primary: ["bg-blue-500", "text-white"],
158+
secondary: ["bg-gray-500", "text-black"]
159+
});
160+
161+
expect(result.primary).toBe("bg-blue-500 text-white");
162+
expect(result.secondary).toBe("bg-gray-500 text-black");
163+
});
164+
165+
it("handles mixed input types in map", () => {
166+
const isHidden = false;
167+
const result = cx.multiple({
168+
strings: "foo bar",
169+
array: ["baz", "qux"],
170+
object: { enabled: true, disabled: false },
171+
conditional: ["base", isHidden && "hidden"]
172+
});
173+
174+
expect(result.strings).toBe("foo bar");
175+
expect(result.array).toBe("baz qux");
176+
expect(result.object).toBe("enabled");
177+
expect(result.conditional).toBe("base");
178+
});
179+
180+
it("handles empty map", () => {
181+
const result = cx.multiple({});
182+
expect(result).toEqual({});
183+
});
184+
185+
it("preserves keys with empty values", () => {
186+
const result = cx.multiple({
187+
empty: [],
188+
falsy: null
189+
});
190+
191+
expect(result.empty).toBe("");
192+
expect(result.falsy).toBe("");
193+
});
194+
195+
it("cx.multiple returns correct type", () => {
196+
const result = cx.multiple({
197+
a: "foo",
198+
b: ["bar"]
199+
});
200+
201+
assertType<{ a: string; b: string }>(result);
202+
});
203+
});
130204
}

packages/css/src/classname/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,9 @@ export type ClassDictionary = Record<string, unknown>;
2424
* Array of class values (supports nesting)
2525
*/
2626
export type ClassArray = ClassValue[];
27+
28+
export type ClassMultipleInput = Record<string, ClassValue>;
29+
30+
export type ClassMultipleResult<T extends ClassMultipleInput> = {
31+
[K in keyof T]: string;
32+
};

0 commit comments

Comments
 (0)