diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000..3751ba8
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,19 @@
+{
+ "plugins": [
+ "add-module-exports",
+ "lodash",
+ "transform-class-properties",
+ [
+ "transform-es2015-classes",
+ {
+ "loose": true
+ }
+ ],
+ "transform-proto-to-assign"
+ ],
+ "presets": [
+ "es2015",
+ "stage-0",
+ "react"
+ ]
+}
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..0f17867
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,9 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..bcaad1b
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,6 @@
+{
+ "extends": [
+ "canonical",
+ "canonical/mocha"
+ ]
+}
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..2f093a7
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,2 @@
+github: gajus
+patreon: gajus
diff --git a/.gitignore b/.gitignore
index cf9c7f9..df978de 100755
--- a/.gitignore
+++ b/.gitignore
@@ -3,8 +3,10 @@ coverage
dist
*.log
.*
-!.README
+!.babelrc
+!.editorconfig
+!.eslintrc
!.gitignore
!.npmignore
-!.babelrc
+!.README
!.travis.yml
diff --git a/.travis.yml b/.travis.yml
index e07d95b..a786167 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,14 @@
language: node_js
node_js:
- - 5
- - 4
+ - node
+ - 8
+before_install:
+ - npm config set depth 0
notifications:
- email: false
+ email: false
sudo: false
+script:
+ - npm run test
+ - npm run lint
+after_success:
+ - semantic-release pre && npm publish && semantic-release post
diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..109eb2e
--- /dev/null
+++ b/ISSUE_TEMPLATE.md
@@ -0,0 +1,5 @@
+
diff --git a/README.md b/README.md
index 5c8c0ef..a6b7399 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,6 @@
# React CSS Modules
+[](https://gitspo.com/mentions/gajus/react-css-modules)
[](https://travis-ci.org/gajus/react-css-modules)
[](https://www.npmjs.org/package/react-css-modules)
[](https://github.com/gajus/canonical)
@@ -8,6 +9,16 @@
React CSS Modules implement automatic mapping of CSS modules. Every CSS class is assigned a local-scoped identifier with a global unique name. CSS Modules enable a modular and reusable CSS!
+> ## ⚠️⚠️⚠️ DEPRECATION NOTICE ⚠️⚠️⚠️
+>
+> If you are considering to use `react-css-modules`, evaluate if [`babel-plugin-react-css-modules`](https://github.com/gajus/babel-plugin-react-css-modules) covers your use case.
+> `babel-plugin-react-css-modules` is a lightweight alternative of `react-css-modules`.
+>
+> `babel-plugin-react-css-modules` is not a drop-in replacement and does not cover all the use cases of `react-css-modules`.
+> However, it has a lot smaller performance overhead (0-10% vs +50%; see [Performance](https://github.com/gajus/babel-plugin-react-css-modules#performance)) and a lot smaller size footprint (less than 2kb vs +17kb).
+>
+> It is easy to get started! See the demo https://github.com/gajus/babel-plugin-react-css-modules/tree/master/demo
+
- [CSS Modules](#css-modules)
- [webpack `css-loader`](#webpack-css-loader)
- [What's the Problem?](#whats-the-problem)
@@ -24,19 +35,18 @@ React CSS Modules implement automatic mapping of CSS modules. Every CSS class is
- [Decorator](#decorator)
- [Options](#options)
- [`allowMultiple`](#allowmultiple)
- - [`errorWhenNotFound`](#errorwhennotfound)
+ - [`handleNotFoundStyleName`](#handlenotfoundstylename)
- [SASS, SCSS, LESS and other CSS Preprocessors](#sass-scss-less-and-other-css-preprocessors)
- [Enable Sourcemaps](#enable-sourcemaps)
- [Class Composition](#class-composition)
- [What Problems does Class Composition Solve?](#what-problems-does-class-composition-solve)
- [Class Composition Using CSS Preprocessors](#class-composition-using-css-preprocessors)
-- [SASS, SCSS, LESS and other CSS Preprocessors](#sass-scss-less-and-other-css-preprocessors)
- [Global CSS](#global-css)
- [Multiple CSS Modules](#multiple-css-modules)
## CSS Modules
-[CSS Modules](https://github.com/css-modules/css-modules) are awesome. If you are not familiar with CSS Modules, it is a concept of using a module bundler such as [webpack](http://webpack.github.io/docs/) to load CSS scoped to a particular document. CSS module loader will generate a unique name for a each CSS class at the time of loading the CSS document ([Interoperable CSS](https://github.com/css-modules/icss) to be precise). To see CSS Modules in practice, [webpack-demo](https://css-modules.github.io/webpack-demo/).
+[CSS Modules](https://github.com/css-modules/css-modules) are awesome. If you are not familiar with CSS Modules, it is a concept of using a module bundler such as [webpack](http://webpack.github.io/docs/) to load CSS scoped to a particular document. CSS module loader will generate a unique name for each CSS class at the time of loading the CSS document ([Interoperable CSS](https://github.com/css-modules/icss) to be precise). To see CSS Modules in practice, [webpack-demo](https://css-modules.github.io/webpack-demo/).
In the context of React, CSS Modules look like this:
@@ -61,7 +71,7 @@ Rendering the component will produce a markup similar to:
```js
-
A0
+
A0
B0
@@ -73,7 +83,7 @@ Awesome!
### webpack `css-loader`
-[CSS Modules](https://github.com/css-modules/css-modules) is a specification that can be implemented in multiple ways. `react-css-modules` leverages as existing CSS Modules implementation webpack [css-loader](https://github.com/webpack/css-loader#css-modules).
+[CSS Modules](https://github.com/css-modules/css-modules) is a specification that can be implemented in multiple ways. `react-css-modules` leverages the existing CSS Modules implementation webpack [css-loader](https://github.com/webpack/css-loader#css-modules).
## What's the Problem?
@@ -115,7 +125,7 @@ Using `react-css-modules`:
```
-* You are warned when `styleName` refers to an undefined CSS Module ([`errorWhenNotFound`](#errorwhennotfound) option).
+* You are warned when `styleName` refers to an undefined CSS Module ([`handleNotFoundStyleName`](#handlenotfoundstylename) option).
* You can enforce use of a single CSS module per `ReactElement` ([`allowMultiple`](#allowmultiple) option).
## The Implementation
@@ -149,8 +159,8 @@ Setup:
{
test: /\.css$/,
loaders: [
- 'style?sourceMap',
- 'css?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]'
+ 'style-loader?sourceMap',
+ 'css-loader?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]'
]
}
```
@@ -182,22 +192,48 @@ Setup:
* Install [`style-loader`](https://www.npmjs.com/package/style-loader).
* Install [`css-loader`](https://www.npmjs.com/package/css-loader).
* Use [`extract-text-webpack-plugin`](https://www.npmjs.com/package/extract-text-webpack-plugin) to extract chunks of CSS into a single stylesheet.
+
* Setup `/\.css$/` loader:
-```js
-{
- test: /\.css$/,
- loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]')
-}
-```
+ * ExtractTextPlugin v1x:
+
+ ```js
+ {
+ test: /\.css$/,
+ loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]')
+ }
+ ```
+
+ * ExtractTextPlugin v2x:
+
+ ```js
+ {
+ test: /\.css$/,
+ use: ExtractTextPlugin.extract({
+ fallback: 'style-loader',
+ use: 'css-loader?modules,localIdentName="[name]-[local]-[hash:base64:6]"'
+ }),
+ }
+ ```
* Setup `extract-text-webpack-plugin` plugin:
-```js
-new ExtractTextPlugin('app.css', {
- allChunks: true
-})
-```
+ * ExtractTextPlugin v1x:
+
+ ```js
+ new ExtractTextPlugin('app.css', {
+ allChunks: true
+ })
+ ```
+
+ * ExtractTextPlugin v2x:
+
+ ```js
+ new ExtractTextPlugin({
+ filename: 'app.css',
+ allChunks: true
+ })
+ ```
Refer to [webpack-demo](https://github.com/css-modules/webpack-demo) or [react-css-modules-examples](https://github.com/gajus/react-css-modules-examples) for an example of a complete setup.
@@ -370,7 +406,7 @@ export default CSSModules(CustomList, styles);
* @typedef CSSModules~Options
* @see {@link https://github.com/gajus/react-css-modules#options}
* @property {Boolean} allowMultiple
- * @property {Boolean} errorWhenNotFound
+ * @property {String} handleNotFoundStyleName
*/
/**
@@ -454,11 +490,17 @@ When `false`, the following will cause an error:
```
-#### `errorWhenNotFound`
+#### `handleNotFoundStyleName`
+
+Default: `throw`.
+
+Defines the desired action when `styleName` cannot be mapped to an existing CSS Module.
-Default: `true`.
+Available options:
-Throws an error when `styleName` cannot be mapped to an existing CSS Module.
+* `throw` throws an error
+* `log` logs a warning using `console.warn`
+* `ignore` silently ignores the missing style name
## SASS, SCSS, LESS and other CSS Preprocessors
@@ -548,7 +590,7 @@ This pattern emerged with the advent of OOCSS. The biggest disadvantage of this
### Class Composition Using CSS Preprocessors
-This section of the document is included as a learning exercise to broaden the understanding about the origin of Class Composition. CSS Modules support a native method of composting CSS Modules using [`composes`](https://github.com/css-modules/css-modules#composition) keyword. CSS Preprocessor is not required.
+This section of the document is included as a learning exercise to broaden the understanding about the origin of Class Composition. CSS Modules support a native method of composing CSS Modules using [`composes`](https://github.com/css-modules/css-modules#composition) keyword. CSS Preprocessor is not required.
You can write compositions in SCSS using [`@extend`](http://sass-lang.com/documentation/file.SASS_REFERENCE.html#extend) keyword and using [Mixin Directives](http://sass-lang.com/documentation/file.SASS_REFERENCE.html#mixins), e.g.
diff --git a/package.json b/package.json
index c8e6a8e..00e1ffa 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "react-css-modules",
"description": "Seamless mapping of class names to CSS modules inside of React components.",
- "main": "./dist/",
+ "main": "./dist/index.js",
"repository": {
"type": "git",
"url": "https://github.com/gajus/react-css-modules"
@@ -12,7 +12,7 @@
"css",
"modules"
],
- "version": "3.7.5",
+ "version": "4.3.0",
"author": {
"name": "Gajus Kuizinas",
"email": "gajus@gajus.com",
@@ -20,27 +20,36 @@
},
"license": "BSD-3-Clause",
"dependencies": {
- "es6-map": "^0.1.3",
- "hoist-non-react-statics": "^1.0.5",
- "lodash": "^4.5.1",
- "object-unfreeze": "^1.0.2"
+ "hoist-non-react-statics": "^2.5.5",
+ "lodash": "^4.16.6",
+ "object-unfreeze": "^1.1.0"
},
"devDependencies": {
- "chai": "^3.5.0",
- "jsdom": "^8.0.4",
- "pragmatist": "^3.0.16",
- "react": "^0.15.0-alpha.1",
- "react-addons-test-utils": "^0.15.0-alpha.1",
- "react-dom": "^0.15.0-alpha.1"
+ "babel-cli": "^6.18.0",
+ "babel-plugin-add-module-exports": "^0.2.1",
+ "babel-plugin-lodash": "^3.2.9",
+ "babel-plugin-transform-proto-to-assign": "^6.9.0",
+ "babel-preset-es2015": "^6.18.0",
+ "babel-preset-react": "^6.16.0",
+ "babel-preset-stage-0": "^6.16.0",
+ "babel-register": "^6.18.0",
+ "chai": "^4.0.0-canary.1",
+ "chai-spies": "^0.7.1",
+ "eslint": "^3.10.0",
+ "eslint-config-canonical": "^5.5.0",
+ "husky": "^0.11.9",
+ "jsdom": "^9.8.3",
+ "mocha": "^3.1.2",
+ "react": "^15.4.0-rc.4",
+ "react-addons-shallow-compare": "^15.4.0-rc.4",
+ "react-addons-test-utils": "^15.4.0-rc.4",
+ "react-dom": "^15.4.0-rc.4",
+ "semantic-release": "^6.3.2"
},
"scripts": {
- "pragmatist": "node ./node_modules/.bin/pragmatist --es5",
- "lint": "npm run pragmatist lint",
- "test": "npm run pragmatist test --type-annotations",
- "build": "npm run pragmatist build",
- "watch": "npm run pragmatist watch",
- "watch-lint": "npm run pragmatist watch-lint",
- "watch-test": "npm run pragmatist watch-test --type-annotations",
- "watch-build": "npm run pragmatist watch-build"
+ "lint": "eslint ./src ./tests",
+ "test": "NODE_ENV=development mocha --compilers js:babel-register ./tests/**/*.js && npm run lint && npm run build",
+ "build": "NODE_ENV=production babel ./src --out-dir ./dist",
+ "precommit": "npm run test"
}
}
diff --git a/src/SimpleMap.js b/src/SimpleMap.js
new file mode 100644
index 0000000..7cf618b
--- /dev/null
+++ b/src/SimpleMap.js
@@ -0,0 +1,21 @@
+export default class {
+ constructor () {
+ this.size = 0;
+ this.keys = [];
+ this.values = [];
+ }
+
+ get (key) {
+ const index = this.keys.indexOf(key);
+
+ return this.values[index];
+ }
+
+ set (key, value) {
+ this.keys.push(key);
+ this.values.push(value);
+ this.size = this.keys.length;
+
+ return value;
+ }
+}
diff --git a/src/extendReactClass.js b/src/extendReactClass.js
index 45b36a7..4741bea 100644
--- a/src/extendReactClass.js
+++ b/src/extendReactClass.js
@@ -1,9 +1,10 @@
/* eslint-disable react/prop-types */
-import linkClass from './linkClass';
-import React from 'react';
import _ from 'lodash';
+import React from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics';
+import linkClass from './linkClass';
+import renderNothing from './renderNothing';
/**
* @param {ReactClass} Component
@@ -12,31 +13,62 @@ import hoistNonReactStatics from 'hoist-non-react-statics';
* @returns {ReactClass}
*/
export default (Component: Object, defaultStyles: Object, options: Object) => {
- const WrappedComponent = class extends Component {
- render () {
- let styles;
+ const WrappedComponent = class extends Component {
+ render () {
+ let styles;
- if (this.props.styles) {
- styles = this.props.styles;
- } else if (_.isObject(defaultStyles)) {
- this.props = _.assign({}, this.props, {
- styles: defaultStyles
- });
+ const hasDefaultstyles = _.isObject(defaultStyles);
+
+ let renderResult;
+
+ if (this.props.styles || hasDefaultstyles) {
+ const props = Object.assign({}, this.props);
+
+ if (props.styles) {
+ styles = props.styles;
+ } else if (hasDefaultstyles) {
+ styles = defaultStyles;
+ delete props.styles;
+ }
- styles = defaultStyles;
- } else {
- styles = {};
- }
+ Object.defineProperty(props, 'styles', {
+ configurable: true,
+ enumerable: false,
+ value: styles,
+ writable: false
+ });
- const renderResult = super.render();
+ const originalProps = this.props;
- if (renderResult) {
- return linkClass(renderResult, styles, options);
- }
+ let renderIsSuccessful = false;
- return React.createElement('noscript');
+ try {
+ this.props = props;
+
+ renderResult = super.render();
+
+ renderIsSuccessful = true;
+ } finally {
+ this.props = originalProps;
+ }
+
+ // @see https://github.com/facebook/react/issues/14224
+ if (!renderIsSuccessful) {
+ renderResult = super.render();
}
- };
+ } else {
+ styles = {};
+
+ renderResult = super.render();
+ }
+
+ if (renderResult) {
+ return linkClass(renderResult, styles, options);
+ }
+
+ return renderNothing(React.version);
+ }
+ };
- return hoistNonReactStatics(WrappedComponent, Component);
+ return hoistNonReactStatics(WrappedComponent, Component);
};
diff --git a/src/generateAppendClassName.js b/src/generateAppendClassName.js
index b4e6bd8..127aa2a 100644
--- a/src/generateAppendClassName.js
+++ b/src/generateAppendClassName.js
@@ -1,40 +1,49 @@
-import Map from 'es6-map';
+import SimpleMap from './SimpleMap';
-const stylesIndex = new Map();
+const CustomMap = typeof Map === 'undefined' ? SimpleMap : Map;
-export default (styles, styleNames: Array, errorWhenNotFound: boolean): string => {
- let appendClassName,
- stylesIndexMap;
+const stylesIndex = new CustomMap();
- stylesIndexMap = stylesIndex.get(styles);
+export default (styles, styleNames: Array, handleNotFoundStyleName: "throw" | "log" | "ignore"): string => {
+ let appendClassName;
+ let stylesIndexMap;
- if (stylesIndexMap) {
- const styleNameIndex = stylesIndexMap.get(styleNames);
+ stylesIndexMap = stylesIndex.get(styles);
- if (styleNameIndex) {
- return styleNameIndex;
- }
- } else {
- stylesIndexMap = stylesIndex.set(styles, new Map());
- }
-
- appendClassName = '';
+ if (stylesIndexMap) {
+ const styleNameIndex = stylesIndexMap.get(styleNames);
- for (const styleName in styleNames) {
- if (styleNames.hasOwnProperty(styleName)) {
- const className = styles[styleNames[styleName]];
-
- if (className) {
- appendClassName += ' ' + className;
- } else if (errorWhenNotFound === true) {
- throw new Error('"' + styleNames[styleName] + '" CSS module is undefined.');
- }
+ if (styleNameIndex) {
+ return styleNameIndex;
+ }
+ } else {
+ stylesIndexMap = new CustomMap();
+ stylesIndex.set(styles, new CustomMap());
+ }
+
+ appendClassName = '';
+
+ for (const styleName in styleNames) {
+ if (styleNames.hasOwnProperty(styleName)) {
+ const className = styles[styleNames[styleName]];
+
+ if (className) {
+ appendClassName += ' ' + className;
+ } else {
+ if (handleNotFoundStyleName === 'throw') {
+ throw new Error('"' + styleNames[styleName] + '" CSS module is undefined.');
+ }
+ if (handleNotFoundStyleName === 'log') {
+ // eslint-disable-next-line no-console
+ console.warn('"' + styleNames[styleName] + '" CSS module is undefined.');
}
+ }
}
+ }
- appendClassName = appendClassName.trim();
+ appendClassName = appendClassName.trim();
- stylesIndexMap.set(styleNames, appendClassName);
+ stylesIndexMap.set(styleNames, appendClassName);
- return appendClassName;
+ return appendClassName;
};
diff --git a/src/index.js b/src/index.js
index 6c1a78c..b913b29 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,53 +1,56 @@
import _ from 'lodash';
import extendReactClass from './extendReactClass';
import wrapStatelessFunction from './wrapStatelessFunction';
+import makeConfiguration from './makeConfiguration';
/**
* @see https://github.com/gajus/react-css-modules#options
*/
-type OptionsType = {};
+type TypeOptions = {};
/**
* Determines if the given object has the signature of a class that inherits React.Component.
*/
const isReactComponent = (maybeReactComponent: any): boolean => {
- return 'prototype' in maybeReactComponent && _.isFunction(maybeReactComponent.prototype.render);
+ return 'prototype' in maybeReactComponent && _.isFunction(maybeReactComponent.prototype.render);
};
/**
* When used as a function.
*/
-const functionConstructor = (Component: Function, defaultStyles: Object, options: OptionsType): Function => {
- let decoratedClass;
-
- if (isReactComponent(Component)) {
- decoratedClass = extendReactClass(Component, defaultStyles, options);
- } else {
- decoratedClass = wrapStatelessFunction(Component, defaultStyles, options);
- }
-
- if (Component.displayName) {
- decoratedClass.displayName = Component.displayName;
- } else {
- decoratedClass.displayName = Component.name;
- }
-
- return decoratedClass;
+const functionConstructor = (Component: Function, defaultStyles: Object, options: TypeOptions): Function => {
+ let decoratedClass;
+
+ const configuration = makeConfiguration(options);
+
+ if (isReactComponent(Component)) {
+ decoratedClass = extendReactClass(Component, defaultStyles, configuration);
+ } else {
+ decoratedClass = wrapStatelessFunction(Component, defaultStyles, configuration);
+ }
+
+ if (Component.displayName) {
+ decoratedClass.displayName = Component.displayName;
+ } else {
+ decoratedClass.displayName = Component.name;
+ }
+
+ return decoratedClass;
};
/**
* When used as a ES7 decorator.
*/
-const decoratorConstructor = (defaultStyles: Object, options: OptionsType): Function => {
- return (Component: Function) => {
- return functionConstructor(Component, defaultStyles, options);
- };
+const decoratorConstructor = (defaultStyles: Object, options: TypeOptions): Function => {
+ return (Component: Function) => {
+ return functionConstructor(Component, defaultStyles, options);
+ };
};
export default (...args) => {
- if (_.isFunction(args[0])) {
- return functionConstructor(args[0], args[1], args[2]);
- } else {
- return decoratorConstructor(args[0], args[1]);
- }
+ if (_.isFunction(args[0])) {
+ return functionConstructor(args[0], args[1], args[2]);
+ } else {
+ return decoratorConstructor(args[0], args[1]);
+ }
};
diff --git a/src/isIterable.js b/src/isIterable.js
index ac6dc72..67d0a94 100644
--- a/src/isIterable.js
+++ b/src/isIterable.js
@@ -1,6 +1,6 @@
import _ from 'lodash';
-const ITERATOR_SYMBOL = _.isFunction(Symbol) && Symbol.iterator;
+const ITERATOR_SYMBOL = typeof Symbol !== 'undefined' && _.isFunction(Symbol) && Symbol.iterator;
const OLD_ITERATOR_SYMBOL = '@@iterator';
/**
@@ -8,17 +8,17 @@ const OLD_ITERATOR_SYMBOL = '@@iterator';
* @see https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Iteration_protocols
*/
export default (maybeIterable: any): boolean => {
- let iterator;
+ let iterator;
- if (!_.isObject(maybeIterable)) {
- return false;
- }
+ if (!_.isObject(maybeIterable)) {
+ return false;
+ }
- if (ITERATOR_SYMBOL) {
- iterator = maybeIterable[ITERATOR_SYMBOL];
- } else {
- iterator = maybeIterable[OLD_ITERATOR_SYMBOL];
- }
+ if (ITERATOR_SYMBOL) {
+ iterator = maybeIterable[ITERATOR_SYMBOL];
+ } else {
+ iterator = maybeIterable[OLD_ITERATOR_SYMBOL];
+ }
- return _.isFunction(iterator);
+ return _.isFunction(iterator);
};
diff --git a/src/linkClass.js b/src/linkClass.js
index 618561b..4ed3975 100644
--- a/src/linkClass.js
+++ b/src/linkClass.js
@@ -1,80 +1,106 @@
import _ from 'lodash';
-import React from 'react';
-import makeConfiguration from './makeConfiguration';
+import React, {
+ ReactElement
+} from 'react';
+import objectUnfreeze from 'object-unfreeze';
import isIterable from './isIterable';
import parseStyleName from './parseStyleName';
import generateAppendClassName from './generateAppendClassName';
-import objectUnfreeze from 'object-unfreeze';
-import {
- ReactElement
-} from 'react';
+const linkArray = (array: Array, styles: Object, configuration: Object) => {
+ _.forEach(array, (value, index) => {
+ if (React.isValidElement(value)) {
+ // eslint-disable-next-line no-use-before-define
+ array[index] = linkElement(React.Children.only(value), styles, configuration);
+ } else if (_.isArray(value)) {
+ const unfreezedValue = Object.isFrozen(value) ? objectUnfreeze(value) : value;
-let linkElement;
+ array[index] = linkArray(unfreezedValue, styles, configuration);
+ }
+ });
-linkElement = (element, styles, configuration) => {
- let appendClassName,
- elementIsFrozen,
- elementShallowCopy;
+ return array;
+};
- elementShallowCopy = element;
+const linkElement = (element: ReactElement, styles: Object, configuration: Object): ReactElement => {
+ let appendClassName;
+ let elementShallowCopy;
+
+ elementShallowCopy = element;
+
+ if (Array.isArray(elementShallowCopy)) {
+ return elementShallowCopy.map((arrayElement) => {
+ return linkElement(arrayElement, styles, configuration);
+ });
+ }
+
+ const elementIsFrozen = Object.isFrozen && Object.isFrozen(elementShallowCopy);
+ const propsFrozen = Object.isFrozen && Object.isFrozen(elementShallowCopy.props);
+ const propsNotExtensible = Object.isExtensible && !Object.isExtensible(elementShallowCopy.props);
+
+ if (elementIsFrozen) {
+ // https://github.com/facebook/react/blob/v0.13.3/src/classic/element/ReactElement.js#L131
+ elementShallowCopy = objectUnfreeze(elementShallowCopy);
+ elementShallowCopy.props = objectUnfreeze(elementShallowCopy.props);
+ } else if (propsFrozen || propsNotExtensible) {
+ elementShallowCopy.props = objectUnfreeze(elementShallowCopy.props);
+ }
+
+ const styleNames = parseStyleName(elementShallowCopy.props.styleName || '', configuration.allowMultiple);
+ const {children, ...restProps} = elementShallowCopy.props;
+
+ if (React.isValidElement(children)) {
+ elementShallowCopy.props.children = linkElement(React.Children.only(children), styles, configuration);
+ } else if (_.isArray(children) || isIterable(children)) {
+ elementShallowCopy.props.children = linkArray(objectUnfreeze(children), styles, configuration);
+ }
+
+ _.forEach(restProps, (propValue, propName) => {
+ if (React.isValidElement(propValue)) {
+ elementShallowCopy.props[propName] = linkElement(React.Children.only(propValue), styles, configuration);
+ } else if (_.isArray(propValue)) {
+ elementShallowCopy.props[propName] = linkArray(propValue, styles, configuration);
+ }
+ });
- if (Object.isFrozen && Object.isFrozen(elementShallowCopy)) {
- elementIsFrozen = true;
+ if (styleNames.length) {
+ appendClassName = generateAppendClassName(styles, styleNames, configuration.handleNotFoundStyleName);
- // https://github.com/facebook/react/blob/v0.13.3/src/classic/element/ReactElement.js#L131
- elementShallowCopy = objectUnfreeze(elementShallowCopy);
- elementShallowCopy.props = objectUnfreeze(elementShallowCopy.props);
- }
+ if (appendClassName) {
+ if (elementShallowCopy.props.className) {
+ appendClassName = elementShallowCopy.props.className + ' ' + appendClassName;
+ }
- const styleNames = parseStyleName(elementShallowCopy.props.styleName || '', configuration.allowMultiple);
-
- if (React.isValidElement(elementShallowCopy.props.children)) {
- elementShallowCopy.props.children = linkElement(React.Children.only(elementShallowCopy.props.children), styles, configuration);
- } else if (_.isArray(elementShallowCopy.props.children) || isIterable(elementShallowCopy.props.children)) {
- elementShallowCopy.props.children = React.Children.map(elementShallowCopy.props.children, (node) => {
- if (React.isValidElement(node)) {
- return linkElement(node, styles, configuration);
- } else {
- return node;
- }
- });
+ elementShallowCopy.props.className = appendClassName;
}
+ }
- if (styleNames.length) {
- appendClassName = generateAppendClassName(styles, styleNames, configuration.errorWhenNotFound);
+ delete elementShallowCopy.props.styleName;
- if (appendClassName) {
- if (elementShallowCopy.props.className) {
- appendClassName = elementShallowCopy.props.className + ' ' + appendClassName;
- }
+ if (elementIsFrozen) {
+ Object.freeze(elementShallowCopy.props);
+ Object.freeze(elementShallowCopy);
+ } else if (propsFrozen) {
+ Object.freeze(elementShallowCopy.props);
+ }
- elementShallowCopy.props.className = appendClassName;
- elementShallowCopy.props.styleName = null;
- }
- }
+ if (propsNotExtensible) {
+ Object.preventExtensions(elementShallowCopy.props);
+ }
- if (elementIsFrozen) {
- Object.freeze(elementShallowCopy.props);
- Object.freeze(elementShallowCopy);
- }
-
- return elementShallowCopy;
+ return elementShallowCopy;
};
/**
* @param {ReactElement} element
* @param {Object} styles CSS modules class map.
- * @param {CSSModules~Options} userConfiguration
- * @returns {ReactElement}
+ * @param {CSSModules~Options} configuration
*/
-export default (element, styles = {}, userConfiguration) => {
+export default (element: ReactElement, styles = {}, configuration = {}): ReactElement => {
// @see https://github.com/gajus/react-css-modules/pull/30
- if (!_.isObject(element)) {
- return element;
- }
-
- const configuration = makeConfiguration(userConfiguration);
+ if (!_.isObject(element)) {
+ return element;
+ }
- return linkElement(element, styles, configuration);
+ return linkElement(element, styles, configuration);
};
diff --git a/src/makeConfiguration.js b/src/makeConfiguration.js
index 22d0298..cc3169c 100644
--- a/src/makeConfiguration.js
+++ b/src/makeConfiguration.js
@@ -1,13 +1,10 @@
import _ from 'lodash';
-import Map from 'es6-map';
-
-const userConfigurationIndex = new Map();
/**
* @typedef CSSModules~Options
* @see {@link https://github.com/gajus/react-css-modules#options}
* @property {boolean} allowMultiple
- * @property {boolean} errorWhenNotFound
+ * @property {string} handleNotFoundStyleName
*/
/**
@@ -15,32 +12,26 @@ const userConfigurationIndex = new Map();
* @returns {CSSModules~Options}
*/
export default (userConfiguration = {}) => {
- let configuration;
-
- configuration = userConfigurationIndex.get(userConfiguration);
-
- if (configuration) {
- return configuration;
+ const configuration = {
+ allowMultiple: false,
+ handleNotFoundStyleName: 'throw'
+ };
+
+ _.forEach(userConfiguration, (value, name) => {
+ if (_.isUndefined(configuration[name])) {
+ throw new Error('Unknown configuration property "' + name + '".');
}
- configuration = {
- allowMultiple: false,
- errorWhenNotFound: true
- };
-
- _.forEach(userConfiguration, (value, name) => {
- if (_.isUndefined(configuration[name])) {
- throw new Error('Unknown configuration property "' + name + '".');
- }
-
- if (!_.isBoolean(value)) {
- throw new Error('"' + name + '" property value must be a boolean.');
- }
+ if (name === 'allowMultiple' && !_.isBoolean(value)) {
+ throw new Error('"allowMultiple" property value must be a boolean.');
+ }
- configuration[name] = value;
- });
+ if (name === 'handleNotFoundStyleName' && !_.includes(['throw', 'log', 'ignore'], value)) {
+ throw new Error('"handleNotFoundStyleName" property value must be "throw", "log" or "ignore".');
+ }
- userConfigurationIndex.set(userConfiguration, configuration);
+ configuration[name] = value;
+ });
- return configuration;
+ return configuration;
};
diff --git a/src/parseStyleName.js b/src/parseStyleName.js
index 39c328a..937b2d6 100644
--- a/src/parseStyleName.js
+++ b/src/parseStyleName.js
@@ -3,20 +3,20 @@ import _ from 'lodash';
const styleNameIndex = {};
export default (styleNamePropertyValue: string, allowMultiple: boolean): Array => {
- let styleNames;
+ let styleNames;
- if (styleNameIndex[styleNamePropertyValue]) {
- styleNames = styleNameIndex[styleNamePropertyValue];
- } else {
- styleNames = _.trim(styleNamePropertyValue).split(' ');
- styleNames = _.filter(styleNames);
+ if (styleNameIndex[styleNamePropertyValue]) {
+ styleNames = styleNameIndex[styleNamePropertyValue];
+ } else {
+ styleNames = _.trim(styleNamePropertyValue).split(/\s+/);
+ styleNames = _.filter(styleNames);
- styleNameIndex[styleNamePropertyValue] = styleNames;
- }
+ styleNameIndex[styleNamePropertyValue] = styleNames;
+ }
- if (allowMultiple === false && styleNames.length > 1) {
- throw new Error('ReactElement styleName property defines multiple module names ("' + styleNamePropertyValue + '").');
- }
+ if (allowMultiple === false && styleNames.length > 1) {
+ throw new Error('ReactElement styleName property defines multiple module names ("' + styleNamePropertyValue + '").');
+ }
- return styleNames;
+ return styleNames;
};
diff --git a/src/renderNothing.js b/src/renderNothing.js
new file mode 100644
index 0000000..93675fe
--- /dev/null
+++ b/src/renderNothing.js
@@ -0,0 +1,7 @@
+import React from 'react';
+
+export default function (version) {
+ const major = version.split('.')[0];
+
+ return parseInt(major, 10) < 15 ? React.createElement('noscript') : null;
+}
diff --git a/src/wrapStatelessFunction.js b/src/wrapStatelessFunction.js
index 6ac5cea..b6b9162 100644
--- a/src/wrapStatelessFunction.js
+++ b/src/wrapStatelessFunction.js
@@ -1,39 +1,49 @@
+/* eslint-disable react/prop-types */
+
import _ from 'lodash';
import React from 'react';
import linkClass from './linkClass';
+import renderNothing from './renderNothing';
/**
* @see https://facebook.github.io/react/blog/2015/09/10/react-v0.14-rc1.html#stateless-function-components
*/
export default (Component: Function, defaultStyles: Object, options: Object): Function => {
- const WrappedComponent = (props = {}, ...args) => {
- let styles,
- useProps;
-
- if (props.styles) {
- useProps = props;
- styles = props.styles;
- } else if (_.isObject(defaultStyles)) {
- useProps = _.assign({}, props, {
- styles: defaultStyles
- });
-
- styles = defaultStyles;
- } else {
- useProps = props;
- styles = {};
- }
-
- const renderResult = Component(useProps, ...args);
-
- if (renderResult) {
- return linkClass(renderResult, styles, options);
- }
-
- return React.createElement('noscript');
- };
-
- _.assign(WrappedComponent, Component);
-
- return WrappedComponent;
+ const WrappedComponent = (props = {}, ...args) => {
+ let styles;
+ let useProps;
+ const hasDefaultstyles = _.isObject(defaultStyles);
+
+ if (props.styles || hasDefaultstyles) {
+ useProps = Object.assign({}, props);
+
+ if (props.styles) {
+ styles = props.styles;
+ } else {
+ styles = defaultStyles;
+ }
+
+ Object.defineProperty(useProps, 'styles', {
+ configurable: true,
+ enumerable: false,
+ value: styles,
+ writable: false
+ });
+ } else {
+ useProps = props;
+ styles = {};
+ }
+
+ const renderResult = Component(useProps, ...args);
+
+ if (renderResult) {
+ return linkClass(renderResult, styles, options);
+ }
+
+ return renderNothing(React.version);
+ };
+
+ _.assign(WrappedComponent, Component);
+
+ return WrappedComponent;
};
diff --git a/tests/SimpleMap.js b/tests/SimpleMap.js
new file mode 100644
index 0000000..08ca888
--- /dev/null
+++ b/tests/SimpleMap.js
@@ -0,0 +1,38 @@
+import {
+ expect
+} from 'chai';
+import SimpleMap from './../src/SimpleMap';
+
+describe('SimpleMap', () => {
+ context('simple map with primitive or object as keys', () => {
+ const values = [
+ [1, 'something'],
+ ['1', 'somethingElse'],
+ [{}, []],
+ [null, null]
+ ];
+
+ let map;
+
+ beforeEach(() => {
+ map = new SimpleMap();
+ });
+
+ it('should set', () => {
+ values.forEach(([key, value]) => {
+ map.set(key, value);
+ });
+ expect(map.size).to.equal(values.length);
+ });
+
+ it('should get', () => {
+ values.forEach(([key, value]) => {
+ map.set(key, value);
+ });
+
+ values.forEach(([key, value]) => {
+ expect(map.get(key)).to.equal(value);
+ });
+ });
+ });
+});
diff --git a/tests/extendReactClass.js b/tests/extendReactClass.js
index 2348bd5..01733ee 100644
--- a/tests/extendReactClass.js
+++ b/tests/extendReactClass.js
@@ -1,116 +1,167 @@
-/* eslint-disable max-nested-callbacks */
+/* eslint-disable max-nested-callbacks, react/prefer-stateless-function, react/prop-types, react/no-multi-comp, class-methods-use-this */
import {
expect
} from 'chai';
-
import React from 'react';
import TestUtils from 'react-addons-test-utils';
+import shallowCompare from 'react-addons-shallow-compare';
import jsdom from 'jsdom';
import extendReactClass from './../src/extendReactClass';
describe('extendReactClass', () => {
- beforeEach(() => {
- global.document = jsdom.jsdom('');
+ beforeEach(() => {
+ global.document = jsdom.jsdom('');
+
+ global.window = document.defaultView;
+ });
+ context('using default styles', () => {
+ it('exposes styles through this.props.styles property', (done) => {
+ let Component;
+
+ const styles = {
+ foo: 'foo-1'
+ };
+
+ Component = class extends React.Component {
+ render () {
+ expect(this.props.styles).to.equal(styles);
+ done();
+ }
+ };
+
+ Component = extendReactClass(Component, styles);
+
+ TestUtils.renderIntoDocument();
+ });
+ it('exposes non-enumerable styles property', (done) => {
+ let Component;
+
+ const styles = {
+ foo: 'foo-1'
+ };
+
+ Component = class extends React.Component {
+ render () {
+ expect(this.props.propertyIsEnumerable('styles')).to.equal(false);
+ done();
+ }
+ };
+
+ Component = extendReactClass(Component, styles);
+
+ TestUtils.renderIntoDocument();
+ });
+ it('does not affect the other instance properties', (done) => {
+ let Component;
- global.window = document.defaultView;
+ Component = class extends React.Component {
+ render () {
+ expect(this.props.bar).to.equal('baz');
+ done();
+ }
+ };
+
+ const styles = {
+ foo: 'foo-1'
+ };
+
+ Component = extendReactClass(Component, styles);
+
+ TestUtils.renderIntoDocument();
});
- context('using default styles', () => {
- it('exposes styles through this.props.styles property', (done) => {
- let Component;
-
- const styles = {
- foo: 'foo-1'
- };
-
- Component = class extends React.Component {
- render () {
- expect(this.props.styles).to.equal(styles);
- done();
- }
- };
-
- Component = extendReactClass(Component, styles);
-
- TestUtils.renderIntoDocument();
- });
- it('does not affect the other instance properties', (done) => {
- let Component;
-
- Component = class extends React.Component {
- render () {
- expect(this.props.bar).to.equal('baz');
- done();
- }
- };
-
- const styles = {
- foo: 'foo-1'
- };
-
- Component = extendReactClass(Component, styles);
-
- TestUtils.renderIntoDocument();
- });
+ it('does not affect pure-render logic', (done) => {
+ let Component;
+ let rendered;
+
+ rendered = false;
+
+ const styles = {
+ foo: 'foo-1'
+ };
+
+ Component = class extends React.Component {
+ shouldComponentUpdate (newProps) {
+ if (rendered) {
+ expect(shallowCompare(this.props, newProps)).to.equal(true);
+
+ done();
+ }
+
+ return true;
+ }
+
+ render () {
+ rendered = true;
+ }
+ };
+
+ Component = extendReactClass(Component, styles);
+
+ const instance = TestUtils.renderIntoDocument();
+
+ // trigger shouldComponentUpdate
+ instance.setState({});
});
- context('overwriting default styles using "styles" property of the extended component', () => {
- it('overwrites default styles', (done) => {
- let Component;
-
- const styles = {
- foo: 'foo-1'
- };
-
- Component = class extends React.Component {
- render () {
- expect(this.props.styles).to.equal(styles);
- done();
- }
- };
-
- Component = extendReactClass(Component, {
- bar: 'bar-0',
- foo: 'foo-0'
- });
-
- TestUtils.renderIntoDocument();
- });
+ });
+ context('overwriting default styles using "styles" property of the extended component', () => {
+ it('overwrites default styles', (done) => {
+ let Component;
+
+ const styles = {
+ foo: 'foo-1'
+ };
+
+ Component = class extends React.Component {
+ render () {
+ expect(this.props.styles).to.equal(styles);
+ done();
+ }
+ };
+
+ Component = extendReactClass(Component, {
+ bar: 'bar-0',
+ foo: 'foo-0'
+ });
+
+ TestUtils.renderIntoDocument();
});
- context('rendering Component that returns null', () => {
- it('generates