222 | ```
223 |
224 | The wrapping is in fact [extremely thin](https://github.com/jareware/css-ns/blob/b62b5d4aef6d8c43707622da2fa63eeb601bdb66/css-ns.js#L70-L77), and everything except `React.createElement()` is just inherited from React proper.
225 |
226 | ### Namespacing existing React elements
227 |
228 | Providing the `React` option will also enable support for React elements in the `ns()` function, so that:
229 |
230 | ```jsx
231 | var React = require('react'); // vanilla, non-wrapped React instance
232 |
233 | ns(
234 | ```
235 |
236 | This can be useful in the (rare) cases where your namespace has to be dynamically applied to elements created by some other module. Because the only safe way to do this is to invoke `React.cloneElement()` under the hood, it can have performance implications if overused.
237 |
238 | ## Use with Sass
239 |
240 | Use with [Sass](http://sass-lang.com/) requires no special support or plugins. This input:
241 |
242 | ```scss
243 | .MyComponent {
244 | background: cyan;
245 |
246 | &-row {
247 | color: red;
248 | }
249 | }
250 | ```
251 |
252 | will be compiled to this CSS:
253 |
254 | ```css
255 | .MyComponent {
256 | background: cyan;
257 | }
258 | .MyComponent-row {
259 | color: red;
260 | }
261 | ```
262 |
263 | ## Use with PostCSS
264 |
265 | Use with [PostCSS](https://github.com/postcss/postcss) is easiest with the [`postcss-nested`](https://github.com/postcss/postcss-nested) plugin. With it, this input:
266 |
267 | ```scss
268 | .MyComponent {
269 | background: cyan;
270 |
271 | &-row {
272 | color: red;
273 | }
274 | }
275 | ```
276 |
277 | will be compiled to this CSS:
278 |
279 | ```css
280 | .MyComponent {
281 | background: cyan;
282 | }
283 | .MyComponent-row {
284 | color: red;
285 | }
286 | ```
287 |
288 | ## Use with Less
289 |
290 | Use with [Less](http://lesscss.org/) requires no special support or plugins. This input:
291 |
292 | ```less
293 | .MyComponent {
294 | background: cyan;
295 |
296 | &-row {
297 | color: red;
298 | }
299 | }
300 | ```
301 |
302 | will be compiled to this CSS:
303 |
304 | ```css
305 | .MyComponent {
306 | background: cyan;
307 | }
308 | .MyComponent-row {
309 | color: red;
310 | }
311 | ```
312 |
313 | ## Use with Stylus
314 |
315 | Use with [Stylus](http://stylus-lang.com/) requires no special support or plugins. This input:
316 |
317 | ```
318 | .MyComponent
319 | background: cyan
320 |
321 | &-row
322 | color: red
323 | ```
324 |
325 | will be compiled to this CSS:
326 |
327 | ```css
328 | .MyComponent {
329 | background: cyan;
330 | }
331 | .MyComponent-row {
332 | color: red;
333 | }
334 | ```
335 |
336 | ## Test suite
337 |
338 | The web-based test suite is available at http://jrw.fi/css-ns/.
339 |
340 | ## Release
341 |
342 | 1. Bump version number in `package.json`
343 | 1. `$ git commit -m "Version bump for release." && git push`
344 | 1. `$ cp css-ns.{js,d.ts} package.json dist/`
345 | 1. `$ cd dist && npm publish`
346 | 1. Create release on GitHub, e.g. `v1.1.3`
347 |
348 | ## Licence
349 |
350 | [MIT](https://opensource.org/licenses/MIT)
351 |
--------------------------------------------------------------------------------
/css-ns.spec.js:
--------------------------------------------------------------------------------
1 | var assert = require('chai').assert; // @see http://chaijs.com/api/assert/
2 | var cssNs = require('./css-ns');
3 | var React = require('react');
4 | var ReactDOMServer = require('react-dom/server');
5 |
6 | function assertEqualHtml(Component, expectedHtml) {
7 | assert.deepEqual(
8 | ReactDOMServer.renderToStaticMarkup(React.createElement(Component)),
9 | expectedHtml
10 | );
11 | }
12 |
13 | // Enable this to repeat the test suite a few times
14 | // for (var i = 0; i < 100; i++)
15 |
16 | describe('css-ns', function() {
17 |
18 | describe('createOptions()', function() {
19 |
20 | it('accepts a string', function() {
21 | assert.deepEqual(
22 | cssNs.createOptions('MyComponent').namespace,
23 | 'MyComponent'
24 | );
25 | });
26 |
27 | it('accepts a file path', function() {
28 | assert.deepEqual(
29 | cssNs.createOptions('/path/to/MyComponent.jsx').namespace,
30 | 'MyComponent'
31 | );
32 | });
33 |
34 | it('accepts a relative file path', function() {
35 | assert.deepEqual(
36 | cssNs.createOptions('../MyComponent.jsx').namespace,
37 | 'MyComponent'
38 | );
39 | });
40 |
41 | it('accepts a full Windows file path', function() {
42 | assert.deepEqual(
43 | cssNs.createOptions('C:\\Users\\path\\to\\MyComponent.jsx').namespace,
44 | 'MyComponent'
45 | );
46 | });
47 |
48 | it('accepts a full Windows file path with whitespace', function() {
49 | assert.deepEqual(
50 | cssNs.createOptions('C:\\Program Files\\path\\to\\MyComponent.js').namespace,
51 | 'MyComponent'
52 | );
53 | });
54 |
55 | it('accepts underscores in file names', function() {
56 | assert.deepEqual(
57 | cssNs.createOptions('../my_component.jsx').namespace,
58 | 'my_component'
59 | );
60 | });
61 |
62 | it('processes options only once', function() {
63 | assert.deepEqual(
64 | cssNs.createOptions(cssNs.createOptions('MyComponent')).namespace,
65 | 'MyComponent'
66 | );
67 | });
68 |
69 | it('accepts an object', function() {
70 | assert.deepEqual(
71 | cssNs.createOptions({ namespace: 'MyComponent' }).namespace,
72 | 'MyComponent'
73 | );
74 | });
75 |
76 | });
77 |
78 | describe('nsAuto()', function() {
79 |
80 | it('handles falsy input', function() {
81 | assert.equal(cssNs.nsAuto('MyComponent', null), null);
82 | });
83 |
84 | it('handles class list input', function() {
85 | assert.equal(cssNs.nsAuto('Foo', 'bar'), 'Foo-bar');
86 | });
87 |
88 | it('handles React element input', function() {
89 | var MyComponent = function() {
90 | return cssNs.nsAuto(
91 | { namespace: 'MyComponent', React: React },
92 | React.createElement('div', { className: 'row' })
93 | );
94 | };
95 | assertEqualHtml(
96 | MyComponent,
97 | '
'
98 | );
99 | });
100 |
101 | });
102 |
103 | describe('nsString()', function() {
104 |
105 | it('prefixes a single class', function() {
106 | assert.equal(cssNs.nsString('Foo', 'bar'), 'Foo-bar');
107 | });
108 |
109 | it('prefixes multiple classes', function() {
110 | assert.equal(cssNs.nsString('Foo', 'bar baz'), 'Foo-bar Foo-baz');
111 | });
112 |
113 | it('tolerates exotic classNames and whitespace', function() {
114 | // ...not that using these would be a good idea for other reasons, but we won't judge!
115 | assert.equal(cssNs.nsString('Foo', ' bar-baz lol{wtf$why%would__ANYONE"do.this} '), 'Foo-bar-baz Foo-lol{wtf$why%would__ANYONE"do.this}');
116 | });
117 |
118 | it('supports an include option', function() {
119 | var options = {
120 | namespace: 'Foo',
121 | include: /^b/ // only prefix classes that start with b
122 | };
123 | assert.equal(
124 | cssNs.nsString(options, 'bar AnotherComponent car'),
125 | 'Foo-bar AnotherComponent car'
126 | );
127 | });
128 |
129 | it('supports an exclude option', function() {
130 | var options = {
131 | namespace: 'Foo',
132 | exclude: /^([A-Z]|icon)/ // ignore classes that start with caps or "icon"
133 | };
134 | assert.equal(
135 | cssNs.nsString(options, 'bar AnotherComponent iconInfo baz'),
136 | 'Foo-bar AnotherComponent iconInfo Foo-baz'
137 | );
138 | });
139 |
140 | it('supports both include and exclude at the same time', function() {
141 | var options = {
142 | namespace: 'Foo',
143 | include: /^[a-z]/, // include classes that start with lower-case
144 | exclude: /^icon/ // ...but still ignore the "icon" prefix
145 | };
146 | assert.equal(
147 | cssNs.nsString(options, 'bar iconInfo baz'),
148 | 'Foo-bar iconInfo Foo-baz'
149 | );
150 | });
151 |
152 | it('supports escaping', function() {
153 | var options = {
154 | namespace: 'Foo',
155 | escape: '~'
156 | };
157 | assert.equal(cssNs.nsString(options, '~bar'), 'bar');
158 | });
159 |
160 | it('supports an exotic escape string', function() {
161 | var options = {
162 | namespace: 'Foo',
163 | escape: '@]]£20as+d09a+s+fsdkjnf'
164 | };
165 | assert.equal(cssNs.nsString(options, '@]]£20as+d09a+s+fsdkjnfbar car'), 'bar Foo-car');
166 | });
167 |
168 | it('supports escaping with the default option', function() {
169 | var options = {
170 | namespace: 'Foo'
171 | };
172 | assert.equal(cssNs.nsString(options, '=bar car'), 'bar Foo-car');
173 | });
174 |
175 | it('supports a self option', function() {
176 | var options = {
177 | namespace: 'Foo',
178 | self: /^__ns__$/
179 | };
180 | assert.equal(cssNs.nsString(options, '__ns__ bar'), 'Foo Foo-bar');
181 | });
182 |
183 | it('supports a glue option', function() {
184 | var options = {
185 | namespace: 'Foo',
186 | glue: '___'
187 | };
188 | assert.equal(cssNs.nsString(options, 'bar'), 'Foo___bar');
189 | });
190 |
191 | describe('with prefix', function() {
192 |
193 | it('prefixes a single class', function() {
194 | assert.equal(cssNs.nsString({ prefix: 'app-', namespace: 'Foo' }, 'bar'), 'app-Foo-bar');
195 | });
196 |
197 | it('prefixes multiple classes', function() {
198 | assert.equal(cssNs.nsString({ prefix: 'app-', namespace: 'Foo' }, 'bar baz'), 'app-Foo-bar app-Foo-baz');
199 | });
200 |
201 | it('tolerates exotic classNames and whitespace', function() {
202 | // ...not that using these would be a good idea for other reasons, but we won't judge!
203 | assert.equal(cssNs.nsString({ prefix: 'app-', namespace: 'Foo' }, ' bar-baz lol{wtf$why%would__ANYONE"do.this} '), 'app-Foo-bar-baz app-Foo-lol{wtf$why%would__ANYONE"do.this}');
204 | });
205 |
206 | it('supports an include option', function() {
207 | var options = {
208 | prefix: 'app-',
209 | namespace: 'Foo',
210 | include: /^b/ // only prefix classes that start with b
211 | };
212 | assert.equal(
213 | cssNs.nsString(options, 'bar AnotherComponent car'),
214 | 'app-Foo-bar AnotherComponent car'
215 | );
216 | });
217 |
218 | it('supports an exclude option', function() {
219 | var options = {
220 | prefix: 'app-',
221 | namespace: 'Foo',
222 | exclude: /^([A-Z]|icon)/ // ignore classes that start with caps or "icon"
223 | };
224 | assert.equal(
225 | cssNs.nsString(options, 'bar AnotherComponent iconInfo baz'),
226 | 'app-Foo-bar AnotherComponent iconInfo app-Foo-baz'
227 | );
228 | });
229 |
230 | it('supports both include and exclude at the same time', function() {
231 | var options = {
232 | prefix: 'app-',
233 | namespace: 'Foo',
234 | include: /^[a-z]/, // include classes that start with lower-case
235 | exclude: /^icon/ // ...but still ignore the "icon" prefix
236 | };
237 | assert.equal(
238 | cssNs.nsString(options, 'bar iconInfo baz'),
239 | 'app-Foo-bar iconInfo app-Foo-baz'
240 | );
241 | });
242 |
243 | it('supports a self option', function() {
244 | var options = {
245 | prefix: 'app-',
246 | namespace: 'Foo',
247 | self: /^__ns__$/
248 | };
249 | assert.equal(cssNs.nsString(options, '__ns__ bar'), 'app-Foo app-Foo-bar');
250 | });
251 |
252 | it('supports a glue option', function() {
253 | var options = {
254 | prefix: 'app-',
255 | namespace: 'Foo',
256 | glue: '___'
257 | };
258 | assert.equal(cssNs.nsString(options, 'bar'), 'app-Foo___bar');
259 | });
260 |
261 | it('automatically ignores pre-prefixed classes', function() {
262 | var options = {
263 | prefix: 'app-',
264 | namespace: 'Foo'
265 | };
266 | assert.equal(
267 | cssNs.nsString(options, 'bar app-AnotherComponent app-car'),
268 | 'app-Foo-bar app-AnotherComponent app-car'
269 | );
270 | });
271 |
272 | });
273 |
274 | });
275 |
276 | describe('nsArray()', function() {
277 |
278 | it('prefixes classes', function() {
279 | assert.equal(cssNs.nsArray('Foo', [ 'bar', 'baz' ]), 'Foo-bar Foo-baz');
280 | });
281 |
282 | it('tolerates falsy values', function() {
283 | assert.equal(cssNs.nsArray('Foo', [ 'bar', null, 'baz', false ]), 'Foo-bar Foo-baz');
284 | });
285 |
286 | });
287 |
288 | describe('nsObject()', function() {
289 |
290 | it('prefixes classes', function() {
291 | assert.equal(cssNs.nsObject('Foo', { bar: true, baz: true }), 'Foo-bar Foo-baz');
292 | });
293 |
294 | it('tolerates falsy values', function() {
295 | assert.equal(cssNs.nsObject('Foo', { bar: true, ignoreThis: null, baz: true, alsoThis: false }), 'Foo-bar Foo-baz');
296 | });
297 |
298 | });
299 |
300 | describe('createReact()', function() {
301 |
302 | it('creates an ns-bound React instance', function() {
303 | var nsReact = cssNs.createReact({ namespace: 'MyComponent', React: React });
304 | var MyComponent = function() {
305 | return nsReact.createElement('div', { className: 'row' });
306 | };
307 | assertEqualHtml(
308 | MyComponent,
309 | '
'
310 | );
311 | });
312 |
313 | it('prefixes classNames on components as well', function() {
314 | // This is a bit of an edge case, since for a component, a prop called "className" holds no special value.
315 | // But if you're using a prop with that name it's highly likely this is the behaviour you want.
316 | var nsReact = cssNs.createReact({ namespace: 'MyComponent', React: React });
317 | var MyChildComponent = function(props) {
318 | return nsReact.createElement('div', { className: props.className });
319 | // ^ this won't get double-namespaced, but only because we ignore uppercased classes by default; otherwise it would
320 | };
321 | var MyComponent = function() {
322 | return nsReact.createElement(MyChildComponent, { className: 'parentInjectedName' })
323 | };
324 | assertEqualHtml(
325 | MyComponent,
326 | '
'
327 | );
328 | });
329 |
330 | });
331 |
332 | describe('nsReactElement()', function() {
333 |
334 | var nsReactElement = cssNs.nsReactElement.bind(null, { // bind default options
335 | namespace: 'MyComponent',
336 | React: React
337 | });
338 |
339 | it('prefixes a single className', function() {
340 | var MyComponent = function() {
341 | return nsReactElement(
342 | React.createElement('div', { className: 'row' })
343 | );
344 | };
345 | assertEqualHtml(
346 | MyComponent,
347 | '
'
348 | );
349 | });
350 |
351 | it('ignores text nodes', function() {
352 | var MyComponent = function() {
353 | return nsReactElement(
354 | React.createElement('div', { className: 'row' }, 'Hello World')
355 | );
356 | };
357 | assertEqualHtml(
358 | MyComponent,
359 | '
Hello World
'
360 | );
361 | });
362 |
363 | it('supports array input', function() {
364 | var MyComponent = function() {
365 | return nsReactElement(
366 | React.createElement('div', { className: [ 'row' ] })
367 | );
368 | };
369 | assertEqualHtml(
370 | MyComponent,
371 | '
'
372 | );
373 | });
374 |
375 | it('supports object input', function() {
376 | var MyComponent = function() {
377 | return nsReactElement(
378 | React.createElement('div', { className: { row: true } })
379 | );
380 | };
381 | assertEqualHtml(
382 | MyComponent,
383 | '
'
384 | );
385 | });
386 |
387 | it('prefixes classNames recursively', function() {
388 | var MyComponent = function() {
389 | return nsReactElement(
390 | React.createElement('div', { className: 'row' },
391 | React.createElement('div', { className: 'column' })
392 | )
393 | );
394 | };
395 | assertEqualHtml(
396 | MyComponent,
397 | '
'
398 | );
399 | });
400 |
401 | it('allows elements without a className', function() {
402 | var MyComponent = function() {
403 | return nsReactElement(
404 | React.createElement('div', { className: 'row' },
405 | React.createElement('section', null,
406 | React.createElement('div', { className: 'column' })
407 | )
408 | )
409 | );
410 | };
411 | assertEqualHtml(
412 | MyComponent,
413 | '
'
414 | );
415 | });
416 |
417 | it('respects component boundaries', function() {
418 | var MyChildComponent = function() {
419 | return React.createElement('div', { className: 'protected' });
420 | };
421 | var MyComponent = function() {
422 | return nsReactElement(
423 | React.createElement('div', { className: 'container' },
424 | React.createElement(MyChildComponent, null)
425 | )
426 | );
427 | };
428 | assertEqualHtml(
429 | MyComponent,
430 | '
'
431 | );
432 | });
433 |
434 | it('respects ownership of children across components', function() {
435 | var MyChildComponent = function(props) {
436 | return React.createElement('div', { className: 'protected' }, props.children);
437 | };
438 | var MyComponent = function() {
439 | return nsReactElement(
440 | React.createElement(MyChildComponent, null,
441 | React.createElement('div', { className: 'owned' }) // it doesn't matter that "owned" is a child of MyChildComponent, it's still OWNED by MyComponent
442 | )
443 | );
444 | };
445 | assertEqualHtml(
446 | MyComponent,
447 | '
'
448 | );
449 | });
450 |
451 | it('works with nested components', function() {
452 | var MyChildComponent = function() {
453 | return cssNs.nsReactElement(
454 | { namespace: 'MyChildComponent', React: React },
455 | React.createElement('div', { className: 'protected' })
456 | );
457 | };
458 | var MyComponent = function() {
459 | return nsReactElement(
460 | React.createElement('div', { className: 'container' },
461 | React.createElement(MyChildComponent, null)
462 | )
463 | );
464 | };
465 | assertEqualHtml(
466 | MyComponent,
467 | '
'
468 | );
469 | });
470 |
471 | it('prefixes classNames on components as well', function() {
472 | // This is a bit of an edge case, since for a component, a prop called "className" holds no special value.
473 | // But if you're using a prop with that name it's highly likely this is the behaviour you want.
474 | var MyChildComponent = function(props) {
475 | return React.createElement('div', { className: props.className });
476 | };
477 | var MyComponent = function() {
478 | return nsReactElement(
479 | React.createElement(MyChildComponent, { className: 'parentInjectedName' })
480 | );
481 | };
482 | assertEqualHtml(
483 | MyComponent,
484 | '
'
485 | );
486 | });
487 |
488 | });
489 |
490 | });
491 |
--------------------------------------------------------------------------------