Skip to content

Commit

Permalink
feat(cn): add support for condition objects
Browse files Browse the repository at this point in the history
  • Loading branch information
itsjavi committed Oct 7, 2023
1 parent c1b9581 commit e37e798
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 14 deletions.
20 changes: 17 additions & 3 deletions packages/cn/src/cn.test.ts
Expand Up @@ -9,18 +9,32 @@ describe('cn (classNames)', () => {
})

it('should handle conditional class names', () => {
expect(cn([true, 'foo'], [false, 'bar'], [true, 'baz'])).toBe('foo baz')
expect(cn(['foo', true], ['bar', false], ['baz', true])).toBe('foo baz')
})

it('should handle objects with conditions', () => {
expect(
cn({
foo: true,
bar: [false, 'bar-alt'],
baz: true,
}),
).toBe('foo bar-alt baz')
})

it('should handle if-else conditional class names', () => {
expect(cn('foo', [true, 'qux', 'baz'])).toBe('foo qux')
expect(cn('foo', [false, 'qux', 'baz'])).toBe('foo baz')
expect(cn('foo', ['qux', true, 'baz'])).toBe('foo qux')
expect(cn('foo', ['qux', false, 'baz'])).toBe('foo baz')
})

it('should handle undefined and null class names', () => {
expect(cn(undefined, null, 'foo', null)).toBe('foo')
})

it('should return empty string if no conditions match', () => {
expect(cn(undefined, null, [null])).toBe('')
})

it('should handle empty and whitespace-only class names', () => {
expect(cn('', ' ', 'foo', ' ', 'bar')).toBe('foo bar')
})
Expand Down
32 changes: 22 additions & 10 deletions packages/cn/src/cn.ts
@@ -1,4 +1,4 @@
import type { CnClassArg } from './types'
import type { CnClassArg, CnClassCondition, CnClassName } from './types'

/**
* A utility for conditionally joining classNames together.
Expand All @@ -18,23 +18,35 @@ import type { CnClassArg } from './types'
*/
export function cn(...classNames: CnClassArg[]): string {
return classNames
.flatMap((arg) => {
if (
Array.isArray(arg) &&
arg.length >= 2 &&
(typeof arg[0] === 'boolean' || arg[0] === undefined || arg[0] === null)
) {
.flatMap((arg): Array<boolean | CnClassName> | boolean | CnClassName => {
if (Array.isArray(arg) && arg.length >= 2) {
if (arg.length > 2) {
const [condition, valueIfTruthy, ...valuesIfFalsy] = arg
const [valueIfTruthy, condition, elseValue] = arg

return condition ? valueIfTruthy : valuesIfFalsy
return condition ? valueIfTruthy : elseValue
}

const [condition, valueIfTruthy] = arg
const [valueIfTruthy, condition] = arg

return condition ? valueIfTruthy : undefined
}

if (typeof arg === 'object') {
if (arg === null) {
return []
}

return Object.entries(arg).map(([valueIfTruthy, value]: [string, string | [CnClassCondition, string]]) => {
if (Array.isArray(value)) {
const [condition, elseValue] = value

return condition ? valueIfTruthy : elseValue
}

return value ? valueIfTruthy : undefined
})
}

return arg
})
.filter(Boolean)
Expand Down
6 changes: 5 additions & 1 deletion packages/cn/src/types.ts
Expand Up @@ -3,4 +3,8 @@ export type CnClassName = string | undefined | null
export type CnClassArg =
| CnClassName
| CnClassName[]
| [condition: CnClassCondition, ifTrueClass: CnClassName, ...elseClasses: CnClassName[]]
| [ifTrueClass: CnClassName, condition: CnClassCondition]
| [ifTrueClass: CnClassName, condition: CnClassCondition, elseClass: CnClassName]
| {
[key: string]: CnClassCondition | [condition: CnClassCondition, elseClass: CnClassName]
}

0 comments on commit e37e798

Please sign in to comment.