diff --git a/README/react-css-modules.png b/.README/react-css-modules.png similarity index 100% rename from README/react-css-modules.png rename to .README/react-css-modules.png diff --git a/README/react-css-modules.sketch b/.README/react-css-modules.sketch similarity index 100% rename from README/react-css-modules.sketch rename to .README/react-css-modules.sketch 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/.coveralls.yml b/.coveralls.yml deleted file mode 100644 index 65bf7c7..0000000 --- a/.coveralls.yml +++ /dev/null @@ -1 +0,0 @@ -repo_token: bqiXmaOA0xarSUociqxGKJ72bp8SSHlQC diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0f17867 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..bcaad1b --- /dev/null +++ b/.eslintrc @@ -0,0 +1,6 @@ +{ + "extends": [ + "canonical", + "canonical/mocha" + ] +} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..2f093a7 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: gajus +patreon: gajus diff --git a/.gitignore b/.gitignore index fa88f70..df978de 100755 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,12 @@ node_modules coverage -.coveralls.yml +dist *.log +.* +!.babelrc +!.editorconfig +!.eslintrc +!.gitignore +!.npmignore +!.README +!.travis.yml diff --git a/.npmignore b/.npmignore index 4f341b3..e8add85 100755 --- a/.npmignore +++ b/.npmignore @@ -1,7 +1,5 @@ src tests coverage -.coveralls.yml -.travis.yml -.eslintrc +.* *.log diff --git a/.travis.yml b/.travis.yml index e0fced4..a786167 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,14 @@ language: node_js node_js: - - '4.1' - - '4.0' + - node + - 8 +before_install: + - npm config set depth 0 notifications: - email: false + email: false sudo: false +script: + - npm run test + - npm run lint +after_success: + - semantic-release pre && npm publish && semantic-release post diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..109eb2e --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,5 @@ + diff --git a/README.md b/README.md index 362e693..a6b7399 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,24 @@ # React CSS Modules -[![Travis build status](http://img.shields.io/travis/gajus/react-css-modules/master.svg?style=flat)](https://travis-ci.org/gajus/react-css-modules) -[![NPM version](http://img.shields.io/npm/v/react-css-modules.svg?style=flat)](https://www.npmjs.org/package/react-css-modules) -[![Coverage Status](https://coveralls.io/repos/gajus/react-css-modules/badge.svg?branch=master&service=github)](https://coveralls.io/github/gajus/react-css-modules?branch=master) +[![GitSpo Mentions](https://gitspo.com/badges/mentions/gajus/react-css-modules?style=flat-square)](https://gitspo.com/mentions/gajus/react-css-modules) +[![Travis build status](http://img.shields.io/travis/gajus/react-css-modules/master.svg?style=flat-square)](https://travis-ci.org/gajus/react-css-modules) +[![NPM version](http://img.shields.io/npm/v/react-css-modules.svg?style=flat-square)](https://www.npmjs.org/package/react-css-modules) +[![js-canonical-style](https://img.shields.io/badge/code%20style-canonical-blue.svg?style=flat-square)](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) @@ -15,23 +26,27 @@ React CSS Modules implement automatic mapping of CSS modules. Every CSS class is - [Usage](#usage) - [Module Bundler](#module-bundler) - [webpack](#webpack) + - [Development](#development) + - [Production](#production) - [Browserify](#browserify) - [Extending Component Styles](#extending-component-styles) - - [Styles Property](#styles-property) + - [`styles` Property](#styles-property) + - [Loops and Child Components](#loops-and-child-components) - [Decorator](#decorator) - [Options](#options) - [`allowMultiple`](#allowmultiple) - - [`errorWhenNotFound`](#errorwhennotfound) + - [`handleNotFoundStyleName`](#handlenotfoundstylename) +- [SASS, SCSS, LESS and other CSS Preprocessors](#sass-scss-less-and-other-css-preprocessors) + - [Enable Sourcemaps](#enable-sourcemaps) - [Class Composition](#class-composition) - [What Problems does Class Composition Solve?](#what-problems-does-class-composition-solve) - [Class Composition Using CSS Preprocessors](#class-composition-using-css-preprocessors) -- [SASS, SCSS, LESS and other CSS Preprocessors](#sass-scss-less-and-other-css-preprocessors) - [Global CSS](#global-css) - [Multiple CSS Modules](#multiple-css-modules) ## CSS Modules -[CSS Modules](https://github.com/css-modules/css-modules) are awesome. If you are not familiar with CSS Modules, it is a concept of using a module bundler such as [webpack](http://webpack.github.io/docs/) to load CSS scoped to a particular document. CSS module loader will generate a unique name for a each CSS class at the time of loading the CSS document ([Interoperable CSS](https://github.com/css-modules/icss) to be precise). To see CSS Modules in practice, [webpack-demo](https://css-modules.github.io/webpack-demo/). +[CSS Modules](https://github.com/css-modules/css-modules) are awesome. If you are not familiar with CSS Modules, it is a concept of using a module bundler such as [webpack](http://webpack.github.io/docs/) to load CSS scoped to a particular document. CSS module loader will generate a unique name for each CSS class at the time of loading the CSS document ([Interoperable CSS](https://github.com/css-modules/icss) to be precise). To see CSS Modules in practice, [webpack-demo](https://css-modules.github.io/webpack-demo/). In the context of React, CSS Modules look like this: @@ -56,7 +71,7 @@ Rendering the component will produce a markup similar to: ```js
-
A0
+
A0
B0
@@ -68,7 +83,7 @@ Awesome! ### webpack `css-loader` -[CSS Modules](https://github.com/css-modules/css-modules) is a specification that can be implemented in multiple ways. `react-css-modules` leverages as existing CSS Modules implementation webpack [css-loader](https://github.com/webpack/css-loader#css-modules). +[CSS Modules](https://github.com/css-modules/css-modules) is a specification that can be implemented in multiple ways. `react-css-modules` leverages the existing CSS Modules implementation webpack [css-loader](https://github.com/webpack/css-loader#css-modules). ## What's the Problem? @@ -110,7 +125,7 @@ Using `react-css-modules`:
``` -* You are warned when `styleName` refers to an undefined CSS Module ([`errorWhenNotFound`](#errorwhennotfound) option). +* You are warned when `styleName` refers to an undefined CSS Module ([`handleNotFoundStyleName`](#handlenotfoundstylename) option). * You can enforce use of a single CSS module per `ReactElement` ([`allowMultiple`](#allowmultiple) option). ## The Implementation @@ -123,35 +138,106 @@ Using `react-css-modules`: Setup consists of: -* Setting up a [module bundler](#modulebundler) to load the [Interoperable CSS](https://github.com/css-modules/icss). +* Setting up a [module bundler](#module-bundler) to load the [Interoperable CSS](https://github.com/css-modules/icss). * [Decorating](#decorator) your component using `react-css-modules`. ### Module Bundler #### webpack -* Install [`style-loader`](https://www.npmjs.com/package/style-loader) and [`css-loader`](https://www.npmjs.com/package/css-loader). -* You need to use [`extract-text-webpack-plugin`](https://www.npmjs.com/package/extract-text-webpack-plugin) to aggregate the CSS into a single file. +##### Development + +In development environment, you want to [Enable Sourcemaps](#enable-sourcemaps) and webpack [Hot Module Replacement](https://webpack.github.io/docs/hot-module-replacement.html) (HMR). [`style-loader`](https://github.com/webpack/style-loader) already supports HMR. Therefore, Hot Module Replacement will work out of the box. + +Setup: + +* Install [`style-loader`](https://www.npmjs.com/package/style-loader). +* Install [`css-loader`](https://www.npmjs.com/package/css-loader). * Setup `/\.css$/` loader: ```js { test: /\.css$/, - loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]') + loaders: [ + 'style-loader?sourceMap', + 'css-loader?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]' + ] } ``` +##### Production + +In production environment, you want to extract chunks of CSS into a single stylesheet file. + +> Advantages: +> +> * Fewer style tags (older IE has a limit) +> * CSS SourceMap (with `devtool: "source-map"` and `css-loader?sourceMap`) +> * CSS requested in parallel +> * CSS cached separate +> * Faster runtime (less code and DOM operations) +> +> Caveats: +> +> * Additional HTTP request +> * Longer compilation time +> * More complex configuration +> * No runtime public path modification +> * No Hot Module Replacement + +– [extract-text-webpack-plugin](https://github.com/webpack/extract-text-webpack-plugin) + +Setup: + +* Install [`style-loader`](https://www.npmjs.com/package/style-loader). +* Install [`css-loader`](https://www.npmjs.com/package/css-loader). +* Use [`extract-text-webpack-plugin`](https://www.npmjs.com/package/extract-text-webpack-plugin) to extract chunks of CSS into a single stylesheet. + +* Setup `/\.css$/` loader: + + * ExtractTextPlugin v1x: + + ```js + { + test: /\.css$/, + loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]') + } + ``` + + * ExtractTextPlugin v2x: + + ```js + { + test: /\.css$/, + use: ExtractTextPlugin.extract({ + fallback: 'style-loader', + use: 'css-loader?modules,localIdentName="[name]-[local]-[hash:base64:6]"' + }), + } + ``` + * Setup `extract-text-webpack-plugin` plugin: -```js -new ExtractTextPlugin('app.css', { - allChunks: true -}) -``` + * ExtractTextPlugin v1x: + + ```js + new ExtractTextPlugin('app.css', { + allChunks: true + }) + ``` + + * ExtractTextPlugin v2x: + + ```js + new ExtractTextPlugin({ + filename: 'app.css', + allChunks: true + }) + ``` Refer to [webpack-demo](https://github.com/css-modules/webpack-demo) or [react-css-modules-examples](https://github.com/gajus/react-css-modules-examples) for an example of a complete setup. -#### Browserify +##### Browserify Refer to [`css-modulesify`](https://github.com/css-modules/css-modulesify). @@ -219,45 +305,99 @@ In this example, `table-custom-styles.css` selectively extends `table.css` (the 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 +### `styles` Property Decorated components inherit `styles` property that describes the mapping between CSS modules and CSS classes. -`styles` property within the component itself is designed to be used only when `styleNames` cannot be used. +```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). -`styleNames` cannot be used within a component to define styles of a `ReactElement` that will be generated by another component (https://github.com/gajus/react-css-modules/issues/11), e.g. +### Loops and Child Components + +`styleName` cannot be used to define styles of a `ReactElement` that will be generated by another component, e.g. ```js -class extends React.Component { +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
  • {name}
  • ; }; - return ; + return ; } } + +export default CSSModules(CustomList, styles); ``` -For that purpose, the decorated component inherits `styles` property that you can use just as a regular CSS Modules object. The earlier example can be therefore rewritten to: +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 -class extends React.Component { +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
  • {name}
  • ; }; - return ; + return ; } } + +export default CSSModules(CustomList, styles); ``` -`styles` property works with ES6 classes and stateless function components. +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 @@ -266,7 +406,7 @@ class extends React.Component { * @typedef CSSModules~Options * @see {@link https://github.com/gajus/react-css-modules#options} * @property {Boolean} allowMultiple - * @property {Boolean} errorWhenNotFound + * @property {String} handleNotFoundStyleName */ /** @@ -350,11 +490,17 @@ When `false`, the following will cause an error:
    ``` -#### `errorWhenNotFound` +#### `handleNotFoundStyleName` + +Default: `throw`. -Default: `true`. +Defines the desired action when `styleName` cannot be mapped to an existing CSS Module. -Throws an error when `styleName` cannot be mapped to an existing CSS Module. +Available options: + +* `throw` throws an error +* `log` logs a warning using `console.warn` +* `ignore` silently ignores the missing style name ## SASS, SCSS, LESS and other CSS Preprocessors @@ -363,7 +509,28 @@ Throws an error when `styleName` cannot be mapped to an existing CSS Module. ```js { test: /\.scss$/, - loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!sass') + loaders: [ + 'style', + 'css?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]', + 'resolve-url', + 'sass' + ] +} +``` + +### Enable Sourcemaps + +To enable CSS Source maps, add `sourceMap` parameter to the css-loader and to the `sass-loader`: + +```js +{ + test: /\.scss$/, + loaders: [ + 'style?sourceMap', + 'css?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]', + 'resolve-url', + 'sass?sourceMap' + ] } ``` @@ -423,7 +590,7 @@ This pattern emerged with the advent of OOCSS. The biggest disadvantage of this ### Class Composition Using CSS Preprocessors -This section of the document is included as a learning exercise to broaden the understanding about the origin of Class Composition. CSS Modules support a native method of composting CSS Modules using [`composes`](https://github.com/css-modules/css-modules#composition) keyword. CSS Preprocessor is not required. +This section of the document is included as a learning exercise to broaden the understanding about the origin of Class Composition. CSS Modules support a native method of composing CSS Modules using [`composes`](https://github.com/css-modules/css-modules#composition) keyword. CSS Preprocessor is not required. You can write compositions in SCSS using [`@extend`](http://sass-lang.com/documentation/file.SASS_REFERENCE.html#extend) keyword and using [Mixin Directives](http://sass-lang.com/documentation/file.SASS_REFERENCE.html#mixins), e.g. diff --git a/dist/extendReactClass.js b/dist/extendReactClass.js deleted file mode 100644 index bec936c..0000000 --- a/dist/extendReactClass.js +++ /dev/null @@ -1,85 +0,0 @@ -'use strict'; - -var _lodashLangIsObject2 = require('lodash/lang/isObject'); - -var _lodashLangIsObject3 = _interopRequireDefault(_lodashLangIsObject2); - -var _lodashObjectAssign2 = require('lodash/object/assign'); - -var _lodashObjectAssign3 = _interopRequireDefault(_lodashObjectAssign2); - -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; _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; desc = parent = undefined; 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 _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 _react = require('react'); - -var _react2 = _interopRequireDefault(_react); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - -var extendReactClass = undefined; - -/** - * @param {ReactClass} Component - * @param {Object} defaultStyles - * @param {Object} options - * @returns {ReactClass} - */ -extendReactClass = function (Component, defaultStyles, options) { - 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() { - var renderResult = undefined, - styles = undefined; - - if (this.props.styles) { - styles = this.props.styles; - } else if ((0, _lodashLangIsObject3['default'])(defaultStyles)) { - this.props = (0, _lodashObjectAssign3['default'])({}, this.props, { - styles: defaultStyles - }); - - styles = defaultStyles; - } else { - styles = {}; - } - - renderResult = _get(Object.getPrototypeOf(_class.prototype), 'render', this).call(this); - - if (renderResult) { - return (0, _linkClass2['default'])(renderResult, styles, options); - } - - return _react2['default'].createElement('noscript'); - } - }]); - - return _class; - })(Component); -}; - -exports['default'] = extendReactClass; -module.exports = exports['default']; -//# sourceMappingURL=extendReactClass.js.map diff --git a/dist/extendReactClass.js.map b/dist/extendReactClass.js.map deleted file mode 100644 index 63c37a9..0000000 --- a/dist/extendReactClass.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["extendReactClass.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;yBAAsB,aAAa;;;;qBACjB,OAAO;;;;;;AAGzB,IAAI,gBAAgB,YAAA,CAAC;;;;;;;;AAQrB,gBAAgB,GAAG,UAAC,SAAS,EAAE,aAAa,EAAE,OAAO,EAAK;AACtD;;;;;;;;;;;mBACW,kBAAG;AACN,oBAAI,YAAY,YAAA;oBACZ,MAAM,YAAA,CAAC;;AAEX,oBAAI,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE;AACnB,0BAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;iBAC9B,MAAM,IAAI,qCAAW,aAAa,CAAC,EAAE;AAClC,wBAAI,CAAC,KAAK,GAAG,qCAAS,EAAE,EAAE,IAAI,CAAC,KAAK,EAAE;AAClC,8BAAM,EAAE,aAAa;qBACxB,CAAC,CAAC;;AAEH,0BAAM,GAAG,aAAa,CAAC;iBAC1B,MAAM;AACH,0BAAM,GAAG,EAAE,CAAC;iBACf;;AAED,4BAAY,2EAAiB,CAAC;;AAE9B,oBAAI,YAAY,EAAE;AACd,2BAAO,4BAAU,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;iBACnD;;AAED,uBAAO,mBAAM,aAAa,CAAC,UAAU,CAAC,CAAC;aAC1C;;;;OAxBgB,SAAS,EAyB5B;CACL,CAAC;;qBAEa,gBAAgB","file":"extendReactClass.js","sourcesContent":["import linkClass from './linkClass';\nimport React from 'react';\nimport _ from 'lodash';\n\nlet extendReactClass;\n\n/**\n * @param {ReactClass} Component\n * @param {Object} defaultStyles\n * @param {Object} options\n * @returns {ReactClass}\n */\nextendReactClass = (Component, defaultStyles, options) => {\n return class extends Component {\n render () {\n let renderResult,\n styles;\n\n if (this.props.styles) {\n styles = this.props.styles;\n } else if (_.isObject(defaultStyles)) {\n this.props = _.assign({}, this.props, {\n styles: defaultStyles\n });\n\n styles = defaultStyles;\n } else {\n styles = {};\n }\n\n renderResult = super.render();\n\n if (renderResult) {\n return linkClass(renderResult, styles, options);\n }\n\n return React.createElement('noscript');\n }\n };\n};\n\nexport default extendReactClass;\n"],"sourceRoot":"/source/"} \ No newline at end of file diff --git a/dist/index.js b/dist/index.js deleted file mode 100644 index ef6b8b4..0000000 --- a/dist/index.js +++ /dev/null @@ -1,79 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, '__esModule', { - value: true -}); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - -var _extendReactClass = require('./extendReactClass'); - -var _extendReactClass2 = _interopRequireDefault(_extendReactClass); - -var _wrapStatelessFunction = require('./wrapStatelessFunction'); - -var _wrapStatelessFunction2 = _interopRequireDefault(_wrapStatelessFunction); - -var decoratorConstructor = undefined, - functionConstructor = undefined, - isReactComponent = undefined; - -/** - * Determines if the given object has the signature of a class that inherits React.Component. - * - * @param {*} Component - * @return {boolean} - */ -isReactComponent = function (Component) { - return 'prototype' in Component && typeof Component.prototype.render === 'function'; -}; - -/** - * When used as a function. - * - * @param {Function} Component - * @param {Object} defaultStyles CSS Modules class map. - * @param {Object} options {@link https://github.com/gajus/react-css-modules#options} - * @return {Function} - */ -functionConstructor = function (Component, defaultStyles, options) { - var decoratedClass = undefined; - - if (isReactComponent(Component)) { - decoratedClass = (0, _extendReactClass2['default'])(Component, defaultStyles, options); - } else { - decoratedClass = (0, _wrapStatelessFunction2['default'])(Component, defaultStyles, options); - } - - if (Component.displayName) { - decoratedClass.displayName = Component.displayName; - } else { - decoratedClass.displayName = Component.name; - } - - return decoratedClass; -}; - -/** - * When used as a ES7 decorator. - * - * @param {Object} defaultStyles CSS Modules class map. - * @param {Object} options {@link https://github.com/gajus/react-css-modules#options} - * @return {Function} - */ -decoratorConstructor = function (defaultStyles, options) { - return function (Component) { - return functionConstructor(Component, defaultStyles, 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']; -//# sourceMappingURL=index.js.map diff --git a/dist/index.js.map b/dist/index.js.map deleted file mode 100644 index 161c75a..0000000 --- a/dist/index.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["index.js"],"names":[],"mappings":";;;;;;;;gCAA6B,oBAAoB;;;;qCACf,yBAAyB;;;;AAE3D,IAAI,oBAAoB,YAAA;IACpB,mBAAmB,YAAA;IACnB,gBAAgB,YAAA,CAAC;;;;;;;;AAQrB,gBAAgB,GAAG,UAAC,SAAS,EAAK;AAC9B,WAAO,WAAW,IAAI,SAAS,IAAI,OAAO,SAAS,CAAC,SAAS,CAAC,MAAM,KAAK,UAAU,CAAC;CACvF,CAAC;;;;;;;;;;AAUF,mBAAmB,GAAG,UAAC,SAAS,EAAE,aAAa,EAAE,OAAO,EAAK;AACzD,QAAI,cAAc,YAAA,CAAC;;AAEnB,QAAI,gBAAgB,CAAC,SAAS,CAAC,EAAE;AAC7B,sBAAc,GAAG,mCAAiB,SAAS,EAAE,aAAa,EAAE,OAAO,CAAC,CAAC;KACxE,MAAM;AACH,sBAAc,GAAG,wCAAsB,SAAS,EAAE,aAAa,EAAE,OAAO,CAAC,CAAC;KAC7E;;AAED,QAAI,SAAS,CAAC,WAAW,EAAE;AACvB,sBAAc,CAAC,WAAW,GAAG,SAAS,CAAC,WAAW,CAAC;KACtD,MAAM;AACH,sBAAc,CAAC,WAAW,GAAG,SAAS,CAAC,IAAI,CAAC;KAC/C;;AAED,WAAO,cAAc,CAAC;CACzB,CAAC;;;;;;;;;AASF,oBAAoB,GAAG,UAAC,aAAa,EAAE,OAAO,EAAK;AAC/C,WAAO,UAAC,SAAS,EAAK;AAClB,eAAO,mBAAmB,CAAC,SAAS,EAAE,aAAa,EAAE,OAAO,CAAC,CAAC;KACjE,CAAC;CACL,CAAC;;qBAEa,YAAa;AACxB,QAAI,OAAO,UAAK,CAAC,CAAC,KAAK,UAAU,EAAE;AAC/B,eAAO,mBAAmB,CAAC,UAAK,CAAC,CAAC,EAAE,UAAK,CAAC,CAAC,EAAE,UAAK,CAAC,CAAC,CAAC,CAAC;KACzD,MAAM;AACH,eAAO,oBAAoB,CAAC,UAAK,CAAC,CAAC,EAAE,UAAK,CAAC,CAAC,CAAC,CAAC;KACjD;CACJ","file":"index.js","sourcesContent":["import extendReactClass from './extendReactClass';\nimport wrapStatelessFunction from './wrapStatelessFunction';\n\nlet decoratorConstructor,\n functionConstructor,\n isReactComponent;\n\n/**\n * Determines if the given object has the signature of a class that inherits React.Component.\n *\n * @param {*} Component\n * @return {boolean}\n */\nisReactComponent = (Component) => {\n return 'prototype' in Component && typeof Component.prototype.render === 'function';\n};\n\n/**\n * When used as a function.\n *\n * @param {Function} Component\n * @param {Object} defaultStyles CSS Modules class map.\n * @param {Object} options {@link https://github.com/gajus/react-css-modules#options}\n * @return {Function}\n */\nfunctionConstructor = (Component, defaultStyles, options) => {\n let decoratedClass;\n\n if (isReactComponent(Component)) {\n decoratedClass = extendReactClass(Component, defaultStyles, options);\n } else {\n decoratedClass = wrapStatelessFunction(Component, defaultStyles, options);\n }\n\n if (Component.displayName) {\n decoratedClass.displayName = Component.displayName;\n } else {\n decoratedClass.displayName = Component.name;\n }\n\n return decoratedClass;\n};\n\n/**\n * When used as a ES7 decorator.\n *\n * @param {Object} defaultStyles CSS Modules class map.\n * @param {Object} options {@link https://github.com/gajus/react-css-modules#options}\n * @return {Function}\n */\ndecoratorConstructor = (defaultStyles, options) => {\n return (Component) => {\n return functionConstructor(Component, defaultStyles, options);\n };\n};\n\nexport default (...args) => {\n if (typeof args[0] === 'function') {\n return functionConstructor(args[0], args[1], args[2]);\n } else {\n return decoratorConstructor(args[0], args[1]);\n }\n};\n"],"sourceRoot":"/source/"} \ No newline at end of file diff --git a/dist/linkClass.js b/dist/linkClass.js deleted file mode 100644 index 7b48e71..0000000 --- a/dist/linkClass.js +++ /dev/null @@ -1,125 +0,0 @@ -'use strict'; - -var _lodashCollectionFilter2 = require('lodash/collection/filter'); - -var _lodashCollectionFilter3 = _interopRequireDefault(_lodashCollectionFilter2); - -var _lodashLangIsArray2 = require('lodash/lang/isArray'); - -var _lodashLangIsArray3 = _interopRequireDefault(_lodashLangIsArray2); - -Object.defineProperty(exports, '__esModule', { - value: true -}); - -var _react = require('react'); - -var _react2 = _interopRequireDefault(_react); - -var _makeConfiguration = require('./makeConfiguration'); - -var _makeConfiguration2 = _interopRequireDefault(_makeConfiguration); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - -var linkClass = undefined; - -/** - * @param {ReactElement} element - * @param {Object} styles CSS modules class map. - * @param {CSSModules~Options} userConfiguration - * @return {ReactElement} - */ -linkClass = function (element, styles, userConfiguration) { - if (styles === undefined) styles = {}; - - var appendClassName = undefined, - clonedElement = undefined, - configuration = undefined, - newChildren = undefined, - newProps = undefined, - styleNames = undefined; - - // @see https://github.com/gajus/react-css-modules/pull/30 - if (!element) { - return element; - } - - configuration = (0, _makeConfiguration2['default'])(userConfiguration); - - styleNames = element.props.styleName; - - if (styleNames) { - styleNames = styleNames.split(' '); - styleNames = (0, _lodashCollectionFilter3['default'])(styleNames); - - if (configuration.allowMultiple === false && styleNames.length > 1) { - throw new Error('ReactElement styleName property defines multiple module names ("' + element.props.styleName + '").'); - } - - appendClassName = styleNames.map(function (styleName) { - if (styles[styleName]) { - return styles[styleName]; - } else { - if (configuration.errorWhenNotFound === true) { - throw new Error('"' + styleName + '" CSS module is undefined.'); - } - - return ''; - } - }); - - appendClassName = appendClassName.filter(function (className) { - return className.length; - }); - - appendClassName = appendClassName.join(' '); - } - - // element.props.children can be one of the following: - // 'text' - // ['text'] - // [ReactElement, 'text'] - // ReactElement - - // console.log(`element.props.children`, element.props.children, `React.Children.count(element.props.children)`, React.Children.count(element.props.children)); - - if (_react2['default'].isValidElement(element.props.children)) { - newChildren = linkClass(_react2['default'].Children.only(element.props.children), styles, configuration); - } else if ((0, _lodashLangIsArray3['default'])(element.props.children)) { - newChildren = _react2['default'].Children.map(element.props.children, function (node) { - if (_react2['default'].isValidElement(node)) { - return linkClass(node, styles, configuration); - } else { - return node; - } - }); - - // https://github.com/facebook/react/issues/4723#issuecomment-135555277 - // Forcing children into an array produces the following error: - // Warning: A ReactFragment is an opaque type. Accessing any of its properties is deprecated. Pass it to one of the React.Children helpers. - // newChildren = _.values(newChildren); - } - - if (appendClassName) { - if (element.props.className) { - appendClassName = element.props.className + ' ' + appendClassName; - } - - newProps = { - className: appendClassName - }; - } - - if (newChildren) { - clonedElement = _react2['default'].cloneElement(element, newProps, newChildren); - } else { - clonedElement = _react2['default'].cloneElement(element, newProps); - } - - return clonedElement; -}; - -exports['default'] = linkClass; -module.exports = exports['default']; -//# sourceMappingURL=linkClass.js.map diff --git a/dist/linkClass.js.map b/dist/linkClass.js.map deleted file mode 100644 index 3367091..0000000 --- a/dist/linkClass.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["linkClass.js"],"names":[],"mappings":";;;;;;;;;;;;;;qBAAkB,OAAO;;;;iCACK,qBAAqB;;;;;;AAGnD,IAAI,SAAS,YAAA,CAAC;;;;;;;;AAQd,SAAS,GAAG,UAAC,OAAO,EAAE,MAAM,EAAO,iBAAiB,EAAK;QAAnC,MAAM,gBAAN,MAAM,GAAG,EAAE;;AAC7B,QAAI,eAAe,YAAA;QACf,aAAa,YAAA;QACb,aAAa,YAAA;QACb,WAAW,YAAA;QACX,QAAQ,YAAA;QACR,UAAU,YAAA,CAAC;;;AAGf,QAAI,CAAC,OAAO,EAAE;AACV,eAAO,OAAO,CAAC;KAClB;;AAED,iBAAa,GAAG,oCAAkB,iBAAiB,CAAC,CAAC;;AAErD,cAAU,GAAG,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC;;AAErC,QAAI,UAAU,EAAE;AACZ,kBAAU,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AACnC,kBAAU,GAAG,yCAAS,UAAU,CAAC,CAAC;;AAElC,YAAI,aAAa,CAAC,aAAa,KAAK,KAAK,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE;AAChE,kBAAM,IAAI,KAAK,sEAAoE,OAAO,CAAC,KAAK,CAAC,SAAS,SAAM,CAAC;SACpH;;AAED,uBAAe,GAAG,UAAU,CAAC,GAAG,CAAC,UAAC,SAAS,EAAK;AAC5C,gBAAI,MAAM,CAAC,SAAS,CAAC,EAAE;AACnB,uBAAO,MAAM,CAAC,SAAS,CAAC,CAAC;aAC5B,MAAM;AACH,oBAAI,aAAa,CAAC,iBAAiB,KAAK,IAAI,EAAE;AAC1C,0BAAM,IAAI,KAAK,OAAK,SAAS,gCAA6B,CAAC;iBAC9D;;AAED,uBAAO,EAAE,CAAC;aACb;SACJ,CAAC,CAAC;;AAEH,uBAAe,GAAG,eAAe,CAAC,MAAM,CAAC,UAAC,SAAS,EAAK;AACpD,mBAAO,SAAS,CAAC,MAAM,CAAC;SAC3B,CAAC,CAAC;;AAEH,uBAAe,GAAG,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;KAC/C;;;;;;;;;;AAUD,QAAI,mBAAM,cAAc,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE;AAC9C,mBAAW,GAAG,SAAS,CAAC,mBAAM,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC;KAC/F,MAAM,IAAI,oCAAU,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE;AAC1C,mBAAW,GAAG,mBAAM,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,UAAC,IAAI,EAAK;AAC/D,gBAAI,mBAAM,cAAc,CAAC,IAAI,CAAC,EAAE;AAC5B,uBAAO,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC;aACjD,MAAM;AACH,uBAAO,IAAI,CAAC;aACf;SACJ,CAAC,CAAC;;;;;;KAMN;;AAED,QAAI,eAAe,EAAE;AACjB,YAAI,OAAO,CAAC,KAAK,CAAC,SAAS,EAAE;AACzB,2BAAe,GAAM,OAAO,CAAC,KAAK,CAAC,SAAS,SAAI,eAAe,AAAE,CAAC;SACrE;;AAED,gBAAQ,GAAG;AACP,qBAAS,EAAE,eAAe;SAC7B,CAAC;KACL;;AAED,QAAI,WAAW,EAAE;AACb,qBAAa,GAAG,mBAAM,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC;KACtE,MAAM;AACH,qBAAa,GAAG,mBAAM,YAAY,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;KACzD;;AAED,WAAO,aAAa,CAAC;CACxB,CAAC;;qBAEa,SAAS","file":"linkClass.js","sourcesContent":["import React from 'react';\nimport makeConfiguration from './makeConfiguration';\nimport _ from 'lodash';\n\nlet linkClass;\n\n/**\n * @param {ReactElement} element\n * @param {Object} styles CSS modules class map.\n * @param {CSSModules~Options} userConfiguration\n * @return {ReactElement}\n */\nlinkClass = (element, styles = {}, userConfiguration) => {\n let appendClassName,\n clonedElement,\n configuration,\n newChildren,\n newProps,\n styleNames;\n\n // @see https://github.com/gajus/react-css-modules/pull/30\n if (!element) {\n return element;\n }\n\n configuration = makeConfiguration(userConfiguration);\n\n styleNames = element.props.styleName;\n\n if (styleNames) {\n styleNames = styleNames.split(' ');\n styleNames = _.filter(styleNames);\n\n if (configuration.allowMultiple === false && styleNames.length > 1) {\n throw new Error(`ReactElement styleName property defines multiple module names (\"${element.props.styleName}\").`);\n }\n\n appendClassName = styleNames.map((styleName) => {\n if (styles[styleName]) {\n return styles[styleName];\n } else {\n if (configuration.errorWhenNotFound === true) {\n throw new Error(`\"${styleName}\" CSS module is undefined.`);\n }\n\n return '';\n }\n });\n\n appendClassName = appendClassName.filter((className) => {\n return className.length;\n });\n\n appendClassName = appendClassName.join(' ');\n }\n\n // element.props.children can be one of the following:\n // 'text'\n // ['text']\n // [ReactElement, 'text']\n // ReactElement\n\n // console.log(`element.props.children`, element.props.children, `React.Children.count(element.props.children)`, React.Children.count(element.props.children));\n\n if (React.isValidElement(element.props.children)) {\n newChildren = linkClass(React.Children.only(element.props.children), styles, configuration);\n } else if (_.isArray(element.props.children)) {\n newChildren = React.Children.map(element.props.children, (node) => {\n if (React.isValidElement(node)) {\n return linkClass(node, styles, configuration);\n } else {\n return node;\n }\n });\n\n // https://github.com/facebook/react/issues/4723#issuecomment-135555277\n // Forcing children into an array produces the following error:\n // Warning: A ReactFragment is an opaque type. Accessing any of its properties is deprecated. Pass it to one of the React.Children helpers.\n // newChildren = _.values(newChildren);\n }\n\n if (appendClassName) {\n if (element.props.className) {\n appendClassName = `${element.props.className} ${appendClassName}`;\n }\n\n newProps = {\n className: appendClassName\n };\n }\n\n if (newChildren) {\n clonedElement = React.cloneElement(element, newProps, newChildren);\n } else {\n clonedElement = React.cloneElement(element, newProps);\n }\n\n return clonedElement;\n};\n\nexport default linkClass;\n"],"sourceRoot":"/source/"} \ No newline at end of file diff --git a/dist/makeConfiguration.js b/dist/makeConfiguration.js deleted file mode 100644 index 178a574..0000000 --- a/dist/makeConfiguration.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -var _lodashCollectionForEach2 = require('lodash/collection/forEach'); - -var _lodashCollectionForEach3 = _interopRequireDefault(_lodashCollectionForEach2); - -Object.defineProperty(exports, '__esModule', { - value: true -}); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - -/** - * @typedef CSSModules~Options - * @see {@link https://github.com/gajus/react-css-modules#options} - * @property {boolean} allowMultiple - * @property {boolean} errorWhenNotFound - */ - -/** - * @param {CSSModules~Options} userConfiguration - * @return {CSSModules~Options} - */ - -exports['default'] = function () { - var userConfiguration = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; - - var configuration = undefined; - - configuration = { - allowMultiple: false, - errorWhenNotFound: true - }; - - (0, _lodashCollectionForEach3['default'])(userConfiguration, function (value, name) { - if (typeof configuration[name] === 'undefined') { - throw new Error('Unknown configuration property "' + name + '".'); - } - - if (typeof value !== 'boolean') { - throw new Error('"' + name + '" property value must be a boolean.'); - } - - configuration[name] = value; - }); - - return configuration; -}; - -module.exports = exports['default']; -//# sourceMappingURL=makeConfiguration.js.map diff --git a/dist/makeConfiguration.js.map b/dist/makeConfiguration.js.map deleted file mode 100644 index dd0546d..0000000 --- a/dist/makeConfiguration.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["makeConfiguration.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;qBAae,YAA4B;QAA3B,iBAAiB,yDAAG,EAAE;;AAClC,QAAI,aAAa,YAAA,CAAC;;AAElB,iBAAa,GAAG;AACZ,qBAAa,EAAE,KAAK;AACpB,yBAAiB,EAAE,IAAI;KAC1B,CAAC;;AAEF,8CAAU,iBAAiB,EAAE,UAAC,KAAK,EAAE,IAAI,EAAK;AAC1C,YAAI,OAAO,aAAa,CAAC,IAAI,CAAC,KAAK,WAAW,EAAE;AAC5C,kBAAM,IAAI,KAAK,sCAAoC,IAAI,QAAK,CAAC;SAChE;;AAED,YAAI,OAAO,KAAK,KAAK,SAAS,EAAE;AAC5B,kBAAM,IAAI,KAAK,OAAK,IAAI,yCAAsC,CAAC;SAClE;;AAED,qBAAa,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;KAC/B,CAAC,CAAC;;AAEH,WAAO,aAAa,CAAC;CACxB","file":"makeConfiguration.js","sourcesContent":["import _ from 'lodash';\n\n/**\n * @typedef CSSModules~Options\n * @see {@link https://github.com/gajus/react-css-modules#options}\n * @property {boolean} allowMultiple\n * @property {boolean} errorWhenNotFound\n */\n\n/**\n * @param {CSSModules~Options} userConfiguration\n * @return {CSSModules~Options}\n */\nexport default (userConfiguration = {}) => {\n let configuration;\n\n configuration = {\n allowMultiple: false,\n errorWhenNotFound: true\n };\n\n _.forEach(userConfiguration, (value, name) => {\n if (typeof configuration[name] === 'undefined') {\n throw new Error(`Unknown configuration property \"${name}\".`);\n }\n\n if (typeof value !== 'boolean') {\n throw new Error(`\"${name}\" property value must be a boolean.`);\n }\n\n configuration[name] = value;\n });\n\n return configuration;\n};\n"],"sourceRoot":"/source/"} \ No newline at end of file diff --git a/dist/wrapStatelessFunction.js b/dist/wrapStatelessFunction.js deleted file mode 100644 index 7723dfa..0000000 --- a/dist/wrapStatelessFunction.js +++ /dev/null @@ -1,78 +0,0 @@ -'use strict'; - -var _lodashLangIsObject2 = require('lodash/lang/isObject'); - -var _lodashLangIsObject3 = _interopRequireDefault(_lodashLangIsObject2); - -var _lodashObjectAssign2 = require('lodash/object/assign'); - -var _lodashObjectAssign3 = _interopRequireDefault(_lodashObjectAssign2); - -Object.defineProperty(exports, '__esModule', { - value: true -}); - -var _linkClass = require('./linkClass'); - -var _linkClass2 = _interopRequireDefault(_linkClass); - -var _react = require('react'); - -var _react2 = _interopRequireDefault(_react); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - -var wrapStatelessFunction = undefined; - -/** - * @see https://facebook.github.io/react/blog/2015/09/10/react-v0.14-rc1.html#stateless-function-components - * @param {Function} Component - * @param {Object} defaultStyles - * @param {Object} options - * @returns {Function} - */ -wrapStatelessFunction = function (Component, defaultStyles, options) { - var WrappedComponent = undefined; - - WrappedComponent = function () { - for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - args[_key - 1] = arguments[_key]; - } - - var props = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; - - var renderResult = undefined, - styles = undefined, - useProps = undefined; - - if (props.styles) { - useProps = props; - styles = props.styles; - } else if ((0, _lodashLangIsObject3['default'])(defaultStyles)) { - useProps = (0, _lodashObjectAssign3['default'])({}, props, { - styles: defaultStyles - }); - - styles = defaultStyles; - } else { - useProps = props; - styles = {}; - } - - renderResult = Component.apply(undefined, [useProps].concat(args)); - - if (renderResult) { - return (0, _linkClass2['default'])(renderResult, styles, options); - } - - return _react2['default'].createElement('noscript'); - }; - - (0, _lodashObjectAssign3['default'])(WrappedComponent, Component); - - return WrappedComponent; -}; - -exports['default'] = wrapStatelessFunction; -module.exports = exports['default']; -//# sourceMappingURL=wrapStatelessFunction.js.map diff --git a/dist/wrapStatelessFunction.js.map b/dist/wrapStatelessFunction.js.map deleted file mode 100644 index 78ef1e4..0000000 --- a/dist/wrapStatelessFunction.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["wrapStatelessFunction.js"],"names":[],"mappings":";;;;;;;;;;;;;;yBAAsB,aAAa;;;;qBACjB,OAAO;;;;;;AAGzB,IAAI,qBAAqB,YAAA,CAAC;;;;;;;;;AAS1B,qBAAqB,GAAG,UAAC,SAAS,EAAE,aAAa,EAAE,OAAO,EAAK;AAC3D,QAAI,gBAAgB,YAAA,CAAC;;AAErB,oBAAgB,GAAG,YAAyB;0CAAT,IAAI;AAAJ,gBAAI;;;YAAnB,KAAK,yDAAG,EAAE;;AAC1B,YAAI,YAAY,YAAA;YACZ,MAAM,YAAA;YACN,QAAQ,YAAA,CAAC;;AAEb,YAAI,KAAK,CAAC,MAAM,EAAE;AACd,oBAAQ,GAAG,KAAK,CAAC;AACjB,kBAAM,GAAG,KAAK,CAAC,MAAM,CAAC;SACzB,MAAM,IAAI,qCAAW,aAAa,CAAC,EAAE;AAClC,oBAAQ,GAAG,qCAAS,EAAE,EAAE,KAAK,EAAE;AAC3B,sBAAM,EAAE,aAAa;aACxB,CAAC,CAAC;;AAEH,kBAAM,GAAG,aAAa,CAAC;SAC1B,MAAM;AACH,oBAAQ,GAAG,KAAK,CAAC;AACjB,kBAAM,GAAG,EAAE,CAAC;SACf;;AAED,oBAAY,GAAG,SAAS,mBAAC,QAAQ,SAAK,IAAI,EAAC,CAAC;;AAE5C,YAAI,YAAY,EAAE;AACd,mBAAO,4BAAU,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;SACnD;;AAED,eAAO,mBAAM,aAAa,CAAC,UAAU,CAAC,CAAC;KAC1C,CAAC;;AAEF,yCAAS,gBAAgB,EAAE,SAAS,CAAC,CAAC;;AAEtC,WAAO,gBAAgB,CAAC;CAC3B,CAAC;;qBAEa,qBAAqB","file":"wrapStatelessFunction.js","sourcesContent":["import linkClass from './linkClass';\nimport React from 'react';\nimport _ from 'lodash';\n\nlet wrapStatelessFunction;\n\n/**\n * @see https://facebook.github.io/react/blog/2015/09/10/react-v0.14-rc1.html#stateless-function-components\n * @param {Function} Component\n * @param {Object} defaultStyles\n * @param {Object} options\n * @returns {Function}\n */\nwrapStatelessFunction = (Component, defaultStyles, options) => {\n let WrappedComponent;\n\n WrappedComponent = (props = {}, ...args) => {\n let renderResult,\n styles,\n useProps;\n\n if (props.styles) {\n useProps = props;\n styles = props.styles;\n } else if (_.isObject(defaultStyles)) {\n useProps = _.assign({}, props, {\n styles: defaultStyles\n });\n\n styles = defaultStyles;\n } else {\n useProps = props;\n styles = {};\n }\n\n renderResult = Component(useProps, ...args);\n\n if (renderResult) {\n return linkClass(renderResult, styles, options);\n }\n\n return React.createElement('noscript');\n };\n\n _.assign(WrappedComponent, Component);\n\n return WrappedComponent;\n};\n\nexport default wrapStatelessFunction;\n"],"sourceRoot":"/source/"} \ No newline at end of file diff --git a/package.json b/package.json index ca140d3..00e1ffa 100644 --- a/package.json +++ b/package.json @@ -12,28 +12,44 @@ "css", "modules" ], - "version": "3.6.1", + "version": "4.3.0", "author": { "name": "Gajus Kuizinas", - "email": "gk@anuary.com", + "email": "gajus@gajus.com", "url": "http://gajus.com" }, "license": "BSD-3-Clause", "dependencies": { - "lodash": "^3.10.1" + "hoist-non-react-statics": "^2.5.5", + "lodash": "^4.16.6", + "object-unfreeze": "^1.1.0" }, "devDependencies": { - "chai": "^3.4.0", - "coveralls": "^2.11.4", - "jsdom": "^7.0.2", - "pragmatist": "^1.1.5", - "react": "^0.14.0", - "react-addons-test-utils": "^0.14.0" + "babel-cli": "^6.18.0", + "babel-plugin-add-module-exports": "^0.2.1", + "babel-plugin-lodash": "^3.2.9", + "babel-plugin-transform-proto-to-assign": "^6.9.0", + "babel-preset-es2015": "^6.18.0", + "babel-preset-react": "^6.16.0", + "babel-preset-stage-0": "^6.16.0", + "babel-register": "^6.18.0", + "chai": "^4.0.0-canary.1", + "chai-spies": "^0.7.1", + "eslint": "^3.10.0", + "eslint-config-canonical": "^5.5.0", + "husky": "^0.11.9", + "jsdom": "^9.8.3", + "mocha": "^3.1.2", + "react": "^15.4.0-rc.4", + "react-addons-shallow-compare": "^15.4.0-rc.4", + "react-addons-test-utils": "^15.4.0-rc.4", + "react-dom": "^15.4.0-rc.4", + "semantic-release": "^6.3.2" }, "scripts": { - "pragmatist": "node ./node_modules/.bin/pragmatist", - "lint": "npm run pragmatist lint", - "build": "npm run pragmatist build", - "test": "npm run pragmatist test && cat ./coverage/lcov.info | ./node_modules/.bin/coveralls" + "lint": "eslint ./src ./tests", + "test": "NODE_ENV=development mocha --compilers js:babel-register ./tests/**/*.js && npm run lint && npm run build", + "build": "NODE_ENV=production babel ./src --out-dir ./dist", + "precommit": "npm run test" } } diff --git a/src/SimpleMap.js b/src/SimpleMap.js new file mode 100644 index 0000000..7cf618b --- /dev/null +++ b/src/SimpleMap.js @@ -0,0 +1,21 @@ +export default class { + constructor () { + this.size = 0; + this.keys = []; + this.values = []; + } + + get (key) { + const index = this.keys.indexOf(key); + + return this.values[index]; + } + + set (key, value) { + this.keys.push(key); + this.values.push(value); + this.size = this.keys.length; + + return value; + } +} diff --git a/src/extendReactClass.js b/src/extendReactClass.js index 4ff5114..4741bea 100644 --- a/src/extendReactClass.js +++ b/src/extendReactClass.js @@ -1,8 +1,10 @@ -import linkClass from './linkClass'; -import React from 'react'; -import _ from 'lodash'; +/* eslint-disable react/prop-types */ -let extendReactClass; +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 @@ -10,33 +12,63 @@ let extendReactClass; * @param {Object} options * @returns {ReactClass} */ -extendReactClass = (Component, defaultStyles, options) => { - return class extends Component { - render () { - let renderResult, - styles; - - if (this.props.styles) { - styles = this.props.styles; - } else if (_.isObject(defaultStyles)) { - this.props = _.assign({}, this.props, { - styles: defaultStyles - }); - - styles = defaultStyles; - } else { - styles = {}; - } - - renderResult = super.render(); - - if (renderResult) { - return linkClass(renderResult, styles, options); - } - - return React.createElement('noscript'); +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; } - }; -}; -export default extendReactClass; + 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 9f1ff1e..b913b29 100644 --- a/src/index.js +++ b/src/index.js @@ -1,63 +1,56 @@ +import _ from 'lodash'; import extendReactClass from './extendReactClass'; import wrapStatelessFunction from './wrapStatelessFunction'; +import makeConfiguration from './makeConfiguration'; -let decoratorConstructor, - functionConstructor, - isReactComponent; +/** + * @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. - * - * @param {*} Component - * @return {boolean} */ -isReactComponent = (Component) => { - return 'prototype' in Component && typeof Component.prototype.render === 'function'; +const isReactComponent = (maybeReactComponent: any): boolean => { + return 'prototype' in maybeReactComponent && _.isFunction(maybeReactComponent.prototype.render); }; /** * When used as a function. - * - * @param {Function} Component - * @param {Object} defaultStyles CSS Modules class map. - * @param {Object} options {@link https://github.com/gajus/react-css-modules#options} - * @return {Function} */ -functionConstructor = (Component, defaultStyles, options) => { - let decoratedClass; - - if (isReactComponent(Component)) { - decoratedClass = extendReactClass(Component, defaultStyles, options); - } else { - decoratedClass = wrapStatelessFunction(Component, defaultStyles, options); - } - - if (Component.displayName) { - decoratedClass.displayName = Component.displayName; - } else { - decoratedClass.displayName = Component.name; - } - - return decoratedClass; +const functionConstructor = (Component: Function, defaultStyles: Object, options: TypeOptions): Function => { + let decoratedClass; + + const configuration = makeConfiguration(options); + + if (isReactComponent(Component)) { + decoratedClass = extendReactClass(Component, defaultStyles, configuration); + } else { + decoratedClass = wrapStatelessFunction(Component, defaultStyles, configuration); + } + + if (Component.displayName) { + decoratedClass.displayName = Component.displayName; + } else { + decoratedClass.displayName = Component.name; + } + + return decoratedClass; }; /** * When used as a ES7 decorator. - * - * @param {Object} defaultStyles CSS Modules class map. - * @param {Object} options {@link https://github.com/gajus/react-css-modules#options} - * @return {Function} */ -decoratorConstructor = (defaultStyles, options) => { - return (Component) => { - return functionConstructor(Component, defaultStyles, 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 index 2a5b74c..67d0a94 100644 --- a/src/isIterable.js +++ b/src/isIterable.js @@ -1,10 +1,24 @@ -import { isObject } from 'lodash'; +import _ from 'lodash'; -const ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator; +const ITERATOR_SYMBOL = typeof Symbol !== 'undefined' && _.isFunction(Symbol) && Symbol.iterator; const OLD_ITERATOR_SYMBOL = '@@iterator'; -export default function isIterable(obj) { - return isObject(obj) && - typeof ((ITERATOR_SYMBOL && obj[ITERATOR_SYMBOL]) - || obj[OLD_ITERATOR_SYMBOL]) === 'function'; -} +/** + * @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 58dd47a..4ed3975 100644 --- a/src/linkClass.js +++ b/src/linkClass.js @@ -1,105 +1,106 @@ -import React from 'react'; -import makeConfiguration from './makeConfiguration'; -import isIterable from './isIterable'; import _ from 'lodash'; - -let linkClass; - -/** - * @param {ReactElement} element - * @param {Object} styles CSS modules class map. - * @param {CSSModules~Options} userConfiguration - * @return {ReactElement} - */ -linkClass = (element, styles = {}, userConfiguration) => { - let appendClassName, - children, - clonedElement, - configuration, - newChildren, - newProps, - styleNames; - - // @see https://github.com/gajus/react-css-modules/pull/30 - if (!element) { - return element; +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); } + }); - configuration = makeConfiguration(userConfiguration); - - styleNames = element.props.styleName; - - if (styleNames) { - styleNames = styleNames.split(' '); - styleNames = _.filter(styleNames); - - if (configuration.allowMultiple === false && styleNames.length > 1) { - throw new Error(`ReactElement styleName property defines multiple module names ("${element.props.styleName}").`); - } - - appendClassName = styleNames.map((styleName) => { - if (styles[styleName]) { - return styles[styleName]; - } else { - if (configuration.errorWhenNotFound === true) { - throw new Error(`"${styleName}" CSS module is undefined.`); - } - - return ''; - } - }); - - appendClassName = appendClassName.filter((className) => { - return className.length; - }); + return array; +}; - appendClassName = appendClassName.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); } + }); - // element.props.children can be one of the following: - // 'text' - // ['text'] - // [ReactElement, 'text'] - // ReactElement - - // console.log(`element.props.children`, element.props.children, `React.Children.count(element.props.children)`, React.Children.count(element.props.children)); - - children = element.props.children; - - if (React.isValidElement(children)) { - newChildren = linkClass(React.Children.only(children), styles, configuration); - } else if (_.isArray(children) || isIterable(children)) { - newChildren = React.Children.map(children, (node) => { - if (React.isValidElement(node)) { - return linkClass(node, styles, configuration); - } else { - return node; - } - }); - - // https://github.com/facebook/react/issues/4723#issuecomment-135555277 - // Forcing children into an array produces the following error: - // Warning: A ReactFragment is an opaque type. Accessing any of its properties is deprecated. Pass it to one of the React.Children helpers. - // newChildren = _.values(newChildren); - } + if (styleNames.length) { + appendClassName = generateAppendClassName(styles, styleNames, configuration.handleNotFoundStyleName); if (appendClassName) { - if (element.props.className) { - appendClassName = `${element.props.className} ${appendClassName}`; - } + if (elementShallowCopy.props.className) { + appendClassName = elementShallowCopy.props.className + ' ' + appendClassName; + } - newProps = { - className: appendClassName - }; + elementShallowCopy.props.className = appendClassName; } + } - if (newChildren) { - clonedElement = React.cloneElement(element, newProps, newChildren); - } else { - clonedElement = React.cloneElement(element, newProps); - } + delete elementShallowCopy.props.styleName; + + if (elementIsFrozen) { + Object.freeze(elementShallowCopy.props); + Object.freeze(elementShallowCopy); + } else if (propsFrozen) { + Object.freeze(elementShallowCopy.props); + } + + if (propsNotExtensible) { + Object.preventExtensions(elementShallowCopy.props); + } - return clonedElement; + 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 index dae8ca5..cc3169c 100644 --- a/src/makeConfiguration.js +++ b/src/makeConfiguration.js @@ -4,32 +4,34 @@ import _ from 'lodash'; * @typedef CSSModules~Options * @see {@link https://github.com/gajus/react-css-modules#options} * @property {boolean} allowMultiple - * @property {boolean} errorWhenNotFound + * @property {string} handleNotFoundStyleName */ /** * @param {CSSModules~Options} userConfiguration - * @return {CSSModules~Options} + * @returns {CSSModules~Options} */ export default (userConfiguration = {}) => { - let configuration; + const configuration = { + allowMultiple: false, + handleNotFoundStyleName: 'throw' + }; - configuration = { - allowMultiple: false, - errorWhenNotFound: true - }; + _.forEach(userConfiguration, (value, name) => { + if (_.isUndefined(configuration[name])) { + throw new Error('Unknown configuration property "' + name + '".'); + } - _.forEach(userConfiguration, (value, name) => { - if (typeof configuration[name] === 'undefined') { - throw new Error(`Unknown configuration property "${name}".`); - } + if (name === 'allowMultiple' && !_.isBoolean(value)) { + throw new Error('"allowMultiple" property value must be a boolean.'); + } - if (typeof value !== 'boolean') { - throw new Error(`"${name}" 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; - }); + configuration[name] = value; + }); - return configuration; + 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 index cacf1e4..b6b9162 100644 --- a/src/wrapStatelessFunction.js +++ b/src/wrapStatelessFunction.js @@ -1,50 +1,49 @@ -import linkClass from './linkClass'; -import React from 'react'; -import _ from 'lodash'; +/* eslint-disable react/prop-types */ -let wrapStatelessFunction; +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 - * @param {Function} Component - * @param {Object} defaultStyles - * @param {Object} options - * @returns {Function} */ -wrapStatelessFunction = (Component, defaultStyles, options) => { - let WrappedComponent; - - WrappedComponent = (props = {}, ...args) => { - let renderResult, - styles, - useProps; - - if (props.styles) { - useProps = props; - styles = props.styles; - } else if (_.isObject(defaultStyles)) { - useProps = _.assign({}, props, { - styles: defaultStyles - }); - - styles = defaultStyles; - } else { - useProps = props; - styles = {}; - } - - renderResult = Component(useProps, ...args); - - if (renderResult) { - return linkClass(renderResult, styles, options); - } - - return React.createElement('noscript'); - }; - - _.assign(WrappedComponent, Component); - - return WrappedComponent; +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; }; - -export default wrapStatelessFunction; diff --git a/tests/SimpleMap.js b/tests/SimpleMap.js new file mode 100644 index 0000000..08ca888 --- /dev/null +++ b/tests/SimpleMap.js @@ -0,0 +1,38 @@ +import { + expect +} from 'chai'; +import SimpleMap from './../src/SimpleMap'; + +describe('SimpleMap', () => { + context('simple map with primitive or object as keys', () => { + const values = [ + [1, 'something'], + ['1', 'somethingElse'], + [{}, []], + [null, null] + ]; + + let map; + + beforeEach(() => { + map = new SimpleMap(); + }); + + it('should set', () => { + values.forEach(([key, value]) => { + map.set(key, value); + }); + expect(map.size).to.equal(values.length); + }); + + it('should get', () => { + values.forEach(([key, value]) => { + map.set(key, value); + }); + + values.forEach(([key, value]) => { + expect(map.get(key)).to.equal(value); + }); + }); + }); +}); diff --git a/tests/extendReactClass.js b/tests/extendReactClass.js index 2b32174..01733ee 100644 --- a/tests/extendReactClass.js +++ b/tests/extendReactClass.js @@ -1,89 +1,167 @@ -/* eslint-disable max-nested-callbacks */ +/* eslint-disable max-nested-callbacks, react/prefer-stateless-function, react/prop-types, react/no-multi-comp, class-methods-use-this */ import { expect } from 'chai'; - import React from 'react'; import TestUtils from 'react-addons-test-utils'; +import shallowCompare from 'react-addons-shallow-compare'; import jsdom from 'jsdom'; import extendReactClass from './../src/extendReactClass'; describe('extendReactClass', () => { - beforeEach(() => { - global.document = jsdom.jsdom(` - - - - - - - - `); - - global.window = document.defaultView; + 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); - context('using default styles', () => { - it('exposes styles through styles property', (done) => { - let Component, - styles; - - Component = class extends React.Component { - render () { - expect(this.props.styles).to.equal(styles); - done(); - } - }; - - styles = { - foo: 'foo-1' - }; - - Component = extendReactClass(Component, styles); - - TestUtils.renderIntoDocument(); - }); - it('does not affect the other instance properties', (done) => { - let Component, - styles; - - Component = class extends React.Component { - render () { - expect(this.props.bar).to.equal('baz'); - done(); - } - }; - - styles = { - foo: 'foo-1' - }; - - Component = extendReactClass(Component, styles); - - TestUtils.renderIntoDocument(); - }); + 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; + } + }; - context('using explicit styles', () => { - it('exposes styles through styles property', (done) => { - let Component, - styles; + Component = extendReactClass(Component); - Component = class extends React.Component { - render () { - expect(this.props.styles).to.equal(styles); - done(); - } - }; + 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'; - styles = { - foo: 'foo-1' - }; + render () { + return null; + } + }; - Component = extendReactClass(Component); + const WrappedComponent = extendReactClass(Component); - TestUtils.renderIntoDocument(); - }); + expect(Component.foo).to.equal('FOO'); + expect(WrappedComponent.foo).to.equal(Component.foo); }); + }); }); diff --git a/tests/linkClass.js b/tests/linkClass.js index e0778a8..cf7c289 100644 --- a/tests/linkClass.js +++ b/tests/linkClass.js @@ -1,261 +1,483 @@ -/* eslint-disable max-nested-callbacks */ +/* eslint-disable max-nested-callbacks, react/prefer-stateless-function, class-methods-use-this, no-console, no-unused-expressions */ -import { +import chai, { expect } from 'chai'; - +import spies from 'chai-spies'; import React from 'react'; import TestUtils from 'react-addons-test-utils'; import jsdom from 'jsdom'; import linkClass from './../src/linkClass'; +chai.use(spies); + describe('linkClass', () => { - context('ReactElement does not define styleName', () => { - it('does not affect element properties', () => { - expect(linkClass(
    )).to.deep.equal(
    ); + 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' }); - it('does not affect element properties with a single element child', () => { - expect(linkClass(

    )).to.deep.equal(

    ); + 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' }); - it('does not affect element properties with a single text child', () => { - expect(linkClass(

    test
    )).to.deep.equal(
    test
    ); + 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' }); - it('does not affect the className', () => { - expect(linkClass(
    )).to.deep.equal(
    ); + 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' }); - xit('does not affect element with a single children when that children is contained in an array', () => { - let outcome, - subject; + 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 = React.createElement('div', null, [ - React.createElement('p') - ]); - outcome = React.createElement('div', null, [ - React.createElement('p') - ]); + subject =
    +

    +

    +

    ; - expect(linkClass(subject)).to.deep.equal(outcome); + subject = linkClass(subject, { + bar: 'bar-1', + foo: 'foo-1' }); - 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(

    ); - - let outcome, - subject; - - subject = React.createElement('div', null, [ - React.createElement('p'), - React.createElement('p') - ]); - outcome = React.createElement('div', null, [ - React.createElement('p'), - React.createElement('p') - ]); - - expect(linkClass(subject)).to.deep.equal(outcome); + 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, - context('called with null instead of ReactElement', () => { - it('returns null', () => { - let subject; + // eslint-disable-next-line no-use-extend-native/no-use-extend-native + [Symbol.iterator]: Array.prototype[Symbol.iterator] + }; - subject = linkClass(null); + subject =

    {iterable}
    ; - expect(subject).to.equal(null); + 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; - context('styleName matches an existing CSS module', () => { - context('when a descendant element has styleName', () => { - it('assigns a generated className', () => { - let subject; + const iterable = { + 0:

    , + 1:

    , + length: 2, - subject =

    -

    -
    ; + // eslint-disable-next-line no-use-extend-native/no-use-extend-native + [Symbol.iterator]: Array.prototype[Symbol.iterator] + }; - subject = linkClass(subject, { - foo: 'foo-1' - }); + subject =
    ; - expect(subject.props.children.props.className).to.equal('foo-1'); - }); - }); - context('when multiple descendant elements have styleName', () => { - it('assigns a generated className', () => { - let subject; - - subject =
    -

    -

    -
    ; - - subject = linkClass(subject, { - foo: 'foo-1', - bar: 'bar-1' - }); - - expect(subject.props.children[0].props.className).to.equal('foo-1'); - expect(subject.props.children[1].props.className).to.equal('bar-1'); - }); + subject = linkClass(subject, { + bar: 'bar-1', + foo: 'foo-1' }); - context('when multiple descendants have styleName and are iterable', () => { - it('assigns a generated className', () => { - let subject, iterable; - - iterable = { - 0:

    , - 1:

    , - length: 2, - [Symbol.iterator]: Array.prototype[Symbol.iterator] - }; - - subject =
    {iterable}
    ; - - subject = linkClass(subject, { - foo: 'foo-1', - bar: 'bar-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 ReactElement does not have an existing className', () => { - it('uses the generated class name to set the className property', () => { - let subject; - subject =
    ; + 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 = linkClass(subject, { - foo: 'foo-1' - }); + subject =
    ; - expect(subject.props.className).to.deep.equal('foo-1'); - }); + subject = linkClass(subject, { + foo: 'foo-1' }); - context('when ReactElement has an existing className', () => { - it('appends the generated class name to the className property', () => { - let subject; - subject =
    ; + 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 = linkClass(subject, { - bar: 'bar-1' - }); + subject =
    ; - expect(subject.props.className).to.deep.equal('foo bar-1'); - }); + 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; - context('styleName includes multiple whitespace characters', () => { - it('resolves CSS modules', () => { - let subject; + 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'); + }); + }); - subject = linkClass(subject, { - foo: 'foo-1', - bar: 'bar-1' - }, { - allowMultiple: true - }); + context('can\'t write to properties', () => { + context('when the element is frozen', () => { + it('adds className but is still frozen', () => { + let subject; - expect(subject.props.children.props.className).to.equal('foo-1 bar-1'); + 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; - describe('options.allowMultiple', () => { - context('when multiple module names are used', () => { - context('when false', () => { - it('throws an error', () => { - expect(() => { - linkClass(
    , {}, {allowMultiple: false}); - }).to.throw(Error, 'ReactElement styleName property defines multiple module names ("foo bar").'); - }); - }); - context('when true', () => { - it('appends a generated class name for every referenced CSS module', () => { - let subject; - - subject =
    ; - - subject = linkClass(subject, { - foo: 'foo-1', - bar: 'bar-1' - }, {allowMultiple: true}); - - expect(subject.props.className).to.deep.equal('foo-1 bar-1'); - }); - }); + 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; - describe('options.errorWhenNotFound', () => { - context('when styleName does not match an existing CSS module', () => { - context('when false', () => { - it('ignores the missing CSS module', () => { - let subject; - - subject =
    ; - - subject = linkClass(subject, {}, {errorWhenNotFound: false}); - - expect(subject.props.className).to.be.an('undefined'); - }); - }); - context('when is true', () => { - it('throws an error', () => { - expect(() => { - linkClass(
    , {}, {errorWhenNotFound: true}); - }).to.throw(Error, '"foo" CSS module is undefined.'); - }); - }); + 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; - context('when ReactElement includes ReactComponent', () => { - let Foo, - nodeList; - - beforeEach(() => { - global.document = jsdom.jsdom(` - - - - - - - - `); - - global.window = document.defaultView; - - Foo = class extends React.Component { - render () { - return
    Hello
    ; - } - }; - - nodeList = TestUtils.renderIntoDocument(linkClass(
    , {foo: 'foo-1'})); + 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.'); }); - it('processes ReactElement nodes', () => { - expect(nodeList.className).to.equal('foo-1'); + }); + 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(); }); - it('does not process ReactComponent nodes', () => { - expect(nodeList.firstChild.className).to.equal(''); + }); + 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 index 3a74ae7..4193311 100644 --- a/tests/makeConfiguration.js +++ b/tests/makeConfiguration.js @@ -3,43 +3,40 @@ import { expect } from 'chai'; - import makeConfiguration from './../src/makeConfiguration'; describe('makeConfiguration', () => { - describe('when using default configuration', () => { - let configuration; + describe('when using default configuration', () => { + let configuration; - beforeEach(() => { - configuration = makeConfiguration(); - }); - describe('allowMultiple property', () => { - it('defaults to false', () => { - expect(configuration.allowMultiple).to.equal(false); - }); - }); - describe('errorWhenNotFound property', () => { - it('defaults to true', () => { - expect(configuration.errorWhenNotFound).to.equal(true); - }); - }); + beforeEach(() => { + configuration = makeConfiguration(); }); - describe('when unknown property is provided', () => { - it('throws an error', () => { - expect(() => { - makeConfiguration({ - unknownProperty: true - }); - }).to.throw(Error, 'Unknown configuration property "unknownProperty".'); + 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', () => { - let userConfiguration; + }); + it('does not mutate user configuration', () => { + const userConfiguration = {}; - userConfiguration = {}; + makeConfiguration(userConfiguration); - makeConfiguration(userConfiguration); - - expect(userConfiguration).to.deep.equal({}); - }); + expect(userConfiguration).to.deep.equal({}); + }); }); diff --git a/tests/mocha.opts b/tests/mocha.opts deleted file mode 100644 index 13374d0..0000000 --- a/tests/mocha.opts +++ /dev/null @@ -1 +0,0 @@ ---compilers js:babel/register diff --git a/tests/reactCssModules.js b/tests/reactCssModules.js index 685988f..d484d95 100644 --- a/tests/reactCssModules.js +++ b/tests/reactCssModules.js @@ -1,133 +1,184 @@ -/* eslint-disable max-nested-callbacks */ +/* 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; + context('a ReactComponent is decorated using react-css-modules', () => { + it('inherits displayName', () => { + let Foo; - Foo = class extends React.Component { - static displayName = 'Bar'; - }; + Foo = class extends React.Component {}; - Foo = reactCssModules(Foo); + // @todo https://phabricator.babeljs.io/T2779 + Foo.displayName = 'Bar'; - expect(Foo.displayName).to.equal('Bar'); - }); - context('target component does not name displayName', () => { - it('uses name for displayName', () => { - let Foo; + Foo = reactCssModules(Foo); - Foo = class Bar extends React.Component {}; + expect(Foo.displayName).to.equal('Bar'); + }); + context('target component does not name displayName', () => { + it('uses name for displayName', () => { + let Foo; - Foo = reactCssModules(Foo); + Foo = class Bar extends React.Component {}; - 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', () => { - it('that element should contain the equivalent className', () => { - let Foo, - component, - shallowRenderer; + Foo = reactCssModules(Foo); - shallowRenderer = TestUtils.createRenderer(); + 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' + }); - Foo = class extends React.Component { - render () { - return
    Hello
    ; - } - }; + shallowRenderer.render(); - Foo = reactCssModules(Foo, { - foo: 'foo-1' - }); + 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; - shallowRenderer.render(); + beforeEach(() => { + const shallowRenderer = TestUtils.createRenderer(); - component = shallowRenderer.getRenderOutput(); + Foo = () => { + return
    Hello
    ; + }; - expect(component.props.className).to.equal('foo-1'); - }); + Foo = reactCssModules(Foo, { + foo: 'foo-1' }); - context('the component is a stateless function component', () => { - it('that element should contain the equivalent className', () => { - let Foo, - component, - shallowRenderer; - - shallowRenderer = TestUtils.createRenderer(); - Foo = () =>
    Hello
    ; + shallowRenderer.render(); - Foo = reactCssModules(Foo, { - foo: 'foo-1' - }); + 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(); + shallowRenderer.render(); - component = shallowRenderer.getRenderOutput(); + const component = shallowRenderer.getRenderOutput(); - expect(component.props.className).to.equal('foo-1'); - }); - }); + expect(typeof component).to.equal('object'); + }); }); + context('the component is a stateless function component', () => { + it('that element should contain the equivalent className', () => { + let Foo; - context('a ReactComponent renders nothing', () => { - context('the component is a class that extends React.Component', () => { - it('linkClass must not intervene', () => { - let Foo, - component, - shallowRenderer; + const shallowRenderer = TestUtils.createRenderer(); - shallowRenderer = TestUtils.createRenderer(); + Foo = () => { + return null; + }; - Foo = class extends React.Component { - render () { - return null; - } - }; + Foo = reactCssModules(Foo, { + foo: 'foo-1' + }); - Foo = reactCssModules(Foo, { - foo: 'foo-1' - }); + shallowRenderer.render(); - shallowRenderer.render(); + const component = shallowRenderer.getRenderOutput(); - component = shallowRenderer.getRenderOutput(); + expect(typeof component).to.equal('object'); + }); + }); + }); + context('rendering element', () => { + beforeEach(() => { + global.document = jsdom.jsdom(''); - expect(typeof component).to.equal('object'); - }); + 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' }); - context('the component is a stateless function component', () => { - it('that element should contain the equivalent className', () => { - let Foo, - component, - shallowRenderer; - - shallowRenderer = TestUtils.createRenderer(); - Foo = () => null; + Bar = class extends React.Component { + render () { + return
    {this.props.children}
    ; + } + }; - Foo = reactCssModules(Foo, { - foo: 'foo-1' - }); + Bar = reactCssModules(Bar, { + test: 'bar-0' + }); - shallowRenderer.render(); + subject = TestUtils.renderIntoDocument(); - component = shallowRenderer.getRenderOutput(); + // eslint-disable-next-line react/no-find-dom-node + subject = ReactDOM.findDOMNode(subject); - expect(typeof component).to.equal('object'); - }); - }); + 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 index 44d027f..9c71935 100644 --- a/tests/wrapStatelessFunction.js +++ b/tests/wrapStatelessFunction.js @@ -3,69 +3,90 @@ 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', () => { - let Component, WrappedComponent, styles; + it('hoists static own properties from the input component to the wrapped component', () => { + const styles = { + foo: 'foo-1' + }; - styles = { - foo: 'foo-1' - }; + const InnerComponent = () => { + return null; + }; - Component = function InnerComponent () { return null; }; - Component.propTypes = {}; - Component.defaultProps = {}; + InnerComponent.propTypes = {}; + InnerComponent.defaultProps = {}; - WrappedComponent = wrapStatelessFunction(Component, styles); + const WrappedComponent = wrapStatelessFunction(InnerComponent, styles); - expect(WrappedComponent.propTypes).to.equal(Component.propTypes); - expect(WrappedComponent.defaultProps).to.equal(Component.defaultProps); - expect(WrappedComponent.name).not.to.equal(Component.name); - }); - context('using default styles', () => { - it('exposes styles through styles property', (done) => { - let 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' + }; - 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.styles).to.equal(styles); - done(); - }, styles)(); - }); - it('does not affect the other instance properties', (done) => { - let styles; + 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' + }; - 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.bar).to.equal('baz'); - done(); - }, styles)({ - bar: 'baz' - }); - }); + wrapStatelessFunction((props) => { + expect(props.styles).to.equal(styles); + done(); + })({ + styles + }); }); - context('using explicit styles', () => { - it('exposes styles through styles property', (done) => { - let styles; + }); + context('rendering Component that returns null', () => { + it('generates null', () => { + const shallowRenderer = TestUtils.createRenderer(); + + const Component = wrapStatelessFunction(() => { + return null; + }); + + shallowRenderer.render(); - styles = { - foo: 'foo-1' - }; + const component = shallowRenderer.getRenderOutput(); - wrapStatelessFunction((props) => { - expect(props.styles).to.equal(styles); - done(); - })({ - styles: styles - }); - }); + expect(component).to.equal(null); }); + }); });