├── .babelrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── package.json └── src ├── DOMComponent.js ├── __tests__ ├── hoc-test.js ├── inject-test.js ├── mixin-test.js └── setup.js ├── constants.js ├── hoc.js ├── index.js ├── inject.js └── mixin.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | 4 | .DS_Store 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | 3 | .gitignore 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.0" 4 | - "5.0" 5 | before_install: 6 | install: 7 | script: 8 | - npm test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-classmap 2 | 3 | [![Build Status](https://travis-ci.org/botify-labs/react-classmap.svg)](https://travis-ci.org/botify-labs/react-classmap) 4 | 5 | This hook lets you reconcile third-party React components with your CSS framework of choice by defining a mapping of additional class names to apply to children DOM components that have a given class name. 6 | 7 | ## Usage 8 | 9 | ```js 10 | import React, { PropTypes } from 'react'; 11 | import { ClassMapMixin } from 'react-classmap'; 12 | 13 | const GenericButton = React.createClass({ 14 | render() { 15 | return ( 16 | 37 | ``` 38 | 39 | If you're using ES6 classes instead of `React.createClass`, there's a [higher-order component](https://gist.github.com/sebmarkbage/ef0bf1f338a7182b6775). 40 | 41 | ```js 42 | import { classMap } from 'react-classmap'; 43 | 44 | classMap(MyButton, { 'generic-button': 'fa fa-cog' }); 45 | ``` 46 | 47 | With [ES7 decorators](https://github.com/wycats/javascript-decorators): 48 | 49 | ```js 50 | @classMap({ 'generic-button': 'fa fa-cog' }) 51 | class MyButton { 52 | // ... 53 | } 54 | ``` 55 | 56 | ## Installing 57 | 58 | ``` 59 | npm install react-classmap 60 | ``` 61 | 62 | ## Building 63 | 64 | ``` 65 | npm run build 66 | ``` 67 | 68 | ## Running tests 69 | 70 | ``` 71 | npm test 72 | ``` 73 | 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-classmap", 3 | "version": "1.0.0", 4 | "description": "Define a mapping of additional class names to apply to your React DOM components", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "babel src/ -d lib/", 8 | "prepublish": "npm run build", 9 | "test": "mocha src/__tests__ --require babel/register --require src/__tests__/setup" 10 | }, 11 | "author": "Botify ", 12 | "repository": { 13 | "type": "git", 14 | "url": "git@github.com:botify-labs/react-classmap.git" 15 | }, 16 | "license": "MIT", 17 | "devEngines": { 18 | "node": "4.x" 19 | }, 20 | "peerDependencies": { 21 | "react": "^15.0.0", 22 | "react-dom": "^15.0.0" 23 | }, 24 | "devDependencies": { 25 | "babel": "^5.8.23", 26 | "babel-core": "^5.8.23", 27 | "expect": "^1.12.2", 28 | "jsdom": "^7.0.2", 29 | "mocha": "^2.3.3", 30 | "react": "^15.0.0", 31 | "react-addons-test-utils": "^15.0.0", 32 | "react-dom": "^15.0.0" 33 | }, 34 | "dependencies": { 35 | "hoist-non-react-statics": "^1.0.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/DOMComponent.js: -------------------------------------------------------------------------------- 1 | import ReactDOMComponent from 'react/lib/ReactDOMComponent'; 2 | import { cloneElement } from 'react'; 3 | 4 | import { CLASSMAP_KEY } from './constants'; 5 | 6 | function applyClassMap(value, classMap) { 7 | if (!value || !classMap) { 8 | return value; 9 | } 10 | let classNames = value.split(/\s+/); 11 | classNames = classNames.map(c => { 12 | if (!classMap[c]) { 13 | return c; 14 | } 15 | return [c, classMap[c]].join(' '); 16 | }).join(' '); 17 | return classNames; 18 | } 19 | 20 | function applyClassMapToElement(element, context) { 21 | let className = applyClassMap( 22 | element.props.className, 23 | context[CLASSMAP_KEY] 24 | ); 25 | if (className === element.props.className) { 26 | return element; 27 | } 28 | return cloneElement(element, { className }); 29 | } 30 | 31 | class DOMComponent extends ReactDOMComponent { 32 | mountComponent(transaction, hostParent, hostContainerInfo, context) { 33 | this._currentElement = applyClassMapToElement( 34 | this._currentElement, 35 | context 36 | ); 37 | return super.mountComponent(...arguments); 38 | } 39 | 40 | updateComponent(transaction, prevElement, nextElement, context) { 41 | this._currentElement = applyClassMapToElement( 42 | this._currentElement, 43 | context 44 | ); 45 | return super.updateComponent(...arguments); 46 | } 47 | } 48 | 49 | export default DOMComponent; 50 | -------------------------------------------------------------------------------- /src/__tests__/hoc-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import TestUtils from 'react-addons-test-utils'; 5 | 6 | import classMap from '../hoc'; 7 | 8 | describe('classMap', () => { 9 | 10 | it('applies classes correctly', () => { 11 | 12 | const Test = classMap(React.createClass({ 13 | render() { 14 | return
; 15 | }, 16 | }), { child: 'class1 class2' }); 17 | 18 | let test = TestUtils.renderIntoDocument(); 19 | 20 | // For some reason `TestUtils.findRenderedDOMComponentWithClass` doesn't 21 | // work with higher-order components. 22 | expect(ReactDOM.findDOMNode(test).className).toEqual('child class1 class2'); 23 | 24 | }); 25 | 26 | it('works as a decorator', () => { 27 | 28 | @classMap({ child: 'class1 class2' }) 29 | class Test extends React.Component { 30 | render() { 31 | return
; 32 | } 33 | } 34 | 35 | let test = TestUtils.renderIntoDocument(); 36 | 37 | expect(ReactDOM.findDOMNode(test).className).toEqual('child class1 class2'); 38 | 39 | }); 40 | 41 | it('preserves non react statics', () => { 42 | 43 | @classMap({}) 44 | class Test extends React.Component { 45 | static foo = 'bar'; 46 | 47 | render() { 48 | return
; 49 | } 50 | } 51 | 52 | expect(Test.foo).toBe('bar'); 53 | 54 | }); 55 | 56 | }); 57 | -------------------------------------------------------------------------------- /src/__tests__/inject-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import React, { PropTypes } from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import ReactDOMServer from 'react-dom/server'; 5 | import TestUtils from 'react-addons-test-utils'; 6 | 7 | import { CLASSMAP_KEY } from '../constants'; 8 | import '../inject'; 9 | 10 | describe('inject', () => { 11 | 12 | it('only applies additional classNames as props to DOM components', () => { 13 | 14 | const Child = React.createClass({ 15 | render() { 16 | return
; 17 | }, 18 | }); 19 | 20 | const Test = React.createClass({ 21 | childContextTypes: { 22 | [CLASSMAP_KEY]: PropTypes.object, 23 | }, 24 | 25 | getChildContext() { 26 | return { 27 | [CLASSMAP_KEY]: { Child: 'class1 class2' }, 28 | }; 29 | }, 30 | 31 | render() { 32 | return ( 33 |
34 | 35 |
36 | ); 37 | }, 38 | }); 39 | 40 | let test = TestUtils.renderIntoDocument(); 41 | 42 | let child = TestUtils.findRenderedComponentWithType(test, Child); 43 | expect(child.props.className).toEqual('Child'); 44 | 45 | let childDOM = TestUtils.findRenderedDOMComponentWithClass(test, 'Child'); 46 | expect(childDOM.className).toEqual('Child class1 class2'); 47 | 48 | }); 49 | 50 | it('updates correctly', () => { 51 | 52 | const Test = React.createClass({ 53 | childContextTypes: { 54 | [CLASSMAP_KEY]: PropTypes.object, 55 | }, 56 | 57 | getChildContext() { 58 | return { 59 | [CLASSMAP_KEY]: { class1: 'class3', class2: 'class4' }, 60 | }; 61 | }, 62 | 63 | render() { 64 | return ( 65 |
66 | ); 67 | }, 68 | }); 69 | 70 | let div = document.createElement('div'); 71 | let test = ReactDOM.render(, div); 72 | 73 | TestUtils.findRenderedDOMComponentWithClass(test, 'class3'); 74 | 75 | ReactDOM.render(, div); 76 | 77 | TestUtils.findRenderedDOMComponentWithClass(test, 'class4'); 78 | 79 | }); 80 | 81 | it('doesn\'t leak', () => { 82 | 83 | const Child1 = React.createClass({ 84 | childContextTypes: { 85 | [CLASSMAP_KEY]: PropTypes.object, 86 | }, 87 | 88 | getChildContext() { 89 | return { 90 | [CLASSMAP_KEY]: { Child: 'class1' }, 91 | }; 92 | }, 93 | 94 | render() { 95 | return
; 96 | }, 97 | }); 98 | 99 | const Child2 = React.createClass({ 100 | render() { 101 | return
; 102 | }, 103 | }); 104 | 105 | const Test = React.createClass({ 106 | render() { 107 | return ( 108 |
109 | 110 | 111 |
112 | ); 113 | }, 114 | }); 115 | 116 | let test = TestUtils.renderIntoDocument(); 117 | 118 | let child1 = TestUtils.findRenderedComponentWithType(test, Child1); 119 | expect(child1.props.className).toEqual('Child'); 120 | 121 | let child2 = TestUtils.findRenderedComponentWithType(test, Child2); 122 | expect(child2.props.className).toEqual('Child'); 123 | 124 | let child1DOM = TestUtils.findRenderedDOMComponentWithClass(child1, 'Child'); 125 | expect(child1DOM.className).toEqual('Child class1'); 126 | 127 | let child2DOM = TestUtils.findRenderedDOMComponentWithClass(child2, 'Child'); 128 | expect(child2DOM.className).toEqual('Child'); 129 | }); 130 | 131 | it('works with `ReactDOMServer.renderToString()`', () => { 132 | 133 | const FooBar = React.createClass({ 134 | childContextTypes: { 135 | [CLASSMAP_KEY]: PropTypes.object, 136 | }, 137 | 138 | getChildContext() { 139 | return { 140 | [CLASSMAP_KEY]: { foo: 'bar' }, 141 | }; 142 | }, 143 | 144 | render() { 145 | return
; 146 | }, 147 | }); 148 | 149 | expect(ReactDOMServer.renderToString()).toMatch(/class="foo bar"/); 150 | }); 151 | 152 | }); 153 | -------------------------------------------------------------------------------- /src/__tests__/mixin-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import React from 'react'; 3 | import TestUtils from 'react-addons-test-utils'; 4 | 5 | import ClassMapMixin from '../mixin'; 6 | 7 | describe('ClassMapMixin', () => { 8 | 9 | it('applies classes correctly', () => { 10 | 11 | const Test = React.createClass({ 12 | mixins: [ 13 | ClassMapMixin({ 14 | child: 'class1 class2', 15 | }), 16 | ], 17 | 18 | render() { 19 | return
; 20 | }, 21 | }); 22 | 23 | let test = TestUtils.renderIntoDocument(); 24 | 25 | let childDOM = TestUtils.findRenderedDOMComponentWithClass(test, 'child'); 26 | expect(childDOM.className).toEqual('child class1 class2'); 27 | 28 | }); 29 | 30 | }); 31 | -------------------------------------------------------------------------------- /src/__tests__/setup.js: -------------------------------------------------------------------------------- 1 | var jsdom = require('jsdom'); 2 | 3 | global.document = jsdom.jsdom(''); 4 | global.window = document.defaultView; 5 | global.navigator = window.navigator; 6 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const CLASSMAP_KEY = '__classMap'; 2 | -------------------------------------------------------------------------------- /src/hoc.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import hoistNonReactStatics from 'hoist-non-react-statics'; 3 | 4 | import { CLASSMAP_KEY } from './constants'; 5 | import './inject'; 6 | 7 | export default function classMap(...args) { 8 | if (args.length === 1) { 9 | let [map] = args; 10 | return function classMapDecorator(Composed) { 11 | return classMap(Composed, map); 12 | }; 13 | } 14 | 15 | let [Composed, map] = args; 16 | 17 | class ClassMap extends React.Component { 18 | static childContextTypes = { [CLASSMAP_KEY]: PropTypes.object }; 19 | 20 | getChildContext() { 21 | return { [CLASSMAP_KEY]: map }; 22 | } 23 | 24 | render() { 25 | return ; 26 | } 27 | } 28 | 29 | return hoistNonReactStatics(ClassMap, Composed); 30 | } 31 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export ClassMapMixin from './mixin'; 2 | export classMap from './hoc'; 3 | -------------------------------------------------------------------------------- /src/inject.js: -------------------------------------------------------------------------------- 1 | import DOMComponent from './DOMComponent'; 2 | import ReactInjection from 'react/lib/ReactInjection'; 3 | 4 | ReactInjection.HostComponent.injectGenericComponentClass(DOMComponent); 5 | -------------------------------------------------------------------------------- /src/mixin.js: -------------------------------------------------------------------------------- 1 | import { PropTypes } from 'react'; 2 | 3 | import { CLASSMAP_KEY } from './constants'; 4 | import './inject'; 5 | 6 | export default function ClassMapMixin(map) { 7 | return { 8 | childContextTypes: { [CLASSMAP_KEY]: PropTypes.object }, 9 | 10 | getChildContext() { 11 | return { [CLASSMAP_KEY]: map }; 12 | }, 13 | }; 14 | } 15 | --------------------------------------------------------------------------------