├── .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 | [](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 |
17 | );
18 | },
19 | });
20 |
21 | const MyButton = React.createClass({
22 | mixins: [
23 | ClassMapMixin({
24 | // Children DOM components with the `generic-button` className will also
25 | // have the `fa fa-cog` classNames applied to them.
26 | 'generic-button': 'fa fa-cog',
27 | }),
28 | ],
29 |
30 | render() {
31 | return ;
32 | },
33 | });
34 |
35 | React.renderToString();
36 | // =>
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 |
--------------------------------------------------------------------------------