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 140c7d1..bcaad1b 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -1,390 +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": 2,
- "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",
- "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 07e6e47..df978de 100755
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,12 @@
-/node_modules
+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 9221f03..a6b7399 100644
--- a/README.md
+++ b/README.md
@@ -1,39 +1,66 @@
# React CSS Modules
-[](https://travis-ci.org/gajus/react-css-modules)
-[](https://www.npmjs.org/package/react-css-modules)
-
-React CSS Modules implement automatic mapping of class names to CSS modules. Every CSS class is assigned a local-scoped identifier with a global unique name. CSS Modules enable a modular and reusable CSS!
-
+[](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)
- - [`keepOriginal`](#keeporiginal)
- - [`errorNotFound`](#errornotfound)
+ - [`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)
- [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 modules loader will generate a unique name for a each CSS class at the time of loading the CSS. Refer to [webpack-demo](https://css-modules.github.io/webpack-demo/) for a full example.
+[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, this looks like this:
+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
;
}
}
@@ -42,9 +69,11 @@ export default class Car extends React.Component {
Rendering the component will produce a markup similar to:
```js
-
-
front-door
-
back-door
+
```
@@ -52,33 +81,56 @@ 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`.
+* Mixing CSS Modules and global CSS classes is cumbersome.
+* Reference to an undefined CSS Module resolves to `undefined` without a warning.
-React CSS Modules enables seamless CSS modules for React, e.g.
+React CSS Modules component automates loading of CSS Modules using `styleName` property, 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);
```
-`CSSModules` extends `Car` `render` method. It will look for CSS classes in `./car.css` that match CSS class names in `ReactElement` `className` and will replace/append the matching unique class names to `className` declaration.
+Using `react-css-modules`:
+
+* You are not forced to use the `camelCase` naming convention.
+* You do not need to refer to the `styles` object every time you use a CSS Module.
+* There is clear distinction between global CSS and CSS Modules, e.g.
+
+```js
+
+```
+
+* 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
-Refer to the [react-css-modules-examples](https://github.com/gajus/react-css-modules-examples) repository for a complete usage example.
+`react-css-modules` extends `render` method of the target component. It will use the value of `styleName` to look for CSS Modules in the associated styles object and will append the matching unique CSS class names to the `ReactElement` `className` property value.
[Awesome!](https://twitter.com/intent/retweet?tweet_id=636497036603428864)
@@ -86,53 +138,282 @@ Refer to the [react-css-modules-examples](https://github.com/gajus/react-css-mod
Setup consists of:
-* Setting up a module bundler to load your [ICSS](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 will also need to use [`extract-text-webpack-plugin`](https://www.npmjs.com/package/extract-text-webpack-plugin) to aggregate the CSS into a single file.
-* Setup a `/\.css$/` loader:
+##### 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:
+ * 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
+
+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
-new ExtractTextPlugin('app.css', {
- allChunks: true
-})
+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);
```
-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 a complete setup.
+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.
-#### Browserify
+Using `styles` property you can overwrite the default component `styles` object, e.g.
-Refer to [`css-modulesify`](https://github.com/css-modules/css-modulesify).
+```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
/**
* @typedef CSSModules~Options
- * @property {Boolean} allowMultiple Determines whether `className` can have multiple class names. Throws an error when the constrained is not met. Default: true.
- * @property {Boolean} keepOriginal Determines whether the original `className` value is kept in addition to the appended CSS modules styles CSS class name. Default: true.
- * @property {Boolean} errorNotFound Determines whether an error is raised if `className` defines a CSS class(es) that is not present in the CSS modules styles. Default: false.
+ * @see {@link https://github.com/gajus/react-css-modules#options}
+ * @property {Boolean} allowMultiple
+ * @property {String} handleNotFoundStyleName
*/
/**
- * @param {ReactClass} Component
- * @param {Object} styles CSS modules class map.
+ * @param {Function} Component
+ * @param {Object} defaultStyles CSS Modules class map.
* @param {CSSModules~Options} options
- * @return {ReactClass}
+ * @return {Function}
*/
```
@@ -140,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!
@@ -161,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
;
}
}
@@ -177,6 +462,8 @@ export default class extends React.Component {
[Awesome!](https://twitter.com/intent/retweet?tweet_id=636497036603428864)
+Refer to the [react-css-modules-examples](https://github.com/gajus/react-css-modules-examples) repository for an example of webpack setup.
+
### Options
Options are supplied as the third parameter to the `CSSModules` function.
@@ -185,7 +472,7 @@ Options are supplied as the third parameter to the `CSSModules` function.
CSSModules(Component, styles, options);
```
-or as a second parameter when using `CSSModules` as a decorator:
+or as a second parameter to the decorator:
```js
@CSSModules(styles, options);
@@ -193,80 +480,213 @@ or as a second parameter when using `CSSModules` as a decorator:
#### `allowMultiple`
-Allows multiple CSS class names. Default: `true`.
+Default: `false`.
+
+Allows multiple CSS Module names.
When `false`, the following will cause an error:
```js
-
+
```
-#### `keepOriginal`
+#### `handleNotFoundStyleName`
-Keeps original CSS class name in addition to the names of the CSS Modules. Default: `true`.
+Default: `throw`.
-When `true`, the following `ReactElement`:
+Defines the desired action when `styleName` cannot be mapped to an existing CSS Module.
-```js
-
-```
+Available options:
-will be rendered with a `className` property `foo component__foo___2w27N bar component__bar__1oVw5`.
+* `throw` throws an error
+* `log` logs a warning using `console.warn`
+* `ignore` silently ignores the missing style name
-#### `errorNotFound`
+## SASS, SCSS, LESS and other CSS Preprocessors
-Throws an error when class name cannot be mapped to a CSS Module. Default: `false`.
+[Interoperable CSS](https://github.com/css-modules/icss) is compatible with the CSS preprocessors. To use a preprocessor, all you need to do is add the preprocessor to the chain of loaders, e.g. in the case of webpack it is as simple as installing `sass-loader` and adding `!sass` to the end of the `style-loader` loader query (loaders are processed from right to left):
-## SASS, SCSS, LESS and other CSS Preprocessors
+```js
+{
+ test: /\.scss$/,
+ loaders: [
+ 'style',
+ 'css?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]',
+ 'resolve-url',
+ 'sass'
+ ]
+}
+```
+
+### Enable Sourcemaps
-[ICSS](https://github.com/css-modules/icss) is compatible with the CSS Preprocessors. All you need is to add the preprocessor to the chain of loaders, e.g. in the case of webpack it is as simple as installing `sass-loader` and adding `!sass` to the end of the `style-loader` loader chain declaration (loaders are processed from right to left):
+To enable CSS Source maps, add `sourceMap` parameter to the css-loader and to the `sass-loader`:
```js
{
test: /\.scss$/,
- loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!sass')
+ loaders: [
+ 'style?sourceMap',
+ 'css?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]',
+ 'resolve-url',
+ 'sass?sourceMap'
+ ]
}
```
-## Global CSS
+## Class Composition
-CSS Modules does not restrict you from using global CSS.
+CSS Modules promote composition pattern, i.e. every CSS Module that is used in a component should define all properties required to describe an element, e.g.
```css
-:global .foo {
+.box {
+ width: 100px;
+ height: 100px;
+}
+.empty {
+ composes: box;
+
+ background: #4CAF50;
+}
+
+.full {
+ composes: box;
+
+ background: #F44336;
}
```
-When using global CSS, you need to enable [`keepOriginal`](#keeporiginal) option.
+Composition promotes better separation of markup and style using semantics that would be hard to achieve without CSS Modules.
+
+Because CSS Module names are local, it is perfectly fine to use generic style names such as "empty" or "full", without "box-" prefix.
-Use global CSS with caution. With CSS Modules, there are only a handful of valid use cases for global CSS (e.g. [normalization](https://github.com/necolas/normalize.css/)).
+To learn more about composing CSS rules, I suggest reading Glen Maddern article about [CSS Modules](http://glenmaddern.com/articles/css-modules) and the official [spec of the CSS Modules](https://github.com/css-modules/css-modules).
-## Multiple CSS Classes
+### What Problems does Class Composition Solve?
-CSS modules promote composition pattern, i.e. every CSS class that is used in a component should define all properties required to describe the element, e.g.
+Consider the same example in CSS and HTML:
```css
-.button {
+.box {
+ width: 100px;
+ height: 100px;
+}
+.box-empty {
+ background: #4CAF50;
}
-.active {
- composes: common;
+.box-full {
+ background: #F44336;
+}
+```
+
+```html
+
+```
- /* anything that only applies to active state of the button */
+This pattern emerged with the advent of OOCSS. The biggest disadvantage of this implementation is that you will need to change HTML almost every time you want to change the style.
+
+### 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 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.
+
+Using `@extend`:
+
+```css
+%box {
+ width: 100px;
+ height: 100px;
}
-.disabled {
- composes: common;
+.box-empty {
+ @extend %box;
- /* anything that only applies to disabled state of the button */
+ background: #4CAF50;
+}
+
+.box-full {
+ @extend %box;
+
+ background: #F44336;
}
```
-To learn more about composing CSS rules, I suggest reading Glen Maddern article about [CSS Modules](http://glenmaddern.com/articles/css-modules) and the official [CSS modules spec](https://github.com/css-modules/css-modules).
+This translates to:
+
+```css
+.box-empty,
+.box-full {
+ width: 100px;
+ height: 100px;
+}
-Using React CSS Modules, you can map as many CSS classes to the element as you want. `CSSModules` will append a unique class name for every class name it matches in the `className` declaration, e.g.
+.box-empty {
+ background: #4CAF50;
+}
+
+.box-full {
+ background: #F44336;
+}
+```
+
+Using mixins:
+
+```css
+@mixin box {
+ width: 100px;
+ height: 100px;
+}
+
+.box-empty {
+ @include box;
+
+ background: #4CAF50;
+}
+
+.box-full {
+ @include box;
+
+ background: #F44336;
+}
+```
+
+This translates to:
+
+```css
+.box-empty {
+ width: 100px;
+ height: 100px;
+ background: #4CAF50;
+}
+
+.box-full {
+ width: 100px;
+ height: 100px;
+ background: #F44336;
+}
+```
+
+## Global CSS
+
+CSS Modules does not restrict you from using global CSS.
+
+```css
+:global .foo {
+
+}
+```
+
+However, use global CSS with caution. With CSS Modules, there are only a handful of valid use cases for global CSS (e.g. [normalization](https://github.com/necolas/normalize.css/)).
+
+## Multiple CSS Modules
+
+Avoid using multiple CSS Modules to describe a single element. Read about [Class Composition](#class-composition).
+
+That said, if you require to use multiple CSS Modules to describe an element, enable the [`allowMultiple`](#allowmultiple) option. When multiple CSS Modules are used to describe an element, `react-css-modules` will append a unique class name for every CSS Module it matches in the `styleName` declaration, e.g.
```css
.button {
@@ -279,9 +699,7 @@ Using React CSS Modules, you can map as many CSS classes to the element as you w
```
```js
-
+
```
-This will map both [ICSS](https://github.com/css-modules/icss) CSS classes to the target element.
-
-However, I encourage you to use composition whenever possible. Composition promotes better separation of markup from style sheets using semantics that would be hard to achieve without CSS modules. You can enforce one CSS class name per `className` using [`allowMultiple` option](#usage).
+This will map both [Interoperable CSS](https://github.com/css-modules/icss) CSS classes to the target element.
diff --git a/dist/index copy.js b/dist/index copy.js
deleted file mode 100644
index 2a27213..0000000
--- a/dist/index copy.js
+++ /dev/null
@@ -1,127 +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(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; 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 { _x = parent; _x2 = property; _x3 = 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 _react = require('react');
-
-var _react2 = _interopRequireDefault(_react);
-
-var _reactDom = require('react-dom');
-
-var _reactDom2 = _interopRequireDefault(_reactDom);
-
-var _lodashLangIsArray = require('lodash/lang/isArray');
-
-var _lodashLangIsArray2 = _interopRequireDefault(_lodashLangIsArray);
-
-var _lodash = require('lodash');
-
-var _lodash2 = _interopRequireDefault(_lodash);
-
-var unfreeze = undefined;
-
-/**
- * Make a shallow copy of the object.
- *
- * @param {Object} source Frozen object.
- * @return {Object}
- */
-unfreeze = function (source) {
- var property = undefined,
- target = undefined;
-
- target = {};
-
- for (property in source) {
- target[property] = source[property];
- }
-
- return target;
-};
-
-/**
- * @param {ReactClass} Target
- * @return {ReactClass}
- */
-
-exports['default'] = function (Target, styles) {
- var linkClass = undefined;
-
- console.log('styles', styles);
-
- /**
- * @param {ReactElement} element
- * @return {ReactElement}
- */
- linkClass = function (element) {
- var isFrozen = undefined;
-
- if (Object.isFrozen && Object.isFrozen(element)) {
- isFrozen = true;
-
- // https://github.com/facebook/react/blob/v0.13.3/src/classic/element/ReactElement.js#L131
- element = unfreeze(element);
- element.props = unfreeze(element.props);
- }
-
- if (element.props.className) {
- element.props.className = element.props.className.split(' ').map(function (className) {
- if (styles[className]) {
- return className + ' ' + styles[className];
- } else {
- return className;
- }
- }).join(' ');
- }
-
- if ((0, _lodashLangIsArray2['default'])(element.props.children)) {
- element.props.children = element.props.children.map(function (node) {
- if (_react2['default'].isValidElement(node)) {
- return linkClass(node);
- } else {
- return node;
- }
- });
- }
-
- if (isFrozen) {
- Object.freeze(element);
- Object.freeze(element.props);
- }
-
- return element;
- };
-
- return (function (_Target) {
- _inherits(_class, _Target);
-
- function _class() {
- _classCallCheck(this, _class);
-
- _get(Object.getPrototypeOf(_class.prototype), 'constructor', this).apply(this, arguments);
- }
-
- _createClass(_class, [{
- key: 'render',
- value: function render() {
- return linkClass(_get(Object.getPrototypeOf(_class.prototype), 'render', this).call(this));
- }
- }]);
-
- return _class;
- })(Target);
-};
-
-module.exports = exports['default'];
\ No newline at end of file
diff --git a/dist/index.js b/dist/index.js
deleted file mode 100644
index 7477f0a..0000000
--- a/dist/index.js
+++ /dev/null
@@ -1,88 +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 functionConstructor = undefined,
- decoratorConstructor = 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() {
- if (options.allowMultiple !== false) {
- options.allowMultiple = true;
- }
-
- if (options.includeOriginal !== false) {
- options.includeOriginal = true;
- }
-
- if (options.errorNotFound !== true) {
- options.errorNotFound = false;
- }
-
- return (0, _linkClass2['default'])(_get(Object.getPrototypeOf(_class.prototype), 'render', this).call(this), styles, 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 3b8fc21..0000000
--- a/dist/linkClass.js
+++ /dev/null
@@ -1,105 +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;
-
-/**
- * @typedef CSSModules~Options
- * @property {Boolean} allowMultiple Determines whether `className` can have multiple class names. Throws an error when the constrained is not met. Default: true.
- * @property {Boolean} keepOriginal Determines whether the original `className` value is kept in addition to the appended CSS modules styles CSS class name. Default: true.
- * @property {Boolean} errorNotFound Determines whether an error is raised if `className` defines a CSS class(es) that is not present in the CSS modules styles. Default: false.
- */
-
-/**
- * @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 newProps = undefined,
- newClassName = undefined,
- newChildren = undefined,
- childrenCount = undefined;
-
- if (element.props.className) {
- newClassName = element.props.className.split(' ');
-
- if (options.allowMultiple === false && newClassName.length > 1) {
- throw new Error('ReactElement defines multiple class names ("' + element.props.className + '") in className declaration.');
- }
-
- newClassName = newClassName.map(function (className) {
- if (!styles[className] && options.errorNotFound === true) {
- throw new Error('"' + className + '" CSS class name is not found in CSS modules styles.');
- }
-
- if (options.includeOriginal === false) {
- if (styles[className]) {
- return styles[className];
- } else {
- return '';
- }
- } else {
- if (styles[className]) {
- return className + ' ' + styles[className];
- } else {
- return className;
- }
- }
- });
-
- newClassName = newClassName.filter(function (className) {
- return className.length;
- });
-
- newClassName = newClassName.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 (newClassName) {
- newProps = {
- className: newClassName
- };
- }
-
- if (newChildren) {
- element = _react2['default'].cloneElement(element, newProps, newChildren);
- } else {
- element = _react2['default'].cloneElement(element, newProps);
- }
-
- return element;
-};
-
-exports['default'] = linkClass;
-module.exports = exports['default'];
\ No newline at end of file
diff --git a/package.json b/package.json
index 2f5fc53..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": "1.1.0",
+ "version": "4.3.0",
"author": {
"name": "Gajus Kuizinas",
- "email": "gk@anuary.com",
+ "email": "gajus@gajus.com",
"url": "http://gajus.com"
},
"license": "BSD-3-Clause",
- "peerDependencies": {
- "react": "^0.14.0-beta3"
- },
"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",
- "chai": "^3.2.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": "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 0c71a14..b913b29 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,53 +1,56 @@
-import linkClass from './linkClass';
+import _ from 'lodash';
+import extendReactClass from './extendReactClass';
+import wrapStatelessFunction from './wrapStatelessFunction';
+import makeConfiguration from './makeConfiguration';
-let functionConstructor,
- decoratorConstructor;
+/**
+ * @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 () {
- if (options.allowMultiple !== false) {
- options.allowMultiple = true;
- }
-
- if (options.includeOriginal !== false) {
- options.includeOriginal = true;
- }
-
- if (options.errorNotFound !== true) {
- options.errorNotFound = false;
- }
-
- return linkClass(super.render(), styles, 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 c2f6993..4ed3975 100644
--- a/src/linkClass.js
+++ b/src/linkClass.js
@@ -1,92 +1,106 @@
-import React from 'react';
-
-let linkClass;
+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);
+ }
+ });
-/**
- * @typedef CSSModules~Options
- * @property {Boolean} allowMultiple Determines whether `className` can have multiple class names. Throws an error when the constrained is not met. Default: true.
- * @property {Boolean} keepOriginal Determines whether the original `className` value is kept in addition to the appended CSS modules styles CSS class name. Default: true.
- * @property {Boolean} errorNotFound Determines whether an error is raised if `className` defines a CSS class(es) that is not present in the CSS modules styles. Default: false.
- */
+ return array;
+};
-/**
- * @param {ReactElement} element
- * @param {Object} styles CSS modules class map.
- * @param {CSSModules~Options} options
- * @return {ReactElement}
- */
-linkClass = (element, styles = {}, options = {}) => {
- let newProps,
- newClassName,
- newChildren,
- childrenCount;
-
- if (element.props.className) {
- newClassName = element.props.className.split(' ');
-
- if (options.allowMultiple === false && newClassName.length > 1) {
- throw new Error(`ReactElement defines multiple class names ("${element.props.className}") in className declaration.`);
- }
-
- newClassName = newClassName.map((className) => {
- if (!styles[className] && options.errorNotFound === true) {
- throw new Error(`"${className}" CSS class name is not found in CSS modules styles.`);
- }
-
- if (options.includeOriginal === false) {
- if (styles[className]) {
- return styles[className];
- } else {
- return '';
- }
- } else {
- if (styles[className]) {
- return `${className} ${styles[className]}`;
- } else {
- return className;
- }
- }
- });
-
- newClassName = newClassName.filter(function (className) {
- return className.length;
- });
-
- newClassName = newClassName.join(' ');
+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);
}
+ });
- // 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);
- }
- }
+ if (styleNames.length) {
+ appendClassName = generateAppendClassName(styles, styleNames, configuration.handleNotFoundStyleName);
+ if (appendClassName) {
+ if (elementShallowCopy.props.className) {
+ appendClassName = elementShallowCopy.props.className + ' ' + appendClassName;
+ }
- if (newClassName) {
- newProps = {
- className: newClassName
- };
+ elementShallowCopy.props.className = appendClassName;
}
+ }
- if (newChildren) {
- element = React.cloneElement(element, newProps, newChildren);
- } else {
- element = React.cloneElement(element, newProps);
- }
+ delete elementShallowCopy.props.styleName;
- return element;
+ 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/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/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 9c58286..0000000
--- a/test/linkClass.js
+++ /dev/null
@@ -1,159 +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', () => {
- describe('when elements do not define className', () => {
- it('does not affect the element declaration', () => {
- expect(linkClass()).to.deep.equal();
- });
-
- it('does not affect element with a single element child', () => {
- expect(linkClass()).to.deep.equal();
- });
-
- it('does not affect element with a single text child', () => {
- expect(linkClass(test
)).to.deep.equal(test
);
- });
-
- // 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();
- });
- });
-
- describe('when element className does not match an existing CSS class', () => {
- it('does not affect element className', () => {
- let subject;
-
- subject = ;
-
- subject = linkClass(subject, {});
-
- expect(subject.props.className).to.deep.equal('foo');
- });
- });
-
- describe('when element className matches an existing CSS class', () => {
- it('appends the generated class name to the className property', () => {
- let subject;
-
- subject = ;
-
- subject = linkClass(subject, {
- foo: 'foo-1'
- });
-
- expect(subject.props.className).to.deep.equal('foo foo-1');
- });
- });
-
- describe('when element classNames refers to multiple CSS classes', () => {
- describe('when all referenced CSS classes exist', () => {
- it('appends a generated class name for every referenced CSS class', () => {
- let subject;
-
- subject = ;
-
- subject = linkClass(subject, {
- foo: 'foo-1',
- bar: 'bar-1'
- });
-
- expect(subject.props.className).to.deep.equal('foo foo-1 bar bar-1');
- });
- });
- describe('when some referenced CSS classes exist', () => {
- it('appends a generated class name for the matched CSS classes', () => {
- let subject;
-
- subject = ;
-
- subject = linkClass(subject, {
- foo: 'foo-1'
- });
-
- expect(subject.props.className).to.deep.equal('foo foo-1 bar');
- });
- });
- describe('when none of the referenced CSS classes exist', () => {
- it('does not append anything', () => {
- let subject;
-
- subject = ;
-
- subject = linkClass(subject, {});
-
- expect(subject.props.className).to.deep.equal('foo bar');
- });
- });
- });
-
- describe('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 foo-1');
- });
- it('does not process ReactComponent nodes', () => {
- expect(nodeList.firstChild.className).to.equal('foo');
- });
- });
-
- describe('when options.allowMultiple is false', () => {
- describe('when it finds multiple CSS class names in a className', () => {
- it('throws an error', () => {
- expect(() => {
- linkClass(, {}, {allowMultiple: false});
- }).to.throw(Error, 'ReactElement defines multiple class names ("foo bar") in className declaration.');
- });
- });
- });
-
- describe('when options.includeOriginal is false', () => {
- it('does not include the original class name', () => {
- let subject;
-
- subject = linkClass(, {foo: 'foo-1'}, {includeOriginal: false});
-
- expect(subject.props.className).to.equal('foo-1');
- });
- });
-
- describe('when options.errorNotFound is true', () => {
- it('throws an error when className defines a CSS class that does not exist in CSS modules styles', () => {
- expect(() => {
- linkClass(, {}, {errorNotFound: true})
- }).to.throw(Error, '"foo" CSS class name is not found in CSS modules styles.');
- });
- });
-});
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);
+ });
+ });
+});