diff --git a/.babelrc b/.babelrc
index 197c614..3751ba8 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,14 +1,19 @@
{
- "presets": [
- "es2015",
- "stage-0",
- "react"
+ "plugins": [
+ "add-module-exports",
+ "lodash",
+ "transform-class-properties",
+ [
+ "transform-es2015-classes",
+ {
+ "loose": true
+ }
],
- "plugins": [
- "add-module-exports",
- "lodash",
- "transform-class-properties",
- ["transform-es2015-classes", { "loose": true }],
- "transform-proto-to-assign"
- ]
+ "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/.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 d54786c..df978de 100755
--- a/.gitignore
+++ b/.gitignore
@@ -3,9 +3,10 @@ coverage
dist
*.log
.*
-!.README
+!.babelrc
+!.editorconfig
+!.eslintrc
!.gitignore
!.npmignore
-!.babelrc
+!.README
!.travis.yml
-!.eslintrc
diff --git a/.travis.yml b/.travis.yml
index bfa13bf..a786167 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,7 @@
language: node_js
node_js:
- - 7
- - 6
- - 5
+ - node
+ - 8
before_install:
- npm config set depth 0
notifications:
diff --git a/README.md b/README.md
index 4721800..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,9 +9,7 @@
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!
-> ⚠️⚠️⚠️
->
-> Note:
+> ## ⚠️⚠️⚠️ 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`.
@@ -18,8 +17,7 @@ React CSS Modules implement automatic mapping of CSS modules. Every CSS class is
> `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
+> 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)
@@ -37,7 +35,7 @@ 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)
@@ -127,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
@@ -161,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]'
]
}
```
@@ -408,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
*/
/**
@@ -492,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
diff --git a/package.json b/package.json
index 3253cc6..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"
@@ -20,7 +20,7 @@
},
"license": "BSD-3-Clause",
"dependencies": {
- "hoist-non-react-statics": "^1.2.0",
+ "hoist-non-react-statics": "^2.5.5",
"lodash": "^4.16.6",
"object-unfreeze": "^1.1.0"
},
@@ -34,6 +34,7 @@
"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",
diff --git a/src/extendReactClass.js b/src/extendReactClass.js
index 6eba084..4741bea 100644
--- a/src/extendReactClass.js
+++ b/src/extendReactClass.js
@@ -19,14 +19,16 @@ export default (Component: Object, defaultStyles: Object, options: Object) => {
const hasDefaultstyles = _.isObject(defaultStyles);
+ let renderResult;
+
if (this.props.styles || hasDefaultstyles) {
const props = Object.assign({}, this.props);
- if (this.props.styles) {
- styles = this.props.styles;
+ if (props.styles) {
+ styles = props.styles;
} else if (hasDefaultstyles) {
styles = defaultStyles;
- delete this.props.styles;
+ delete props.styles;
}
Object.defineProperty(props, 'styles', {
@@ -36,12 +38,29 @@ export default (Component: Object, defaultStyles: Object, options: Object) => {
writable: false
});
- this.props = props;
+ const originalProps = this.props;
+
+ let renderIsSuccessful = false;
+
+ 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 = {};
- }
- const renderResult = super.render();
+ renderResult = super.render();
+ }
if (renderResult) {
return linkClass(renderResult, styles, options);
diff --git a/src/generateAppendClassName.js b/src/generateAppendClassName.js
index 9479698..127aa2a 100644
--- a/src/generateAppendClassName.js
+++ b/src/generateAppendClassName.js
@@ -4,7 +4,7 @@ const CustomMap = typeof Map === 'undefined' ? SimpleMap : Map;
const stylesIndex = new CustomMap();
-export default (styles, styleNames: Array, errorWhenNotFound: boolean): string => {
+export default (styles, styleNames: Array, handleNotFoundStyleName: "throw" | "log" | "ignore"): string => {
let appendClassName;
let stylesIndexMap;
@@ -29,8 +29,14 @@ export default (styles, styleNames: Array, errorWhenNotFound: boolean):
if (className) {
appendClassName += ' ' + className;
- } else if (errorWhenNotFound === true) {
- throw new Error('"' + styleNames[styleName] + '" CSS module is undefined.');
+ } 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.');
+ }
}
}
}
diff --git a/src/linkClass.js b/src/linkClass.js
index 1a05878..4ed3975 100644
--- a/src/linkClass.js
+++ b/src/linkClass.js
@@ -13,7 +13,9 @@ const linkArray = (array: Array, styles: Object, configuration: Object) => {
// eslint-disable-next-line no-use-before-define
array[index] = linkElement(React.Children.only(value), styles, configuration);
} else if (_.isArray(value)) {
- array[index] = linkArray(value, styles, configuration);
+ const unfreezedValue = Object.isFrozen(value) ? objectUnfreeze(value) : value;
+
+ array[index] = linkArray(unfreezedValue, styles, configuration);
}
});
@@ -22,17 +24,26 @@ const linkArray = (array: Array, styles: Object, configuration: Object) => {
const linkElement = (element: ReactElement, styles: Object, configuration: Object): ReactElement => {
let appendClassName;
- let elementIsFrozen;
let elementShallowCopy;
elementShallowCopy = element;
- if (Object.isFrozen && Object.isFrozen(elementShallowCopy)) {
- elementIsFrozen = true;
+ 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);
- // https://github.com/facebook/react/blob/v0.13.3/src/classic/element/ReactElement.js#L131
+ 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);
@@ -41,34 +52,19 @@ const linkElement = (element: ReactElement, styles: Object, configuration: Objec
if (React.isValidElement(children)) {
elementShallowCopy.props.children = linkElement(React.Children.only(children), styles, configuration);
} else if (_.isArray(children) || isIterable(children)) {
- elementShallowCopy.props.children = React.Children.map(children, (node) => {
- if (React.isValidElement(node)) {
- // eslint-disable-next-line no-use-before-define
- return linkElement(React.Children.only(node), styles, configuration);
- } else {
- return node;
- }
- });
+ 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] = _.map(propValue, (node) => {
- if (React.isValidElement(node)) {
- return linkElement(React.Children.only(node), styles, configuration);
- } else if (_.isArray(node)) {
- return linkArray(node, styles, configuration);
- }
-
- return node;
- });
+ elementShallowCopy.props[propName] = linkArray(propValue, styles, configuration);
}
});
if (styleNames.length) {
- appendClassName = generateAppendClassName(styles, styleNames, configuration.errorWhenNotFound);
+ appendClassName = generateAppendClassName(styles, styleNames, configuration.handleNotFoundStyleName);
if (appendClassName) {
if (elementShallowCopy.props.className) {
@@ -84,6 +80,12 @@ const linkElement = (element: ReactElement, styles: Object, configuration: Objec
if (elementIsFrozen) {
Object.freeze(elementShallowCopy.props);
Object.freeze(elementShallowCopy);
+ } else if (propsFrozen) {
+ Object.freeze(elementShallowCopy.props);
+ }
+
+ if (propsNotExtensible) {
+ Object.preventExtensions(elementShallowCopy.props);
}
return elementShallowCopy;
diff --git a/src/makeConfiguration.js b/src/makeConfiguration.js
index 0eae15f..cc3169c 100644
--- a/src/makeConfiguration.js
+++ b/src/makeConfiguration.js
@@ -4,7 +4,7 @@ import _ from 'lodash';
* @typedef CSSModules~Options
* @see {@link https://github.com/gajus/react-css-modules#options}
* @property {boolean} allowMultiple
- * @property {boolean} errorWhenNotFound
+ * @property {string} handleNotFoundStyleName
*/
/**
@@ -14,7 +14,7 @@ import _ from 'lodash';
export default (userConfiguration = {}) => {
const configuration = {
allowMultiple: false,
- errorWhenNotFound: true
+ handleNotFoundStyleName: 'throw'
};
_.forEach(userConfiguration, (value, name) => {
@@ -22,8 +22,12 @@ export default (userConfiguration = {}) => {
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.');
+ }
+
+ if (name === 'handleNotFoundStyleName' && !_.includes(['throw', 'log', 'ignore'], value)) {
+ throw new Error('"handleNotFoundStyleName" property value must be "throw", "log" or "ignore".');
}
configuration[name] = value;
diff --git a/tests/linkClass.js b/tests/linkClass.js
index 0475563..cf7c289 100644
--- a/tests/linkClass.js
+++ b/tests/linkClass.js
@@ -1,13 +1,16 @@
-/* eslint-disable max-nested-callbacks, react/prefer-stateless-function, class-methods-use-this */
+/* eslint-disable max-nested-callbacks, react/prefer-stateless-function, class-methods-use-this, no-console, no-unused-expressions */
-import {
+import chai, {
expect
} from 'chai';
+import spies from 'chai-spies';
import React from 'react';
import TestUtils from 'react-addons-test-utils';
import jsdom from 'jsdom';
import linkClass from './../src/linkClass';
+chai.use(spies);
+
describe('linkClass', () => {
context('ReactElement does not define styleName', () => {
it('does not affect element properties', () => {
@@ -122,6 +125,33 @@ describe('linkClass', () => {
expect(subject.props.children[0].props.className).to.equal('foo-1');
expect(subject.props.children[1].props.className).to.equal('bar-1');
});
+ it('assigns a generated className to elements inside nested arrays', () => {
+ let subject;
+
+ subject =
+ {[
+ [
+
,
+
+ ],
+ [
+
,
+
+ ]
+ ]}
+
;
+
+ subject = linkClass(subject, {
+ bar: 'bar-1',
+ foo: 'foo-1'
+ });
+
+ expect(subject.props.children[0][0].props.className).to.equal('foo-1');
+ expect(subject.props.children[0][1].props.className).to.equal('bar-1');
+
+ expect(subject.props.children[1][0].props.className).to.equal('foo-1');
+ expect(subject.props.children[1][1].props.className).to.equal('bar-1');
+ });
it('styleName is deleted from props', () => {
let subject;
@@ -138,6 +168,22 @@ describe('linkClass', () => {
expect(subject.props.children[0].props).not.to.have.property('styleName');
expect(subject.props.children[1].props).not.to.have.property('styleName');
});
+ it('preserves original keys', () => {
+ let subject;
+
+ subject = ;
+
+ subject = linkClass(subject, {
+ bar: 'bar-1',
+ foo: 'foo-1'
+ });
+
+ expect(subject.props.children[0].key).to.equal('1');
+ expect(subject.props.children[1].key).to.equal('2');
+ });
});
context('when multiple descendants have styleName and are iterable', () => {
it('assigns a generated className', () => {
@@ -236,6 +282,76 @@ describe('linkClass', () => {
});
});
+ context('can\'t write to properties', () => {
+ context('when the element is frozen', () => {
+ it('adds className but is still frozen', () => {
+ let subject;
+
+ subject = ;
+
+ Object.freeze(subject);
+ subject = linkClass(subject, {
+ foo: 'foo-1'
+ });
+
+ expect(subject).to.be.frozen;
+ expect(subject.props.className).to.equal('foo-1');
+ });
+ });
+ context('when the element\'s props are frozen', () => {
+ it('adds className and only props are still frozen', () => {
+ let subject;
+
+ subject = ;
+
+ Object.freeze(subject.props);
+ subject = linkClass(subject, {
+ foo: 'foo-1'
+ });
+
+ expect(subject.props).to.be.frozen;
+ expect(subject.props.className).to.equal('foo-1');
+ });
+ });
+ context('when the element\'s props are not extensible', () => {
+ it('adds className and props are still not extensible', () => {
+ let subject;
+
+ subject = ;
+
+ Object.preventExtensions(subject.props);
+ subject = linkClass(subject, {
+ foo: 'foo-1'
+ });
+
+ expect(subject.props).to.not.be.extensible;
+ expect(subject.props.className).to.equal('foo-1');
+ });
+ });
+ });
+
+ context('when element is an array', () => {
+ it('handles each element individually', () => {
+ let subject;
+
+ subject = [
+ ,
+
+ ];
+
+ subject = linkClass(subject, {
+ bar: 'bar-1',
+ foo: 'foo-1'
+ });
+
+ expect(subject).to.be.an('array');
+ expect(subject[0].props.className).to.equal('foo-1');
+ expect(subject[1].props.children.props.className).to.equal('bar-1');
+ });
+ });
+
describe('options.allowMultiple', () => {
context('when multiple module names are used', () => {
context('when false', () => {
@@ -264,24 +380,37 @@ describe('linkClass', () => {
});
});
- describe('options.errorWhenNotFound', () => {
+ describe('options.handleNotFoundStyleName', () => {
context('when styleName does not match an existing CSS module', () => {
- context('when false', () => {
- it('ignores the missing CSS module', () => {
- let subject;
-
- subject = ;
-
- subject = linkClass(subject, {}, {errorWhenNotFound: false});
+ context('when throw', () => {
+ it('throws an error', () => {
+ expect(() => {
+ linkClass(, {}, {handleNotFoundStyleName: 'throw'});
+ }).to.throw(Error, '"foo" CSS module is undefined.');
+ });
+ });
+ context('when log', () => {
+ it('logs a warning to the console', () => {
+ const warnSpy = chai.spy(() => {});
- expect(subject.props.className).to.be.an('undefined');
+ console.warn = warnSpy;
+ linkClass(, {}, {handleNotFoundStyleName: 'log'});
+ expect(warnSpy).to.have.been.called();
});
});
- context('when is true', () => {
- it('throws an error', () => {
+ context('when ignore', () => {
+ it('does not log a warning', () => {
+ const warnSpy = chai.spy(() => {});
+
+ console.warn = warnSpy;
+ linkClass(, {}, {handleNotFoundStyleName: 'ignore'});
+ expect(warnSpy).to.not.have.been.called();
+ });
+
+ it('does not throw an error', () => {
expect(() => {
- linkClass(, {}, {errorWhenNotFound: true});
- }).to.throw(Error, '"foo" CSS module is undefined.');
+ linkClass(, {}, {handleNotFoundStyleName: 'ignore'});
+ }).to.not.throw(Error, '"foo" CSS module is undefined.');
});
});
});
diff --git a/tests/makeConfiguration.js b/tests/makeConfiguration.js
index 94129fc..4193311 100644
--- a/tests/makeConfiguration.js
+++ b/tests/makeConfiguration.js
@@ -17,9 +17,9 @@ describe('makeConfiguration', () => {
expect(configuration.allowMultiple).to.equal(false);
});
});
- describe('errorWhenNotFound property', () => {
- it('defaults to true', () => {
- expect(configuration.errorWhenNotFound).to.equal(true);
+ describe('handleNotFoundStyleName property', () => {
+ it('defaults to "throw"', () => {
+ expect(configuration.handleNotFoundStyleName).to.equal('throw');
});
});
});