├── .README ├── react-css-modules.png └── react-css-modules.sketch ├── .babelrc ├── .editorconfig ├── .eslintrc ├── .github └── FUNDING.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── package.json ├── src ├── SimpleMap.js ├── extendReactClass.js ├── generateAppendClassName.js ├── index.js ├── isIterable.js ├── linkClass.js ├── makeConfiguration.js ├── parseStyleName.js ├── renderNothing.js └── wrapStatelessFunction.js └── tests ├── SimpleMap.js ├── extendReactClass.js ├── linkClass.js ├── makeConfiguration.js ├── reactCssModules.js ├── renderNothing.js └── wrapStatelessFunction.js /.README/react-css-modules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gajus/react-css-modules/629e80e25069e1b7597da76534395c21a9d499d4/.README/react-css-modules.png -------------------------------------------------------------------------------- /.README/react-css-modules.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gajus/react-css-modules/629e80e25069e1b7597da76534395c21a9d499d4/.README/react-css-modules.sketch -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "add-module-exports", 4 | "lodash", 5 | "transform-class-properties", 6 | [ 7 | "transform-es2015-classes", 8 | { 9 | "loose": true 10 | } 11 | ], 12 | "transform-proto-to-assign" 13 | ], 14 | "presets": [ 15 | "es2015", 16 | "stage-0", 17 | "react" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "canonical", 4 | "canonical/mocha" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: gajus 2 | patreon: gajus 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | *.log 5 | .* 6 | !.babelrc 7 | !.editorconfig 8 | !.eslintrc 9 | !.gitignore 10 | !.npmignore 11 | !.README 12 | !.travis.yml 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | tests 3 | coverage 4 | .* 5 | *.log 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | - 8 5 | before_install: 6 | - npm config set depth 0 7 | notifications: 8 | email: false 9 | sudo: false 10 | script: 11 | - npm run test 12 | - npm run lint 13 | after_success: 14 | - semantic-release pre && npm publish && semantic-release post 15 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Gajus Kuizinas (http://gajus.com/) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the Gajus Kuizinas (http://gajus.com/) nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL ANUARY BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React CSS Modules 2 | 3 | [![GitSpo Mentions](https://gitspo.com/badges/mentions/gajus/react-css-modules?style=flat-square)](https://gitspo.com/mentions/gajus/react-css-modules) 4 | [![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) 5 | [![NPM version](http://img.shields.io/npm/v/react-css-modules.svg?style=flat-square)](https://www.npmjs.org/package/react-css-modules) 6 | [![js-canonical-style](https://img.shields.io/badge/code%20style-canonical-blue.svg?style=flat-square)](https://github.com/gajus/canonical) 7 | 8 | 9 | 10 | 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! 11 | 12 | > ## ⚠️⚠️⚠️ DEPRECATION NOTICE ⚠️⚠️⚠️ 13 | > 14 | > 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. 15 | > `babel-plugin-react-css-modules` is a lightweight alternative of `react-css-modules`. 16 | > 17 | > `babel-plugin-react-css-modules` is not a drop-in replacement and does not cover all the use cases of `react-css-modules`. 18 | > 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). 19 | > 20 | > It is easy to get started! See the demo https://github.com/gajus/babel-plugin-react-css-modules/tree/master/demo 21 | 22 | - [CSS Modules](#css-modules) 23 | - [webpack `css-loader`](#webpack-css-loader) 24 | - [What's the Problem?](#whats-the-problem) 25 | - [The Implementation](#the-implementation) 26 | - [Usage](#usage) 27 | - [Module Bundler](#module-bundler) 28 | - [webpack](#webpack) 29 | - [Development](#development) 30 | - [Production](#production) 31 | - [Browserify](#browserify) 32 | - [Extending Component Styles](#extending-component-styles) 33 | - [`styles` Property](#styles-property) 34 | - [Loops and Child Components](#loops-and-child-components) 35 | - [Decorator](#decorator) 36 | - [Options](#options) 37 | - [`allowMultiple`](#allowmultiple) 38 | - [`handleNotFoundStyleName`](#handlenotfoundstylename) 39 | - [SASS, SCSS, LESS and other CSS Preprocessors](#sass-scss-less-and-other-css-preprocessors) 40 | - [Enable Sourcemaps](#enable-sourcemaps) 41 | - [Class Composition](#class-composition) 42 | - [What Problems does Class Composition Solve?](#what-problems-does-class-composition-solve) 43 | - [Class Composition Using CSS Preprocessors](#class-composition-using-css-preprocessors) 44 | - [Global CSS](#global-css) 45 | - [Multiple CSS Modules](#multiple-css-modules) 46 | 47 | ## CSS Modules 48 | 49 | [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/). 50 | 51 | In the context of React, CSS Modules look like this: 52 | 53 | ```js 54 | import React from 'react'; 55 | import styles from './table.css'; 56 | 57 | export default class Table extends React.Component { 58 | render () { 59 | return
60 |
61 |
A0
62 |
B0
63 |
64 |
; 65 | } 66 | } 67 | ``` 68 | 69 | Rendering the component will produce a markup similar to: 70 | 71 | ```js 72 |
73 |
74 |
A0
75 |
B0
76 |
77 |
78 | ``` 79 | 80 | and a corresponding CSS file that matches those CSS classes. 81 | 82 | Awesome! 83 | 84 | ### webpack `css-loader` 85 | 86 | [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). 87 | 88 | ## What's the Problem? 89 | 90 | webpack [css-loader](https://github.com/webpack/css-loader#css-modules) itself has several disadvantages: 91 | 92 | * You have to use `camelCase` CSS class names. 93 | * You have to use `styles` object whenever constructing a `className`. 94 | * Mixing CSS Modules and global CSS classes is cumbersome. 95 | * Reference to an undefined CSS Module resolves to `undefined` without a warning. 96 | 97 | React CSS Modules component automates loading of CSS Modules using `styleName` property, e.g. 98 | 99 | ```js 100 | import React from 'react'; 101 | import CSSModules from 'react-css-modules'; 102 | import styles from './table.css'; 103 | 104 | class Table extends React.Component { 105 | render () { 106 | return
107 |
108 |
A0
109 |
B0
110 |
111 |
; 112 | } 113 | } 114 | 115 | export default CSSModules(Table, styles); 116 | ``` 117 | 118 | Using `react-css-modules`: 119 | 120 | * You are not forced to use the `camelCase` naming convention. 121 | * You do not need to refer to the `styles` object every time you use a CSS Module. 122 | * There is clear distinction between global CSS and CSS Modules, e.g. 123 | 124 | ```js 125 |
126 | ``` 127 | 128 | * You are warned when `styleName` refers to an undefined CSS Module ([`handleNotFoundStyleName`](#handlenotfoundstylename) option). 129 | * You can enforce use of a single CSS module per `ReactElement` ([`allowMultiple`](#allowmultiple) option). 130 | 131 | ## The Implementation 132 | 133 | `react-css-modules` extends `render` method of the target component. It will use the value of `styleName` to look for CSS Modules in the associated styles object and will append the matching unique CSS class names to the `ReactElement` `className` property value. 134 | 135 | [Awesome!](https://twitter.com/intent/retweet?tweet_id=636497036603428864) 136 | 137 | ## Usage 138 | 139 | Setup consists of: 140 | 141 | * Setting up a [module bundler](#module-bundler) to load the [Interoperable CSS](https://github.com/css-modules/icss). 142 | * [Decorating](#decorator) your component using `react-css-modules`. 143 | 144 | ### Module Bundler 145 | 146 | #### webpack 147 | 148 | ##### Development 149 | 150 | 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. 151 | 152 | Setup: 153 | 154 | * Install [`style-loader`](https://www.npmjs.com/package/style-loader). 155 | * Install [`css-loader`](https://www.npmjs.com/package/css-loader). 156 | * Setup `/\.css$/` loader: 157 | 158 | ```js 159 | { 160 | test: /\.css$/, 161 | loaders: [ 162 | 'style-loader?sourceMap', 163 | 'css-loader?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]' 164 | ] 165 | } 166 | ``` 167 | 168 | ##### Production 169 | 170 | In production environment, you want to extract chunks of CSS into a single stylesheet file. 171 | 172 | > Advantages: 173 | > 174 | > * Fewer style tags (older IE has a limit) 175 | > * CSS SourceMap (with `devtool: "source-map"` and `css-loader?sourceMap`) 176 | > * CSS requested in parallel 177 | > * CSS cached separate 178 | > * Faster runtime (less code and DOM operations) 179 | > 180 | > Caveats: 181 | > 182 | > * Additional HTTP request 183 | > * Longer compilation time 184 | > * More complex configuration 185 | > * No runtime public path modification 186 | > * No Hot Module Replacement 187 | 188 | – [extract-text-webpack-plugin](https://github.com/webpack/extract-text-webpack-plugin) 189 | 190 | Setup: 191 | 192 | * Install [`style-loader`](https://www.npmjs.com/package/style-loader). 193 | * Install [`css-loader`](https://www.npmjs.com/package/css-loader). 194 | * Use [`extract-text-webpack-plugin`](https://www.npmjs.com/package/extract-text-webpack-plugin) to extract chunks of CSS into a single stylesheet. 195 | 196 | * Setup `/\.css$/` loader: 197 | 198 | * ExtractTextPlugin v1x: 199 | 200 | ```js 201 | { 202 | test: /\.css$/, 203 | loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]') 204 | } 205 | ``` 206 | 207 | * ExtractTextPlugin v2x: 208 | 209 | ```js 210 | { 211 | test: /\.css$/, 212 | use: ExtractTextPlugin.extract({ 213 | fallback: 'style-loader', 214 | use: 'css-loader?modules,localIdentName="[name]-[local]-[hash:base64:6]"' 215 | }), 216 | } 217 | ``` 218 | 219 | * Setup `extract-text-webpack-plugin` plugin: 220 | 221 | * ExtractTextPlugin v1x: 222 | 223 | ```js 224 | new ExtractTextPlugin('app.css', { 225 | allChunks: true 226 | }) 227 | ``` 228 | 229 | * ExtractTextPlugin v2x: 230 | 231 | ```js 232 | new ExtractTextPlugin({ 233 | filename: 'app.css', 234 | allChunks: true 235 | }) 236 | ``` 237 | 238 | 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. 239 | 240 | ##### Browserify 241 | 242 | Refer to [`css-modulesify`](https://github.com/css-modules/css-modulesify). 243 | 244 | ### Extending Component Styles 245 | 246 | Use `styles` property to overwrite the default component styles. 247 | 248 | Explanation using `Table` component: 249 | 250 | ```js 251 | import React from 'react'; 252 | import CSSModules from 'react-css-modules'; 253 | import styles from './table.css'; 254 | 255 | class Table extends React.Component { 256 | render () { 257 | return
258 |
259 |
A0
260 |
B0
261 |
262 |
; 263 | } 264 | } 265 | 266 | export default CSSModules(Table, styles); 267 | ``` 268 | 269 | In this example, `CSSModules` is used to decorate `Table` component using `./table.css` CSS Modules. When `Table` component is rendered, it will use the properties of the `styles` object to construct `className` values. 270 | 271 | Using `styles` property you can overwrite the default component `styles` object, e.g. 272 | 273 | ```js 274 | import customStyles from './table-custom-styles.css'; 275 | 276 | ; 277 | ``` 278 | 279 | [Interoperable CSS](https://github.com/css-modules/icss) can [extend other ICSS](https://github.com/css-modules/css-modules#dependencies). Use this feature to extend default styles, e.g. 280 | 281 | ```css 282 | /* table-custom-styles.css */ 283 | .table { 284 | composes: table from './table.css'; 285 | } 286 | 287 | .row { 288 | composes: row from './table.css'; 289 | } 290 | 291 | /* .cell { 292 | composes: cell from './table.css'; 293 | } */ 294 | 295 | .table { 296 | width: 400px; 297 | } 298 | 299 | .cell { 300 | float: left; width: 154px; background: #eee; padding: 10px; margin: 10px 0 10px 10px; 301 | } 302 | ``` 303 | 304 | In this example, `table-custom-styles.css` selectively extends `table.css` (the default styles of `Table` component). 305 | 306 | Refer to the [`UsingStylesProperty` example](https://github.com/gajus/react-css-modules-examples/tree/master/src/UsingStylesProperty) for an example of a working implementation. 307 | 308 | ### `styles` Property 309 | 310 | Decorated components inherit `styles` property that describes the mapping between CSS modules and CSS classes. 311 | 312 | ```js 313 | class extends React.Component { 314 | render () { 315 |
316 |

317 |

318 |
; 319 | } 320 | } 321 | ``` 322 | 323 | In the above example, `styleName='foo'` and `className={this.props.styles.foo}` are equivalent. 324 | 325 | `styles` property is designed to enable component decoration of [Loops and Child Components](#loops-and-child-components). 326 | 327 | ### Loops and Child Components 328 | 329 | `styleName` cannot be used to define styles of a `ReactElement` that will be generated by another component, e.g. 330 | 331 | ```js 332 | import React from 'react'; 333 | import CSSModules from 'react-css-modules'; 334 | import List from './List'; 335 | import styles from './table.css'; 336 | 337 | class CustomList extends React.Component { 338 | render () { 339 | let itemTemplate; 340 | 341 | itemTemplate = (name) => { 342 | return
  • {name}
  • ; 343 | }; 344 | 345 | return ; 346 | } 347 | } 348 | 349 | export default CSSModules(CustomList, styles); 350 | ``` 351 | 352 | The above example will not work. `CSSModules` is used to decorate `CustomList` component. However, it is the `List` component that will render `itemTemplate`. 353 | 354 | 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: 355 | 356 | ```js 357 | import React from 'react'; 358 | import CSSModules from 'react-css-modules'; 359 | import List from './List'; 360 | import styles from './table.css'; 361 | 362 | class CustomList extends React.Component { 363 | render () { 364 | let itemTemplate; 365 | 366 | itemTemplate = (name) => { 367 | return
  • {name}
  • ; 368 | }; 369 | 370 | return ; 371 | } 372 | } 373 | 374 | export default CSSModules(CustomList, styles); 375 | ``` 376 | 377 | 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. 378 | 379 | ```js 380 | import React from 'react'; 381 | import CSSModules from 'react-css-modules'; 382 | import List from './List'; 383 | import styles from './table.css'; 384 | 385 | class CustomList extends React.Component { 386 | render () { 387 | let itemTemplate; 388 | 389 | itemTemplate = (name) => { 390 | return
  • {name}
  • ; 391 | }; 392 | 393 | itemTemplate = CSSModules(itemTemplate, this.props.styles); 394 | 395 | return ; 396 | } 397 | } 398 | 399 | export default CSSModules(CustomList, styles); 400 | ``` 401 | 402 | ### Decorator 403 | 404 | ```js 405 | /** 406 | * @typedef CSSModules~Options 407 | * @see {@link https://github.com/gajus/react-css-modules#options} 408 | * @property {Boolean} allowMultiple 409 | * @property {String} handleNotFoundStyleName 410 | */ 411 | 412 | /** 413 | * @param {Function} Component 414 | * @param {Object} defaultStyles CSS Modules class map. 415 | * @param {CSSModules~Options} options 416 | * @return {Function} 417 | */ 418 | ``` 419 | 420 | You need to decorate your component using `react-css-modules`, e.g. 421 | 422 | ```js 423 | import React from 'react'; 424 | import CSSModules from 'react-css-modules'; 425 | import styles from './table.css'; 426 | 427 | class Table extends React.Component { 428 | render () { 429 | return
    430 |
    431 |
    A0
    432 |
    B0
    433 |
    434 |
    ; 435 | } 436 | } 437 | 438 | export default CSSModules(Table, styles); 439 | ``` 440 | 441 | Thats it! 442 | 443 | As the name implies, `react-css-modules` is compatible with the [ES7 decorators](https://github.com/wycats/javascript-decorators) syntax: 444 | 445 | ```js 446 | import React from 'react'; 447 | import CSSModules from 'react-css-modules'; 448 | import styles from './table.css'; 449 | 450 | @CSSModules(styles) 451 | export default class extends React.Component { 452 | render () { 453 | return
    454 |
    455 |
    A0
    456 |
    B0
    457 |
    458 |
    ; 459 | } 460 | } 461 | ``` 462 | 463 | [Awesome!](https://twitter.com/intent/retweet?tweet_id=636497036603428864) 464 | 465 | Refer to the [react-css-modules-examples](https://github.com/gajus/react-css-modules-examples) repository for an example of webpack setup. 466 | 467 | ### Options 468 | 469 | Options are supplied as the third parameter to the `CSSModules` function. 470 | 471 | ```js 472 | CSSModules(Component, styles, options); 473 | ``` 474 | 475 | or as a second parameter to the decorator: 476 | 477 | ```js 478 | @CSSModules(styles, options); 479 | ``` 480 | 481 | #### `allowMultiple` 482 | 483 | Default: `false`. 484 | 485 | Allows multiple CSS Module names. 486 | 487 | When `false`, the following will cause an error: 488 | 489 | ```js 490 |
    491 | ``` 492 | 493 | #### `handleNotFoundStyleName` 494 | 495 | Default: `throw`. 496 | 497 | Defines the desired action when `styleName` cannot be mapped to an existing CSS Module. 498 | 499 | Available options: 500 | 501 | * `throw` throws an error 502 | * `log` logs a warning using `console.warn` 503 | * `ignore` silently ignores the missing style name 504 | 505 | ## SASS, SCSS, LESS and other CSS Preprocessors 506 | 507 | [Interoperable CSS](https://github.com/css-modules/icss) is compatible with the CSS preprocessors. To use a preprocessor, all you need to do is add the preprocessor to the chain of loaders, e.g. in the case of webpack it is as simple as installing `sass-loader` and adding `!sass` to the end of the `style-loader` loader query (loaders are processed from right to left): 508 | 509 | ```js 510 | { 511 | test: /\.scss$/, 512 | loaders: [ 513 | 'style', 514 | 'css?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]', 515 | 'resolve-url', 516 | 'sass' 517 | ] 518 | } 519 | ``` 520 | 521 | ### Enable Sourcemaps 522 | 523 | To enable CSS Source maps, add `sourceMap` parameter to the css-loader and to the `sass-loader`: 524 | 525 | ```js 526 | { 527 | test: /\.scss$/, 528 | loaders: [ 529 | 'style?sourceMap', 530 | 'css?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]', 531 | 'resolve-url', 532 | 'sass?sourceMap' 533 | ] 534 | } 535 | ``` 536 | 537 | ## Class Composition 538 | 539 | CSS Modules promote composition pattern, i.e. every CSS Module that is used in a component should define all properties required to describe an element, e.g. 540 | 541 | ```css 542 | .box { 543 | width: 100px; 544 | height: 100px; 545 | } 546 | 547 | .empty { 548 | composes: box; 549 | 550 | background: #4CAF50; 551 | } 552 | 553 | .full { 554 | composes: box; 555 | 556 | background: #F44336; 557 | } 558 | ``` 559 | 560 | Composition promotes better separation of markup and style using semantics that would be hard to achieve without CSS Modules. 561 | 562 | Because CSS Module names are local, it is perfectly fine to use generic style names such as "empty" or "full", without "box-" prefix. 563 | 564 | To learn more about composing CSS rules, I suggest reading Glen Maddern article about [CSS Modules](http://glenmaddern.com/articles/css-modules) and the official [spec of the CSS Modules](https://github.com/css-modules/css-modules). 565 | 566 | ### What Problems does Class Composition Solve? 567 | 568 | Consider the same example in CSS and HTML: 569 | 570 | ```css 571 | .box { 572 | width: 100px; 573 | height: 100px; 574 | } 575 | 576 | .box-empty { 577 | background: #4CAF50; 578 | } 579 | 580 | .box-full { 581 | background: #F44336; 582 | } 583 | ``` 584 | 585 | ```html 586 |
    587 | ``` 588 | 589 | This pattern emerged with the advent of OOCSS. The biggest disadvantage of this implementation is that you will need to change HTML almost every time you want to change the style. 590 | 591 | ### Class Composition Using CSS Preprocessors 592 | 593 | 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. 594 | 595 | 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. 596 | 597 | Using `@extend`: 598 | 599 | ```css 600 | %box { 601 | width: 100px; 602 | height: 100px; 603 | } 604 | 605 | .box-empty { 606 | @extend %box; 607 | 608 | background: #4CAF50; 609 | } 610 | 611 | .box-full { 612 | @extend %box; 613 | 614 | background: #F44336; 615 | } 616 | ``` 617 | 618 | This translates to: 619 | 620 | ```css 621 | .box-empty, 622 | .box-full { 623 | width: 100px; 624 | height: 100px; 625 | } 626 | 627 | .box-empty { 628 | background: #4CAF50; 629 | } 630 | 631 | .box-full { 632 | background: #F44336; 633 | } 634 | ``` 635 | 636 | Using mixins: 637 | 638 | ```css 639 | @mixin box { 640 | width: 100px; 641 | height: 100px; 642 | } 643 | 644 | .box-empty { 645 | @include box; 646 | 647 | background: #4CAF50; 648 | } 649 | 650 | .box-full { 651 | @include box; 652 | 653 | background: #F44336; 654 | } 655 | ``` 656 | 657 | This translates to: 658 | 659 | ```css 660 | .box-empty { 661 | width: 100px; 662 | height: 100px; 663 | background: #4CAF50; 664 | } 665 | 666 | .box-full { 667 | width: 100px; 668 | height: 100px; 669 | background: #F44336; 670 | } 671 | ``` 672 | 673 | ## Global CSS 674 | 675 | CSS Modules does not restrict you from using global CSS. 676 | 677 | ```css 678 | :global .foo { 679 | 680 | } 681 | ``` 682 | 683 | However, use global CSS with caution. With CSS Modules, there are only a handful of valid use cases for global CSS (e.g. [normalization](https://github.com/necolas/normalize.css/)). 684 | 685 | ## Multiple CSS Modules 686 | 687 | Avoid using multiple CSS Modules to describe a single element. Read about [Class Composition](#class-composition). 688 | 689 | That said, if you require to use multiple CSS Modules to describe an element, enable the [`allowMultiple`](#allowmultiple) option. When multiple CSS Modules are used to describe an element, `react-css-modules` will append a unique class name for every CSS Module it matches in the `styleName` declaration, e.g. 690 | 691 | ```css 692 | .button { 693 | 694 | } 695 | 696 | .active { 697 | 698 | } 699 | ``` 700 | 701 | ```js 702 |
    703 | ``` 704 | 705 | This will map both [Interoperable CSS](https://github.com/css-modules/icss) CSS classes to the target element. 706 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-css-modules", 3 | "description": "Seamless mapping of class names to CSS modules inside of React components.", 4 | "main": "./dist/index.js", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/gajus/react-css-modules" 8 | }, 9 | "keywords": [ 10 | "react-component", 11 | "react", 12 | "css", 13 | "modules" 14 | ], 15 | "version": "4.3.0", 16 | "author": { 17 | "name": "Gajus Kuizinas", 18 | "email": "gajus@gajus.com", 19 | "url": "http://gajus.com" 20 | }, 21 | "license": "BSD-3-Clause", 22 | "dependencies": { 23 | "hoist-non-react-statics": "^2.5.5", 24 | "lodash": "^4.16.6", 25 | "object-unfreeze": "^1.1.0" 26 | }, 27 | "devDependencies": { 28 | "babel-cli": "^6.18.0", 29 | "babel-plugin-add-module-exports": "^0.2.1", 30 | "babel-plugin-lodash": "^3.2.9", 31 | "babel-plugin-transform-proto-to-assign": "^6.9.0", 32 | "babel-preset-es2015": "^6.18.0", 33 | "babel-preset-react": "^6.16.0", 34 | "babel-preset-stage-0": "^6.16.0", 35 | "babel-register": "^6.18.0", 36 | "chai": "^4.0.0-canary.1", 37 | "chai-spies": "^0.7.1", 38 | "eslint": "^3.10.0", 39 | "eslint-config-canonical": "^5.5.0", 40 | "husky": "^0.11.9", 41 | "jsdom": "^9.8.3", 42 | "mocha": "^3.1.2", 43 | "react": "^15.4.0-rc.4", 44 | "react-addons-shallow-compare": "^15.4.0-rc.4", 45 | "react-addons-test-utils": "^15.4.0-rc.4", 46 | "react-dom": "^15.4.0-rc.4", 47 | "semantic-release": "^6.3.2" 48 | }, 49 | "scripts": { 50 | "lint": "eslint ./src ./tests", 51 | "test": "NODE_ENV=development mocha --compilers js:babel-register ./tests/**/*.js && npm run lint && npm run build", 52 | "build": "NODE_ENV=production babel ./src --out-dir ./dist", 53 | "precommit": "npm run test" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/SimpleMap.js: -------------------------------------------------------------------------------- 1 | export default class { 2 | constructor () { 3 | this.size = 0; 4 | this.keys = []; 5 | this.values = []; 6 | } 7 | 8 | get (key) { 9 | const index = this.keys.indexOf(key); 10 | 11 | return this.values[index]; 12 | } 13 | 14 | set (key, value) { 15 | this.keys.push(key); 16 | this.values.push(value); 17 | this.size = this.keys.length; 18 | 19 | return value; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/extendReactClass.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | 3 | import _ from 'lodash'; 4 | import React from 'react'; 5 | import hoistNonReactStatics from 'hoist-non-react-statics'; 6 | import linkClass from './linkClass'; 7 | import renderNothing from './renderNothing'; 8 | 9 | /** 10 | * @param {ReactClass} Component 11 | * @param {Object} defaultStyles 12 | * @param {Object} options 13 | * @returns {ReactClass} 14 | */ 15 | export default (Component: Object, defaultStyles: Object, options: Object) => { 16 | const WrappedComponent = class extends Component { 17 | render () { 18 | let styles; 19 | 20 | const hasDefaultstyles = _.isObject(defaultStyles); 21 | 22 | let renderResult; 23 | 24 | if (this.props.styles || hasDefaultstyles) { 25 | const props = Object.assign({}, this.props); 26 | 27 | if (props.styles) { 28 | styles = props.styles; 29 | } else if (hasDefaultstyles) { 30 | styles = defaultStyles; 31 | delete props.styles; 32 | } 33 | 34 | Object.defineProperty(props, 'styles', { 35 | configurable: true, 36 | enumerable: false, 37 | value: styles, 38 | writable: false 39 | }); 40 | 41 | const originalProps = this.props; 42 | 43 | let renderIsSuccessful = false; 44 | 45 | try { 46 | this.props = props; 47 | 48 | renderResult = super.render(); 49 | 50 | renderIsSuccessful = true; 51 | } finally { 52 | this.props = originalProps; 53 | } 54 | 55 | // @see https://github.com/facebook/react/issues/14224 56 | if (!renderIsSuccessful) { 57 | renderResult = super.render(); 58 | } 59 | } else { 60 | styles = {}; 61 | 62 | renderResult = super.render(); 63 | } 64 | 65 | if (renderResult) { 66 | return linkClass(renderResult, styles, options); 67 | } 68 | 69 | return renderNothing(React.version); 70 | } 71 | }; 72 | 73 | return hoistNonReactStatics(WrappedComponent, Component); 74 | }; 75 | -------------------------------------------------------------------------------- /src/generateAppendClassName.js: -------------------------------------------------------------------------------- 1 | import SimpleMap from './SimpleMap'; 2 | 3 | const CustomMap = typeof Map === 'undefined' ? SimpleMap : Map; 4 | 5 | const stylesIndex = new CustomMap(); 6 | 7 | export default (styles, styleNames: Array, handleNotFoundStyleName: "throw" | "log" | "ignore"): string => { 8 | let appendClassName; 9 | let stylesIndexMap; 10 | 11 | stylesIndexMap = stylesIndex.get(styles); 12 | 13 | if (stylesIndexMap) { 14 | const styleNameIndex = stylesIndexMap.get(styleNames); 15 | 16 | if (styleNameIndex) { 17 | return styleNameIndex; 18 | } 19 | } else { 20 | stylesIndexMap = new CustomMap(); 21 | stylesIndex.set(styles, new CustomMap()); 22 | } 23 | 24 | appendClassName = ''; 25 | 26 | for (const styleName in styleNames) { 27 | if (styleNames.hasOwnProperty(styleName)) { 28 | const className = styles[styleNames[styleName]]; 29 | 30 | if (className) { 31 | appendClassName += ' ' + className; 32 | } else { 33 | if (handleNotFoundStyleName === 'throw') { 34 | throw new Error('"' + styleNames[styleName] + '" CSS module is undefined.'); 35 | } 36 | if (handleNotFoundStyleName === 'log') { 37 | // eslint-disable-next-line no-console 38 | console.warn('"' + styleNames[styleName] + '" CSS module is undefined.'); 39 | } 40 | } 41 | } 42 | } 43 | 44 | appendClassName = appendClassName.trim(); 45 | 46 | stylesIndexMap.set(styleNames, appendClassName); 47 | 48 | return appendClassName; 49 | }; 50 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import extendReactClass from './extendReactClass'; 3 | import wrapStatelessFunction from './wrapStatelessFunction'; 4 | import makeConfiguration from './makeConfiguration'; 5 | 6 | /** 7 | * @see https://github.com/gajus/react-css-modules#options 8 | */ 9 | type TypeOptions = {}; 10 | 11 | /** 12 | * Determines if the given object has the signature of a class that inherits React.Component. 13 | */ 14 | const isReactComponent = (maybeReactComponent: any): boolean => { 15 | return 'prototype' in maybeReactComponent && _.isFunction(maybeReactComponent.prototype.render); 16 | }; 17 | 18 | /** 19 | * When used as a function. 20 | */ 21 | const functionConstructor = (Component: Function, defaultStyles: Object, options: TypeOptions): Function => { 22 | let decoratedClass; 23 | 24 | const configuration = makeConfiguration(options); 25 | 26 | if (isReactComponent(Component)) { 27 | decoratedClass = extendReactClass(Component, defaultStyles, configuration); 28 | } else { 29 | decoratedClass = wrapStatelessFunction(Component, defaultStyles, configuration); 30 | } 31 | 32 | if (Component.displayName) { 33 | decoratedClass.displayName = Component.displayName; 34 | } else { 35 | decoratedClass.displayName = Component.name; 36 | } 37 | 38 | return decoratedClass; 39 | }; 40 | 41 | /** 42 | * When used as a ES7 decorator. 43 | */ 44 | const decoratorConstructor = (defaultStyles: Object, options: TypeOptions): Function => { 45 | return (Component: Function) => { 46 | return functionConstructor(Component, defaultStyles, options); 47 | }; 48 | }; 49 | 50 | export default (...args) => { 51 | if (_.isFunction(args[0])) { 52 | return functionConstructor(args[0], args[1], args[2]); 53 | } else { 54 | return decoratorConstructor(args[0], args[1]); 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/isIterable.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | const ITERATOR_SYMBOL = typeof Symbol !== 'undefined' && _.isFunction(Symbol) && Symbol.iterator; 4 | const OLD_ITERATOR_SYMBOL = '@@iterator'; 5 | 6 | /** 7 | * @see https://github.com/lodash/lodash/issues/1668 8 | * @see https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Iteration_protocols 9 | */ 10 | export default (maybeIterable: any): boolean => { 11 | let iterator; 12 | 13 | if (!_.isObject(maybeIterable)) { 14 | return false; 15 | } 16 | 17 | if (ITERATOR_SYMBOL) { 18 | iterator = maybeIterable[ITERATOR_SYMBOL]; 19 | } else { 20 | iterator = maybeIterable[OLD_ITERATOR_SYMBOL]; 21 | } 22 | 23 | return _.isFunction(iterator); 24 | }; 25 | -------------------------------------------------------------------------------- /src/linkClass.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React, { 3 | ReactElement 4 | } from 'react'; 5 | import objectUnfreeze from 'object-unfreeze'; 6 | import isIterable from './isIterable'; 7 | import parseStyleName from './parseStyleName'; 8 | import generateAppendClassName from './generateAppendClassName'; 9 | 10 | const linkArray = (array: Array, styles: Object, configuration: Object) => { 11 | _.forEach(array, (value, index) => { 12 | if (React.isValidElement(value)) { 13 | // eslint-disable-next-line no-use-before-define 14 | array[index] = linkElement(React.Children.only(value), styles, configuration); 15 | } else if (_.isArray(value)) { 16 | const unfreezedValue = Object.isFrozen(value) ? objectUnfreeze(value) : value; 17 | 18 | array[index] = linkArray(unfreezedValue, styles, configuration); 19 | } 20 | }); 21 | 22 | return array; 23 | }; 24 | 25 | const linkElement = (element: ReactElement, styles: Object, configuration: Object): ReactElement => { 26 | let appendClassName; 27 | let elementShallowCopy; 28 | 29 | elementShallowCopy = element; 30 | 31 | if (Array.isArray(elementShallowCopy)) { 32 | return elementShallowCopy.map((arrayElement) => { 33 | return linkElement(arrayElement, styles, configuration); 34 | }); 35 | } 36 | 37 | const elementIsFrozen = Object.isFrozen && Object.isFrozen(elementShallowCopy); 38 | const propsFrozen = Object.isFrozen && Object.isFrozen(elementShallowCopy.props); 39 | const propsNotExtensible = Object.isExtensible && !Object.isExtensible(elementShallowCopy.props); 40 | 41 | if (elementIsFrozen) { 42 | // https://github.com/facebook/react/blob/v0.13.3/src/classic/element/ReactElement.js#L131 43 | elementShallowCopy = objectUnfreeze(elementShallowCopy); 44 | elementShallowCopy.props = objectUnfreeze(elementShallowCopy.props); 45 | } else if (propsFrozen || propsNotExtensible) { 46 | elementShallowCopy.props = objectUnfreeze(elementShallowCopy.props); 47 | } 48 | 49 | const styleNames = parseStyleName(elementShallowCopy.props.styleName || '', configuration.allowMultiple); 50 | const {children, ...restProps} = elementShallowCopy.props; 51 | 52 | if (React.isValidElement(children)) { 53 | elementShallowCopy.props.children = linkElement(React.Children.only(children), styles, configuration); 54 | } else if (_.isArray(children) || isIterable(children)) { 55 | elementShallowCopy.props.children = linkArray(objectUnfreeze(children), styles, configuration); 56 | } 57 | 58 | _.forEach(restProps, (propValue, propName) => { 59 | if (React.isValidElement(propValue)) { 60 | elementShallowCopy.props[propName] = linkElement(React.Children.only(propValue), styles, configuration); 61 | } else if (_.isArray(propValue)) { 62 | elementShallowCopy.props[propName] = linkArray(propValue, styles, configuration); 63 | } 64 | }); 65 | 66 | if (styleNames.length) { 67 | appendClassName = generateAppendClassName(styles, styleNames, configuration.handleNotFoundStyleName); 68 | 69 | if (appendClassName) { 70 | if (elementShallowCopy.props.className) { 71 | appendClassName = elementShallowCopy.props.className + ' ' + appendClassName; 72 | } 73 | 74 | elementShallowCopy.props.className = appendClassName; 75 | } 76 | } 77 | 78 | delete elementShallowCopy.props.styleName; 79 | 80 | if (elementIsFrozen) { 81 | Object.freeze(elementShallowCopy.props); 82 | Object.freeze(elementShallowCopy); 83 | } else if (propsFrozen) { 84 | Object.freeze(elementShallowCopy.props); 85 | } 86 | 87 | if (propsNotExtensible) { 88 | Object.preventExtensions(elementShallowCopy.props); 89 | } 90 | 91 | return elementShallowCopy; 92 | }; 93 | 94 | /** 95 | * @param {ReactElement} element 96 | * @param {Object} styles CSS modules class map. 97 | * @param {CSSModules~Options} configuration 98 | */ 99 | export default (element: ReactElement, styles = {}, configuration = {}): ReactElement => { 100 | // @see https://github.com/gajus/react-css-modules/pull/30 101 | if (!_.isObject(element)) { 102 | return element; 103 | } 104 | 105 | return linkElement(element, styles, configuration); 106 | }; 107 | -------------------------------------------------------------------------------- /src/makeConfiguration.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | /** 4 | * @typedef CSSModules~Options 5 | * @see {@link https://github.com/gajus/react-css-modules#options} 6 | * @property {boolean} allowMultiple 7 | * @property {string} handleNotFoundStyleName 8 | */ 9 | 10 | /** 11 | * @param {CSSModules~Options} userConfiguration 12 | * @returns {CSSModules~Options} 13 | */ 14 | export default (userConfiguration = {}) => { 15 | const configuration = { 16 | allowMultiple: false, 17 | handleNotFoundStyleName: 'throw' 18 | }; 19 | 20 | _.forEach(userConfiguration, (value, name) => { 21 | if (_.isUndefined(configuration[name])) { 22 | throw new Error('Unknown configuration property "' + name + '".'); 23 | } 24 | 25 | if (name === 'allowMultiple' && !_.isBoolean(value)) { 26 | throw new Error('"allowMultiple" property value must be a boolean.'); 27 | } 28 | 29 | if (name === 'handleNotFoundStyleName' && !_.includes(['throw', 'log', 'ignore'], value)) { 30 | throw new Error('"handleNotFoundStyleName" property value must be "throw", "log" or "ignore".'); 31 | } 32 | 33 | configuration[name] = value; 34 | }); 35 | 36 | return configuration; 37 | }; 38 | -------------------------------------------------------------------------------- /src/parseStyleName.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | const styleNameIndex = {}; 4 | 5 | export default (styleNamePropertyValue: string, allowMultiple: boolean): Array => { 6 | let styleNames; 7 | 8 | if (styleNameIndex[styleNamePropertyValue]) { 9 | styleNames = styleNameIndex[styleNamePropertyValue]; 10 | } else { 11 | styleNames = _.trim(styleNamePropertyValue).split(/\s+/); 12 | styleNames = _.filter(styleNames); 13 | 14 | styleNameIndex[styleNamePropertyValue] = styleNames; 15 | } 16 | 17 | if (allowMultiple === false && styleNames.length > 1) { 18 | throw new Error('ReactElement styleName property defines multiple module names ("' + styleNamePropertyValue + '").'); 19 | } 20 | 21 | return styleNames; 22 | }; 23 | -------------------------------------------------------------------------------- /src/renderNothing.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function (version) { 4 | const major = version.split('.')[0]; 5 | 6 | return parseInt(major, 10) < 15 ? React.createElement('noscript') : null; 7 | } 8 | -------------------------------------------------------------------------------- /src/wrapStatelessFunction.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | 3 | import _ from 'lodash'; 4 | import React from 'react'; 5 | import linkClass from './linkClass'; 6 | import renderNothing from './renderNothing'; 7 | 8 | /** 9 | * @see https://facebook.github.io/react/blog/2015/09/10/react-v0.14-rc1.html#stateless-function-components 10 | */ 11 | export default (Component: Function, defaultStyles: Object, options: Object): Function => { 12 | const WrappedComponent = (props = {}, ...args) => { 13 | let styles; 14 | let useProps; 15 | const hasDefaultstyles = _.isObject(defaultStyles); 16 | 17 | if (props.styles || hasDefaultstyles) { 18 | useProps = Object.assign({}, props); 19 | 20 | if (props.styles) { 21 | styles = props.styles; 22 | } else { 23 | styles = defaultStyles; 24 | } 25 | 26 | Object.defineProperty(useProps, 'styles', { 27 | configurable: true, 28 | enumerable: false, 29 | value: styles, 30 | writable: false 31 | }); 32 | } else { 33 | useProps = props; 34 | styles = {}; 35 | } 36 | 37 | const renderResult = Component(useProps, ...args); 38 | 39 | if (renderResult) { 40 | return linkClass(renderResult, styles, options); 41 | } 42 | 43 | return renderNothing(React.version); 44 | }; 45 | 46 | _.assign(WrappedComponent, Component); 47 | 48 | return WrappedComponent; 49 | }; 50 | -------------------------------------------------------------------------------- /tests/SimpleMap.js: -------------------------------------------------------------------------------- 1 | import { 2 | expect 3 | } from 'chai'; 4 | import SimpleMap from './../src/SimpleMap'; 5 | 6 | describe('SimpleMap', () => { 7 | context('simple map with primitive or object as keys', () => { 8 | const values = [ 9 | [1, 'something'], 10 | ['1', 'somethingElse'], 11 | [{}, []], 12 | [null, null] 13 | ]; 14 | 15 | let map; 16 | 17 | beforeEach(() => { 18 | map = new SimpleMap(); 19 | }); 20 | 21 | it('should set', () => { 22 | values.forEach(([key, value]) => { 23 | map.set(key, value); 24 | }); 25 | expect(map.size).to.equal(values.length); 26 | }); 27 | 28 | it('should get', () => { 29 | values.forEach(([key, value]) => { 30 | map.set(key, value); 31 | }); 32 | 33 | values.forEach(([key, value]) => { 34 | expect(map.get(key)).to.equal(value); 35 | }); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/extendReactClass.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-nested-callbacks, react/prefer-stateless-function, react/prop-types, react/no-multi-comp, class-methods-use-this */ 2 | 3 | import { 4 | expect 5 | } from 'chai'; 6 | import React from 'react'; 7 | import TestUtils from 'react-addons-test-utils'; 8 | import shallowCompare from 'react-addons-shallow-compare'; 9 | import jsdom from 'jsdom'; 10 | import extendReactClass from './../src/extendReactClass'; 11 | 12 | describe('extendReactClass', () => { 13 | beforeEach(() => { 14 | global.document = jsdom.jsdom(''); 15 | 16 | global.window = document.defaultView; 17 | }); 18 | context('using default styles', () => { 19 | it('exposes styles through this.props.styles property', (done) => { 20 | let Component; 21 | 22 | const styles = { 23 | foo: 'foo-1' 24 | }; 25 | 26 | Component = class extends React.Component { 27 | render () { 28 | expect(this.props.styles).to.equal(styles); 29 | done(); 30 | } 31 | }; 32 | 33 | Component = extendReactClass(Component, styles); 34 | 35 | TestUtils.renderIntoDocument(); 36 | }); 37 | it('exposes non-enumerable styles property', (done) => { 38 | let Component; 39 | 40 | const styles = { 41 | foo: 'foo-1' 42 | }; 43 | 44 | Component = class extends React.Component { 45 | render () { 46 | expect(this.props.propertyIsEnumerable('styles')).to.equal(false); 47 | done(); 48 | } 49 | }; 50 | 51 | Component = extendReactClass(Component, styles); 52 | 53 | TestUtils.renderIntoDocument(); 54 | }); 55 | it('does not affect the other instance properties', (done) => { 56 | let Component; 57 | 58 | Component = class extends React.Component { 59 | render () { 60 | expect(this.props.bar).to.equal('baz'); 61 | done(); 62 | } 63 | }; 64 | 65 | const styles = { 66 | foo: 'foo-1' 67 | }; 68 | 69 | Component = extendReactClass(Component, styles); 70 | 71 | TestUtils.renderIntoDocument(); 72 | }); 73 | it('does not affect pure-render logic', (done) => { 74 | let Component; 75 | let rendered; 76 | 77 | rendered = false; 78 | 79 | const styles = { 80 | foo: 'foo-1' 81 | }; 82 | 83 | Component = class extends React.Component { 84 | shouldComponentUpdate (newProps) { 85 | if (rendered) { 86 | expect(shallowCompare(this.props, newProps)).to.equal(true); 87 | 88 | done(); 89 | } 90 | 91 | return true; 92 | } 93 | 94 | render () { 95 | rendered = true; 96 | } 97 | }; 98 | 99 | Component = extendReactClass(Component, styles); 100 | 101 | const instance = TestUtils.renderIntoDocument(); 102 | 103 | // trigger shouldComponentUpdate 104 | instance.setState({}); 105 | }); 106 | }); 107 | context('overwriting default styles using "styles" property of the extended component', () => { 108 | it('overwrites default styles', (done) => { 109 | let Component; 110 | 111 | const styles = { 112 | foo: 'foo-1' 113 | }; 114 | 115 | Component = class extends React.Component { 116 | render () { 117 | expect(this.props.styles).to.equal(styles); 118 | done(); 119 | } 120 | }; 121 | 122 | Component = extendReactClass(Component, { 123 | bar: 'bar-0', 124 | foo: 'foo-0' 125 | }); 126 | 127 | TestUtils.renderIntoDocument(); 128 | }); 129 | }); 130 | context('rendering Component that returns null', () => { 131 | it('generates null', () => { 132 | let Component; 133 | 134 | const shallowRenderer = TestUtils.createRenderer(); 135 | 136 | Component = class extends React.Component { 137 | render () { 138 | return null; 139 | } 140 | }; 141 | 142 | Component = extendReactClass(Component); 143 | 144 | shallowRenderer.render(); 145 | 146 | const component = shallowRenderer.getRenderOutput(); 147 | 148 | expect(component).to.equal(null); 149 | }); 150 | }); 151 | context('target component have static properties', () => { 152 | it('hoists static properties', () => { 153 | const Component = class extends React.Component { 154 | static foo = 'FOO'; 155 | 156 | render () { 157 | return null; 158 | } 159 | }; 160 | 161 | const WrappedComponent = extendReactClass(Component); 162 | 163 | expect(Component.foo).to.equal('FOO'); 164 | expect(WrappedComponent.foo).to.equal(Component.foo); 165 | }); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /tests/linkClass.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-nested-callbacks, react/prefer-stateless-function, class-methods-use-this, no-console, no-unused-expressions */ 2 | 3 | import chai, { 4 | expect 5 | } from 'chai'; 6 | import spies from 'chai-spies'; 7 | import React from 'react'; 8 | import TestUtils from 'react-addons-test-utils'; 9 | import jsdom from 'jsdom'; 10 | import linkClass from './../src/linkClass'; 11 | 12 | chai.use(spies); 13 | 14 | describe('linkClass', () => { 15 | context('ReactElement does not define styleName', () => { 16 | it('does not affect element properties', () => { 17 | expect(linkClass(
    )).to.deep.equal(
    ); 18 | }); 19 | 20 | it('does not affect element properties with a single element child', () => { 21 | expect(linkClass(

    )).to.deep.equal(

    ); 22 | }); 23 | 24 | it('does not affect element properties with a single element child in non-`children` prop', () => { 25 | expect(linkClass(
    } />)).to.deep.equal(
    } />); 26 | }); 27 | 28 | it('does not affect element properties with a single text child', () => { 29 | expect(linkClass(
    test
    )).to.deep.equal(
    test
    ); 30 | }); 31 | 32 | it('does not affect the className', () => { 33 | expect(linkClass(
    )).to.deep.equal(
    ); 34 | }); 35 | 36 | xit('does not affect element with a single children when that children is contained in an array', () => { 37 | const subject = React.createElement('div', null, [ 38 | React.createElement('p') 39 | ]); 40 | const outcome = React.createElement('div', null, [ 41 | React.createElement('p') 42 | ]); 43 | 44 | expect(linkClass(subject)).to.deep.equal(outcome); 45 | }); 46 | 47 | xit('does not affect element with multiple children', () => { 48 | // Using array instead of object causes the following error: 49 | // Warning: Each child in an array or iterator should have a unique "key" prop. 50 | // Check the render method of _class. See https://fb.me/react-warning-keys for more information. 51 | // @see https://github.com/facebook/react/issues/4723#issuecomment-135555277 52 | // expect(linkClass(

    )).to.deep.equal(

    ); 53 | 54 | const subject = React.createElement('div', null, [ 55 | React.createElement('p'), 56 | React.createElement('p') 57 | ]); 58 | const outcome = React.createElement('div', null, [ 59 | React.createElement('p'), 60 | React.createElement('p') 61 | ]); 62 | 63 | expect(linkClass(subject)).to.deep.equal(outcome); 64 | }); 65 | }); 66 | 67 | context('called with null instead of ReactElement', () => { 68 | it('returns null', () => { 69 | const subject = linkClass(null); 70 | 71 | expect(subject).to.equal(null); 72 | }); 73 | }); 74 | 75 | context('styleName matches an existing CSS module', () => { 76 | context('when a descendant element has styleName', () => { 77 | it('assigns a generated className', () => { 78 | let subject; 79 | 80 | subject =
    81 |

    82 |

    ; 83 | 84 | subject = linkClass(subject, { 85 | foo: 'foo-1' 86 | }); 87 | 88 | expect(subject.props.children.props.className).to.equal('foo-1'); 89 | }); 90 | }); 91 | context('when a descendant element in non-`children` prop has styleName', () => { 92 | it('assigns a generated className', () => { 93 | let subject; 94 | 95 | subject =
    } 97 | els={[

    , [

    ]]} 98 | />; 99 | 100 | subject = linkClass(subject, { 101 | bar: 'bar-1', 102 | baz: 'baz-1', 103 | foo: 'foo-1' 104 | }); 105 | 106 | expect(subject.props.el.props.className).to.equal('foo-1'); 107 | expect(subject.props.els[0].props.className).to.equal('bar-1'); 108 | expect(subject.props.els[1][0].props.className).to.equal('baz-1'); 109 | }); 110 | }); 111 | context('when multiple descendant elements have styleName', () => { 112 | it('assigns a generated className', () => { 113 | let subject; 114 | 115 | subject =

    116 |

    117 |

    118 |

    ; 119 | 120 | subject = linkClass(subject, { 121 | bar: 'bar-1', 122 | foo: 'foo-1' 123 | }); 124 | 125 | expect(subject.props.children[0].props.className).to.equal('foo-1'); 126 | expect(subject.props.children[1].props.className).to.equal('bar-1'); 127 | }); 128 | it('assigns a generated className to elements inside nested arrays', () => { 129 | let subject; 130 | 131 | subject =
    132 | {[ 133 | [ 134 |

    , 135 |

    136 | ], 137 | [ 138 |

    , 139 |

    140 | ] 141 | ]} 142 |

    ; 143 | 144 | subject = linkClass(subject, { 145 | bar: 'bar-1', 146 | foo: 'foo-1' 147 | }); 148 | 149 | expect(subject.props.children[0][0].props.className).to.equal('foo-1'); 150 | expect(subject.props.children[0][1].props.className).to.equal('bar-1'); 151 | 152 | expect(subject.props.children[1][0].props.className).to.equal('foo-1'); 153 | expect(subject.props.children[1][1].props.className).to.equal('bar-1'); 154 | }); 155 | it('styleName is deleted from props', () => { 156 | let subject; 157 | 158 | subject =
    159 |

    160 |

    161 |

    ; 162 | 163 | subject = linkClass(subject, { 164 | bar: 'bar-1', 165 | foo: 'foo-1' 166 | }); 167 | 168 | expect(subject.props.children[0].props).not.to.have.property('styleName'); 169 | expect(subject.props.children[1].props).not.to.have.property('styleName'); 170 | }); 171 | it('preserves original keys', () => { 172 | let subject; 173 | 174 | subject =
    175 |

    176 |

    177 |

    ; 178 | 179 | subject = linkClass(subject, { 180 | bar: 'bar-1', 181 | foo: 'foo-1' 182 | }); 183 | 184 | expect(subject.props.children[0].key).to.equal('1'); 185 | expect(subject.props.children[1].key).to.equal('2'); 186 | }); 187 | }); 188 | context('when multiple descendants have styleName and are iterable', () => { 189 | it('assigns a generated className', () => { 190 | let subject; 191 | 192 | const iterable = { 193 | 0:

    , 194 | 1:

    , 195 | length: 2, 196 | 197 | // eslint-disable-next-line no-use-extend-native/no-use-extend-native 198 | [Symbol.iterator]: Array.prototype[Symbol.iterator] 199 | }; 200 | 201 | subject =

    {iterable}
    ; 202 | 203 | subject = linkClass(subject, { 204 | bar: 'bar-1', 205 | foo: 'foo-1' 206 | }); 207 | 208 | expect(subject.props.children[0].props.className).to.equal('foo-1'); 209 | expect(subject.props.children[1].props.className).to.equal('bar-1'); 210 | }); 211 | }); 212 | context('when non-`children` prop is an iterable', () => { 213 | it('it is left untouched', () => { 214 | let subject; 215 | 216 | const iterable = { 217 | 0:

    , 218 | 1:

    , 219 | length: 2, 220 | 221 | // eslint-disable-next-line no-use-extend-native/no-use-extend-native 222 | [Symbol.iterator]: Array.prototype[Symbol.iterator] 223 | }; 224 | 225 | subject =

    ; 226 | 227 | subject = linkClass(subject, { 228 | bar: 'bar-1', 229 | foo: 'foo-1' 230 | }); 231 | 232 | expect(subject.props.els[0].props.styleName).to.equal('foo'); 233 | expect(subject.props.els[1].props.styleName).to.equal('bar'); 234 | expect(subject.props.els[0].props).not.to.have.property('className'); 235 | expect(subject.props.els[1].props).not.to.have.property('className'); 236 | }); 237 | }); 238 | context('when ReactElement does not have an existing className', () => { 239 | it('uses the generated class name to set the className property', () => { 240 | let subject; 241 | 242 | subject =
    ; 243 | 244 | subject = linkClass(subject, { 245 | foo: 'foo-1' 246 | }); 247 | 248 | expect(subject.props.className).to.deep.equal('foo-1'); 249 | }); 250 | }); 251 | context('when ReactElement has an existing className', () => { 252 | it('appends the generated class name to the className property', () => { 253 | let subject; 254 | 255 | subject =
    ; 256 | 257 | subject = linkClass(subject, { 258 | bar: 'bar-1' 259 | }); 260 | 261 | expect(subject.props.className).to.deep.equal('foo bar-1'); 262 | }); 263 | }); 264 | }); 265 | 266 | context('styleName includes multiple whitespace characters', () => { 267 | it('resolves CSS modules', () => { 268 | let subject; 269 | 270 | subject =
    271 |

    272 |

    ; 273 | 274 | subject = linkClass(subject, { 275 | bar: 'bar-1', 276 | foo: 'foo-1' 277 | }, { 278 | allowMultiple: true 279 | }); 280 | 281 | expect(subject.props.children.props.className).to.equal('foo-1 bar-1'); 282 | }); 283 | }); 284 | 285 | context('can\'t write to properties', () => { 286 | context('when the element is frozen', () => { 287 | it('adds className but is still frozen', () => { 288 | let subject; 289 | 290 | subject =
    ; 291 | 292 | Object.freeze(subject); 293 | subject = linkClass(subject, { 294 | foo: 'foo-1' 295 | }); 296 | 297 | expect(subject).to.be.frozen; 298 | expect(subject.props.className).to.equal('foo-1'); 299 | }); 300 | }); 301 | context('when the element\'s props are frozen', () => { 302 | it('adds className and only props are still frozen', () => { 303 | let subject; 304 | 305 | subject =
    ; 306 | 307 | Object.freeze(subject.props); 308 | subject = linkClass(subject, { 309 | foo: 'foo-1' 310 | }); 311 | 312 | expect(subject.props).to.be.frozen; 313 | expect(subject.props.className).to.equal('foo-1'); 314 | }); 315 | }); 316 | context('when the element\'s props are not extensible', () => { 317 | it('adds className and props are still not extensible', () => { 318 | let subject; 319 | 320 | subject =
    ; 321 | 322 | Object.preventExtensions(subject.props); 323 | subject = linkClass(subject, { 324 | foo: 'foo-1' 325 | }); 326 | 327 | expect(subject.props).to.not.be.extensible; 328 | expect(subject.props.className).to.equal('foo-1'); 329 | }); 330 | }); 331 | }); 332 | 333 | context('when element is an array', () => { 334 | it('handles each element individually', () => { 335 | let subject; 336 | 337 | subject = [ 338 |
    , 339 |
    340 |

    341 |

    342 | ]; 343 | 344 | subject = linkClass(subject, { 345 | bar: 'bar-1', 346 | foo: 'foo-1' 347 | }); 348 | 349 | expect(subject).to.be.an('array'); 350 | expect(subject[0].props.className).to.equal('foo-1'); 351 | expect(subject[1].props.children.props.className).to.equal('bar-1'); 352 | }); 353 | }); 354 | 355 | describe('options.allowMultiple', () => { 356 | context('when multiple module names are used', () => { 357 | context('when false', () => { 358 | it('throws an error', () => { 359 | expect(() => { 360 | linkClass(
    , {}, {allowMultiple: false}); 361 | }).to.throw(Error, 'ReactElement styleName property defines multiple module names ("foo bar").'); 362 | }); 363 | }); 364 | context('when true', () => { 365 | it('appends a generated class name for every referenced CSS module', () => { 366 | let subject; 367 | 368 | subject =
    ; 369 | 370 | subject = linkClass(subject, { 371 | bar: 'bar-1', 372 | foo: 'foo-1' 373 | }, { 374 | allowMultiple: true 375 | }); 376 | 377 | expect(subject.props.className).to.deep.equal('foo-1 bar-1'); 378 | }); 379 | }); 380 | }); 381 | }); 382 | 383 | describe('options.handleNotFoundStyleName', () => { 384 | context('when styleName does not match an existing CSS module', () => { 385 | context('when throw', () => { 386 | it('throws an error', () => { 387 | expect(() => { 388 | linkClass(
    , {}, {handleNotFoundStyleName: 'throw'}); 389 | }).to.throw(Error, '"foo" CSS module is undefined.'); 390 | }); 391 | }); 392 | context('when log', () => { 393 | it('logs a warning to the console', () => { 394 | const warnSpy = chai.spy(() => {}); 395 | 396 | console.warn = warnSpy; 397 | linkClass(
    , {}, {handleNotFoundStyleName: 'log'}); 398 | expect(warnSpy).to.have.been.called(); 399 | }); 400 | }); 401 | context('when ignore', () => { 402 | it('does not log a warning', () => { 403 | const warnSpy = chai.spy(() => {}); 404 | 405 | console.warn = warnSpy; 406 | linkClass(
    , {}, {handleNotFoundStyleName: 'ignore'}); 407 | expect(warnSpy).to.not.have.been.called(); 408 | }); 409 | 410 | it('does not throw an error', () => { 411 | expect(() => { 412 | linkClass(
    , {}, {handleNotFoundStyleName: 'ignore'}); 413 | }).to.not.throw(Error, '"foo" CSS module is undefined.'); 414 | }); 415 | }); 416 | }); 417 | }); 418 | 419 | context('when ReactElement includes ReactComponent', () => { 420 | let Foo; 421 | let nodeList; 422 | 423 | beforeEach(() => { 424 | global.document = jsdom.jsdom(''); 425 | global.window = document.defaultView; 426 | 427 | Foo = class extends React.Component { 428 | render () { 429 | return
    Hello
    ; 430 | } 431 | }; 432 | 433 | nodeList = TestUtils.renderIntoDocument(linkClass(
    , {foo: 'foo-1'})); 434 | }); 435 | it('processes ReactElement nodes', () => { 436 | expect(nodeList.className).to.equal('foo-1'); 437 | }); 438 | it('does not process ReactComponent nodes', () => { 439 | expect(nodeList.firstChild.className).to.equal(''); 440 | }); 441 | }); 442 | 443 | it('deletes styleName property from the target element', () => { 444 | let subject; 445 | 446 | subject =
    ; 447 | 448 | subject = linkClass(subject, { 449 | foo: 'foo-1' 450 | }); 451 | 452 | expect(subject.props.className).to.deep.equal('foo-1'); 453 | expect(subject.props).not.to.have.property('styleName'); 454 | }); 455 | 456 | it('deletes styleName property from the target element (deep)', () => { 457 | let subject; 458 | 459 | subject =
    } 461 | els={[, []]} 462 | styleName='foo' 463 | > 464 |
    465 |
    466 |
    ; 467 | 468 | subject = linkClass(subject, { 469 | bar: 'bar-1', 470 | baz: 'baz-1', 471 | foo: 'foo-1' 472 | }); 473 | 474 | expect(subject.props.children[0].props.className).to.deep.equal('bar-1'); 475 | expect(subject.props.children[0].props).not.to.have.property('styleName'); 476 | expect(subject.props.el.props.className).to.deep.equal('baz-1'); 477 | expect(subject.props.el.props).not.to.have.property('styleName'); 478 | expect(subject.props.els[0].props.className).to.deep.equal('foo-1'); 479 | expect(subject.props.els[0].props).not.to.have.property('styleName'); 480 | expect(subject.props.els[1][0].props.className).to.deep.equal('bar-1'); 481 | expect(subject.props.els[1][0].props).not.to.have.property('styleName'); 482 | }); 483 | }); 484 | -------------------------------------------------------------------------------- /tests/makeConfiguration.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-nested-callbacks */ 2 | 3 | import { 4 | expect 5 | } from 'chai'; 6 | import makeConfiguration from './../src/makeConfiguration'; 7 | 8 | describe('makeConfiguration', () => { 9 | describe('when using default configuration', () => { 10 | let configuration; 11 | 12 | beforeEach(() => { 13 | configuration = makeConfiguration(); 14 | }); 15 | describe('allowMultiple property', () => { 16 | it('defaults to false', () => { 17 | expect(configuration.allowMultiple).to.equal(false); 18 | }); 19 | }); 20 | describe('handleNotFoundStyleName property', () => { 21 | it('defaults to "throw"', () => { 22 | expect(configuration.handleNotFoundStyleName).to.equal('throw'); 23 | }); 24 | }); 25 | }); 26 | describe('when unknown property is provided', () => { 27 | it('throws an error', () => { 28 | expect(() => { 29 | makeConfiguration({ 30 | unknownProperty: true 31 | }); 32 | }).to.throw(Error, 'Unknown configuration property "unknownProperty".'); 33 | }); 34 | }); 35 | it('does not mutate user configuration', () => { 36 | const userConfiguration = {}; 37 | 38 | makeConfiguration(userConfiguration); 39 | 40 | expect(userConfiguration).to.deep.equal({}); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/reactCssModules.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-nested-callbacks, react/no-multi-comp, react/prop-types, react/prefer-stateless-function, class-methods-use-this */ 2 | 3 | import { 4 | expect 5 | } from 'chai'; 6 | import React from 'react'; 7 | import ReactDOM from 'react-dom'; 8 | import TestUtils from 'react-addons-test-utils'; 9 | import jsdom from 'jsdom'; 10 | import reactCssModules from './../src/index'; 11 | 12 | describe('reactCssModules', () => { 13 | context('a ReactComponent is decorated using react-css-modules', () => { 14 | it('inherits displayName', () => { 15 | let Foo; 16 | 17 | Foo = class extends React.Component {}; 18 | 19 | // @todo https://phabricator.babeljs.io/T2779 20 | Foo.displayName = 'Bar'; 21 | 22 | Foo = reactCssModules(Foo); 23 | 24 | expect(Foo.displayName).to.equal('Bar'); 25 | }); 26 | context('target component does not name displayName', () => { 27 | it('uses name for displayName', () => { 28 | let Foo; 29 | 30 | Foo = class Bar extends React.Component {}; 31 | 32 | Foo = reactCssModules(Foo); 33 | 34 | expect(Foo.displayName).to.equal('Bar'); 35 | }); 36 | }); 37 | }); 38 | context('a ReactComponent renders an element with the styleName prop', () => { 39 | context('the component is a class that extends React.Component', () => { 40 | let Foo; 41 | let component; 42 | 43 | beforeEach(() => { 44 | const shallowRenderer = TestUtils.createRenderer(); 45 | 46 | Foo = class extends React.Component { 47 | render () { 48 | return
    Hello
    ; 49 | } 50 | }; 51 | 52 | Foo = reactCssModules(Foo, { 53 | foo: 'foo-1' 54 | }); 55 | 56 | shallowRenderer.render(); 57 | 58 | component = shallowRenderer.getRenderOutput(); 59 | }); 60 | it('that element should contain the equivalent className', () => { 61 | expect(component.props.className).to.equal('foo-1'); 62 | }); 63 | it('the styleName prop should be "consumed" in the process', () => { 64 | expect(component.props).not.to.have.property('styleName'); 65 | }); 66 | }); 67 | context('the component is a stateless function component', () => { 68 | let Foo; 69 | let component; 70 | 71 | beforeEach(() => { 72 | const shallowRenderer = TestUtils.createRenderer(); 73 | 74 | Foo = () => { 75 | return
    Hello
    ; 76 | }; 77 | 78 | Foo = reactCssModules(Foo, { 79 | foo: 'foo-1' 80 | }); 81 | 82 | shallowRenderer.render(); 83 | 84 | component = shallowRenderer.getRenderOutput(); 85 | }); 86 | it('that element should contain the equivalent className', () => { 87 | expect(component.props.className).to.equal('foo-1'); 88 | }); 89 | it('the styleName prop should be "consumed" in the process', () => { 90 | expect(component.props).not.to.have.property('styleName'); 91 | }); 92 | }); 93 | }); 94 | context('a ReactComponent renders nothing', () => { 95 | context('the component is a class that extends React.Component', () => { 96 | it('linkClass must not intervene', () => { 97 | let Foo; 98 | 99 | const shallowRenderer = TestUtils.createRenderer(); 100 | 101 | Foo = class extends React.Component { 102 | render () { 103 | return null; 104 | } 105 | }; 106 | 107 | Foo = reactCssModules(Foo, { 108 | foo: 'foo-1' 109 | }); 110 | 111 | shallowRenderer.render(); 112 | 113 | const component = shallowRenderer.getRenderOutput(); 114 | 115 | expect(typeof component).to.equal('object'); 116 | }); 117 | }); 118 | context('the component is a stateless function component', () => { 119 | it('that element should contain the equivalent className', () => { 120 | let Foo; 121 | 122 | const shallowRenderer = TestUtils.createRenderer(); 123 | 124 | Foo = () => { 125 | return null; 126 | }; 127 | 128 | Foo = reactCssModules(Foo, { 129 | foo: 'foo-1' 130 | }); 131 | 132 | shallowRenderer.render(); 133 | 134 | const component = shallowRenderer.getRenderOutput(); 135 | 136 | expect(typeof component).to.equal('object'); 137 | }); 138 | }); 139 | }); 140 | context('rendering element', () => { 141 | beforeEach(() => { 142 | global.document = jsdom.jsdom(''); 143 | 144 | global.window = document.defaultView; 145 | }); 146 | context('parent component is using react-css-modules and interpolates props.children', () => { 147 | // @see https://github.com/gajus/react-css-modules/issues/76 148 | it('unsets the styleName property', () => { 149 | let Bar; 150 | let Foo; 151 | let subject; 152 | 153 | Foo = class extends React.Component { 154 | render () { 155 | return 156 |
    foo
    157 |
    ; 158 | } 159 | }; 160 | 161 | Foo = reactCssModules(Foo, { 162 | test: 'foo-0' 163 | }); 164 | 165 | Bar = class extends React.Component { 166 | render () { 167 | return
    {this.props.children}
    ; 168 | } 169 | }; 170 | 171 | Bar = reactCssModules(Bar, { 172 | test: 'bar-0' 173 | }); 174 | 175 | subject = TestUtils.renderIntoDocument(); 176 | 177 | // eslint-disable-next-line react/no-find-dom-node 178 | subject = ReactDOM.findDOMNode(subject); 179 | 180 | expect(subject.firstChild.className).to.equal('foo-0'); 181 | }); 182 | }); 183 | }); 184 | }); 185 | -------------------------------------------------------------------------------- /tests/renderNothing.js: -------------------------------------------------------------------------------- 1 | import { 2 | expect 3 | } from 'chai'; 4 | import renderNothing from '../src/renderNothing'; 5 | 6 | describe('renderNothing', () => { 7 | context('renderNothing should return different node types for various React versions', () => { 8 | it('should return noscript tag for React v14 or lower', () => { 9 | expect(renderNothing('14.0.0').type).to.equal('noscript'); 10 | }); 11 | 12 | it('should return null for React v15 or higher', () => { 13 | expect(renderNothing('15.0.0')).to.equal(null); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/wrapStatelessFunction.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-nested-callbacks */ 2 | 3 | import { 4 | expect 5 | } from 'chai'; 6 | import React from 'react'; 7 | import TestUtils from 'react-addons-test-utils'; 8 | import wrapStatelessFunction from './../src/wrapStatelessFunction'; 9 | 10 | describe('wrapStatelessFunction', () => { 11 | it('hoists static own properties from the input component to the wrapped component', () => { 12 | const styles = { 13 | foo: 'foo-1' 14 | }; 15 | 16 | const InnerComponent = () => { 17 | return null; 18 | }; 19 | 20 | InnerComponent.propTypes = {}; 21 | InnerComponent.defaultProps = {}; 22 | 23 | const WrappedComponent = wrapStatelessFunction(InnerComponent, styles); 24 | 25 | expect(WrappedComponent.propTypes).to.equal(InnerComponent.propTypes); 26 | expect(WrappedComponent.defaultProps).to.equal(InnerComponent.defaultProps); 27 | expect(WrappedComponent.name).not.to.equal(InnerComponent.name); 28 | }); 29 | context('using default styles', () => { 30 | it('exposes styles through styles property', (done) => { 31 | const styles = { 32 | foo: 'foo-1' 33 | }; 34 | 35 | wrapStatelessFunction((props) => { 36 | expect(props.styles).to.equal(styles); 37 | done(); 38 | }, styles)(); 39 | }); 40 | it('exposes non-enumerable styles property', (done) => { 41 | const styles = { 42 | foo: 'foo-1' 43 | }; 44 | 45 | wrapStatelessFunction((props) => { 46 | expect(props.propertyIsEnumerable('styles')).to.equal(false); 47 | done(); 48 | }, styles)(); 49 | }); 50 | it('does not affect the other instance properties', (done) => { 51 | const styles = { 52 | foo: 'foo-1' 53 | }; 54 | 55 | wrapStatelessFunction((props) => { 56 | expect(props.bar).to.equal('baz'); 57 | done(); 58 | }, styles)({ 59 | bar: 'baz' 60 | }); 61 | }); 62 | }); 63 | context('using explicit styles', () => { 64 | it('exposes styles through styles property', (done) => { 65 | const styles = { 66 | foo: 'foo-1' 67 | }; 68 | 69 | wrapStatelessFunction((props) => { 70 | expect(props.styles).to.equal(styles); 71 | done(); 72 | })({ 73 | styles 74 | }); 75 | }); 76 | }); 77 | context('rendering Component that returns null', () => { 78 | it('generates null', () => { 79 | const shallowRenderer = TestUtils.createRenderer(); 80 | 81 | const Component = wrapStatelessFunction(() => { 82 | return null; 83 | }); 84 | 85 | shallowRenderer.render(); 86 | 87 | const component = shallowRenderer.getRenderOutput(); 88 | 89 | expect(component).to.equal(null); 90 | }); 91 | }); 92 | }); 93 | --------------------------------------------------------------------------------