Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 58 additions & 22 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,50 @@
import React from 'react'

let JOIN_MODIFIERS = '-'
let JOIN_WORDS = '-'
let TRANSFORM_CASE = true
const DEFAULT_CONFIG = {
transformCase: true,
join: {
block: '-',
modifier: '-',
value: '-',
words: '-'
}
}

let config = DEFAULT_CONFIG

export function configure(opts) {
JOIN_MODIFIERS = (opts.join && opts.join.modifiers) || JOIN_MODIFIERS
JOIN_WORDS = (opts.join && opts.join.words) || JOIN_WORDS
TRANSFORM_CASE = opts.hasOwnProperty('transformCase')
? Boolean(opts['transformCase'])
: TRANSFORM_CASE
config = {
...DEFAULT_CONFIG,
...opts,
join: { ...DEFAULT_CONFIG.join, ...opts.join }
}
}

function transformName(name) {
function toStyleName(modifier, value) {
const name =
value === true ? modifier : `${modifier}${config.join.value}${value}`
// Might need to customize the separator
// This is aZ | aXYZ
let style = name
.split(/(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/)
.join(JOIN_WORDS)
.join(config.join.words)

if (TRANSFORM_CASE) {
style = style[0] + style.substring(1).toLowerCase()
if (config.transformCase) {
style = style.toLowerCase()
}

return style
}

function toStyleName(name, modifiers) {
return transformName(
modifiers ? `${name}${JOIN_MODIFIERS}${modifiers}` : name
)
}

function toClassNames(props) {
return Object.keys(props)
.filter(name => !!props[name])
.filter(name => props[name] !== false)
.map(name => {
if (Array.isArray(props[name])) {
return props[name].map(inner => toStyleName(name, inner)).join(' ')
}

return toStyleName(name, props[name] === true ? undefined : props[name])
return toStyleName(name, props[name])
})
}

Expand Down Expand Up @@ -78,14 +82,46 @@ export const Box = ({
A wrapper that injects its children with style and className
using shallow merging and classNameTransform
*/
export const Comp = ({ style, className, children, ...propClasses }) =>
export const Comp = ({ as, style, className, children, ...propClasses }) =>
React.Children.map(
children,
child =>
child
? React.cloneElement(child, {
style: { ...style, ...child.props.style },
className: cx(propClasses, className, child.props.className)
className: cx(
propClasses,
className,
child.props.className,
as && as.__classier
)
})
: null
)

export function mapToNamespace(name, props) {
return Object.keys(props).reduce(
(res, key) => {
res[`${name}${config.join.modifier}${key}`] = props[key]
return res
},
{ [name]: true }
)
}

export const createBlock = name => {
const render = props => <Box {...mapToNamespace(name, props)} />

const proxy = new Proxy(render, {
get(obj, nested) {
return nested in obj
? obj[nested]
: (obj[nested] = createBlock(`${name}${config.join.block}${nested}`))
}
})

// Store this so we can use it in Comp
proxy.__classier = toStyleName(name, true)

return proxy
}
36 changes: 36 additions & 0 deletions test/blocks.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const { mapToNamespace, createBlock } = require('../src')

test('mapToNamespace', () => {
const res = mapToNamespace('wakka', {
chicken: 'dinner',
sum: 41
})

expect(res).toEqual({
wakka: true,
'wakka-chicken': 'dinner',
'wakka-sum': 41
})
})

describe('createBlock', () => {
test('should have __classier', () => {
const ns = createBlock('NSWeAreTesting')
expect(ns).toHaveProperty('__classier', 'ns-we-are-testing')
})

test('should proxy to factory', () => {
const ns = createBlock('NS')
expect(ns.TestEl).toHaveProperty('__classier', 'ns-test-el')
})

xtest('should render root', () => {
const NS = createBlock('NS')
expect(<NS sum={41} />).toMatchSnapshot()
})

xtest('should render proxy', () => {
const NS = createBlock('NS')
expect(<NS.TestEl chicken="dinner" />).toMatchSnapshot()
})
})
105 changes: 0 additions & 105 deletions test/classier.test.js

This file was deleted.

35 changes: 35 additions & 0 deletions test/react.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const { Box, Comp } = require('../src')

describe('Box:', () => {
xtest('should render with class name', () => {
// expect(render(<Box testClass />)).toMatch(snapshot)
})

xtest('should render as span with class name', () => {
// expect(render(<Box is='span' testClass />)).toMatch(snapshot)
})

xtest('should render with prop values in class names', () => {
// expect(render(<Box text='red' py={5} />)).toMatch(snapshot)
})

xtest('should ignore onClick', () => {
// const fn = mock(() => ())
// expect(render(<Box clickMe text='red' onClick={fn} />).click('click-me')).toMatch(snapshot)
// expect(fn).toHaveBeenCalled().once()
})
})

describe('Comp:', () => {
xtest('should render with class name', () => {
// expect(render(<Comp testClass children={<div />} />)).toMatch(snapshot)
})

xtest('should render with parametric class names', () => {
// expect(render(<Comp text='red' py={5} children={<div />} />)).toMatch(snapshot)
})

xtest('should inject style', () => {
// expect(render(<Comp style={{ color: 'red' }} children={<div />} />)).toMatch(snapshot)
})
})
99 changes: 99 additions & 0 deletions test/translation.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
const { cx } = require('../src')

describe('cx:', () => {
test('should transform propClasses with booleans', () => {
const res = cx({
chicken: true,
dinner: true
})

expect(res).toEqual('chicken dinner')
})

test('should transform propClasses with normal values', () => {
const res = cx({
chicken: 'dinner',
sum: 41
})

expect(res).toEqual('chicken-dinner sum-41')
})

test('should transform propClasses with arrays', () => {
const res = cx({
chicken: ['tasty', 'dinner']
})

expect(res).toEqual('chicken-tasty chicken-dinner')
})

test('should transform propClasses with CamelCase names', () => {
const res = cx({
CamelHumps: 'LovelyCamelHumps'
})

expect(res).toEqual('camel-humps-lovely-camel-humps')
})
})

describe('configure:', () => {
let configure, cx
beforeEach(() => {
jest.resetModules()
const lib = require('../src')
cx = lib.cx
configure = lib.configure
})

test('should allow changing value joiner', () => {
configure({
join: {
value: '__'
}
})
const res = cx({
George: 'Foreman'
})

expect(res).toEqual('george__foreman')
})

test('should allow changing modifier joiner', () => {
const { mapToNamespace } = require('../src')
configure({
join: {
modifier: '__'
}
})
const res = mapToNamespace('Mr', {
George: 'Foreman'
})

expect(res).toHaveProperty('Mr__George')
})

test('should allow changing block joiner', () => {
const { createBlock } = require('../src')
configure({
join: {
block: '__'
}
})
const MFG = createBlock('BlackAndDecker')

expect(MFG.GeorgeForeman.__classier).toEqual(
'black-and-decker__george-foreman'
)
})

test('should allow changing transformCase', () => {
configure({
transformCase: false
})
const res = cx({
George: 'Foreman'
})

expect(res).toEqual('George-Foreman')
})
})