diff --git a/.README/react-css-modules.png b/.README/react-css-modules.png
new file mode 100644
index 0000000..2614951
Binary files /dev/null and b/.README/react-css-modules.png differ
diff --git a/.README/react-css-modules.sketch b/.README/react-css-modules.sketch
new file mode 100644
index 0000000..a014d75
Binary files /dev/null and b/.README/react-css-modules.sketch differ
diff --git a/.babelrc b/.babelrc
index 12606a3..3751ba8 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,3 +1,19 @@
{
- "stage": 0
+ "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
index a2a446a..bcaad1b 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -1,393 +1,6 @@
{
- "rules": {
- "comma-dangle": [
- 2,
- "never"
- ],
- "no-cond-assign": 2,
- "no-console": 1,
- "no-constant-condition": 1,
- "no-control-regex": 2,
- "no-debugger": 1,
- "no-dupe-args": 2,
- "no-dupe-keys": 2,
- "no-duplicate-case": 2,
- "no-empty-character-class": 2,
- "no-empty": 1,
- "no-ex-assign": 2,
- "no-extra-boolean-cast": 0,
- "no-extra-parens": 2,
- "no-extra-semi": 2,
- "no-func-assign": 2,
- "no-inner-declarations": 2,
- "no-invalid-regexp": 2,
- "no-irregular-whitespace": 2,
- "no-negated-in-lhs": 2,
- "no-obj-calls": 2,
- "no-regex-spaces": 2,
- "no-sparse-arrays": 2,
- "no-unreachable": 1,
- "use-isnan": 2,
- "valid-jsdoc": [
- 2,
- {
- "requireParamDescription": false,
- "requireReturnDescription": false
- }
- ],
- "valid-typeof": 2,
- "no-unexpected-multiline": 2,
- "accessor-pairs": 2,
- "block-scoped-var": 2,
- "complexity": [
- 1,
- 10
- ],
- "consistent-return": 2,
- "curly": 2,
- "default-case": 0,
- "dot-notation": 2,
- "dot-location": [
- 2,
- "property"
- ],
- "eqeqeq": 2,
- "guard-for-in": 2,
- "no-alert": 2,
- "no-caller": 2,
- "no-div-regex": 2,
- "no-else-return": 0,
- "no-empty-label": 2,
- "no-eq-null": 2,
- "no-eval": 2,
- "no-extend-native": 2,
- "no-extra-bind": 2,
- "no-fallthrough": 2,
- "no-floating-decimal": 2,
- "no-implicit-coercion": 2,
- "no-implied-eval": 2,
- "no-invalid-this": 0,
- "no-iterator": 2,
- "no-labels": 2,
- "no-lone-blocks": 2,
- "no-loop-func": 2,
- "no-multi-spaces": 2,
- "no-multi-str": 2,
- "no-native-reassign": 2,
- "no-new-func": 2,
- "no-new-wrappers": 2,
- "no-new": 2,
- "no-octal-escape": 2,
- "no-octal": 2,
- "no-param-reassign": [
- 2,
- {
- "props": false
- }
- ],
- "no-process-env": 2,
- "no-proto": 2,
- "no-redeclare": [
- 2,
- {
- "builtinGlobals": true
- }
- ],
- "no-return-assign": 2,
- "no-script-url": 2,
- "no-self-compare": 2,
- "no-sequences": 2,
- "no-throw-literal": 2,
- "no-unused-expressions": 2,
- "no-useless-call": 2,
- "no-void": 2,
- "no-warning-comments": [
- 1,
- {
- "terms": [
- "todo",
- "@toto"
- ],
- "location": "start"
- }
- ],
- "no-with": 2,
- "radix": 2,
- "vars-on-top": 2,
- "wrap-iife": [
- 2,
- "inside"
- ],
- "yoda": 0,
- "strict": [
- 2,
- "never"
- ],
- "init-declarations": [
- 2,
- "never"
- ],
- "no-catch-shadow": 2,
- "no-delete-var": 2,
- "no-label-var": 2,
- "no-shadow-restricted-names": 2,
- "no-shadow": [
- 2,
- {
- "builtinGlobals": false,
- "hoist": "functions"
- }
- ],
- "no-undef-init": 2,
- "no-undef": 2,
- "no-undefined": 2,
- "no-unused-vars": 2,
- "no-use-before-define": 2,
- "callback-return": 2,
- "handle-callback-err": 2,
- "no-mixed-requires": 0,
- "no-new-require": 2,
- "no-path-concat": 2,
- "no-process-exit": 2,
- "no-sync": 0,
- "array-bracket-spacing": [
- 2,
- "never"
- ],
- "block-spacing": [
- 2,
- "always"
- ],
- "brace-style": [
- 2,
- "1tbs",
- {
- "allowSingleLine": true
- }
- ],
- "camelcase": [
- 2,
- {
- "properties": "always"
- }
- ],
- "comma-spacing": [
- 2,
- {
- "before": false,
- "after": true
- }
- ],
- "comma-style": [
- 2,
- "last"
- ],
- "computed-property-spacing": [
- 2,
- "never"
- ],
- "consistent-this": [
- 2,
- "self"
- ],
- "eol-last": 2,
- "func-names": 0,
- "func-style": [
- 2,
- "expression"
- ],
- "id-length": [
- 2,
- {
- "min": 2,
- "max": 30,
- "exceptions": [
- "_"
- ]
- }
- ],
- "id-match": [
- 2,
- "(^[A-Za-z]+(?:[A-Z][a-z]*)*\\d*$)|(^[A-Z]+(_[A-Z]+)*(_\\d$)*$)|(^(_|\\$)$)",
- {
- "properties": true
- }
- ],
- "indent": [
- 2,
- 4
- ],
- "key-spacing": [
- 2,
- {
- "beforeColon": false,
- "afterColon": true
- }
- ],
- "lines-around-comment": [
- 2,
- {
- "beforeBlockComment": true,
- "beforeLineComment": false
- }
- ],
- "linebreak-style": [
- 2,
- "unix"
- ],
- "max-nested-callbacks": [
- 1,
- 3
- ],
- "new-cap": [
- 2,
- {
- "newIsCap": true,
- "capIsNew": false
- }
- ],
- "new-parens": 2,
- "newline-after-var": [
- 2,
- "always"
- ],
- "no-array-constructor": 2,
- "no-continue": 2,
- "no-inline-comments": 2,
- "no-lonely-if": 0,
- "no-mixed-spaces-and-tabs": 2,
- "no-multiple-empty-lines": [
- 2,
- {
- "max": 2
- }
- ],
- "no-nested-ternary": 2,
- "no-new-object": 2,
- "no-spaced-func": 2,
- "no-ternary": 0,
- "no-trailing-spaces": 2,
- "no-underscore-dangle": 2,
- "no-unneeded-ternary": 2,
- "object-curly-spacing": [
- 2,
- "never"
- ],
- "one-var": [
- 2,
- "always"
- ],
- "operator-assignment": [
- 2,
- "always"
- ],
- "operator-linebreak": [
- 2,
- "after"
- ],
- "padded-blocks": [
- 2,
- "never"
- ],
- "quote-props": [
- 2,
- "as-needed"
- ],
- "quotes": [
- 2,
- "single"
- ],
- "semi-spacing": [
- 2,
- {
- "before": false,
- "after": true
- }
- ],
- "semi": [
- 2,
- "always"
- ],
- "sort-vars": 2,
- "space-after-keywords": [
- 2,
- "always"
- ],
- "space-before-blocks": [
- 2,
- "always"
- ],
- "space-before-function-paren": [
- 2,
- "always"
- ],
- "space-in-parens": [
- 2,
- "never"
- ],
- "space-infix-ops": 2,
- "space-return-throw-case": 2,
- "space-unary-ops": [
- 2,
- {
- "words": true,
- "nonwords": false
- }
- ],
- "spaced-comment": [
- 2,
- "always"
- ],
- "wrap-regex": 0,
- "arrow-parens": [
- 2,
- "always"
- ],
- "arrow-spacing": [
- 2,
- {
- "before": true,
- "after": true
- }
- ],
- "constructor-super": 2,
- "generator-star-spacing": [
- 2,
- {
- "before": true,
- "after": false
- }
- ],
- "no-class-assign": 2,
- "no-const-assign": 2,
- "no-dupe-class-members": 2,
- "no-this-before-super": 2,
- "no-var": 2,
- "object-shorthand": [
- 2,
- "always"
- ],
- "prefer-arrow-callback": 2,
- "prefer-const": 0,
- "prefer-spread": 2,
- "prefer-reflect": 2,
- "prefer-template": 2,
- "require-yield": 2
- },
- "ecmaFeatures": {
- "jsx": true,
- "modules": true
- },
- "plugins": [
- "react"
- ],
- "parser": "babel-eslint",
- "globals": {
- "global": true
- },
- "env": {
- "browser": true,
- "mocha": true
- // "node": true
- }
+ "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 6edd850..df978de 100755
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,12 @@
-/node_modules
-npm-debug.log
+node_modules
+coverage
+dist
+*.log
+.*
+!.babelrc
+!.editorconfig
+!.eslintrc
+!.gitignore
+!.npmignore
+!.README
+!.travis.yml
diff --git a/.npmignore b/.npmignore
new file mode 100755
index 0000000..e8add85
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,5 @@
+src
+tests
+coverage
+.*
+*.log
diff --git a/.travis.yml b/.travis.yml
index 3ed46a2..a786167 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,14 @@
language: node_js
node_js:
- - 'iojs-v3.0.0'
- - 'iojs-v2.5.0'
-install:
- - npm install
+ - 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/LICENSE b/LICENSE
new file mode 100644
index 0000000..183e8d8
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,24 @@
+Copyright (c) 2015, Gajus Kuizinas (http://gajus.com/)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of the Gajus Kuizinas (http://gajus.com/) nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL ANUARY BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
index 62b0c20..a6b7399 100644
--- a/README.md
+++ b/README.md
@@ -1,42 +1,66 @@
# React CSS Modules
-[](https://travis-ci.org/gajus/react-css-modules)
-[](https://www.npmjs.org/package/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)
+
+
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)
- [The Implementation](#the-implementation)
- [Usage](#usage)
- [Module Bundler](#module-bundler)
- [webpack](#webpack)
+ - [Development](#development)
+ - [Production](#production)
- [Browserify](#browserify)
+ - [Extending Component Styles](#extending-component-styles)
+ - [`styles` Property](#styles-property)
+ - [Loops and Child Components](#loops-and-child-components)
- [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 Classes](#multiple-css-classes)
+- [Multiple CSS Modules](#multiple-css-modules)
-## What's the Problem?
+## 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:
```js
import React from 'react';
-import styles from './car.css';
+import styles from './table.css';
-export default class Car extends React.Component {
+export default class Table extends React.Component {
render () {
- return
-
-
+ return
;
}
}
@@ -45,9 +69,11 @@ export default class Car extends React.Component {
Rendering the component will produce a markup similar to:
```js
-
-
front-door
-
back-door
+
```
@@ -55,7 +81,13 @@ and a corresponding CSS file that matches those CSS classes.
Awesome!
-However, this approach has several disadvantages:
+### 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 the existing CSS Modules implementation webpack [css-loader](https://github.com/webpack/css-loader#css-modules).
+
+## What's the Problem?
+
+webpack [css-loader](https://github.com/webpack/css-loader#css-modules) itself has several disadvantages:
* You have to use `camelCase` CSS class names.
* You have to use `styles` object whenever constructing a `className`.
@@ -66,19 +98,21 @@ React CSS Modules component automates loading of CSS Modules using `styleName` p
```js
import React from 'react';
-import styles from './car.css';
import CSSModules from 'react-css-modules';
+import styles from './table.css';
-class Car extends React.Component {
+class Table extends React.Component {
render () {
- return
-
-
+ return
;
}
}
-export default CSSModules(Car, styles);
+export default CSSModules(Table, styles);
```
Using `react-css-modules`:
@@ -91,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
@@ -104,38 +138,267 @@ Using `react-css-modules`:
Setup consists of:
-* Setting up a [module bundler](#modulebundler) to load the [Interoperable CSS](https://github.com/css-modules/icss).
+* Setting up a [module bundler](#module-bundler) to load the [Interoperable CSS](https://github.com/css-modules/icss).
* [Decorating](#decorator) your component using `react-css-modules`.
### Module Bundler
#### webpack
-* Install [`style-loader`](https://www.npmjs.com/package/style-loader) and [`css-loader`](https://www.npmjs.com/package/css-loader).
-* You need to use [`extract-text-webpack-plugin`](https://www.npmjs.com/package/extract-text-webpack-plugin) to aggregate the CSS into a single file.
+##### Development
+
+In development environment, you want to [Enable Sourcemaps](#enable-sourcemaps) and webpack [Hot Module Replacement](https://webpack.github.io/docs/hot-module-replacement.html) (HMR). [`style-loader`](https://github.com/webpack/style-loader) already supports HMR. Therefore, Hot Module Replacement will work out of the box.
+
+Setup:
+
+* Install [`style-loader`](https://www.npmjs.com/package/style-loader).
+* Install [`css-loader`](https://www.npmjs.com/package/css-loader).
* Setup `/\.css$/` loader:
```js
{
test: /\.css$/,
- loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]')
+ loaders: [
+ 'style-loader?sourceMap',
+ 'css-loader?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]'
+ ]
}
```
+##### Production
+
+In production environment, you want to extract chunks of CSS into a single stylesheet file.
+
+> Advantages:
+>
+> * Fewer style tags (older IE has a limit)
+> * CSS SourceMap (with `devtool: "source-map"` and `css-loader?sourceMap`)
+> * CSS requested in parallel
+> * CSS cached separate
+> * Faster runtime (less code and DOM operations)
+>
+> Caveats:
+>
+> * Additional HTTP request
+> * Longer compilation time
+> * More complex configuration
+> * No runtime public path modification
+> * No Hot Module Replacement
+
+– [extract-text-webpack-plugin](https://github.com/webpack/extract-text-webpack-plugin)
+
+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:
+
+ * 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.
-#### Browserify
+##### Browserify
Refer to [`css-modulesify`](https://github.com/css-modules/css-modulesify).
+### Extending Component Styles
+
+Use `styles` property to overwrite the default component styles.
+
+Explanation using `Table` component:
+
+```js
+import React from 'react';
+import CSSModules from 'react-css-modules';
+import styles from './table.css';
+
+class Table extends React.Component {
+ render () {
+ return
;
+ }
+}
+
+export default CSSModules(Table, styles);
+```
+
+In this example, `CSSModules` is used to decorate `Table` component using `./table.css` CSS Modules. When `Table` component is rendered, it will use the properties of the `styles` object to construct `className` values.
+
+Using `styles` property you can overwrite the default component `styles` object, e.g.
+
+```js
+import customStyles from './table-custom-styles.css';
+
+
;
+```
+
+[Interoperable CSS](https://github.com/css-modules/icss) can [extend other ICSS](https://github.com/css-modules/css-modules#dependencies). Use this feature to extend default styles, e.g.
+
+```css
+/* table-custom-styles.css */
+.table {
+ composes: table from './table.css';
+}
+
+.row {
+ composes: row from './table.css';
+}
+
+/* .cell {
+ composes: cell from './table.css';
+} */
+
+.table {
+ width: 400px;
+}
+
+.cell {
+ float: left; width: 154px; background: #eee; padding: 10px; margin: 10px 0 10px 10px;
+}
+```
+
+In this example, `table-custom-styles.css` selectively extends `table.css` (the default styles of `Table` component).
+
+Refer to the [`UsingStylesProperty` example](https://github.com/gajus/react-css-modules-examples/tree/master/src/UsingStylesProperty) for an example of a working implementation.
+
+### `styles` Property
+
+Decorated components inherit `styles` property that describes the mapping between CSS modules and CSS classes.
+
+```js
+class extends React.Component {
+ render () {
+
;
+ }
+}
+```
+
+In the above example, `styleName='foo'` and `className={this.props.styles.foo}` are equivalent.
+
+`styles` property is designed to enable component decoration of [Loops and Child Components](#loops-and-child-components).
+
+### Loops and Child Components
+
+`styleName` cannot be used to define styles of a `ReactElement` that will be generated by another component, e.g.
+
+```js
+import React from 'react';
+import CSSModules from 'react-css-modules';
+import List from './List';
+import styles from './table.css';
+
+class CustomList extends React.Component {
+ render () {
+ let itemTemplate;
+
+ itemTemplate = (name) => {
+ return
{name};
+ };
+
+ return
;
+ }
+}
+
+export default CSSModules(CustomList, styles);
+```
+
+The above example will not work. `CSSModules` is used to decorate `CustomList` component. However, it is the `List` component that will render `itemTemplate`.
+
+For that purpose, the decorated component inherits [`styles` property](#styles-property) that you can use just as a regular CSS Modules object. The earlier example can be therefore rewritten to:
+
+```js
+import React from 'react';
+import CSSModules from 'react-css-modules';
+import List from './List';
+import styles from './table.css';
+
+class CustomList extends React.Component {
+ render () {
+ let itemTemplate;
+
+ itemTemplate = (name) => {
+ return
{name};
+ };
+
+ return
;
+ }
+}
+
+export default CSSModules(CustomList, styles);
+```
+
+You can use `styleName` property within the child component if you decorate the child component using `CSSModules` before passing it to the rendering component, e.g.
+
+```js
+import React from 'react';
+import CSSModules from 'react-css-modules';
+import List from './List';
+import styles from './table.css';
+
+class CustomList extends React.Component {
+ render () {
+ let itemTemplate;
+
+ itemTemplate = (name) => {
+ return
{name};
+ };
+
+ itemTemplate = CSSModules(itemTemplate, this.props.styles);
+
+ return
;
+ }
+}
+
+export default CSSModules(CustomList, styles);
+```
+
### Decorator
```js
@@ -143,12 +406,12 @@ Refer to [`css-modulesify`](https://github.com/css-modules/css-modulesify).
* @typedef CSSModules~Options
* @see {@link https://github.com/gajus/react-css-modules#options}
* @property {Boolean} allowMultiple
- * @property {Boolean} errorWhenNotFound
+ * @property {String} handleNotFoundStyleName
*/
/**
* @param {Function} Component
- * @param {Object} styles CSS Modules class map.
+ * @param {Object} defaultStyles CSS Modules class map.
* @param {CSSModules~Options} options
* @return {Function}
*/
@@ -158,19 +421,21 @@ You need to decorate your component using `react-css-modules`, e.g.
```js
import React from 'react';
-import styles from './car.css';
import CSSModules from 'react-css-modules';
+import styles from './table.css';
-class Car extends React.Component {
+class Table extends React.Component {
render () {
- return
-
-
+ return
;
}
}
-export default CSSModules(Car, styles);
+export default CSSModules(Table, styles);
```
Thats it!
@@ -179,15 +444,17 @@ As the name implies, `react-css-modules` is compatible with the [ES7 decorators]
```js
import React from 'react';
-import styles from './car.css';
import CSSModules from 'react-css-modules';
+import styles from './table.css';
@CSSModules(styles)
export default class extends React.Component {
render () {
- return
-
front-door
-
back-door
+ return
;
}
}
@@ -223,11 +490,17 @@ When `false`, the following will cause an error:
```
-#### `errorWhenNotFound`
+#### `handleNotFoundStyleName`
+
+Default: `throw`.
-Default: `true`.
+Defines the desired action when `styleName` cannot be mapped to an existing CSS Module.
-Throws an error when `styleName` cannot be mapped to an existing CSS Module.
+Available options:
+
+* `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
@@ -236,7 +509,28 @@ Throws an error when `styleName` cannot be mapped to an existing CSS Module.
```js
{
test: /\.scss$/,
- loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!sass')
+ loaders: [
+ 'style',
+ 'css?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]',
+ 'resolve-url',
+ 'sass'
+ ]
+}
+```
+
+### Enable Sourcemaps
+
+To enable CSS Source maps, add `sourceMap` parameter to the css-loader and to the `sass-loader`:
+
+```js
+{
+ test: /\.scss$/,
+ loaders: [
+ 'style?sourceMap',
+ 'css?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]',
+ 'resolve-url',
+ 'sass?sourceMap'
+ ]
}
```
@@ -296,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/dist/index.js b/dist/index.js
deleted file mode 100644
index f7f655d..0000000
--- a/dist/index.js
+++ /dev/null
@@ -1,80 +0,0 @@
-'use strict';
-
-Object.defineProperty(exports, '__esModule', {
- value: true
-});
-
-var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
-
-var _get = function get(_x2, _x3, _x4) { var _again = true; _function: while (_again) { var object = _x2, property = _x3, receiver = _x4; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x2 = parent; _x3 = property; _x4 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } };
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
-
-function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
-
-function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
-
-var _linkClass = require('./linkClass');
-
-var _linkClass2 = _interopRequireDefault(_linkClass);
-
-var _makeConfig = require('./makeConfig');
-
-var _makeConfig2 = _interopRequireDefault(_makeConfig);
-
-var decoratorConstructor = undefined,
- functionConstructor = undefined;
-
-/**
- * When used as a function.
- *
- * @param {Function} Component
- * @param {Object} styles CSS Modules class map.
- * @param {Object} options {@link https://github.com/gajus/react-css-modules#options}
- * @return {Function}
- */
-functionConstructor = function (Component, styles) {
- var options = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2];
-
- return (function (_Component) {
- _inherits(_class, _Component);
-
- function _class() {
- _classCallCheck(this, _class);
-
- _get(Object.getPrototypeOf(_class.prototype), 'constructor', this).apply(this, arguments);
- }
-
- _createClass(_class, [{
- key: 'render',
- value: function render() {
- return (0, _linkClass2['default'])(_get(Object.getPrototypeOf(_class.prototype), 'render', this).call(this), styles, (0, _makeConfig2['default'])(options));
- }
- }]);
-
- return _class;
- })(Component);
-};
-
-/**
- * When used as a ES7 decorator.
- *
- * @param {Object} styles CSS Modules class map.
- * @param {Object} options {@link https://github.com/gajus/react-css-modules#options}
- * @return {Function}
- */
-decoratorConstructor = function (styles, options) {
- return function (Component) {
- return functionConstructor(Component, styles, options);
- };
-};
-
-exports['default'] = function () {
- if (typeof arguments[0] === 'function') {
- return functionConstructor(arguments[0], arguments[1], arguments[2]);
- } else {
- return decoratorConstructor(arguments[0], arguments[1]);
- }
-};
-
-module.exports = exports['default'];
\ No newline at end of file
diff --git a/dist/linkClass.js b/dist/linkClass.js
deleted file mode 100644
index 6176156..0000000
--- a/dist/linkClass.js
+++ /dev/null
@@ -1,98 +0,0 @@
-'use strict';
-
-Object.defineProperty(exports, '__esModule', {
- value: true
-});
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
-
-var _react = require('react');
-
-var _react2 = _interopRequireDefault(_react);
-
-var linkClass = undefined;
-
-/**
- * @param {ReactElement} element
- * @param {Object} styles CSS modules class map.
- * @param {CSSModules~Options} options
- * @return {ReactElement}
- */
-linkClass = function (element) {
- var styles = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
- var options = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2];
-
- var appendClassName = undefined,
- childrenCount = undefined,
- clonedElement = undefined,
- newChildren = undefined,
- newProps = undefined,
- styleNames = undefined;
-
- styleNames = element.props.styleName;
-
- if (styleNames) {
- styleNames = styleNames.split(' ');
-
- if (options.allowMultiple === false && styleNames.length > 1) {
- throw new Error('ReactElement styleName property defines multiple module names ("' + element.props.styleName + '").');
- }
-
- appendClassName = styleNames.map(function (styleName) {
- if (styles[styleName]) {
- return styles[styleName];
- } else {
- if (options.errorWhenNotFound === true) {
- throw new Error('"' + styleName + '" CSS module is undefined.');
- }
-
- return '';
- }
- });
-
- appendClassName = appendClassName.filter(function (className) {
- return className.length;
- });
-
- appendClassName = appendClassName.join(' ');
- }
-
- // A child can be either an array, a sole object or a string.
- //
test
- if (typeof element.props.children !== 'string') {
- childrenCount = _react2['default'].Children.count(element.props.children);
-
- if (childrenCount > 1) {
- newChildren = _react2['default'].Children.map(element.props.children, function (node) {
- if (_react2['default'].isValidElement(node)) {
- return linkClass(node, styles, options);
- } else {
- return node;
- }
- });
- } else if (childrenCount === 1) {
- newChildren = linkClass(_react2['default'].Children.only(element.props.children), styles, options);
- }
- }
-
- if (appendClassName) {
- if (element.props.className) {
- appendClassName = element.props.className + ' ' + appendClassName;
- }
-
- newProps = {
- className: appendClassName
- };
- }
-
- if (newChildren) {
- clonedElement = _react2['default'].cloneElement(element, newProps, newChildren);
- } else {
- clonedElement = _react2['default'].cloneElement(element, newProps);
- }
-
- return clonedElement;
-};
-
-exports['default'] = linkClass;
-module.exports = exports['default'];
\ No newline at end of file
diff --git a/dist/makeConfig.js b/dist/makeConfig.js
deleted file mode 100644
index b286719..0000000
--- a/dist/makeConfig.js
+++ /dev/null
@@ -1,56 +0,0 @@
-'use strict';
-
-Object.defineProperty(exports, '__esModule', {
- value: true
-});
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
-
-var _utils = require('./utils');
-
-var _utils2 = _interopRequireDefault(_utils);
-
-/**
- * @typedef CSSModules~Options
- * @see {@link https://github.com/gajus/react-css-modules#options}
- * @property {Boolean} allowMultiple
- * @property {Boolean} errorWhenNotFound
- */
-
-/**
- * @param {Options} userConfig
- * @return {CSSModules~Options}
- */
-
-exports['default'] = function () {
- var userConfig = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
-
- var knownProperties = undefined,
- unknownProperties = undefined;
-
- knownProperties = ['allowMultiple', 'errorWhenNotFound'];
-
- unknownProperties = _utils2['default'].difference(_utils2['default'].keys(userConfig), knownProperties);
-
- if (unknownProperties.length) {
- throw new Error('Unknown config property "' + unknownProperties[0] + '".');
- }
-
- _utils2['default'].forEach(userConfig, function (value, name) {
- if (typeof value !== 'boolean') {
- throw new Error('"' + name + '" property value must be a boolean.');
- }
- });
-
- if (typeof userConfig.allowMultiple === 'undefined') {
- userConfig.allowMultiple = false;
- }
-
- if (typeof userConfig.errorWhenNotFound === 'undefined') {
- userConfig.errorWhenNotFound = false;
- }
-
- return userConfig;
-};
-
-module.exports = exports['default'];
\ No newline at end of file
diff --git a/dist/utils.js b/dist/utils.js
deleted file mode 100644
index 94ee160..0000000
--- a/dist/utils.js
+++ /dev/null
@@ -1,26 +0,0 @@
-'use strict';
-
-Object.defineProperty(exports, '__esModule', {
- value: true
-});
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
-
-var _lodashObjectKeys = require('lodash/object/keys');
-
-var _lodashObjectKeys2 = _interopRequireDefault(_lodashObjectKeys);
-
-var _lodashArrayDifference = require('lodash/array/difference');
-
-var _lodashArrayDifference2 = _interopRequireDefault(_lodashArrayDifference);
-
-var _lodashCollectionForEach = require('lodash/collection/forEach');
-
-var _lodashCollectionForEach2 = _interopRequireDefault(_lodashCollectionForEach);
-
-exports['default'] = {
- keys: _lodashObjectKeys2['default'],
- difference: _lodashArrayDifference2['default'],
- forEach: _lodashCollectionForEach2['default']
-};
-module.exports = exports['default'];
\ No newline at end of file
diff --git a/package.json b/package.json
index ad165e2..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/index.js",
+ "main": "./dist/index.js",
"repository": {
"type": "git",
"url": "https://github.com/gajus/react-css-modules"
@@ -12,30 +12,44 @@
"css",
"modules"
],
- "version": "3.0.1",
+ "version": "4.3.0",
"author": {
"name": "Gajus Kuizinas",
- "email": "gk@anuary.com",
+ "email": "gajus@gajus.com",
"url": "http://gajus.com"
},
"license": "BSD-3-Clause",
"dependencies": {
- "lodash": "^3.10.1"
+ "hoist-non-react-statics": "^2.5.5",
+ "lodash": "^4.16.6",
+ "object-unfreeze": "^1.1.0"
},
"devDependencies": {
- "babel": "^5.8.21",
- "babel-eslint": "^4.1.0",
- "chai": "^3.2.0",
- "eslint": "^1.2.1",
- "eslint-plugin-react": "^3.3.0",
- "jsdom": "^6.2.0",
- "mocha": "^2.2.5",
- "react": "^0.14.0-beta3",
- "react-addons-test-utils": "^0.14.0-beta3"
+ "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": {
- "test": "./node_modules/.bin/eslint ./src/ ./test/ && mocha",
- "build": "babel ./src/ --out-dir ./dist/",
- "watch": "babel --watch ./src/ --out-dir ./dist/"
+ "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
new file mode 100644
index 0000000..4741bea
--- /dev/null
+++ b/src/extendReactClass.js
@@ -0,0 +1,74 @@
+/* eslint-disable react/prop-types */
+
+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
+ * @param {Object} defaultStyles
+ * @param {Object} options
+ * @returns {ReactClass}
+ */
+export default (Component: Object, defaultStyles: Object, options: Object) => {
+ const WrappedComponent = class extends Component {
+ render () {
+ let styles;
+
+ 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;
+ }
+
+ Object.defineProperty(props, 'styles', {
+ configurable: true,
+ enumerable: false,
+ value: styles,
+ writable: false
+ });
+
+ 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 = {};
+
+ renderResult = super.render();
+ }
+
+ if (renderResult) {
+ return linkClass(renderResult, styles, options);
+ }
+
+ return renderNothing(React.version);
+ }
+ };
+
+ return hoistNonReactStatics(WrappedComponent, Component);
+};
diff --git a/src/generateAppendClassName.js b/src/generateAppendClassName.js
new file mode 100644
index 0000000..127aa2a
--- /dev/null
+++ b/src/generateAppendClassName.js
@@ -0,0 +1,49 @@
+import SimpleMap from './SimpleMap';
+
+const CustomMap = typeof Map === 'undefined' ? SimpleMap : Map;
+
+const stylesIndex = new CustomMap();
+
+export default (styles, styleNames: Array
, handleNotFoundStyleName: "throw" | "log" | "ignore"): string => {
+ let appendClassName;
+ let stylesIndexMap;
+
+ stylesIndexMap = stylesIndex.get(styles);
+
+ if (stylesIndexMap) {
+ const styleNameIndex = stylesIndexMap.get(styleNames);
+
+ 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();
+
+ stylesIndexMap.set(styleNames, appendClassName);
+
+ return appendClassName;
+};
diff --git a/src/index.js b/src/index.js
index aab47ea..b913b29 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,42 +1,56 @@
-import linkClass from './linkClass';
-import makeConfig from './makeConfig';
+import _ from 'lodash';
+import extendReactClass from './extendReactClass';
+import wrapStatelessFunction from './wrapStatelessFunction';
+import makeConfiguration from './makeConfiguration';
-let decoratorConstructor,
- functionConstructor;
+/**
+ * @see https://github.com/gajus/react-css-modules#options
+ */
+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);
+};
/**
* When used as a function.
- *
- * @param {Function} Component
- * @param {Object} styles CSS Modules class map.
- * @param {Object} options {@link https://github.com/gajus/react-css-modules#options}
- * @return {Function}
*/
-functionConstructor = (Component, styles, options = {}) => {
- return class extends Component {
- render () {
- return linkClass(super.render(), styles, makeConfig(options));
- }
- };
+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.
- *
- * @param {Object} styles CSS Modules class map.
- * @param {Object} options {@link https://github.com/gajus/react-css-modules#options}
- * @return {Function}
*/
-decoratorConstructor = (styles, options) => {
- return (Component) => {
- return functionConstructor(Component, styles, options);
- };
+const decoratorConstructor = (defaultStyles: Object, options: TypeOptions): Function => {
+ return (Component: Function) => {
+ return functionConstructor(Component, defaultStyles, options);
+ };
};
export default (...args) => {
- if (typeof args[0] === 'function') {
- 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
new file mode 100644
index 0000000..67d0a94
--- /dev/null
+++ b/src/isIterable.js
@@ -0,0 +1,24 @@
+import _ from 'lodash';
+
+const ITERATOR_SYMBOL = typeof Symbol !== 'undefined' && _.isFunction(Symbol) && Symbol.iterator;
+const OLD_ITERATOR_SYMBOL = '@@iterator';
+
+/**
+ * @see https://github.com/lodash/lodash/issues/1668
+ * @see https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Iteration_protocols
+ */
+export default (maybeIterable: any): boolean => {
+ let iterator;
+
+ if (!_.isObject(maybeIterable)) {
+ return false;
+ }
+
+ if (ITERATOR_SYMBOL) {
+ iterator = maybeIterable[ITERATOR_SYMBOL];
+ } else {
+ iterator = maybeIterable[OLD_ITERATOR_SYMBOL];
+ }
+
+ return _.isFunction(iterator);
+};
diff --git a/src/linkClass.js b/src/linkClass.js
index b058657..4ed3975 100644
--- a/src/linkClass.js
+++ b/src/linkClass.js
@@ -1,85 +1,106 @@
-import React from 'react';
-
-let linkClass;
-
-/**
- * @param {ReactElement} element
- * @param {Object} styles CSS modules class map.
- * @param {CSSModules~Options} options
- * @return {ReactElement}
- */
-linkClass = (element, styles = {}, options = {}) => {
- let appendClassName,
- childrenCount,
- clonedElement,
- newChildren,
- newProps,
- styleNames;
-
- styleNames = element.props.styleName;
-
- if (styleNames) {
- styleNames = styleNames.split(' ');
-
- if (options.allowMultiple === false && styleNames.length > 1) {
- throw new Error(`ReactElement styleName property defines multiple module names ("${element.props.styleName}").`);
- }
-
- appendClassName = styleNames.map((styleName) => {
- if (styles[styleName]) {
- return styles[styleName];
- } else {
- if (options.errorWhenNotFound === true) {
- throw new Error(`"${styleName}" CSS module is undefined.`);
- }
-
- return '';
- }
- });
-
- appendClassName = appendClassName.filter((className) => {
- return className.length;
- });
-
- appendClassName = appendClassName.join(' ');
+import _ from 'lodash';
+import React, {
+ ReactElement
+} from 'react';
+import objectUnfreeze from 'object-unfreeze';
+import isIterable from './isIterable';
+import parseStyleName from './parseStyleName';
+import generateAppendClassName from './generateAppendClassName';
+
+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;
+
+ array[index] = linkArray(unfreezedValue, styles, configuration);
}
+ });
+
+ return array;
+};
- // A child can be either an array, a sole object or a string.
- // test
- if (typeof element.props.children !== 'string') {
- childrenCount = React.Children.count(element.props.children);
-
- if (childrenCount > 1) {
- newChildren = React.Children.map(element.props.children, (node) => {
- if (React.isValidElement(node)) {
- return linkClass(node, styles, options);
- } else {
- return node;
- }
- });
- } else if (childrenCount === 1) {
- newChildren = linkClass(React.Children.only(element.props.children), styles, options);
- }
+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 (styleNames.length) {
+ appendClassName = generateAppendClassName(styles, styleNames, configuration.handleNotFoundStyleName);
if (appendClassName) {
- if (element.props.className) {
- appendClassName = `${element.props.className} ${appendClassName}`;
- }
+ if (elementShallowCopy.props.className) {
+ appendClassName = elementShallowCopy.props.className + ' ' + appendClassName;
+ }
- newProps = {
- className: appendClassName
- };
+ elementShallowCopy.props.className = appendClassName;
}
+ }
- if (newChildren) {
- clonedElement = React.cloneElement(element, newProps, newChildren);
- } else {
- clonedElement = React.cloneElement(element, newProps);
- }
+ delete elementShallowCopy.props.styleName;
- return clonedElement;
+ if (elementIsFrozen) {
+ Object.freeze(elementShallowCopy.props);
+ Object.freeze(elementShallowCopy);
+ } else if (propsFrozen) {
+ Object.freeze(elementShallowCopy.props);
+ }
+
+ if (propsNotExtensible) {
+ Object.preventExtensions(elementShallowCopy.props);
+ }
+
+ return elementShallowCopy;
};
-export default linkClass;
+/**
+ * @param {ReactElement} element
+ * @param {Object} styles CSS modules class map.
+ * @param {CSSModules~Options} configuration
+ */
+export default (element: ReactElement, styles = {}, configuration = {}): ReactElement => {
+ // @see https://github.com/gajus/react-css-modules/pull/30
+ if (!_.isObject(element)) {
+ return element;
+ }
+
+ return linkElement(element, styles, configuration);
+};
diff --git a/src/makeConfig.js b/src/makeConfig.js
deleted file mode 100644
index f9c8715..0000000
--- a/src/makeConfig.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import _ from './utils';
-
-/**
- * @typedef CSSModules~Options
- * @see {@link https://github.com/gajus/react-css-modules#options}
- * @property {Boolean} allowMultiple
- * @property {Boolean} errorWhenNotFound
- */
-
-/**
- * @param {Options} userConfig
- * @return {CSSModules~Options}
- */
-export default (userConfig = {}) => {
- let knownProperties,
- unknownProperties;
-
- knownProperties = [
- 'allowMultiple',
- 'errorWhenNotFound'
- ];
-
- unknownProperties = _.difference(_.keys(userConfig), knownProperties);
-
- if (unknownProperties.length) {
- throw new Error(`Unknown config property "${unknownProperties[0]}".`);
- }
-
- _.forEach(userConfig, (value, name) => {
- if (typeof value !== 'boolean') {
- throw new Error(`"${name}" property value must be a boolean.`);
- }
- });
-
- if (typeof userConfig.allowMultiple === 'undefined') {
- userConfig.allowMultiple = false;
- }
-
- if (typeof userConfig.errorWhenNotFound === 'undefined') {
- userConfig.errorWhenNotFound = false;
- }
-
- return userConfig;
-};
diff --git a/src/makeConfiguration.js b/src/makeConfiguration.js
new file mode 100644
index 0000000..cc3169c
--- /dev/null
+++ b/src/makeConfiguration.js
@@ -0,0 +1,37 @@
+import _ from 'lodash';
+
+/**
+ * @typedef CSSModules~Options
+ * @see {@link https://github.com/gajus/react-css-modules#options}
+ * @property {boolean} allowMultiple
+ * @property {string} handleNotFoundStyleName
+ */
+
+/**
+ * @param {CSSModules~Options} userConfiguration
+ * @returns {CSSModules~Options}
+ */
+export default (userConfiguration = {}) => {
+ const configuration = {
+ allowMultiple: false,
+ handleNotFoundStyleName: 'throw'
+ };
+
+ _.forEach(userConfiguration, (value, name) => {
+ if (_.isUndefined(configuration[name])) {
+ throw new Error('Unknown configuration property "' + name + '".');
+ }
+
+ 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;
+ });
+
+ return configuration;
+};
diff --git a/src/parseStyleName.js b/src/parseStyleName.js
new file mode 100644
index 0000000..937b2d6
--- /dev/null
+++ b/src/parseStyleName.js
@@ -0,0 +1,22 @@
+import _ from 'lodash';
+
+const styleNameIndex = {};
+
+export default (styleNamePropertyValue: string, allowMultiple: boolean): Array => {
+ let styleNames;
+
+ if (styleNameIndex[styleNamePropertyValue]) {
+ styleNames = styleNameIndex[styleNamePropertyValue];
+ } else {
+ styleNames = _.trim(styleNamePropertyValue).split(/\s+/);
+ styleNames = _.filter(styleNames);
+
+ styleNameIndex[styleNamePropertyValue] = styleNames;
+ }
+
+ if (allowMultiple === false && styleNames.length > 1) {
+ throw new Error('ReactElement styleName property defines multiple module names ("' + styleNamePropertyValue + '").');
+ }
+
+ 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/utils.js b/src/utils.js
deleted file mode 100644
index 861dd24..0000000
--- a/src/utils.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import keys from 'lodash/object/keys';
-import difference from 'lodash/array/difference';
-import forEach from 'lodash/collection/forEach';
-
-export default {
- keys,
- difference,
- forEach
-};
diff --git a/src/wrapStatelessFunction.js b/src/wrapStatelessFunction.js
new file mode 100644
index 0000000..b6b9162
--- /dev/null
+++ b/src/wrapStatelessFunction.js
@@ -0,0 +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;
+ 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/test/linkClass.js b/test/linkClass.js
deleted file mode 100644
index f88884b..0000000
--- a/test/linkClass.js
+++ /dev/null
@@ -1,146 +0,0 @@
-import {
- expect
-} from 'chai';
-
-import React from 'react';
-import TestUtils from 'react-addons-test-utils';
-import jsdom from 'jsdom';
-import linkClass from './../dist/linkClass';
-
-describe('linkClass', () => {
- context('when ReactElement does not define styleName', () => {
- it('does not affect element properties', () => {
- expect(linkClass()).to.deep.equal();
- });
-
- it('does not affect element properties with a single element child', () => {
- expect(linkClass()).to.deep.equal();
- });
-
- it('does not affect element properties with a single text child', () => {
- expect(linkClass(test
)).to.deep.equal(test
);
- });
-
- it('does not affect the className', () => {
- expect(linkClass()).to.deep.equal();
- });
-
- // Using array instead of object causes the following error:
- // Warning: Each child in an array or iterator should have a unique "key" prop.
- // Check the render method of _class. See https://fb.me/react-warning-keys for more information.
- xit('does not affect element with multiple children', () => {
- expect(linkClass()).to.deep.equal();
- });
- });
-
- context('when styleName matches an existing CSS module', () => {
- context('when ReactElement does not have an existing className', () => {
- it('uses the generated class name to set the className property', () => {
- let subject;
-
- subject = ;
-
- subject = linkClass(subject, {
- foo: 'foo-1'
- });
-
- expect(subject.props.className).to.deep.equal('foo-1');
- });
- });
- context('when ReactElement has an existing className', () => {
- it('appends the generated class name to the className property', () => {
- let subject;
-
- subject = ;
-
- subject = linkClass(subject, {
- bar: 'bar-1'
- });
-
- expect(subject.props.className).to.deep.equal('foo bar-1');
- });
- });
- });
-
- describe('options.allowMultiple', () => {
- context('when multiple module names are used', () => {
- context('when false', () => {
- it('throws an error', () => {
- expect(() => {
- linkClass(, {}, {allowMultiple: false});
- }).to.throw(Error, 'ReactElement styleName property defines multiple module names ("foo bar").');
- });
- });
- context('when true', () => {
- it('appends a generated class name for every referenced CSS module', () => {
- let subject;
-
- subject = ;
-
- subject = linkClass(subject, {
- foo: 'foo-1',
- bar: 'bar-1'
- }, {allowMultiple: true});
-
- expect(subject.props.className).to.deep.equal('foo-1 bar-1');
- });
- });
- });
- });
-
- describe('options.errorWhenNotFound', () => {
- 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});
-
- expect(subject.props.className).to.be.an('undefined');
- });
- });
- context('when is true', () => {
- it('throws an error', () => {
- expect(() => {
- linkClass(, {}, {errorWhenNotFound: true});
- }).to.throw(Error, '"foo" CSS module is undefined.');
- });
- });
- });
- });
-
- context('when ReactElement includes ReactComponent', () => {
- let Foo,
- nodeList;
-
- beforeEach(() => {
- global.document = jsdom.jsdom(`
-
-
-
-
-
-
-
- `);
-
- global.window = document.defaultView;
-
- Foo = class extends React.Component {
- render () {
- return Hello
;
- }
- };
-
- nodeList = TestUtils.renderIntoDocument(linkClass(
, {foo: 'foo-1'}));
- });
- it('processes ReactElement nodes', () => {
- expect(nodeList.className).to.equal('foo-1');
- });
- it('does not process ReactComponent nodes', () => {
- expect(nodeList.firstChild.className).to.equal('');
- });
- });
-});
diff --git a/test/makeConfig.js b/test/makeConfig.js
deleted file mode 100644
index a48bd86..0000000
--- a/test/makeConfig.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import {
- expect
-} from 'chai';
-
-import makeConfig from './../dist/makeConfig';
-
-describe('makeConfig', () => {
- describe('when using default config', () => {
- let options;
-
- beforeEach(() => {
- options = makeConfig();
- });
- describe('allowMultiple property', () => {
- it('defaults to false', () => {
- expect(options.allowMultiple).to.equal(false);
- });
- });
- describe('errorWhenNotFound property', () => {
- it('defaults to true', () => {
- expect(options.errorWhenNotFound).to.equal(false);
- });
- });
- });
- describe('when unknown property is provided', () => {
- it('throws an error', () => {
- expect(() => {
- makeConfig({
- unknownProperty: true
- });
- }).to.throw(Error, 'Unknown config property "unknownProperty".');
- });
- });
-});
diff --git a/test/mocha.opts b/test/mocha.opts
deleted file mode 100644
index 13374d0..0000000
--- a/test/mocha.opts
+++ /dev/null
@@ -1 +0,0 @@
---compilers js:babel/register
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
new file mode 100644
index 0000000..01733ee
--- /dev/null
+++ b/tests/extendReactClass.js
@@ -0,0 +1,167 @@
+/* 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('');
+
+ 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;
+
+ 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('rendering Component that returns null', () => {
+ it('generates null', () => {
+ let Component;
+
+ const shallowRenderer = TestUtils.createRenderer();
+
+ Component = class extends React.Component {
+ render () {
+ return null;
+ }
+ };
+
+ Component = extendReactClass(Component);
+
+ shallowRenderer.render();
+
+ const component = shallowRenderer.getRenderOutput();
+
+ expect(component).to.equal(null);
+ });
+ });
+ context('target component have static properties', () => {
+ it('hoists static properties', () => {
+ const Component = class extends React.Component {
+ static foo = 'FOO';
+
+ render () {
+ return null;
+ }
+ };
+
+ const WrappedComponent = extendReactClass(Component);
+
+ expect(Component.foo).to.equal('FOO');
+ expect(WrappedComponent.foo).to.equal(Component.foo);
+ });
+ });
+});
diff --git a/tests/linkClass.js b/tests/linkClass.js
new file mode 100644
index 0000000..cf7c289
--- /dev/null
+++ b/tests/linkClass.js
@@ -0,0 +1,483 @@
+/* eslint-disable max-nested-callbacks, react/prefer-stateless-function, class-methods-use-this, no-console, no-unused-expressions */
+
+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', () => {
+ expect(linkClass()).to.deep.equal();
+ });
+
+ it('does not affect element properties with a single element child', () => {
+ expect(linkClass()).to.deep.equal();
+ });
+
+ it('does not affect element properties with a single element child in non-`children` prop', () => {
+ expect(linkClass(} />)).to.deep.equal(} />);
+ });
+
+ it('does not affect element properties with a single text child', () => {
+ expect(linkClass(test
)).to.deep.equal(test
);
+ });
+
+ it('does not affect the className', () => {
+ expect(linkClass()).to.deep.equal();
+ });
+
+ xit('does not affect element with a single children when that children is contained in an array', () => {
+ const subject = React.createElement('div', null, [
+ React.createElement('p')
+ ]);
+ const outcome = React.createElement('div', null, [
+ React.createElement('p')
+ ]);
+
+ expect(linkClass(subject)).to.deep.equal(outcome);
+ });
+
+ xit('does not affect element with multiple children', () => {
+ // Using array instead of object causes the following error:
+ // Warning: Each child in an array or iterator should have a unique "key" prop.
+ // Check the render method of _class. See https://fb.me/react-warning-keys for more information.
+ // @see https://github.com/facebook/react/issues/4723#issuecomment-135555277
+ // expect(linkClass()).to.deep.equal();
+
+ const subject = React.createElement('div', null, [
+ React.createElement('p'),
+ React.createElement('p')
+ ]);
+ const outcome = React.createElement('div', null, [
+ React.createElement('p'),
+ React.createElement('p')
+ ]);
+
+ expect(linkClass(subject)).to.deep.equal(outcome);
+ });
+ });
+
+ context('called with null instead of ReactElement', () => {
+ it('returns null', () => {
+ const subject = linkClass(null);
+
+ expect(subject).to.equal(null);
+ });
+ });
+
+ context('styleName matches an existing CSS module', () => {
+ context('when a descendant element has styleName', () => {
+ it('assigns a generated className', () => {
+ let subject;
+
+ subject = ;
+
+ subject = linkClass(subject, {
+ foo: 'foo-1'
+ });
+
+ expect(subject.props.children.props.className).to.equal('foo-1');
+ });
+ });
+ context('when a descendant element in non-`children` prop has styleName', () => {
+ it('assigns a generated className', () => {
+ let subject;
+
+ subject = }
+ els={[, []]}
+ />;
+
+ subject = linkClass(subject, {
+ bar: 'bar-1',
+ baz: 'baz-1',
+ foo: 'foo-1'
+ });
+
+ expect(subject.props.el.props.className).to.equal('foo-1');
+ expect(subject.props.els[0].props.className).to.equal('bar-1');
+ expect(subject.props.els[1][0].props.className).to.equal('baz-1');
+ });
+ });
+ context('when multiple descendant elements have styleName', () => {
+ it('assigns a generated className', () => {
+ let subject;
+
+ subject = ;
+
+ subject = linkClass(subject, {
+ bar: 'bar-1',
+ foo: 'foo-1'
+ });
+
+ 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;
+
+ subject = ;
+
+ subject = linkClass(subject, {
+ bar: 'bar-1',
+ foo: 'foo-1'
+ });
+
+ 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', () => {
+ let subject;
+
+ const iterable = {
+ 0: ,
+ 1: ,
+ length: 2,
+
+ // eslint-disable-next-line no-use-extend-native/no-use-extend-native
+ [Symbol.iterator]: Array.prototype[Symbol.iterator]
+ };
+
+ subject = {iterable}
;
+
+ subject = linkClass(subject, {
+ bar: 'bar-1',
+ foo: 'foo-1'
+ });
+
+ expect(subject.props.children[0].props.className).to.equal('foo-1');
+ expect(subject.props.children[1].props.className).to.equal('bar-1');
+ });
+ });
+ context('when non-`children` prop is an iterable', () => {
+ it('it is left untouched', () => {
+ let subject;
+
+ const iterable = {
+ 0: ,
+ 1: ,
+ length: 2,
+
+ // eslint-disable-next-line no-use-extend-native/no-use-extend-native
+ [Symbol.iterator]: Array.prototype[Symbol.iterator]
+ };
+
+ subject = ;
+
+ subject = linkClass(subject, {
+ bar: 'bar-1',
+ foo: 'foo-1'
+ });
+
+ expect(subject.props.els[0].props.styleName).to.equal('foo');
+ expect(subject.props.els[1].props.styleName).to.equal('bar');
+ expect(subject.props.els[0].props).not.to.have.property('className');
+ expect(subject.props.els[1].props).not.to.have.property('className');
+ });
+ });
+ context('when ReactElement does not have an existing className', () => {
+ it('uses the generated class name to set the className property', () => {
+ let subject;
+
+ subject = ;
+
+ subject = linkClass(subject, {
+ foo: 'foo-1'
+ });
+
+ expect(subject.props.className).to.deep.equal('foo-1');
+ });
+ });
+ context('when ReactElement has an existing className', () => {
+ it('appends the generated class name to the className property', () => {
+ let subject;
+
+ subject = ;
+
+ subject = linkClass(subject, {
+ bar: 'bar-1'
+ });
+
+ expect(subject.props.className).to.deep.equal('foo bar-1');
+ });
+ });
+ });
+
+ context('styleName includes multiple whitespace characters', () => {
+ it('resolves CSS modules', () => {
+ let subject;
+
+ subject = ;
+
+ subject = linkClass(subject, {
+ bar: 'bar-1',
+ foo: 'foo-1'
+ }, {
+ allowMultiple: true
+ });
+
+ expect(subject.props.children.props.className).to.equal('foo-1 bar-1');
+ });
+ });
+
+ 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', () => {
+ it('throws an error', () => {
+ expect(() => {
+ linkClass(, {}, {allowMultiple: false});
+ }).to.throw(Error, 'ReactElement styleName property defines multiple module names ("foo bar").');
+ });
+ });
+ context('when true', () => {
+ it('appends a generated class name for every referenced CSS module', () => {
+ let subject;
+
+ subject = ;
+
+ subject = linkClass(subject, {
+ bar: 'bar-1',
+ foo: 'foo-1'
+ }, {
+ allowMultiple: true
+ });
+
+ expect(subject.props.className).to.deep.equal('foo-1 bar-1');
+ });
+ });
+ });
+ });
+
+ describe('options.handleNotFoundStyleName', () => {
+ context('when styleName does not match an existing CSS module', () => {
+ 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(() => {});
+
+ console.warn = warnSpy;
+ linkClass(, {}, {handleNotFoundStyleName: 'log'});
+ expect(warnSpy).to.have.been.called();
+ });
+ });
+ 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(, {}, {handleNotFoundStyleName: 'ignore'});
+ }).to.not.throw(Error, '"foo" CSS module is undefined.');
+ });
+ });
+ });
+ });
+
+ context('when ReactElement includes ReactComponent', () => {
+ let Foo;
+ let nodeList;
+
+ beforeEach(() => {
+ global.document = jsdom.jsdom('');
+ global.window = document.defaultView;
+
+ Foo = class extends React.Component {
+ render () {
+ return Hello
;
+ }
+ };
+
+ nodeList = TestUtils.renderIntoDocument(linkClass(
, {foo: 'foo-1'}));
+ });
+ it('processes ReactElement nodes', () => {
+ expect(nodeList.className).to.equal('foo-1');
+ });
+ it('does not process ReactComponent nodes', () => {
+ expect(nodeList.firstChild.className).to.equal('');
+ });
+ });
+
+ it('deletes styleName property from the target element', () => {
+ let subject;
+
+ subject = ;
+
+ subject = linkClass(subject, {
+ foo: 'foo-1'
+ });
+
+ expect(subject.props.className).to.deep.equal('foo-1');
+ expect(subject.props).not.to.have.property('styleName');
+ });
+
+ it('deletes styleName property from the target element (deep)', () => {
+ let subject;
+
+ subject = }
+ els={[, []]}
+ styleName='foo'
+ >
+
+
+ ;
+
+ subject = linkClass(subject, {
+ bar: 'bar-1',
+ baz: 'baz-1',
+ foo: 'foo-1'
+ });
+
+ expect(subject.props.children[0].props.className).to.deep.equal('bar-1');
+ expect(subject.props.children[0].props).not.to.have.property('styleName');
+ expect(subject.props.el.props.className).to.deep.equal('baz-1');
+ expect(subject.props.el.props).not.to.have.property('styleName');
+ expect(subject.props.els[0].props.className).to.deep.equal('foo-1');
+ expect(subject.props.els[0].props).not.to.have.property('styleName');
+ expect(subject.props.els[1][0].props.className).to.deep.equal('bar-1');
+ expect(subject.props.els[1][0].props).not.to.have.property('styleName');
+ });
+});
diff --git a/tests/makeConfiguration.js b/tests/makeConfiguration.js
new file mode 100644
index 0000000..4193311
--- /dev/null
+++ b/tests/makeConfiguration.js
@@ -0,0 +1,42 @@
+/* eslint-disable max-nested-callbacks */
+
+import {
+ expect
+} from 'chai';
+import makeConfiguration from './../src/makeConfiguration';
+
+describe('makeConfiguration', () => {
+ describe('when using default configuration', () => {
+ let configuration;
+
+ beforeEach(() => {
+ configuration = makeConfiguration();
+ });
+ describe('allowMultiple property', () => {
+ it('defaults to false', () => {
+ expect(configuration.allowMultiple).to.equal(false);
+ });
+ });
+ describe('handleNotFoundStyleName property', () => {
+ it('defaults to "throw"', () => {
+ expect(configuration.handleNotFoundStyleName).to.equal('throw');
+ });
+ });
+ });
+ describe('when unknown property is provided', () => {
+ it('throws an error', () => {
+ expect(() => {
+ makeConfiguration({
+ unknownProperty: true
+ });
+ }).to.throw(Error, 'Unknown configuration property "unknownProperty".');
+ });
+ });
+ it('does not mutate user configuration', () => {
+ const userConfiguration = {};
+
+ makeConfiguration(userConfiguration);
+
+ expect(userConfiguration).to.deep.equal({});
+ });
+});
diff --git a/tests/reactCssModules.js b/tests/reactCssModules.js
new file mode 100644
index 0000000..d484d95
--- /dev/null
+++ b/tests/reactCssModules.js
@@ -0,0 +1,184 @@
+/* eslint-disable max-nested-callbacks, react/no-multi-comp, react/prop-types, react/prefer-stateless-function, class-methods-use-this */
+
+import {
+ expect
+} from 'chai';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import TestUtils from 'react-addons-test-utils';
+import jsdom from 'jsdom';
+import reactCssModules from './../src/index';
+
+describe('reactCssModules', () => {
+ context('a ReactComponent is decorated using react-css-modules', () => {
+ it('inherits displayName', () => {
+ let Foo;
+
+ Foo = class extends React.Component {};
+
+ // @todo https://phabricator.babeljs.io/T2779
+ Foo.displayName = 'Bar';
+
+ Foo = reactCssModules(Foo);
+
+ expect(Foo.displayName).to.equal('Bar');
+ });
+ context('target component does not name displayName', () => {
+ it('uses name for displayName', () => {
+ let Foo;
+
+ Foo = class Bar extends React.Component {};
+
+ Foo = reactCssModules(Foo);
+
+ expect(Foo.displayName).to.equal('Bar');
+ });
+ });
+ });
+ context('a ReactComponent renders an element with the styleName prop', () => {
+ context('the component is a class that extends React.Component', () => {
+ let Foo;
+ let component;
+
+ beforeEach(() => {
+ const shallowRenderer = TestUtils.createRenderer();
+
+ Foo = class extends React.Component {
+ render () {
+ return
Hello
;
+ }
+ };
+
+ Foo = reactCssModules(Foo, {
+ foo: 'foo-1'
+ });
+
+ shallowRenderer.render(
);
+
+ component = shallowRenderer.getRenderOutput();
+ });
+ it('that element should contain the equivalent className', () => {
+ expect(component.props.className).to.equal('foo-1');
+ });
+ it('the styleName prop should be "consumed" in the process', () => {
+ expect(component.props).not.to.have.property('styleName');
+ });
+ });
+ context('the component is a stateless function component', () => {
+ let Foo;
+ let component;
+
+ beforeEach(() => {
+ const shallowRenderer = TestUtils.createRenderer();
+
+ Foo = () => {
+ return
Hello
;
+ };
+
+ Foo = reactCssModules(Foo, {
+ foo: 'foo-1'
+ });
+
+ shallowRenderer.render(
);
+
+ component = shallowRenderer.getRenderOutput();
+ });
+ it('that element should contain the equivalent className', () => {
+ expect(component.props.className).to.equal('foo-1');
+ });
+ it('the styleName prop should be "consumed" in the process', () => {
+ expect(component.props).not.to.have.property('styleName');
+ });
+ });
+ });
+ context('a ReactComponent renders nothing', () => {
+ context('the component is a class that extends React.Component', () => {
+ it('linkClass must not intervene', () => {
+ let Foo;
+
+ const shallowRenderer = TestUtils.createRenderer();
+
+ Foo = class extends React.Component {
+ render () {
+ return null;
+ }
+ };
+
+ Foo = reactCssModules(Foo, {
+ foo: 'foo-1'
+ });
+
+ shallowRenderer.render(
);
+
+ const component = shallowRenderer.getRenderOutput();
+
+ expect(typeof component).to.equal('object');
+ });
+ });
+ context('the component is a stateless function component', () => {
+ it('that element should contain the equivalent className', () => {
+ let Foo;
+
+ const shallowRenderer = TestUtils.createRenderer();
+
+ Foo = () => {
+ return null;
+ };
+
+ Foo = reactCssModules(Foo, {
+ foo: 'foo-1'
+ });
+
+ shallowRenderer.render(
);
+
+ const component = shallowRenderer.getRenderOutput();
+
+ expect(typeof component).to.equal('object');
+ });
+ });
+ });
+ context('rendering element', () => {
+ beforeEach(() => {
+ global.document = jsdom.jsdom('');
+
+ global.window = document.defaultView;
+ });
+ context('parent component is using react-css-modules and interpolates props.children', () => {
+ // @see https://github.com/gajus/react-css-modules/issues/76
+ it('unsets the styleName property', () => {
+ let Bar;
+ let Foo;
+ let subject;
+
+ Foo = class extends React.Component {
+ render () {
+ return
+ foo
+ ;
+ }
+ };
+
+ Foo = reactCssModules(Foo, {
+ test: 'foo-0'
+ });
+
+ Bar = class extends React.Component {
+ render () {
+ return
{this.props.children}
;
+ }
+ };
+
+ Bar = reactCssModules(Bar, {
+ test: 'bar-0'
+ });
+
+ subject = TestUtils.renderIntoDocument(
);
+
+ // eslint-disable-next-line react/no-find-dom-node
+ subject = ReactDOM.findDOMNode(subject);
+
+ expect(subject.firstChild.className).to.equal('foo-0');
+ });
+ });
+ });
+});
diff --git a/tests/renderNothing.js b/tests/renderNothing.js
new file mode 100644
index 0000000..7a666e8
--- /dev/null
+++ b/tests/renderNothing.js
@@ -0,0 +1,16 @@
+import {
+ expect
+} from 'chai';
+import renderNothing from '../src/renderNothing';
+
+describe('renderNothing', () => {
+ context('renderNothing should return different node types for various React versions', () => {
+ it('should return noscript tag for React v14 or lower', () => {
+ expect(renderNothing('14.0.0').type).to.equal('noscript');
+ });
+
+ it('should return null for React v15 or higher', () => {
+ expect(renderNothing('15.0.0')).to.equal(null);
+ });
+ });
+});
diff --git a/tests/wrapStatelessFunction.js b/tests/wrapStatelessFunction.js
new file mode 100644
index 0000000..9c71935
--- /dev/null
+++ b/tests/wrapStatelessFunction.js
@@ -0,0 +1,92 @@
+/* eslint-disable max-nested-callbacks */
+
+import {
+ expect
+} from 'chai';
+import React from 'react';
+import TestUtils from 'react-addons-test-utils';
+import wrapStatelessFunction from './../src/wrapStatelessFunction';
+
+describe('wrapStatelessFunction', () => {
+ it('hoists static own properties from the input component to the wrapped component', () => {
+ const styles = {
+ foo: 'foo-1'
+ };
+
+ const InnerComponent = () => {
+ return null;
+ };
+
+ InnerComponent.propTypes = {};
+ InnerComponent.defaultProps = {};
+
+ const WrappedComponent = wrapStatelessFunction(InnerComponent, styles);
+
+ expect(WrappedComponent.propTypes).to.equal(InnerComponent.propTypes);
+ expect(WrappedComponent.defaultProps).to.equal(InnerComponent.defaultProps);
+ expect(WrappedComponent.name).not.to.equal(InnerComponent.name);
+ });
+ context('using default styles', () => {
+ it('exposes styles through styles property', (done) => {
+ const styles = {
+ foo: 'foo-1'
+ };
+
+ wrapStatelessFunction((props) => {
+ expect(props.styles).to.equal(styles);
+ done();
+ }, styles)();
+ });
+ it('exposes non-enumerable styles property', (done) => {
+ const styles = {
+ foo: 'foo-1'
+ };
+
+ wrapStatelessFunction((props) => {
+ expect(props.propertyIsEnumerable('styles')).to.equal(false);
+ done();
+ }, styles)();
+ });
+ it('does not affect the other instance properties', (done) => {
+ const styles = {
+ foo: 'foo-1'
+ };
+
+ wrapStatelessFunction((props) => {
+ expect(props.bar).to.equal('baz');
+ done();
+ }, styles)({
+ bar: 'baz'
+ });
+ });
+ });
+ context('using explicit styles', () => {
+ it('exposes styles through styles property', (done) => {
+ const styles = {
+ foo: 'foo-1'
+ };
+
+ wrapStatelessFunction((props) => {
+ expect(props.styles).to.equal(styles);
+ done();
+ })({
+ styles
+ });
+ });
+ });
+ context('rendering Component that returns null', () => {
+ it('generates null', () => {
+ const shallowRenderer = TestUtils.createRenderer();
+
+ const Component = wrapStatelessFunction(() => {
+ return null;
+ });
+
+ shallowRenderer.render(
);
+
+ const component = shallowRenderer.getRenderOutput();
+
+ expect(component).to.equal(null);
+ });
+ });
+});