From 2c0862f5acf1dea3c7eaf63cdb27a27b2707a934 Mon Sep 17 00:00:00 2001 From: Sam Richard Date: Sat, 22 Sep 2018 16:50:34 -0600 Subject: [PATCH 01/11] refactor to create syntax nodes rather than just strings --- src/config.js | 8 +- src/factories.js | 65 +++++++++++++ src/index.js | 32 ++++++- src/mappers.js | 11 +++ src/syntax.js | 62 +++++++++++++ src/transform.js | 61 ++----------- .../{transform.test.js => classnames.test.js} | 33 ++++--- test/components.test.js | 59 ------------ test/globalStyles.test.js | 91 +++++++++++++++++++ test/moduleStyles.test.js | 26 ++++++ 10 files changed, 321 insertions(+), 127 deletions(-) create mode 100644 src/factories.js create mode 100644 src/mappers.js create mode 100644 src/syntax.js rename test/{transform.test.js => classnames.test.js} (72%) delete mode 100644 test/components.test.js create mode 100644 test/globalStyles.test.js create mode 100644 test/moduleStyles.test.js diff --git a/src/config.js b/src/config.js index 911f10b..72ff4d3 100644 --- a/src/config.js +++ b/src/config.js @@ -1,4 +1,7 @@ +import * as mappers from './mappers' + export const config = { + mapFn: mappers.tailwind, keepSentence: true, kebabCase: true, join: { @@ -10,9 +13,12 @@ export const config = { } } -export function configure (opts) { +export function configure(opts) { Object.assign(config, { ...opts, join: { ...config.join, ...opts.join } }) } + +export { mappers } +export default config diff --git a/src/factories.js b/src/factories.js new file mode 100644 index 0000000..e47afb8 --- /dev/null +++ b/src/factories.js @@ -0,0 +1,65 @@ +import React from 'react' + +/* + A text span that uses its unknown props as css class names + */ +export const makeText = mapper => ({ className, children, ...propClasses }) => ( + +) + +/* + A structural div that uses its unknown props as css class names + */ +export const makeBox = mapper => ({ + title, + style, + onClick, + className, + children, + is = 'div', + ...propClasses +}) => { + const classes = mapper(propClasses, className) + const Tag = is + + const props = { + title, + style, + onClick, + children, + className: classes, + role: onClick ? 'button' : undefined + } + + return +} + +/* + A wrapper that injects its children with style and className + using shallow merging and mapper + */ +export const makeComposer = mapper => ({ + style, + className, + children, + ...propClasses +}) => + React.Children.map( + children, + child => + child + ? React.cloneElement(child, { + style: { ...style, ...child.props.style }, + className: mapper(propClasses, className, child.props.className) + }) + : null + ) + +/* +A group of components with an element-specific mapper + */ +export const makeElement = mapper => { + const El = makeBox(mapper) + El.Comp = makeComposer(mapper) + return El +} diff --git a/src/index.js b/src/index.js index de59460..83c335e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,31 @@ -export { default as cx } from './transform' -export * from './components' +import { + classnames, + classnamesWithBase, + classnamesWithMapper +} from './transform' +import * as factories from './factories' + +export * from './transform' export * from './config' + +// Components +export const Box = factories.makeBox(classnames) +export const Text = factories.makeText(classnames) +export const Comp = factories.makeComposer(classnames) + +export const createElement = block => + factories.makeElement(classnamesWithBase({ block })) + +export const createModuleElement = (block, module) => + factories.makeElement( + classnamesWithMapper(name => module[name] || name, { block }) + ) + +export const wrapModule = module => + Object.keys(module).reduce((res, key) => { + res[key.charAt(0).toUpperCase() + key.substring(1)] = createModuleElement( + key, + module + ) + return res + }, {}) diff --git a/src/mappers.js b/src/mappers.js new file mode 100644 index 0000000..b3383a8 --- /dev/null +++ b/src/mappers.js @@ -0,0 +1,11 @@ +import { config } from './config' + +export function tailwind(node) { + return [ + node.variant && `${node.variant}${config.join.variant}`, + node.block, + node.block && node.modifier && config.join.modifier, + node.modifier, + node.value && `${config.join.value}${node.value}` + ].join('') +} diff --git a/src/syntax.js b/src/syntax.js new file mode 100644 index 0000000..bf06977 --- /dev/null +++ b/src/syntax.js @@ -0,0 +1,62 @@ +import config from './config' +require('array.prototype.flatmap/auto') + +function kebab(camel, sentenceCase) { + // Might need to customize the separator + // This is aZ | aXYZ + return camel + .toString() + .match(/[A-Z]?[^A-Z]+|([A-Z](?![^A-Z]))+/g) + .reduce( + (result, word, index) => + result + + (index //not first + ? config.join.words + word.toLowerCase() + : sentenceCase + ? word + : word.toLowerCase()), + '' + ) +} + +const getNodes = (value, env) => { + if (Array.isArray(value)) { + return value.flatMap(inner => getNodes(inner, env)) + } + + if (typeof value === 'object') { + return Object.keys(value).flatMap( + variant => getNodes(value[variant], { variant, ...env }) //overwrite with existing + ) + } + + return { + ...env, + value: + value !== true ? (config.kebabCase ? kebab(value) : value) : undefined + } +} + +const propsToNodeList = (props, env) => + Object.keys(props).flatMap( + name => + props[name] !== false + ? getNodes(props[name], { + ...env, + modifier: config.kebabCase ? kebab(name, config.keepSentence) : name + }) + : [] + ) + +/** + * Build a list of AST nodes from props and env + * pass it to mapper + * join the parts to one string + */ +export const mapAstToString = (mapper, env) => (props, ...extraClassNames) => { + const nodeList = propsToNodeList(props, env) + return extraClassNames + .filter(i => i) + .concat(mapper(env ? [env, ...nodeList] : nodeList)) + .join(' ') +} diff --git a/src/transform.js b/src/transform.js index ffd42d8..5a150e1 100644 --- a/src/transform.js +++ b/src/transform.js @@ -1,59 +1,14 @@ -import { config } from './config' +import config from './config' +import { mapAstToString } from './syntax' -function kebab(camel, sentenceCase) { - // Might need to customize the separator - // This is aZ | aXYZ - return camel - .toString() - .match(/[A-Z]?[^A-Z]+|([A-Z](?![^A-Z]))+/g) - .reduce( - (result, word, index) => - result + - (index //not first - ? config.join.words + word.toLowerCase() - : sentenceCase - ? word - : word.toLowerCase()), - '' - ) -} +export const classnamesWithMapper = (mapper, base) => + mapAstToString(nodes => nodes.map(config.mapFn).map(mapper), base) -function toStyleName(modifier, value) { - let tm = config.kebabCase ? kebab(modifier, config.keepSentence) : modifier +export const classnamesWithBase = base => + mapAstToString(nodes => nodes.map(config.mapFn), base) - if (value === true) { - return tm - } +export const classnames = classnamesWithBase(undefined) - let tv = config.kebabCase ? kebab(value) : value - - return `${tm}${config.join.value}${tv}` -} - -function toClassNames(name, value) { - if (Array.isArray(value)) { - return value.map(inner => toClassNames(name, inner)) - } - - if (typeof value === 'object') { - return Object.keys(value).map(variant => - toClassNames(`${variant}${config.join.variant}${name}`, value[variant]) - ) - } - - return toStyleName(name, value) -} - -function cx(props, ...extraClassNames) { - return Object.keys(props) - .reduce( - (classes, name) => - props[name] !== false - ? classes.concat(toClassNames(name, props[name])) - : classes, - extraClassNames - ) - .join(' ') -} +export const cx = classnames export default cx diff --git a/test/transform.test.js b/test/classnames.test.js similarity index 72% rename from test/transform.test.js rename to test/classnames.test.js index 1ad44dd..408f149 100644 --- a/test/transform.test.js +++ b/test/classnames.test.js @@ -1,8 +1,8 @@ -import { configure, cx } from '../src' +import { classnames, classnamesWithBase } from '../src' -describe('cx:', () => { +describe('classnames:', () => { test('should transform propClasses with booleans', () => { - const res = cx({ + const res = classnames({ chicken: true, dinner: true }) @@ -11,7 +11,7 @@ describe('cx:', () => { }) test('should transform propClasses with normal values', () => { - const res = cx({ + const res = classnames({ chicken: 'dinner', sum: 41 }) @@ -20,7 +20,7 @@ describe('cx:', () => { }) test('should transform propClasses with arrays', () => { - const res = cx({ + const res = classnames({ chicken: ['tasty', 'dinner'] }) @@ -28,7 +28,7 @@ describe('cx:', () => { }) test('should transform propClasses with variants', () => { - const res = cx({ + const res = classnames({ chicken: ['dinner', { morning: 'breakfast' }] }) @@ -36,20 +36,29 @@ describe('cx:', () => { }) test('should transform propClasses with CamelCase names', () => { - const res = cx({ + const res = classnames({ CamelHumps: 'LovelyCamelHumps' }) expect(res).toEqual('Camel-humps-lovely-camel-humps') }) + + test('classnamesWithBase', () => { + const res = classnamesWithBase({ block: 'wakka' })({ + chicken: 'dinner', + sum: 41 + }) + + expect(res).toEqual('wakka wakka-chicken-dinner wakka-sum-41') + }) }) describe('configure:', () => { - let configure, cx + let configure, classnames beforeEach(() => { jest.resetModules() const lib = require('../src') - cx = lib.cx + classnames = lib.classnames configure = lib.configure }) @@ -59,7 +68,7 @@ describe('configure:', () => { value: '__' } }) - const res = cx({ + const res = classnames({ George: 'Foreman' }) @@ -70,7 +79,7 @@ describe('configure:', () => { configure({ keepSentence: false }) - const res = cx({ + const res = classnames({ George: 'Foreman' }) @@ -81,7 +90,7 @@ describe('configure:', () => { configure({ kebabCase: false }) - const res = cx({ + const res = classnames({ GrillinMachine: 'LeanMean' }) diff --git a/test/components.test.js b/test/components.test.js deleted file mode 100644 index ed0b893..0000000 --- a/test/components.test.js +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react' -import { shallow } from 'enzyme' - -import { Box, Comp } from '../src' - -describe('Box:', () => { - test('should render with class name', () => { - const wrapper = shallow() - expect(wrapper.hasClass('pass-test')).toBe(true) - }) - - test('should render as span with class name', () => { - const wrapper = shallow() - expect(wrapper.hasClass('pass-test')).toBe(true) - expect(wrapper.type()).toBe('span') - }) - - test('should render with prop values in class names', () => { - const wrapper = shallow() - expect(wrapper.hasClass('text-red')).toBe(true) - expect(wrapper.hasClass('py-5')).toBe(true) - }) - - test('should ignore onClick', () => { - const onClick = jest.fn() - const wrapper = shallow() - wrapper.simulate('click') - expect(onClick.mock.calls.length).toBe(1) - }) -}) - -describe('Comp:', () => { - test('should add parametric class names', () => { - const wrapper = shallow(} />) - expect(wrapper.type()).toBe('div') - expect(wrapper.hasClass('text-red')).toBe(true) - expect(wrapper.hasClass('py-5')).toBe(true) - }) - - test('should merge classNames', () => { - const wrapper = shallow( - } /> - ) - expect(wrapper.type()).toBe('div') - expect(wrapper.hasClass('inner-test')).toBe(true) - expect(wrapper.hasClass('pass-test')).toBe(true) - }) - - test('should merge style', () => { - const wrapper = shallow( - } - /> - ) - expect(wrapper.type()).toBe('div') - expect(wrapper.prop('style')).toEqual({ color: 'red', background: 'blue' }) - }) -}) diff --git a/test/globalStyles.test.js b/test/globalStyles.test.js new file mode 100644 index 0000000..7da20ec --- /dev/null +++ b/test/globalStyles.test.js @@ -0,0 +1,91 @@ +import React from 'react' +import { shallow } from 'enzyme' + +import { Text, Box, Comp, createElement } from '../src' + +describe('Text:', () => { + test('should render with class name', () => { + const wrapper = shallow() + expect(wrapper.prop('className')).toEqual('props-test') + expect(wrapper.text()).toBe('test') + }) + + test('should render with prop values in class names', () => { + const wrapper = shallow() + expect(wrapper.prop('className')).toEqual('text-red py-5') + expect(wrapper.text()).toBe('test') + }) +}) + +describe('Box:', () => { + test('should render with class name', () => { + const wrapper = shallow() + expect(wrapper.prop('className')).toEqual('props-test') + }) + + test('should render as header with class name', () => { + const wrapper = shallow() + expect(wrapper.prop('className')).toEqual('props-test') + expect(wrapper.type()).toBe('h1') + }) + + test('should render with prop values in class names', () => { + const wrapper = shallow() + expect(wrapper.prop('className')).toEqual('text-red py-5') + }) + + test('should ignore onClick', () => { + const onClick = jest.fn() + const wrapper = shallow() + wrapper.simulate('click') + expect(onClick.mock.calls.length).toBe(1) + expect(wrapper.prop('className')).toEqual('text-red') + }) +}) + +describe('Comp:', () => { + test('should add parametric class names', () => { + const wrapper = shallow(} />) + expect(wrapper.prop('className')).toEqual('text-red py-5') + expect(wrapper.type()).toBe('div') + }) + + test('should merge classNames', () => { + const wrapper = shallow( + } /> + ) + expect(wrapper.type()).toBe('div') + expect(wrapper.prop('className')).toEqual('inner-test props-test') + }) + + test('should merge style', () => { + const wrapper = shallow( + } + /> + ) + expect(wrapper.type()).toBe('div') + expect(wrapper.prop('style')).toEqual({ color: 'red', background: 'blue' }) + }) +}) + +describe('createElement', () => { + test('should create an element that renders with class name', () => { + const Tester = createElement('tester') + const wrapper = shallow() + expect(wrapper.prop('className')).toEqual('tester tester-props-test') + }) + + test('should create an element that composes with class name', () => { + const Tester = createElement('tester') + const wrapper = shallow( + +
+ + ) + expect(wrapper.prop('className')).toEqual( + 'inner-test tester tester-props-test' + ) + }) +}) diff --git a/test/moduleStyles.test.js b/test/moduleStyles.test.js new file mode 100644 index 0000000..9213a1b --- /dev/null +++ b/test/moduleStyles.test.js @@ -0,0 +1,26 @@ +import React from 'react' +import { shallow } from 'enzyme' + +import { wrapModule, createModuleElement } from '../src' + +// Probably need to make this more complex +const mockedModule = { + element: 'ocy2n5fe2w', + 'element-modifier-value': 'xxb7hghzte' +} + +describe('createModuleElement', () => { + test('should create an element that renders as class name', () => { + const Element = createModuleElement('element', mockedModule) + const wrapper = shallow() + expect(wrapper.prop('className')).toEqual('ocy2n5fe2w xxb7hghzte') + }) +}) + +describe('wrapModule', () => { + test('should create an element that renders as class name', () => { + const NS = wrapModule(mockedModule) + const wrapper = shallow() + expect(wrapper.prop('className')).toEqual('ocy2n5fe2w xxb7hghzte') + }) +}) From 1be07248817777424f6ff8acb4a53b06562cf974 Mon Sep 17 00:00:00 2001 From: Sam Richard Date: Sun, 19 Aug 2018 23:00:55 -0600 Subject: [PATCH 02/11] fix building --- .babelrc | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/.babelrc b/.babelrc index 3ca8c4d..6a415f8 100644 --- a/.babelrc +++ b/.babelrc @@ -1,21 +1,7 @@ { "ignore": ["node_modules/**"], "env": { - "development": { - "presets": [ - [ - "env", - { - "targets": { - "node": 8 - }, - "debug": false, - "useBuiltIns": true, - "modules": "commonjs" - } - ] - ] - }, + "development": { "presets": [["env"]] }, "production": { "presets": [ [ From 0fff530def2bf15c8546fdf28df899f2b3d1a570 Mon Sep 17 00:00:00 2001 From: Sam Richard Date: Thu, 16 Aug 2018 19:49:19 -0600 Subject: [PATCH 03/11] Update README.md --- README.md | 62 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7500bd8..eac5c16 100644 --- a/README.md +++ b/README.md @@ -101,16 +101,38 @@ const MyComponent = ({ option, ...rest }) => ## CSS Modules -`classier-react` supports using CSS Modules through dynamic object keys: +`classier-react` supports CSS Modules by letting you wrap them with `createModuleElement` and `wrapModule`. + +`wrapModule` will expose every style name in your module: + ```jsx +// feature/block.js +import { wrapModule } from 'classier-react' import styles from './styles.css' +export default wrapModule(styles) +``` + +Often, selectors are not elements. You might find it better to pick which elements to export: +```jsx +// feature/block.js +import { createModuleElement } from 'classier-react' +import styles from './styles.css' + +export { + Element: createModuleElement('element', styles) +} +``` + +Your elements can then be used in your other components +```jsx +// feature/component.jsx +import Block from './block' + ... - + ``` ## Avoiding mixing domains @@ -151,7 +173,33 @@ Renders a span with all its props translated to CSS classes. ### `` -A _HOC_ for injecting or "composing" style props. Merges its `style`, `className`, and the rest of its props as classes into the props of a child component. +A declarative wrapper for injecting or "composing" style props. Merges its `style`, `className`, and the rest of its props as classes into the props of a child component. + +--- + +### `createElement(name)` + +Returns a customized `Box` that nests its CSS classes under the {name} block. + +#### members + +- **Comp** - a `Comp` with the same customization. + +--- + +### `createModuleElement(name, module)` + +Returns a customized `Box` that tries to map its CSS classes to the ones in the provided module. + +#### members + +- **Comp** - a `Comp` with the same customization. + +--- + +### `wrapModule(module)` + +Returns an object that maps the selectors of `module` to `createModuleElement`. Might create a lot of elements. --- @@ -167,7 +215,9 @@ Lets you change the global behavior of `cx` #### opts -- **kebabCase** - Transform names and values from camelCase. Reverses `style-loader`. (_default: true_) +- **transformFn** - A function mapping an AST Node to a CSS class name. (_default: mappers.tailwind_) + +- **kebabCase** - Transform names and values from camelCase. You might want to turn it off with `postcss-modules`. (_default: true_) - **keepSentence** - When kebabing, lower-case everything but the first word (_default: true_) From 5e238d6a82d5ed25fc261daac67e9978f573ebc9 Mon Sep 17 00:00:00 2001 From: Sam Richard Date: Sun, 19 Aug 2018 22:59:21 -0600 Subject: [PATCH 04/11] Change the module API --- src/index.js | 17 +++++++++-------- test/moduleStyles.test.js | 18 +++++++++--------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/index.js b/src/index.js index 83c335e..0618e44 100644 --- a/src/index.js +++ b/src/index.js @@ -16,16 +16,17 @@ export const Comp = factories.makeComposer(classnames) export const createElement = block => factories.makeElement(classnamesWithBase({ block })) -export const createModuleElement = (block, module) => +export const createModuleElement = (module, key) => factories.makeElement( - classnamesWithMapper(name => module[name] || name, { block }) + classnamesWithMapper(name => module[name] || name, { block: key }) ) -export const wrapModule = module => - Object.keys(module).reduce((res, key) => { - res[key.charAt(0).toUpperCase() + key.substring(1)] = createModuleElement( - key, - module - ) +export const boxedModule = module => + factories.makeElement(classnamesWithMapper(name => module[name] || name)) + +export const elementModule = (module, pick) => + (pick || Object.keys(module)).reduce((res, key) => { + const capped = key.charAt(0).toUpperCase() + key.substring(1) + res[capped] = createModuleElement(module, key) return res }, {}) diff --git a/test/moduleStyles.test.js b/test/moduleStyles.test.js index 9213a1b..e313d35 100644 --- a/test/moduleStyles.test.js +++ b/test/moduleStyles.test.js @@ -1,7 +1,7 @@ import React from 'react' import { shallow } from 'enzyme' -import { wrapModule, createModuleElement } from '../src' +import { boxedModule, elementModule } from '../src' // Probably need to make this more complex const mockedModule = { @@ -9,17 +9,17 @@ const mockedModule = { 'element-modifier-value': 'xxb7hghzte' } -describe('createModuleElement', () => { - test('should create an element that renders as class name', () => { - const Element = createModuleElement('element', mockedModule) - const wrapper = shallow() - expect(wrapper.prop('className')).toEqual('ocy2n5fe2w xxb7hghzte') +describe('boxedModule', () => { + test('should create an element that maps to module', () => { + const NSBox = boxedModule(mockedModule) + const wrapper = shallow() + expect(wrapper.prop('className')).toEqual('xxb7hghzte') }) }) -describe('wrapModule', () => { - test('should create an element that renders as class name', () => { - const NS = wrapModule(mockedModule) +describe('elementModule', () => { + test('should create an object with an element that maps to module with a base block', () => { + const NS = elementModule(mockedModule, ['element']) const wrapper = shallow() expect(wrapper.prop('className')).toEqual('ocy2n5fe2w xxb7hghzte') }) From 8ac5784bdd7f25ab125e92b94690aeb72fb013ca Mon Sep 17 00:00:00 2001 From: Sam Richard Date: Fri, 24 Aug 2018 21:21:59 -0600 Subject: [PATCH 05/11] Inline flatmap in mapAstToString add simple mapFlat for recursion --- src/mappers.js | 17 ++++++------- src/syntax.js | 62 +++++++++++++++++++++++++++++++++--------------- src/transform.js | 9 ++++--- 3 files changed, 58 insertions(+), 30 deletions(-) diff --git a/src/mappers.js b/src/mappers.js index b3383a8..c1d34ce 100644 --- a/src/mappers.js +++ b/src/mappers.js @@ -1,11 +1,12 @@ import { config } from './config' -export function tailwind(node) { - return [ - node.variant && `${node.variant}${config.join.variant}`, - node.block, - node.block && node.modifier && config.join.modifier, - node.modifier, - node.value && `${config.join.value}${node.value}` +export const tailwind = ({ variant, block, modifier, value }) => + [ + variant, + variant && config.join.variant, + block, + block && modifier && config.join.modifier, + modifier, + value && config.join.value, + value ].join('') -} diff --git a/src/syntax.js b/src/syntax.js index bf06977..c52b650 100644 --- a/src/syntax.js +++ b/src/syntax.js @@ -1,5 +1,17 @@ import config from './config' -require('array.prototype.flatmap/auto') + +function mapFlat(array, fn) { + const result = [] + for (const value of array) { + const out = fn(value) + if (Array.isArray(out)) { + result.push(...out) + } else { + result[result.length] = out + } + } + return result +} function kebab(camel, sentenceCase) { // Might need to customize the separator @@ -21,11 +33,12 @@ function kebab(camel, sentenceCase) { const getNodes = (value, env) => { if (Array.isArray(value)) { - return value.flatMap(inner => getNodes(inner, env)) + return mapFlat(value, inner => getNodes(inner, env)) } if (typeof value === 'object') { - return Object.keys(value).flatMap( + return mapFlat( + Object.keys(value), variant => getNodes(value[variant], { variant, ...env }) //overwrite with existing ) } @@ -37,26 +50,37 @@ const getNodes = (value, env) => { } } -const propsToNodeList = (props, env) => - Object.keys(props).flatMap( - name => - props[name] !== false - ? getNodes(props[name], { - ...env, - modifier: config.kebabCase ? kebab(name, config.keepSentence) : name - }) - : [] - ) - /** * Build a list of AST nodes from props and env * pass it to mapper * join the parts to one string */ export const mapAstToString = (mapper, env) => (props, ...extraClassNames) => { - const nodeList = propsToNodeList(props, env) - return extraClassNames - .filter(i => i) - .concat(mapper(env ? [env, ...nodeList] : nodeList)) - .join(' ') + const list = [] + + if (env) { + list.push(env) + } + + for (const name of Object.keys(props)) { + if (props[name] !== false) { + const nodes = getNodes(props[name], { + ...env, + modifier: config.kebabCase ? kebab(name, config.keepSentence) : name + }) + if (Array.isArray(nodes)) { + list.push(...nodes) + } else { + list.push(nodes) + } + } + } + + for (const i in list) { + list[i] = mapper(list[i]) + } + + list.push(...extraClassNames.filter(i => i)) + + return list.join(' ') } diff --git a/src/transform.js b/src/transform.js index 5a150e1..b87f631 100644 --- a/src/transform.js +++ b/src/transform.js @@ -2,13 +2,16 @@ import config from './config' import { mapAstToString } from './syntax' export const classnamesWithMapper = (mapper, base) => - mapAstToString(nodes => nodes.map(config.mapFn).map(mapper), base) + mapAstToString(node => mapper(config.mapFn(node)), base) -export const classnamesWithBase = base => - mapAstToString(nodes => nodes.map(config.mapFn), base) +export const classnamesWithBase = base => mapAstToString(config.mapFn, base) export const classnames = classnamesWithBase(undefined) export const cx = classnames +export const cxb = classnamesWithBase + +export const cxm = classnamesWithMapper + export default cx From 3e3dd7bdd8a08b2fb64e4d04bf13f2eb6ba98629 Mon Sep 17 00:00:00 2001 From: Sam Richard Date: Sun, 19 Aug 2018 22:59:56 -0600 Subject: [PATCH 06/11] fix class ordering in test --- test/globalStyles.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/globalStyles.test.js b/test/globalStyles.test.js index 7da20ec..19e6b11 100644 --- a/test/globalStyles.test.js +++ b/test/globalStyles.test.js @@ -55,7 +55,7 @@ describe('Comp:', () => { } /> ) expect(wrapper.type()).toBe('div') - expect(wrapper.prop('className')).toEqual('inner-test props-test') + expect(wrapper.prop('className')).toEqual('props-test inner-test') }) test('should merge style', () => { @@ -85,7 +85,7 @@ describe('createElement', () => { ) expect(wrapper.prop('className')).toEqual( - 'inner-test tester tester-props-test' + 'tester tester-props-test inner-test' ) }) }) From 9a2695ba5af70d04cd7fd6aec89bccee86d63ebc Mon Sep 17 00:00:00 2001 From: Sam Richard Date: Sat, 22 Sep 2018 16:52:08 -0600 Subject: [PATCH 07/11] Update README.md --- README.md | 73 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index eac5c16..18fdd53 100644 --- a/README.md +++ b/README.md @@ -101,38 +101,43 @@ const MyComponent = ({ option, ...rest }) => ## CSS Modules -`classier-react` supports CSS Modules by letting you wrap them with `createModuleElement` and `wrapModule`. +`classier-react` can wrap CSS Modules with `elementModule` and `boxedModule`. +`boxedModule` creates an Element that checks the provided module for a mapping after transformation: -`wrapModule` will expose every style name in your module: - ```jsx -// feature/block.js -import { wrapModule } from 'classier-react' +import { createModuleElement } from 'classier-react' import styles from './styles.css' -export default wrapModule(styles) +const MBox = boxedModule(styles) + +... + + ``` -Often, selectors are not elements. You might find it better to pick which elements to export: +`elementModule` will expose the optionally listed blocks as Elements from the module: + ```jsx -// feature/block.js import { createModuleElement } from 'classier-react' import styles from './styles.css' -export { - Element: createModuleElement('element', styles) -} -``` +const Block = elementModule(styles, ['element']) -Your elements can then be used in your other components -```jsx -// feature/component.jsx -import Block from './block' ... - + +``` + +## Passing down styles + +`` only injects the props it merges, it can't make sure they are rendered. If you're wrapping or writing a component, it's a good idea to pass the `style` and `className` props onward to what is rendered. + +Most components are already written this way. + +```jsx +const MyComponent = ({ option, ...rest }) =>
``` ## Avoiding mixing domains @@ -149,8 +154,10 @@ const containerClassName = cx(props.styled) ## API +### React + ```js -import { Box, Text, Comp, cx, configure } from 'classier-react' +import { Box, Text, Comp, createElement } from 'classier-react' ``` --- @@ -187,19 +194,37 @@ Returns a customized `Box` that nests its CSS classes under the {name} block. --- -### `createModuleElement(name, module)` +### CSS Modules -Returns a customized `Box` that tries to map its CSS classes to the ones in the provided module. +```js +import { boxedModule, createModuleElement, elementModule } from 'classier-react' +``` -#### members +--- -- **Comp** - a `Comp` with the same customization. +### `boxedModule(module)` + +Returns an `Element` that tries to map its CSS classes to the ones in the provided module. --- -### `wrapModule(module)` +### `createModuleElement(module, name)` + +Returns an `Element` that tries to map its CSS classes to the ones in the provided module under the {name} block. + +--- -Returns an object that maps the selectors of `module` to `createModuleElement`. Might create a lot of elements. +### `elementModule(module, pick)` + +Returns an object that picks selectors of `module` to `createModuleElement`. + +--- + +### Utility + +```js +import { cx, configure } from 'classier-react' +``` --- From 0f8eb6f6822b7f933c6a67e89a2c8a6c9607a9bd Mon Sep 17 00:00:00 2001 From: Sam Richard Date: Fri, 24 Aug 2018 22:06:20 -0600 Subject: [PATCH 08/11] fix mapper getting minified wrong --- src/mappers.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/mappers.js b/src/mappers.js index c1d34ce..a714b12 100644 --- a/src/mappers.js +++ b/src/mappers.js @@ -1,12 +1,10 @@ import { config } from './config' export const tailwind = ({ variant, block, modifier, value }) => - [ - variant, - variant && config.join.variant, - block, - block && modifier && config.join.modifier, - modifier, - value && config.join.value, - value - ].join('') + (variant || '') + + ((variant && config.join.variant) || '') + + (block || '') + + ((block && modifier && config.join.modifier) || '') + + (modifier || '') + + ((value && config.join.value) || '') + + (value || '') From 05e4f177020e2a0f098674a1f5343921f7aa6773 Mon Sep 17 00:00:00 2001 From: Sam Richard Date: Sat, 22 Sep 2018 17:03:20 -0600 Subject: [PATCH 09/11] move factories to individual components. move modules to their own file. --- src/components/Box.js | 40 +++++++++++++++----------- src/components/Comp.js | 31 +++++++++++--------- src/components/Text.js | 15 +++++++--- src/factories.js | 65 ------------------------------------------ src/index.js | 31 +------------------- src/modules.js | 26 +++++++++++++++++ 6 files changed, 79 insertions(+), 129 deletions(-) delete mode 100644 src/factories.js create mode 100644 src/modules.js diff --git a/src/components/Box.js b/src/components/Box.js index 7980d7e..1e68c4e 100644 --- a/src/components/Box.js +++ b/src/components/Box.js @@ -1,29 +1,35 @@ import React from 'react' -import cx from '../transform' +import { classnames } from '../transform' /* Creates an element that uses its unknown props as css class names. Defaults to a div. */ -export default function Box({ - style, - className, - onClick, - title, - children, - is = 'div', - ...propClasses -}) { - const classes = cx(propClasses, className) - - const props = { +export const factory = mapper => + function Box({ title, style, onClick, + className, children, - className: classes, - role: onClick ? 'button' : undefined + is = 'div', + ...propClasses + }) { + const classes = mapper(propClasses, className) + const Tag = is + + const props = { + title, + style, + onClick, + children, + className: classes, + role: onClick ? 'button' : undefined + } + + return } - return React.createElement(is, props) -} +export const Box = factory(classnames) + +export default Box diff --git a/src/components/Comp.js b/src/components/Comp.js index b5f17ef..b4e24e0 100644 --- a/src/components/Comp.js +++ b/src/components/Comp.js @@ -1,19 +1,24 @@ import React from 'react' -import cx from '../transform' +import { classnames } from '../transform' /* A wrapper that injects its children with style and className using shallow merging and classNameTransform */ -export default function Comp({ style, className, children, ...propClasses }) { - return React.Children.map( - children, - child => - child - ? React.cloneElement(child, { - style: { ...style, ...child.props.style }, - className: cx(propClasses, className, child.props.className) - }) - : null - ) -} +export const factory = mapper => + function Comp({ style, className, children, ...propClasses }) { + return React.Children.map( + children, + child => + child + ? React.cloneElement(child, { + style: { ...style, ...child.props.style }, + className: mapper(propClasses, className, child.props.className) + }) + : null + ) + } + +export const Comp = factory(classnames) + +export default Comp diff --git a/src/components/Text.js b/src/components/Text.js index e9960f3..f1558d5 100644 --- a/src/components/Text.js +++ b/src/components/Text.js @@ -1,9 +1,16 @@ import React from 'react' -import cx from '../transform' +import { classnames } from '../transform' /* Creates a span element that uses its unknown props as css class names. */ -export default function Text({ className, children, ...propClasses }) { - return -} +export const factory = mapper => + function Text({ className, children, ...propClasses }) { + return ( + + ) + } + +export const Text = factory(classnames) + +export default Text diff --git a/src/factories.js b/src/factories.js deleted file mode 100644 index e47afb8..0000000 --- a/src/factories.js +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react' - -/* - A text span that uses its unknown props as css class names - */ -export const makeText = mapper => ({ className, children, ...propClasses }) => ( - -) - -/* - A structural div that uses its unknown props as css class names - */ -export const makeBox = mapper => ({ - title, - style, - onClick, - className, - children, - is = 'div', - ...propClasses -}) => { - const classes = mapper(propClasses, className) - const Tag = is - - const props = { - title, - style, - onClick, - children, - className: classes, - role: onClick ? 'button' : undefined - } - - return -} - -/* - A wrapper that injects its children with style and className - using shallow merging and mapper - */ -export const makeComposer = mapper => ({ - style, - className, - children, - ...propClasses -}) => - React.Children.map( - children, - child => - child - ? React.cloneElement(child, { - style: { ...style, ...child.props.style }, - className: mapper(propClasses, className, child.props.className) - }) - : null - ) - -/* -A group of components with an element-specific mapper - */ -export const makeElement = mapper => { - const El = makeBox(mapper) - El.Comp = makeComposer(mapper) - return El -} diff --git a/src/index.js b/src/index.js index 0618e44..19f9571 100644 --- a/src/index.js +++ b/src/index.js @@ -1,32 +1,3 @@ -import { - classnames, - classnamesWithBase, - classnamesWithMapper -} from './transform' -import * as factories from './factories' - export * from './transform' export * from './config' - -// Components -export const Box = factories.makeBox(classnames) -export const Text = factories.makeText(classnames) -export const Comp = factories.makeComposer(classnames) - -export const createElement = block => - factories.makeElement(classnamesWithBase({ block })) - -export const createModuleElement = (module, key) => - factories.makeElement( - classnamesWithMapper(name => module[name] || name, { block: key }) - ) - -export const boxedModule = module => - factories.makeElement(classnamesWithMapper(name => module[name] || name)) - -export const elementModule = (module, pick) => - (pick || Object.keys(module)).reduce((res, key) => { - const capped = key.charAt(0).toUpperCase() + key.substring(1) - res[capped] = createModuleElement(module, key) - return res - }, {}) +export * from './modules' diff --git a/src/modules.js b/src/modules.js new file mode 100644 index 0000000..b350c65 --- /dev/null +++ b/src/modules.js @@ -0,0 +1,26 @@ +import { classnamesWithBase, classnamesWithMapper } from './transform' +import { factory as makeBox } from './componets/Box' +import { factory as makeComp } from './componets/Comp' + +export const makeElement = mapper => { + const El = makeBox(mapper) + El.Comp = makeComp(mapper) + return El +} + +export const createElement = block => makeElement(classnamesWithBase({ block })) + +export const createModuleElement = (module, key) => + makeElement( + classnamesWithMapper(name => module[name] || name, { block: key }) + ) + +export const boxedModule = module => + makeElement(classnamesWithMapper(name => module[name] || name)) + +export const elementModule = (module, pick) => + (pick || Object.keys(module)).reduce((res, key) => { + const capped = key.charAt(0).toUpperCase() + key.substring(1) + res[capped] = createModuleElement(module, key) + return res + }, {}) From a4f711a6cdbdb5733badbec607e76f29d38ccfc5 Mon Sep 17 00:00:00 2001 From: Sam Richard Date: Sat, 22 Sep 2018 17:34:05 -0600 Subject: [PATCH 10/11] move mappers and inject config --- src/config.js | 27 +++++++++++++++------------ src/index.js | 3 ++- src/mappers.js | 10 ---------- src/mappers/tailwind.js | 10 ++++++++++ src/modules.js | 4 ++-- 5 files changed, 29 insertions(+), 25 deletions(-) delete mode 100644 src/mappers.js create mode 100644 src/mappers/tailwind.js diff --git a/src/config.js b/src/config.js index 72ff4d3..a46832d 100644 --- a/src/config.js +++ b/src/config.js @@ -1,7 +1,18 @@ -import * as mappers from './mappers' +import tailwind from './mappers/tailwind' -export const config = { - mapFn: mappers.tailwind, +export const config = {} + +export function configure(opts) { + Object.assign(config, { + ...opts, + mapFn: config.mapFn || opts.mapFn(config), + join: { ...config.join, ...opts.join } + }) +} + +// Default configuration +configure({ + mapFn: tailwind, keepSentence: true, kebabCase: true, join: { @@ -11,14 +22,6 @@ export const config = { value: '-', words: '-' } -} - -export function configure(opts) { - Object.assign(config, { - ...opts, - join: { ...config.join, ...opts.join } - }) -} +}) -export { mappers } export default config diff --git a/src/index.js b/src/index.js index 19f9571..25b1dcc 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,4 @@ -export * from './transform' export * from './config' export * from './modules' +export * from './transform' +export * from './components' diff --git a/src/mappers.js b/src/mappers.js deleted file mode 100644 index a714b12..0000000 --- a/src/mappers.js +++ /dev/null @@ -1,10 +0,0 @@ -import { config } from './config' - -export const tailwind = ({ variant, block, modifier, value }) => - (variant || '') + - ((variant && config.join.variant) || '') + - (block || '') + - ((block && modifier && config.join.modifier) || '') + - (modifier || '') + - ((value && config.join.value) || '') + - (value || '') diff --git a/src/mappers/tailwind.js b/src/mappers/tailwind.js new file mode 100644 index 0000000..63b10c9 --- /dev/null +++ b/src/mappers/tailwind.js @@ -0,0 +1,10 @@ +export default function tailwind(config) { + return ({ variant, block, modifier, value }) => + (variant || '') + + ((variant && config.join.variant) || '') + + (block || '') + + ((block && modifier && config.join.modifier) || '') + + (modifier || '') + + ((value && config.join.value) || '') + + (value || '') +} diff --git a/src/modules.js b/src/modules.js index b350c65..d3f6204 100644 --- a/src/modules.js +++ b/src/modules.js @@ -1,6 +1,6 @@ import { classnamesWithBase, classnamesWithMapper } from './transform' -import { factory as makeBox } from './componets/Box' -import { factory as makeComp } from './componets/Comp' +import { factory as makeBox } from './components/Box' +import { factory as makeComp } from './components/Comp' export const makeElement = mapper => { const El = makeBox(mapper) From 39c5e4198aff4af2f30998f980a463016853a88d Mon Sep 17 00:00:00 2001 From: Sam Richard Date: Mon, 24 Sep 2018 12:52:20 -0600 Subject: [PATCH 11/11] Fix circular config with DI --- src/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.js b/src/config.js index a46832d..6b0b6d7 100644 --- a/src/config.js +++ b/src/config.js @@ -5,7 +5,7 @@ export const config = {} export function configure(opts) { Object.assign(config, { ...opts, - mapFn: config.mapFn || opts.mapFn(config), + mapFn: opts.mapFn ? opts.mapFn(config) : config.mapFn, join: { ...config.join, ...opts.join } }) }