├── .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 | [](https://gitspo.com/mentions/gajus/react-css-modules)
4 | [](https://travis-ci.org/gajus/react-css-modules)
5 | [](https://www.npmjs.org/package/react-css-modules)
6 | [](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
;
65 | }
66 | }
67 | ```
68 |
69 | Rendering the component will produce a markup similar to:
70 |
71 | ```js
72 |
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 ;
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 ;
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 | ;
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 ;
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 ;
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 = ;
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 = ;
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 = ;
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 = ;
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 = ;
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 |
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 |
--------------------------------------------------------------------------------