├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── bower.json ├── index.js ├── package.json └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Hayato Mizuno 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bem-classnames 2 | 3 | [![CircleCI](https://img.shields.io/circleci/project/pocotan001/bem-classnames.svg)](https://circleci.com/gh/pocotan001/bem-classnames) 4 | [![npm](https://img.shields.io/npm/v/bem-classnames.svg)](https://npmjs.org/package/bem-classnames) 5 | [![Bower](https://img.shields.io/bower/v/bem-classnames.svg)](https://github.com/pocotan001/bem-classnames) 6 | 7 | 8 | bem-classnames is a simple utility to manage BEM class names on React. 9 | 10 | Inspired by [classnames](https://github.com/JedWatson/classnames). 11 | 12 | ``` sh 13 | npm install bem-classnames 14 | ``` 15 | 16 | ## Usage 17 | 18 | ``` js 19 | var cx = require('bem-classnames'); 20 | 21 | cx(/* classes, [...props|className] */); 22 | ``` 23 | 24 | **Simple** 25 | 26 | ``` js 27 | var classes = { 28 | name: 'button', 29 | modifiers: ['color', 'block'], 30 | states: ['disabled'] 31 | }; 32 | 33 | cx(classes, { color: 'green', block: true }); // "button button--green button--block" 34 | cx(classes, { disabled: true }); // "button is-disabled" 35 | cx(classes, 'a b', ['c', 'd']); // "button a b c d" 36 | ``` 37 | 38 | **Custom prefix** 39 | 40 | ``` js 41 | // Default prefixes: 42 | // 43 | // cx.prefixes = { 44 | // modifiers: '{name}--', 45 | // states: 'is-' 46 | // }; 47 | 48 | cx.prefixes.modifiers = '-'; 49 | cx(classes, { color: 'green' }); // "button -green" 50 | 51 | // You can add the prefixes 52 | cx.prefixes.foo = 'foo-'; 53 | classes.foo = ['a', 'b']; 54 | cx(classes, { a: true, b: true }); // "button foo-a foo-b" 55 | ``` 56 | 57 | **with React and ES6** 58 | 59 | ``` js 60 | import React from 'react'; 61 | import cx from 'bem-classnames'; 62 | 63 | class Button extends React.Component { 64 | render() { 65 | let classes = { 66 | name: 'button', 67 | modifiers: ['color', 'size'], 68 | states: ['disabled'] 69 | }; 70 | 71 | return ( 72 | 75 | ); 76 | } 77 | } 78 | 79 | React.render( 80 | , 81 | document.getElementById('example') 82 | ); 83 | 84 | // "button button--green button--xl a b is-disabled" 85 | ``` 86 | 87 | **for manage the elements of BEM** 88 | 89 | ``` js 90 | class Button extends React.Component { 91 | render() { 92 | let classes = { 93 | button: { 94 | name: 'button', 95 | modifiers: ['color', 'size'], 96 | states: ['disabled'] 97 | }, 98 | button__inner: { 99 | name: 'button__inner', 100 | modifiers: ['align'] 101 | } 102 | }; 103 | 104 | return ( 105 | 110 | ); 111 | } 112 | } 113 | 114 | React.render( 115 | , 116 | document.getElementById('example') 117 | ); 118 | 119 | // button -> "button button--green" 120 | // span -> "button__inner button__inner--center" 121 | ``` 122 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bem-classnames", 3 | "main": "index.js", 4 | "version": "1.0.6", 5 | "homepage": "https://github.com/pocotan001/bem-classnames", 6 | "authors": [ 7 | "pocotan001" 8 | ], 9 | "description": "A simple utility to manage BEM class names on React", 10 | "keywords": [ 11 | "bem", 12 | "classname", 13 | "component", 14 | "css", 15 | "react", 16 | "utility" 17 | ], 18 | "license": "MIT", 19 | "ignore": [ 20 | ".editorconfig", 21 | ".gitignore", 22 | "node_modules", 23 | "package.json", 24 | "test.js" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | (function(global, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | define([], factory); // AMD 4 | } else if (typeof exports === 'object') { 5 | module.exports = factory(); // CommonJS 6 | } else { 7 | global.cx = factory(); // Globals 8 | } 9 | }(this, function() { 10 | 'use strict'; 11 | 12 | var prefixes = { 13 | modifiers: '{name}--', 14 | states: 'is-' 15 | }; 16 | 17 | var push = Array.prototype.push; 18 | var slice = Array.prototype.slice; 19 | var toString = Object.prototype.toString; 20 | 21 | /** 22 | * toType([]) -> 'array' 23 | * 24 | * @param {*} object 25 | * @return {string} 26 | */ 27 | function toType(object) { 28 | return toString.call(object).slice(8, -1).toLowerCase(); 29 | } 30 | 31 | /** 32 | * is.array([]) -> true 33 | * 34 | * @param {*} object 35 | * @return {string} 36 | */ 37 | var is = {}; 38 | ['string', 'boolean', 'array', 'object'].forEach(function(type) { 39 | is[type] = function(object) { 40 | return toType(object) === type; 41 | }; 42 | }); 43 | 44 | /** 45 | * uniq(['a', 'b', 'a', 'b']) -> ['a', 'b'] 46 | * 47 | * @param {Array} array 48 | * @return {Array} 49 | */ 50 | function uniq(array) { 51 | return array.filter(function(el, i) { 52 | return array.indexOf(el) === i; 53 | }); 54 | } 55 | 56 | /** 57 | * exclude([null, undefined, 1, 0, true, false, '', 'a', ' b ']) -> ['a', 'b'] 58 | * 59 | * @param {Array} array 60 | * @return {string[]} 61 | */ 62 | function exclude(array) { 63 | return array 64 | .filter(function(el) { 65 | return is.string(el) && el.trim() !== ''; 66 | }) 67 | .map(function(className) { 68 | return className.trim(); 69 | }); 70 | } 71 | 72 | /** 73 | * split(' a b ') -> ['a', 'b'] 74 | * 75 | * @param {string} className 76 | * @return {string[]} 77 | */ 78 | function split(className) { 79 | return className.trim().split(/ +/); 80 | } 81 | 82 | /** 83 | * toClassName(['a', 'b']) -> 'a b' 84 | * 85 | * @param {string[]} names 86 | * @return {string} 87 | */ 88 | function toClassName(names) { 89 | return names.join(' ').trim(); 90 | } 91 | 92 | /** 93 | * detectPrefix('modifiers', { name: 'foo' }) -> 'foo--' 94 | * 95 | * @param {string} prefixName 96 | * @param {Object} classes 97 | * @return {string} 98 | */ 99 | function detectPrefix(prefixName, classes) { 100 | return (prefixes[prefixName] || '').replace(/\{([\w-]*?)\}/g, function (match, p1) { 101 | return classes[p1] || ''; 102 | }); 103 | } 104 | 105 | /** 106 | * getClassNamesByProps(['a'], { a: 'foo' }, '-') -> [ '-foo' ] 107 | * 108 | * @param {string[]} propNames 109 | * @param {Object} props 110 | * @param {string} [prefix] 111 | * @return {string[]} 112 | */ 113 | function getClassNamesByProps(propNames, props, prefix) { 114 | prefix = prefix || ''; 115 | 116 | return propNames 117 | .filter(function(name) { 118 | return !!props[name]; 119 | }) 120 | .map(function(name) { 121 | return prefix + (is.boolean(props[name]) ? name : props[name]); 122 | }); 123 | } 124 | 125 | /** 126 | * @param {Object} classes 127 | * @param {...Object|string} [props|className] 128 | * @return {string} 129 | */ 130 | function cx(classes/* , [...props|className] */) { 131 | if (!classes) { 132 | return ''; 133 | } 134 | 135 | var args = slice.call(arguments).slice(1); 136 | var classNames = []; 137 | 138 | Object.keys(classes).forEach(function(name) { 139 | switch (toType(classes[name])) { 140 | case 'string': 141 | push.apply(classNames, split(classes[name])); 142 | break; 143 | case 'array': 144 | args.forEach(function (arg) { 145 | if (is.object(arg)) { 146 | var names = getClassNamesByProps(classes[name], arg, detectPrefix(name, classes)); 147 | push.apply(classNames, names); 148 | } 149 | }); 150 | break; 151 | default: 152 | } 153 | }); 154 | 155 | args.forEach(function (arg) { 156 | switch (toType(arg)) { 157 | case 'string': 158 | push.apply(classNames, split(arg)); 159 | break; 160 | case 'array': 161 | push.apply(classNames, arg); 162 | break; 163 | default: 164 | } 165 | }); 166 | 167 | return toClassName(exclude(uniq(classNames))); 168 | } 169 | 170 | cx.prefixes = prefixes; 171 | 172 | return cx; 173 | })); 174 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bem-classnames", 3 | "version": "1.0.6", 4 | "description": "A simple utility to manage BEM class names on React", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha test.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/pocotan001/bem-classnames.git" 12 | }, 13 | "keywords": [ 14 | "bem", 15 | "classname", 16 | "component", 17 | "css", 18 | "react", 19 | "utility" 20 | ], 21 | "author": "pocotan001", 22 | "license": "MIT", 23 | "devDependencies": { 24 | "mocha": "^2.2.5" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var cx = require('./'); 3 | 4 | describe('cx', function() { 5 | 6 | var classes = { 7 | name: 'button', 8 | modifiers: ['color', 'block'], 9 | states: ['loading', 'disabled'] 10 | }; 11 | 12 | it('should return empty string', function() { 13 | assert.equal(cx(), ''); 14 | }); 15 | 16 | it('should return block name', function() { 17 | assert.equal(cx(classes), 'button'); 18 | }); 19 | 20 | it('should return modifiers', function() { 21 | assert.equal(cx(classes, { color: 'green' }), 'button button--green'); 22 | assert.equal(cx(classes, { color: 'green', block: true }), 'button button--green button--block'); 23 | assert.equal(cx(classes, { color: 'green' }, { block: true }), 'button button--green button--block'); 24 | }); 25 | 26 | it('should return states', function() { 27 | assert.equal(cx(classes, { loading: true }), 'button is-loading'); 28 | assert.equal(cx(classes, { loading: true, disabled: true }), 'button is-loading is-disabled'); 29 | assert.equal(cx(classes, { loading: true }, { disabled: true }), 'button is-loading is-disabled'); 30 | }); 31 | 32 | it('supports a string of class names', function() { 33 | assert.equal(cx({ name: 'button' }, 'a'), 'button a'); 34 | assert.equal(cx(classes, 'a'), 'button a'); 35 | assert.equal(cx(classes, 'a', 'b c'), 'button a b c'); 36 | }); 37 | 38 | it('supports an array of class names', function() { 39 | assert.equal(cx(classes, ['a']), 'button a'); 40 | assert.equal(cx(classes, ['a'], ['b', 'c']), 'button a b c'); 41 | }); 42 | 43 | it('should ignore, except for valid objects', function() { 44 | assert.equal(cx(classes, null, undefined, 1, 0, true, false, '', { color: 'green' }, 'a', ['b', 'c']), 'button button--green a b c'); 45 | }); 46 | 47 | it('should be trimmed', function() { 48 | assert.equal(cx(classes, '', ' b ', [' ']), 'button b'); 49 | }); 50 | 51 | it('should dedupe', function() { 52 | assert.equal(cx(classes, 'foo', 'bar', 'foo', 'bar'), 'button foo bar'); 53 | }); 54 | 55 | it('should be custom prefixes', function() { 56 | cx.prefixes.modifiers = '-'; 57 | assert.equal(cx(classes, { color: 'green', block: true }), 'button -green -block'); 58 | }); 59 | 60 | it('should be custom rules', function() { 61 | cx.prefixes.foo = 'foo-'; 62 | classes.foo = ['a', 'b']; 63 | assert.equal(cx(classes, { a: true, b: true }), 'button foo-a foo-b'); 64 | }); 65 | 66 | }); 67 | --------------------------------------------------------------------------------