├── .npmignore ├── .travis.yml ├── benchmarks ├── benchmarks.html ├── package.json ├── runChecks.js ├── fixtures.js ├── runSuite.js ├── run.js └── runInBrowser.js ├── .editorconfig ├── .gitignore ├── bower.json ├── package.json ├── CONTRIBUTING.md ├── LICENSE ├── bind.js ├── index.js ├── HISTORY.md ├── tests ├── dedupe.js ├── index.js └── bind.js ├── dedupe.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | /.*/ 2 | /.* 3 | /benchmarks/ 4 | /tests/ 5 | /bower.json 6 | /CONTRIBUTING.md 7 | /HISTORY.md 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | before_install: 3 | - "npm install npm -g" 4 | node_js: 5 | - "stable" 6 | - "4" 7 | - "5" 8 | - "6" 9 | - "0.10" 10 | - "0.12" 11 | -------------------------------------------------------------------------------- /benchmarks/benchmarks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Benchmarks 6 | 7 | 8 |
9 |
Wait please…
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = false 9 | insert_final_newline = true 10 | indent_style = tab 11 | 12 | [*.json] 13 | indent_style = space 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /benchmarks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "classnames-benchmarks", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "", 6 | "main": "run.js", 7 | "scripts": { 8 | "test": "echo \"Tests should be run in the main classnames package.\" && exit 1" 9 | }, 10 | "author": "Jed Watson", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "benchmark": "^1.0.0", 14 | "classnames": "*" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory 14 | coverage 15 | 16 | # Compiled binary addons (http://nodejs.org/api/addons.html) 17 | build/Release 18 | 19 | # Dependency directory 20 | node_modules 21 | 22 | # Users Environment Variables 23 | .lock-wscript 24 | 25 | # Mac OS X DS_Store 26 | .DS_Store 27 | 28 | benchmarks/runInBrowser.bundle.js 29 | -------------------------------------------------------------------------------- /benchmarks/runChecks.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | function sortClasses (str) { 4 | return str.split(' ').sort().join(' '); 5 | } 6 | 7 | function runChecks (local, npm, dedupe, npmDedupe, fixture) { 8 | // sort assertions because dedupe returns results in a different order 9 | assert.equal(sortClasses(local.apply(null, fixture.args)), sortClasses(fixture.expected)); 10 | assert.equal(sortClasses(dedupe.apply(null, fixture.args)), sortClasses(fixture.expected)); 11 | assert.equal(sortClasses(npm.apply(null, fixture.args)), sortClasses(fixture.expected)); 12 | assert.equal(sortClasses(npmDedupe.apply(null, fixture.args)), sortClasses(fixture.expected)); 13 | } 14 | 15 | module.exports = runChecks; 16 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "classnames", 3 | "version": "2.2.5", 4 | "description": "A simple utility for conditionally joining classNames together", 5 | "main": [ 6 | "index.js", 7 | "bind.js", 8 | "dedupe.js" 9 | ], 10 | "homepage": "https://github.com/JedWatson/classnames", 11 | "authors": [ 12 | "Jed Watson" 13 | ], 14 | "moduleType": [ 15 | "amd", 16 | "globals", 17 | "node" 18 | ], 19 | "keywords": [ 20 | "react", 21 | "css", 22 | "classes", 23 | "classname", 24 | "classnames", 25 | "util", 26 | "utility" 27 | ], 28 | "license": "MIT", 29 | "ignore": [ 30 | ".editorconfig", 31 | ".gitignore", 32 | "gulpfile.js", 33 | "package.json", 34 | "node_modules", 35 | "tests.js" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /benchmarks/fixtures.js: -------------------------------------------------------------------------------- 1 | var fixtures = [ 2 | { 3 | description: 'strings', 4 | args: ['one', 'two', 'three'], 5 | expected: 'one two three' 6 | }, 7 | { 8 | description: 'object', 9 | args: [{one: true, two: true, three: false}], 10 | expected: 'one two' 11 | }, 12 | { 13 | description: 'strings, object', 14 | args: ['one', 'two', {four: true, three: false}], 15 | expected: 'one two four' 16 | }, 17 | { 18 | description: 'mix', 19 | args: ['one', {two: true, three: false}, {four: 'four', five: true}, 6, {}], 20 | expected: 'one two four five 6' 21 | }, 22 | { 23 | description: 'arrays', 24 | args: [['one', 'two'], ['three'], ['four', ['five']], [{six: true}, {seven: false}]], 25 | expected: 'one two three four five six' 26 | } 27 | ]; 28 | 29 | module.exports = fixtures; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "classnames", 3 | "version": "2.2.5", 4 | "description": "A simple utility for conditionally joining classNames together", 5 | "main": "index.js", 6 | "author": "Jed Watson", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/JedWatson/classnames.git" 11 | }, 12 | "scripts": { 13 | "benchmarks": "node ./benchmarks/run", 14 | "benchmarks-browserify": "./node_modules/.bin/browserify ./benchmarks/runInBrowser.js >./benchmarks/runInBrowser.bundle.js", 15 | "benchmarks-in-browser": "./node_modules/.bin/opn ./benchmarks/benchmarks.html", 16 | "test": "mocha tests/*.js" 17 | }, 18 | "keywords": [ 19 | "react", 20 | "css", 21 | "classes", 22 | "classname", 23 | "classnames", 24 | "util", 25 | "utility" 26 | ], 27 | "devDependencies": { 28 | "benchmark": "^1.0.0", 29 | "browserify": "^14.1.0", 30 | "mocha": "^2.1.0", 31 | "opn-cli": "^3.1.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for your interest in classNames. Issues, PRs and suggestions welcome :) 4 | 5 | Before working on a PR, please consider the following: 6 | 7 | * Speed is a serious concern for this package as it is likely to be called a 8 | significant number of times in any project that uses it. As such, new features 9 | will only be accepted if they improve (or at least do not negatively impact) 10 | performance. 11 | * To demonstrate performance differences please set up a 12 | [JSPerf](http://jsperf.com) test and link to it from your issue / PR. 13 | * Tests must be added for any change or new feature before it will be accepted. 14 | 15 | A benchmark utilitiy is included so that changes may be tested against the 16 | current published version. To run the benchmarks, `npm install` in the 17 | `./benchmarks` directory then run `npm run benchmarks` in the package root. 18 | 19 | Please be aware though that local benchmarks are just a smoke-signal; they will 20 | run in the v8 version that your node/iojs uses, while classNames is _most_ 21 | often run across a wide variety of browsers and browser versions. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Jed Watson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /benchmarks/runSuite.js: -------------------------------------------------------------------------------- 1 | var benchmark = require('benchmark'); 2 | 3 | function runSuite (local, npm, dedupe, npmDedupe, fixture, log) { 4 | var suite = new benchmark.Suite(); 5 | 6 | suite.add('local#' + fixture.description, function () { 7 | local.apply(null, fixture.args); 8 | }); 9 | 10 | suite.add(' npm#' + fixture.description, function () { 11 | npm.apply(null, fixture.args); 12 | }); 13 | 14 | suite.add('local/dedupe#' + fixture.description, function () { 15 | dedupe.apply(null, fixture.args); 16 | }); 17 | 18 | suite.add(' npm/dedupe#' + fixture.description, function () { 19 | npmDedupe.apply(null, fixture.args); 20 | }); 21 | 22 | // after each cycle 23 | suite.on('cycle', function (event) { 24 | log('* ' + String(event.target)); 25 | }); 26 | 27 | // other handling 28 | suite.on('complete', function () { 29 | log('\n> Fastest is' + (' ' + this.filter('fastest').pluck('name').join(' | ')).replace(/\s+/, ' ') + '\n'); 30 | }); 31 | 32 | suite.on('error', function (event) { 33 | log(event.target.error.message); 34 | throw event.target.error; 35 | }); 36 | 37 | suite.run(); 38 | } 39 | 40 | module.exports = runSuite; 41 | -------------------------------------------------------------------------------- /benchmarks/run.js: -------------------------------------------------------------------------------- 1 | var fixtures = require('./fixtures'); 2 | var local = require('../'); 3 | var dedupe = require('../dedupe'); 4 | var localPackage = require('../package.json'); 5 | 6 | function log (message) { 7 | console.log(message); 8 | } 9 | 10 | try { 11 | var npm = require('classnames'); 12 | var npmDedupe = require('classnames/dedupe'); 13 | var npmPackage = require('./node_modules/classnames/package.json'); 14 | } catch (e) { 15 | log('There was an error loading the benchmark classnames package.\n' + 16 | 'Please make sure you have run `npm install` in ./benchmarks\n'); 17 | process.exit(0); 18 | } 19 | 20 | if (localPackage.version !== npmPackage.version) { 21 | log('Your local version (' + localPackage.version + ') does not match the installed version (' + npmPackage.version + ')\n\n' + 22 | 'Please run `npm update` in ./benchmarks to ensure you are benchmarking\n' + 23 | 'the latest version of this package.\n'); 24 | process.exit(0); 25 | } 26 | 27 | var runChecks = require('./runChecks'); 28 | var runSuite = require('./runSuite'); 29 | 30 | fixtures.forEach(function (f) { 31 | runChecks(local, npm, dedupe, npmDedupe, f); 32 | runSuite(local, npm, dedupe, npmDedupe, f, log); 33 | }); 34 | -------------------------------------------------------------------------------- /bind.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Copyright (c) 2017 Jed Watson. 3 | Licensed under the MIT License (MIT), see 4 | http://jedwatson.github.io/classnames 5 | */ 6 | /* global define */ 7 | 8 | (function () { 9 | 'use strict'; 10 | 11 | var hasOwn = {}.hasOwnProperty; 12 | 13 | function classNames () { 14 | var classes = []; 15 | 16 | for (var i = 0; i < arguments.length; i++) { 17 | var arg = arguments[i]; 18 | if (!arg) continue; 19 | 20 | var argType = typeof arg; 21 | 22 | if (argType === 'string' || argType === 'number') { 23 | classes.push(this && this[arg] || arg); 24 | } else if (Array.isArray(arg)) { 25 | classes.push(classNames.apply(this, arg)); 26 | } else if (argType === 'object') { 27 | for (var key in arg) { 28 | if (hasOwn.call(arg, key) && arg[key]) { 29 | classes.push(this && this[key] || key); 30 | } 31 | } 32 | } 33 | } 34 | 35 | return classes.join(' '); 36 | } 37 | 38 | if (typeof module !== 'undefined' && module.exports) { 39 | module.exports = classNames; 40 | } else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) { 41 | // register as 'classnames', consistent with npm package name 42 | define('classnames', [], function () { 43 | return classNames; 44 | }); 45 | } else { 46 | window.classNames = classNames; 47 | } 48 | }()); 49 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Copyright (c) 2017 Jed Watson. 3 | Licensed under the MIT License (MIT), see 4 | http://jedwatson.github.io/classnames 5 | */ 6 | /* global define */ 7 | 8 | (function () { 9 | 'use strict'; 10 | 11 | var hasOwn = {}.hasOwnProperty; 12 | 13 | function classNames () { 14 | var classes = []; 15 | 16 | for (var i = 0; i < arguments.length; i++) { 17 | var arg = arguments[i]; 18 | if (!arg) continue; 19 | 20 | var argType = typeof arg; 21 | 22 | if (argType === 'string' || argType === 'number') { 23 | classes.push(arg); 24 | } else if (Array.isArray(arg) && arg.length) { 25 | var inner = classNames.apply(null, arg); 26 | if (inner) { 27 | classes.push(inner); 28 | } 29 | } else if (argType === 'object') { 30 | if (hasOwn.call(arg, 'toString') && typeof arg.toString === 'function') { 31 | classes.push(arg.toString()); 32 | } else { 33 | for (var key in arg) { 34 | if (hasOwn.call(arg, key) && arg[key]) { 35 | classes.push(key); 36 | } 37 | } 38 | } 39 | } 40 | } 41 | 42 | return classes.join(' '); 43 | } 44 | 45 | if (typeof module !== 'undefined' && module.exports) { 46 | classNames.default = classNames; 47 | module.exports = classNames; 48 | } else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) { 49 | // register as 'classnames', consistent with npm package name 50 | define('classnames', [], function () { 51 | return classNames; 52 | }); 53 | } else { 54 | window.classNames = classNames; 55 | } 56 | }()); 57 | -------------------------------------------------------------------------------- /benchmarks/runInBrowser.js: -------------------------------------------------------------------------------- 1 | var fixtures = require('./fixtures'); 2 | var local = require('../'); 3 | var dedupe = require('../dedupe'); 4 | var localPackage = require('../package.json'); 5 | 6 | var npm = require('classnames'); 7 | var npmDedupe = require('classnames/dedupe'); 8 | var npmPackage = require('./node_modules/classnames/package.json'); 9 | 10 | function log (message) { 11 | console.log(message); 12 | var results = document.getElementById('results'); 13 | //noinspection InnerHTMLJS 14 | results.innerHTML += (message + '\n').replace(/\n/g, '
'); 15 | } 16 | 17 | if (localPackage.version !== npmPackage.version) { 18 | log('Your local version (' + localPackage.version + ') does not match the installed version (' + npmPackage.version + ')\n\n' + 19 | 'Please run `npm update` in ./benchmarks to ensure you are benchmarking\n' + 20 | 'the latest version of this package.\n'); 21 | return; 22 | } 23 | 24 | function iterate (array, iterator, i, callback) { 25 | if (i >= 0 && i < array.length) { 26 | iterator(array[i], i, array); 27 | setTimeout(iterate.bind(null, array, iterator, i + 1, callback), 1); 28 | } else if (callback) { 29 | callback(); 30 | } 31 | } 32 | 33 | function deferredForEach (array, iterator, callback) { 34 | iterate(array, iterator, 0, callback); 35 | } 36 | 37 | var runSuite = require('./runSuite'); 38 | 39 | window.onload = function () { 40 | //noinspection PlatformDetectionJS 41 | log(navigator.userAgent); 42 | setTimeout(function () { 43 | deferredForEach(fixtures, function (f) { 44 | runSuite(local, npm, dedupe, npmDedupe, f, log); 45 | }, function () { 46 | log('Finished'); 47 | document.getElementById('loader').style.display = 'none'; 48 | }); 49 | }, 100); 50 | }; 51 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v2.2.5 / 2016-05-02 4 | 5 | * Improved performance of `dedupe` variant even further, thanks [Andres Suarez](https://github.com/zertosh) 6 | 7 | ## v2.2.4 / 2016-04-25 8 | 9 | * Improved performance of `dedupe` variant by about 2x, thanks [Bartosz Gościński](https://github.com/bgoscinski) 10 | 11 | ## v2.2.3 / 2016-01-05 12 | 13 | * Updated `bind` variant to use `[].join(' ')` as per the main script in 2.2.2 14 | 15 | ## v2.2.2 / 2016-01-04 16 | 17 | * Switched from string concatenation to `[].join(' ')` for a slight performance gain in the main function. 18 | 19 | ## v2.2.1 / 2015-11-26 20 | 21 | * Add deps parameter to the AMD module, fixes an issue using the Dojo loader, thanks [Chris Jordan](https://github.com/flipperkid) 22 | 23 | ## v2.2.0 / 2015-10-18 24 | 25 | * added a new `bind` variant for use with [css-modules](https://github.com/css-modules/css-modules) and similar abstractions, thanks to [Kirill Yakovenko](https://github.com/blia) 26 | 27 | ## v2.1.5 / 2015-09-30 28 | 29 | * reverted a new usage of `Object.keys` in `dedupe.js` that slipped through in the last release 30 | 31 | ## v2.1.4 / 2015-09-30 32 | 33 | * new case added to benchmarks 34 | * safer `hasOwnProperty` check 35 | * AMD module is now named, so you can do the following: 36 | 37 | ``` 38 | define(["classnames"], function (classNames) { 39 | var style = classNames("foo", "bar"); 40 | // ... 41 | }); 42 | ``` 43 | 44 | ## v2.1.3 / 2015-07-02 45 | 46 | * updated UMD wrapper to support AMD and CommonJS on the same pacge 47 | 48 | ## v2.1.2 / 2015-05-28 49 | 50 | * added a proper UMD wrapper 51 | 52 | ## v2.1.1 / 2015-05-06 53 | 54 | * minor performance improvement thanks to type caching 55 | * improved benchmarking and results output 56 | 57 | ## v2.1.0 / 2015-05-05 58 | 59 | * added alternate `dedupe` version of classNames, which is slower (10x) but ensures that if a class is added then overridden by a falsy value in a subsequent argument, it is excluded from the result. 60 | 61 | ## v2.0.0 / 2015-05-03 62 | 63 | * performance improvement; switched to `Array.isArray` for type detection, which is much faster in modern browsers. A polyfill is now required for IE8 support, see the Readme for details. 64 | 65 | ## v1.2.2 / 2015-04-28 66 | 67 | * license comment updates to simiplify certain build scenarios 68 | 69 | ## v1.2.1 / 2015-04-22 70 | 71 | * added safe exporting for requireJS usage 72 | * clarified Bower usage and instructions 73 | 74 | ## v1.2.0 / 2015-03-17 75 | 76 | * added comprehensive support for array arguments, including nested arrays 77 | * simplified code slightly 78 | 79 | ## Previous 80 | 81 | Please see the git history for the details of previous versions. 82 | -------------------------------------------------------------------------------- /tests/dedupe.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | var assert = require('assert'); 4 | var dedupe = require('../dedupe'); 5 | 6 | describe('dedupe', function () { 7 | it('keeps object keys with truthy values', function () { 8 | assert.equal(dedupe({ 9 | a: true, 10 | b: false, 11 | c: 0, 12 | d: null, 13 | e: undefined, 14 | f: 1 15 | }), 'a f'); 16 | }); 17 | 18 | it('should dedupe dedupe', function () { 19 | assert.equal(dedupe('foo', 'bar', 'foo', 'bar', { foo: true }), 'foo bar'); 20 | }); 21 | 22 | it('should make sure subsequent objects can remove/add classes', function () { 23 | assert.equal(dedupe('foo', { foo: false }, { foo: true, bar: true }), 'foo bar'); 24 | }); 25 | 26 | it('should make sure object with falsy value wipe out previous classes', function () { 27 | assert.equal(dedupe('foo foo', 0, null, undefined, true, 1, 'b', { 'foo': false }), '1 b'); 28 | assert.equal(dedupe('foo', 'foobar', 'bar', { foo: false }), 'foobar bar'); 29 | assert.equal(dedupe('foo', 'foo-bar', 'bar', { foo: false }), 'foo-bar bar'); 30 | assert.equal(dedupe('foo', '-moz-foo-bar', 'bar', { foo: false }), '-moz-foo-bar bar'); 31 | }); 32 | 33 | it('joins arrays of class names and ignore falsy values', function () { 34 | assert.equal(dedupe('a', 0, null, undefined, true, 1, 'b'), '1 a b'); 35 | }); 36 | 37 | it('supports heterogenous arguments', function () { 38 | assert.equal(dedupe({a: true}, 'b', 0), 'a b'); 39 | }); 40 | 41 | it('should be trimmed', function () { 42 | assert.equal(dedupe('', 'b', {}, ''), 'b'); 43 | }); 44 | 45 | it('returns an empty string for an empty configuration', function () { 46 | assert.equal(dedupe({}), ''); 47 | }); 48 | 49 | it('supports an array of class names', function () { 50 | assert.equal(dedupe(['a', 'b']), 'a b'); 51 | }); 52 | 53 | it('joins array arguments with string arguments', function () { 54 | assert.equal(dedupe(['a', 'b'], 'c'), 'a b c'); 55 | assert.equal(dedupe('c', ['a', 'b']), 'c a b'); 56 | }); 57 | 58 | it('handles multiple array arguments', function () { 59 | assert.equal(dedupe(['a', 'b'], ['c', 'd']), 'a b c d'); 60 | }); 61 | 62 | it('handles arrays that include falsy and true values', function () { 63 | assert.equal(dedupe(['a', 0, null, undefined, false, true, 'b']), 'a b'); 64 | }); 65 | 66 | it('handles arrays that include arrays', function () { 67 | assert.equal(dedupe(['a', ['b', 'c']]), 'a b c'); 68 | }); 69 | 70 | it('handles arrays that include objects', function () { 71 | assert.equal(dedupe(['a', {b: true, c: false}]), 'a b'); 72 | }); 73 | 74 | it('handles deep array recursion', function () { 75 | assert.equal(dedupe(['a', ['b', ['c', {d: true}]]]), 'a b c d'); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | var assert = require('assert'); 4 | var classNames = require('../'); 5 | 6 | describe('classNames', function () { 7 | it('keeps object keys with truthy values', function () { 8 | assert.equal(classNames({ 9 | a: true, 10 | b: false, 11 | c: 0, 12 | d: null, 13 | e: undefined, 14 | f: 1 15 | }), 'a f'); 16 | }); 17 | 18 | it('joins arrays of class names and ignore falsy values', function () { 19 | assert.equal(classNames('a', 0, null, undefined, true, 1, 'b'), 'a 1 b'); 20 | }); 21 | 22 | it('supports heterogenous arguments', function () { 23 | assert.equal(classNames({a: true}, 'b', 0), 'a b'); 24 | }); 25 | 26 | it('should be trimmed', function () { 27 | assert.equal(classNames('', 'b', {}, ''), 'b'); 28 | }); 29 | 30 | it('returns an empty string for an empty configuration', function () { 31 | assert.equal(classNames({}), ''); 32 | }); 33 | 34 | it('supports an array of class names', function () { 35 | assert.equal(classNames(['a', 'b']), 'a b'); 36 | }); 37 | 38 | it('joins array arguments with string arguments', function () { 39 | assert.equal(classNames(['a', 'b'], 'c'), 'a b c'); 40 | assert.equal(classNames('c', ['a', 'b']), 'c a b'); 41 | }); 42 | 43 | it('handles multiple array arguments', function () { 44 | assert.equal(classNames(['a', 'b'], ['c', 'd']), 'a b c d'); 45 | }); 46 | 47 | it('handles arrays that include falsy and true values', function () { 48 | assert.equal(classNames(['a', 0, null, undefined, false, true, 'b']), 'a b'); 49 | }); 50 | 51 | it('handles arrays that include arrays', function () { 52 | assert.equal(classNames(['a', ['b', 'c']]), 'a b c'); 53 | }); 54 | 55 | it('handles arrays that include objects', function () { 56 | assert.equal(classNames(['a', {b: true, c: false}]), 'a b'); 57 | }); 58 | 59 | it('handles deep array recursion', function () { 60 | assert.equal(classNames(['a', ['b', ['c', {d: true}]]]), 'a b c d'); 61 | }); 62 | 63 | it('handles arrays that are empty', function () { 64 | assert.equal(classNames('a', []), 'a'); 65 | }); 66 | 67 | it('handles nested arrays that have empty nested arrays', function () { 68 | assert.equal(classNames('a', [[]]), 'a'); 69 | }); 70 | 71 | it('handles all types of truthy and falsy property values as expected', function () { 72 | assert.equal(classNames({ 73 | // falsy: 74 | null: null, 75 | emptyString: "", 76 | noNumber: NaN, 77 | zero: 0, 78 | negativeZero: -0, 79 | false: false, 80 | undefined: undefined, 81 | 82 | // truthy (literally anything else): 83 | nonEmptyString: "foobar", 84 | whitespace: ' ', 85 | function: Object.prototype.toString, 86 | emptyObject: {}, 87 | nonEmptyObject: {a: 1, b: 2}, 88 | emptyList: [], 89 | nonEmptyList: [1, 2, 3], 90 | greaterZero: 1 91 | }), 'nonEmptyString whitespace function emptyObject nonEmptyObject emptyList nonEmptyList greaterZero'); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /dedupe.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Copyright (c) 2017 Jed Watson. 3 | Licensed under the MIT License (MIT), see 4 | http://jedwatson.github.io/classnames 5 | */ 6 | /* global define */ 7 | 8 | (function () { 9 | 'use strict'; 10 | 11 | var classNames = (function () { 12 | // don't inherit from Object so we can skip hasOwnProperty check later 13 | // http://stackoverflow.com/questions/15518328/creating-js-object-with-object-createnull#answer-21079232 14 | function StorageObject() {} 15 | StorageObject.prototype = Object.create(null); 16 | 17 | function _parseArray (resultSet, array) { 18 | var length = array.length; 19 | 20 | for (var i = 0; i < length; ++i) { 21 | _parse(resultSet, array[i]); 22 | } 23 | } 24 | 25 | var hasOwn = {}.hasOwnProperty; 26 | 27 | function _parseNumber (resultSet, num) { 28 | resultSet[num] = true; 29 | } 30 | 31 | function _parseObject (resultSet, object) { 32 | for (var k in object) { 33 | if (hasOwn.call(object, k)) { 34 | // set value to false instead of deleting it to avoid changing object structure 35 | // https://www.smashingmagazine.com/2012/11/writing-fast-memory-efficient-javascript/#de-referencing-misconceptions 36 | resultSet[k] = !!object[k]; 37 | } 38 | } 39 | } 40 | 41 | var SPACE = /\s+/; 42 | function _parseString (resultSet, str) { 43 | var array = str.split(SPACE); 44 | var length = array.length; 45 | 46 | for (var i = 0; i < length; ++i) { 47 | resultSet[array[i]] = true; 48 | } 49 | } 50 | 51 | function _parse (resultSet, arg) { 52 | if (!arg) return; 53 | var argType = typeof arg; 54 | 55 | // 'foo bar' 56 | if (argType === 'string') { 57 | _parseString(resultSet, arg); 58 | 59 | // ['foo', 'bar', ...] 60 | } else if (Array.isArray(arg)) { 61 | _parseArray(resultSet, arg); 62 | 63 | // { 'foo': true, ... } 64 | } else if (argType === 'object') { 65 | _parseObject(resultSet, arg); 66 | 67 | // '130' 68 | } else if (argType === 'number') { 69 | _parseNumber(resultSet, arg); 70 | } 71 | } 72 | 73 | function _classNames () { 74 | // don't leak arguments 75 | // https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#32-leaking-arguments 76 | var len = arguments.length; 77 | var args = Array(len); 78 | for (var i = 0; i < len; i++) { 79 | args[i] = arguments[i]; 80 | } 81 | 82 | var classSet = new StorageObject(); 83 | _parseArray(classSet, args); 84 | 85 | var list = []; 86 | 87 | for (var k in classSet) { 88 | if (classSet[k]) { 89 | list.push(k) 90 | } 91 | } 92 | 93 | return list.join(' '); 94 | } 95 | 96 | return _classNames; 97 | })(); 98 | 99 | if (typeof module !== 'undefined' && module.exports) { 100 | module.exports = classNames; 101 | } else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) { 102 | // register as 'classnames', consistent with npm package name 103 | define('classnames', [], function () { 104 | return classNames; 105 | }); 106 | } else { 107 | window.classNames = classNames; 108 | } 109 | }()); 110 | -------------------------------------------------------------------------------- /tests/bind.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | var assert = require('assert'); 4 | var classNames = require('../bind'); 5 | 6 | var cssModulesMock = { 7 | a: "#a", 8 | b: "#b", 9 | c: "#c", 10 | d: "#d", 11 | e: "#e", 12 | f: "#f" 13 | }; 14 | 15 | var classNamesBound = classNames.bind(cssModulesMock); 16 | 17 | describe('bind', function () { 18 | describe('classNames', function () { 19 | it('keeps object keys with truthy values', function () { 20 | assert.equal(classNames({ 21 | a: true, 22 | b: false, 23 | c: 0, 24 | d: null, 25 | e: undefined, 26 | f: 1 27 | }), 'a f'); 28 | }); 29 | 30 | it('joins arrays of class names and ignore falsy values', function () { 31 | assert.equal(classNames('a', 0, null, undefined, true, 1, 'b'), 'a 1 b'); 32 | }); 33 | 34 | it('supports heterogenous arguments', function () { 35 | assert.equal(classNames({a: true}, 'b', 0), 'a b'); 36 | }); 37 | 38 | it('should be trimmed', function () { 39 | assert.equal(classNames('', 'b', {}, ''), 'b'); 40 | }); 41 | 42 | it('returns an empty string for an empty configuration', function () { 43 | assert.equal(classNames({}), ''); 44 | }); 45 | 46 | it('supports an array of class names', function () { 47 | assert.equal(classNames(['a', 'b']), 'a b'); 48 | }); 49 | 50 | it('joins array arguments with string arguments', function () { 51 | assert.equal(classNames(['a', 'b'], 'c'), 'a b c'); 52 | assert.equal(classNames('c', ['a', 'b']), 'c a b'); 53 | }); 54 | 55 | it('handles multiple array arguments', function () { 56 | assert.equal(classNames(['a', 'b'], ['c', 'd']), 'a b c d'); 57 | }); 58 | 59 | it('handles arrays that include falsy and true values', function () { 60 | assert.equal(classNames(['a', 0, null, undefined, false, true, 'b']), 'a b'); 61 | }); 62 | 63 | it('handles arrays that include arrays', function () { 64 | assert.equal(classNames(['a', ['b', 'c']]), 'a b c'); 65 | }); 66 | 67 | it('handles arrays that include objects', function () { 68 | assert.equal(classNames(['a', {b: true, c: false}]), 'a b'); 69 | }); 70 | 71 | it('handles deep array recursion', function () { 72 | assert.equal(classNames(['a', ['b', ['c', {d: true}]]]), 'a b c d'); 73 | }); 74 | }); 75 | 76 | describe('classNamesBound', function () { 77 | it('keeps object keys with truthy values', function () { 78 | assert.equal(classNamesBound({ 79 | a: true, 80 | b: false, 81 | c: 0, 82 | d: null, 83 | e: undefined, 84 | f: 1 85 | }), '#a #f'); 86 | }); 87 | it('keeps class names undefined in bound hash', function () { 88 | assert.equal(classNamesBound({ 89 | a: true, 90 | b: false, 91 | c: 0, 92 | d: null, 93 | e: undefined, 94 | f: 1, 95 | x: true, 96 | y: null, 97 | z: 1 98 | }), '#a #f x z'); 99 | }) 100 | it('joins arrays of class names and ignore falsy values', function () { 101 | assert.equal(classNamesBound('a', 0, null, undefined, true, 1, 'b'), '#a 1 #b'); 102 | }); 103 | 104 | it('supports heterogenous arguments', function () { 105 | assert.equal(classNamesBound({a: true}, 'b', 0), '#a #b'); 106 | }); 107 | 108 | it('should be trimmed', function () { 109 | assert.equal(classNamesBound('', 'b', {}, ''), '#b'); 110 | }); 111 | 112 | it('returns an empty string for an empty configuration', function () { 113 | assert.equal(classNamesBound({}), ''); 114 | }); 115 | 116 | it('supports an array of class names', function () { 117 | assert.equal(classNamesBound(['a', 'b']), '#a #b'); 118 | }); 119 | 120 | it('joins array arguments with string arguments', function () { 121 | assert.equal(classNamesBound(['a', 'b'], 'c'), '#a #b #c'); 122 | assert.equal(classNamesBound('c', ['a', 'b']), '#c #a #b'); 123 | }); 124 | 125 | it('handles multiple array arguments', function () { 126 | assert.equal(classNamesBound(['a', 'b'], ['c', 'd']), '#a #b #c #d'); 127 | }); 128 | 129 | it('handles arrays that include falsy and true values', function () { 130 | assert.equal(classNamesBound(['a', 0, null, undefined, false, true, 'b']), '#a #b'); 131 | }); 132 | 133 | it('handles arrays that include arrays', function () { 134 | assert.equal(classNamesBound(['a', ['b', 'c']]), '#a #b #c'); 135 | }); 136 | 137 | it('handles arrays that include objects', function () { 138 | assert.equal(classNamesBound(['a', {b: true, c: false}]), '#a #b'); 139 | }); 140 | 141 | it('handles deep array recursion', function () { 142 | assert.equal(classNamesBound(['a', ['b', ['c', {d: true}]]]), '#a #b #c #d'); 143 | }); 144 | }); 145 | 146 | }) 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Classnames 2 | =========== 3 | 4 | [![Version](http://img.shields.io/npm/v/classnames.svg)](https://www.npmjs.org/package/classnames) 5 | [![Build Status](https://travis-ci.org/JedWatson/classnames.svg?branch=master)](https://travis-ci.org/JedWatson/classnames) 6 | [![Supported by Thinkmill](https://thinkmill.github.io/badge/heart.svg)](http://thinkmill.com.au/?utm_source=github&utm_medium=badge&utm_campaign=classnames) 7 | 8 | A simple JavaScript utility for conditionally joining classNames together. 9 | 10 | Install with [npm](https://www.npmjs.com/), [Bower](https://bower.io/), or [Yarn](https://yarnpkg.com/): 11 | 12 | npm: 13 | ```sh 14 | npm install classnames --save 15 | ``` 16 | 17 | Bower: 18 | ```sh 19 | bower install classnames --save 20 | ``` 21 | 22 | Yarn (note that `yarn add` automatically saves the package to the `dependencies` in `package.json`): 23 | ```sh 24 | yarn add classnames 25 | ``` 26 | 27 | Use with [Node.js](https://nodejs.org/en/), [Browserify](http://browserify.org/), or [webpack](https://webpack.github.io/): 28 | 29 | ```js 30 | var classNames = require('classnames'); 31 | classNames('foo', 'bar'); // => 'foo bar' 32 | ``` 33 | 34 | Alternatively, you can simply include `index.js` on your page with a standalone `