diff --git a/src/index.js b/src/index.js index bc88d49..7a7bb45 100644 --- a/src/index.js +++ b/src/index.js @@ -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]) }) } @@ -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 => + + 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 +} diff --git a/test/blocks.test.js b/test/blocks.test.js new file mode 100644 index 0000000..de9c106 --- /dev/null +++ b/test/blocks.test.js @@ -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().toMatchSnapshot() + }) + + xtest('should render proxy', () => { + const NS = createBlock('NS') + expect().toMatchSnapshot() + }) +}) diff --git a/test/classier.test.js b/test/classier.test.js deleted file mode 100644 index 2b98128..0000000 --- a/test/classier.test.js +++ /dev/null @@ -1,105 +0,0 @@ -const { configure, cx, Box, Comp } = 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 modifier symbol', () => { - configure({ - join: { - modifiers: '__' - } - }) - const res = cx({ - George: 'Foreman' - }) - - expect(res).toEqual('George__foreman') - }) - - test('should allow changing transformCase', () => { - configure({ - transformCase: false - }) - const res = cx({ - George: 'Foreman' - }) - - expect(res).toEqual('George-Foreman') - }) -}) - -describe('Box:', () => { - xtest('should render with class name', () => { - // expect(render()).toMatch(snapshot) - }) - - xtest('should render as span with class name', () => { - // expect(render()).toMatch(snapshot) - }) - - xtest('should render with prop values in class names', () => { - // expect(render()).toMatch(snapshot) - }) - - xtest('should ignore onClick', () => { - // const fn = mock(() => ()) - // expect(render().click('click-me')).toMatch(snapshot) - // expect(fn).toHaveBeenCalled().once() - }) -}) - -describe('Comp:', () => { - xtest('should render with class name', () => { - // expect(render(} />)).toMatch(snapshot) - }) - - xtest('should render with parametric class names', () => { - // expect(render(} />)).toMatch(snapshot) - }) - - xtest('should inject style', () => { - // expect(render(} />)).toMatch(snapshot) - }) -}) diff --git a/test/react.test.js b/test/react.test.js new file mode 100644 index 0000000..afec138 --- /dev/null +++ b/test/react.test.js @@ -0,0 +1,35 @@ +const { Box, Comp } = require('../src') + +describe('Box:', () => { + xtest('should render with class name', () => { + // expect(render()).toMatch(snapshot) + }) + + xtest('should render as span with class name', () => { + // expect(render()).toMatch(snapshot) + }) + + xtest('should render with prop values in class names', () => { + // expect(render()).toMatch(snapshot) + }) + + xtest('should ignore onClick', () => { + // const fn = mock(() => ()) + // expect(render().click('click-me')).toMatch(snapshot) + // expect(fn).toHaveBeenCalled().once() + }) +}) + +describe('Comp:', () => { + xtest('should render with class name', () => { + // expect(render(} />)).toMatch(snapshot) + }) + + xtest('should render with parametric class names', () => { + // expect(render(} />)).toMatch(snapshot) + }) + + xtest('should inject style', () => { + // expect(render(} />)).toMatch(snapshot) + }) +}) diff --git a/test/translation.test.js b/test/translation.test.js new file mode 100644 index 0000000..ffb206d --- /dev/null +++ b/test/translation.test.js @@ -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') + }) +})