├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bower.json ├── example ├── components │ ├── button.js │ ├── dropdown.js │ └── popup.js ├── index.build.js ├── index.css ├── index.html └── index.js ├── lib ├── bemJsonToReact.js ├── bemReact.js ├── buildBemClassName.js ├── createBemComponent.js └── createClass.js ├── package.json └── spec ├── buildBemCssClass.spec.js ├── createClass.spec.js ├── render.spec.js ├── support └── jasmine.json └── tag.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | /spec 3 | /example 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.1 4 | branches: 5 | only: 6 | - master 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | See [releases](https://github.com/dfilatov/bem-react/releases). 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2014 Filatov Dmitry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bem-react [![NPM version](https://badge.fury.io/js/bem-react.png)](http://badge.fury.io/js/bem-react) [![Build Status](https://travis-ci.org/dfilatov/bem-react.svg?branch=master)](https://travis-ci.org/dfilatov/bem-react) 2 | 3 | `bem-react` is a module on top of [React](https://github.com/facebook/react/) which joins awesome React with some good BEM-specific features. 4 | 5 | Its main goals: 6 | * provide ability to use some kind of bemjson in templates and during usage (instead of ugly jsx or plain js) 7 | * take over manipulation of css classes based on BEM (instead of annoying string concatenation or `React.addons.classSet` which is also clumsy for BEM-like css classes) 8 | 9 | ## Getting Started 10 | 11 | ### Installation 12 | via npm: `npm install bem-react` 13 | 14 | via bower: `bower install bem-react` 15 | 16 | ## Building a component 17 | BemReact's component is the same as React's one except you should return bemjson from `render` method. 18 | 19 | Example: 20 | ```js 21 | var BemReact = require('bem-react'); 22 | 23 | var Button = BemReact.createClass({ 24 | getInitialState : function() { 25 | return { 26 | focused : this.props.focused 27 | }; 28 | }, 29 | 30 | _onFocus : function() { 31 | this.setState({ focused : true }); 32 | }, 33 | 34 | _onBlur : function() { 35 | this.setState({ focused : false }); 36 | }, 37 | 38 | render : function() { 39 | return { 40 | block : 'button', 41 | tag : 'button', 42 | mods : { 43 | size : this.props.size, 44 | focused : this.state.focused, 45 | disabled : this.props.disabled 46 | }, 47 | props : { 48 | disabled : this.props.disabled, 49 | onFocus : this._onFocus, 50 | onBlur : this._onBlur, 51 | onClick : this.props.onClick 52 | }, 53 | content : this.props.text 54 | }; 55 | } 56 | }); 57 | ``` 58 | 59 | ## Using a component 60 | ```js 61 | BemReact.render( 62 | { block : Button, props : { size : 'xl', disabled : true, text : 'click me' } }, 63 | document.body); 64 | // inserts to body following html: 65 | // 66 | ``` 67 | 68 | ### Composition of components 69 | Let's imagine `Dropdown` component which is the composition of `Button` and `Popup` components: 70 | ```js 71 | var Dropdown = BemReact.createClass({ 72 | getInitialState : function() { 73 | return { 74 | opened : this.props.opened 75 | }; 76 | }, 77 | 78 | _onButtonClick : function() { 79 | this.setState({ opened : !this.state.opened }); 80 | }, 81 | 82 | render : function() { 83 | return { 84 | block : 'dropdown', 85 | mods : { 86 | opened : this.state.opened, 87 | disabled : this.props.disabled 88 | }, 89 | content : [ 90 | { 91 | block : Button, 92 | props : { 93 | key : 'b', 94 | disabled : this.props.disabled, 95 | text : 'click me', 96 | onClick : this._onButtonClick 97 | } 98 | }, 99 | { 100 | block : Popup, 101 | mix : [{ block : 'dropdown', elem : 'popup' }], 102 | props : { 103 | key : 'p', 104 | visible : this.state.opened && !this.props.disabled, 105 | content : this.props.content 106 | } 107 | } 108 | ] 109 | }; 110 | } 111 | }); 112 | ``` 113 | 114 | ## BEMJSON 115 | There're two kinds of bemjson items. 116 | ### 1. Current rendered component 117 | You're able to use following fields in top-level item returned from `render`: 118 | * *String* **block** block name, required 119 | * *String* **tag** html tag, optional, `
` by default 120 | * *Object* **mods** modifiers (boolean modifiers are supported as well), optional 121 | * *Object* **props** properties (similar to the `attrs` in the traditional bemjson), optional 122 | * * **content** inner content, optional 123 | 124 | Be careful, you aren't allowed to use `mix` field there 125 | 126 | ### 2. Usage of components 127 | You're able to use following fields: 128 | * *Function* **block** link to another block, required 129 | * *Object* **props** properties, optional 130 | * *Array* **mix** mixed elements, optional 131 | 132 | ## Top-Level API 133 | 134 | API is the similar to the original React's API: 135 | 136 | #### createClass(*Object* specification) 137 | 138 | #### render(*Object* componentJson, *DOMElement* container, [*Function* callback]) 139 | 140 | #### renderToString(*Object* componentJson) 141 | 142 | #### renderToStaticMarkup(*Object* componentJson) 143 | 144 | #### unmountComponentAtNode(*DOMElement* container) 145 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bem-react", 3 | "version": "0.0.10", 4 | "homepage": "https://github.com/dfilatov/bem-react", 5 | "authors": [ 6 | "Dmitry Filatov " 7 | ], 8 | "description": "BEM-flavoured React", 9 | "main": "lib/bemReact.js", 10 | "moduleType": [ 11 | "node" 12 | ], 13 | "keywords": [ 14 | "react", 15 | "bem", 16 | "json", 17 | "bemjson" 18 | ], 19 | "license": "MIT", 20 | "dependencies": { 21 | "react": "0.12.x" 22 | }, 23 | "ignore": [ 24 | ".*", 25 | "spec", 26 | "example" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /example/components/button.js: -------------------------------------------------------------------------------- 1 | var bemReact = require('../../lib/bemReact'); 2 | 3 | module.exports = bemReact.createClass({ 4 | getInitialState : function() { 5 | return { 6 | pressed : false, 7 | focused : !!this.props.focused 8 | }; 9 | }, 10 | 11 | componentWillUnmount: function() { 12 | document.removeEventListener('mouseup', this._onMouseUp); 13 | }, 14 | 15 | _onMouseDown : function() { 16 | this.setState({ pressed : true }); 17 | document.addEventListener('mouseup', this._onMouseUp); 18 | }, 19 | 20 | _onMouseUp : function(e) { 21 | this.setState({ pressed : false }); 22 | document.removeEventListener('mouseup', this._onMouseUp); 23 | }, 24 | 25 | _onFocus : function() { 26 | this.setState({ focused : true }); 27 | }, 28 | 29 | _onBlur : function() { 30 | this.setState({ focused : false }); 31 | }, 32 | 33 | render : function() { 34 | return { 35 | block : 'button', 36 | mods : { 37 | disabled : this.props.disabled, 38 | pressed : this.state.pressed, 39 | focused : this.state.focused 40 | }, 41 | tag : 'button', 42 | props : { 43 | onMouseDown : this._onMouseDown, 44 | onFocus : this._onFocus, 45 | onBlur : this._onBlur, 46 | onClick : this.props.onClick, 47 | disabled : this.props.disabled 48 | }, 49 | content : this.props.text 50 | }; 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /example/components/dropdown.js: -------------------------------------------------------------------------------- 1 | var bemReact = require('../../lib/bemReact'), 2 | Button = require('./button'), 3 | Popup = require('./popup'); 4 | 5 | module.exports = bemReact.createClass({ 6 | getInitialState : function() { 7 | return { 8 | opened : this.props.opened 9 | }; 10 | }, 11 | 12 | _onButtonClick : function() { 13 | this.setState({ opened : !this.state.opened }); 14 | }, 15 | 16 | render : function() { 17 | return { 18 | block : 'dropdown', 19 | mods : { 20 | opened : this.state.opened, 21 | disabled : this.props.disabled 22 | }, 23 | tag : 'div', 24 | content : [ 25 | { 26 | block : Button, 27 | props : { 28 | key : 'b', 29 | disabled : this.props.disabled, 30 | text : 'dropdown-button', 31 | onClick : this._onButtonClick 32 | } 33 | }, 34 | { 35 | block : Popup, 36 | mix : [{ block : 'dropdown', elem : 'popup' }], 37 | props : { 38 | key : 'p', 39 | visible : this.state.opened && !this.props.disabled, 40 | content : this.props.content 41 | } 42 | } 43 | ] 44 | }; 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /example/components/popup.js: -------------------------------------------------------------------------------- 1 | var bemReact = require('../../lib/bemReact'); 2 | 3 | module.exports = bemReact.createClass({ 4 | render : function() { 5 | return { 6 | block : 'popup', 7 | mods : { 8 | visible : this.props.visible 9 | }, 10 | props : { 11 | onClick : this._onClick 12 | }, 13 | content : this.props.content 14 | }; 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /example/index.css: -------------------------------------------------------------------------------- 1 | .button_focused 2 | { 3 | border: 2px solid red; 4 | } 5 | 6 | .button_pressed 7 | { 8 | background: yellow; 9 | } 10 | 11 | .popup 12 | { 13 | display: none; 14 | border: 1px solid #000; 15 | padding: 10px; 16 | } 17 | 18 | .popup_visible 19 | { 20 | display: block; 21 | } -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | var bemReact = require('../lib/bemReact'), 2 | Dropdown = require('./components/dropdown'); 3 | 4 | bemReact.render( 5 | { block : Dropdown, props : { content : 'dropdown content' } }, 6 | document.body); 7 | -------------------------------------------------------------------------------- /lib/bemJsonToReact.js: -------------------------------------------------------------------------------- 1 | var react = require('react'), 2 | buildBemClassName = require('./buildBemClassName'), 3 | createBemComponent = require('./createBemComponent'); 4 | 5 | module.exports = function bemJsonToReact(json, curBlock, parent) { 6 | if(json) { 7 | if(Array.isArray(json)) { 8 | return json.map(function(item) { 9 | return bemJsonToReact(item, curBlock, parent); 10 | }); 11 | } 12 | 13 | if(json.elem) { 14 | (json.props || (json.props = {})) 15 | .className = buildBemClassName(json.block || curBlock, json.elem, json.mods, json.mix); 16 | 17 | return react.createElement( 18 | json.tag || 'div', 19 | json.props, 20 | bemJsonToReact(json.content, curBlock, parent)); 21 | } 22 | 23 | if(json.block) { 24 | return createBemComponent(json, parent); 25 | } 26 | } 27 | 28 | return json; 29 | }; 30 | -------------------------------------------------------------------------------- /lib/bemReact.js: -------------------------------------------------------------------------------- 1 | var react = require('react'), 2 | createClass = require('./createClass'), 3 | createBemComponent = require('./createBemComponent'); 4 | 5 | exports.createClass = createClass; 6 | 7 | exports.unmountComponentAtNode = function(container) { 8 | return react.unmountComponentAtNode(container); 9 | }; 10 | 11 | ['render', 'renderToString', 'renderToStaticMarkup'].forEach(function(method) { 12 | var reactMethod = react[method]; 13 | exports[method] = function(element) { 14 | element = createBemComponent(element); 15 | return reactMethod.apply(react, arguments); 16 | }; 17 | }); 18 | -------------------------------------------------------------------------------- /lib/buildBemClassName.js: -------------------------------------------------------------------------------- 1 | var MOD_DELIM = '_', 2 | ELEM_DELIM = '__'; 3 | 4 | module.exports = function buildBemClassName(block, elem, mods, mix) { 5 | if(typeof elem !== 'string') { 6 | mix = mods; 7 | mods = elem; 8 | elem = null; 9 | } 10 | 11 | var entity = block + (elem? ELEM_DELIM + elem : ''), 12 | res = entity; 13 | 14 | for(var modName in mods) { 15 | mods.hasOwnProperty(modName) && mods[modName] && 16 | (res += ' ' + entity + MOD_DELIM + modName + 17 | (mods[modName] === true? '' : MOD_DELIM + mods[modName])); 18 | } 19 | 20 | if(mix) { 21 | var i = 0, 22 | mixItem; 23 | 24 | while(mixItem = mix[i++]) { 25 | if(!mixItem.block || !mixItem.elem) { 26 | throw Error('render: both block and elem should be specified in mix'); 27 | } 28 | 29 | res += ' ' + buildBemClassName(mixItem.block, mixItem.elem, mixItem.mods); 30 | } 31 | } 32 | 33 | return res; 34 | }; 35 | -------------------------------------------------------------------------------- /lib/createBemComponent.js: -------------------------------------------------------------------------------- 1 | var react = require('react'); 2 | 3 | module.exports = function(json, parent) { 4 | if(!json || !json.block) { 5 | throw Error('render: invalid bem component json'); 6 | } 7 | 8 | var typeOfBlock = typeof json.block; 9 | if(typeOfBlock !== 'function') { 10 | throw Error('render: reference to block should be a constructor, not a ' + typeOfBlock); 11 | } 12 | 13 | json.props || (json.props = {}); 14 | json.props.__parent || (json.props.__parent = parent); 15 | 16 | return react.createElement(json.block, json.props); 17 | }; 18 | -------------------------------------------------------------------------------- /lib/createClass.js: -------------------------------------------------------------------------------- 1 | var react = require('react'), 2 | buildBemClassName = require('./buildBemClassName'), 3 | bemJsonToReact = require('./bemJsonToReact'); 4 | 5 | module.exports = function(spec) { 6 | var origRender = spec.render; 7 | if(!origRender) { 8 | throw Error('createClass: "render" method should be specified'); 9 | } 10 | spec.render = function() { 11 | var json = origRender.call(this); 12 | 13 | if(!json) { 14 | throw Error('render: should return bemjson'); 15 | } 16 | 17 | if(!json.block) { 18 | throw Error('render: block should be specified in returned bemjson'); 19 | } 20 | 21 | if(typeof json.block !== 'string') { 22 | throw Error('render: block should be a string'); 23 | } 24 | 25 | (json.props || (json.props = {})) 26 | .className = buildBemClassName(json.block, json.mods, this.props.mix); 27 | 28 | return react.createElement( 29 | json.tag || 'div', 30 | json.props, 31 | bemJsonToReact(json.content, json.block, this)); 32 | }; 33 | 34 | spec.getParent = function() { 35 | return this.props.__parent; 36 | }; 37 | 38 | return react.createClass(spec); 39 | }; 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bem-react", 3 | "version": "0.0.10", 4 | "description": "BEM-flavoured React", 5 | "keywords": [ 6 | "react", 7 | "bem", 8 | "json", 9 | "bemjson" 10 | ], 11 | "main": "lib/bemReact.js", 12 | "author": "Dmitry Filatov ", 13 | "peerDependencies": { 14 | "react": "0.13.x" 15 | }, 16 | "devDependencies": { 17 | "browserify": "6.3.3", 18 | "react": "0.13.x", 19 | "jasmine": "2.1.0" 20 | }, 21 | "license": "MIT", 22 | "scripts": { 23 | "test": "jasmine", 24 | "build-example": "browserify example/index.js > example/index.build.js" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /spec/buildBemCssClass.spec.js: -------------------------------------------------------------------------------- 1 | var buildBemClassName = require('../lib/buildBemClassName'); 2 | 3 | describe('buildBemClassName', function() { 4 | it('should build block class name', function() { 5 | expect(buildBemClassName('button')) 6 | .toBe('button'); 7 | }); 8 | 9 | it('should build block with modifiers class name', function() { 10 | expect( 11 | buildBemClassName('button', { mod1 : 'val1', mod2 : 'val2' })) 12 | .toBe('button button_mod1_val1 button_mod2_val2'); 13 | }); 14 | 15 | it('should build elem class name', function() { 16 | expect(buildBemClassName('button', 'box')) 17 | .toBe('button__box'); 18 | }); 19 | 20 | it('should build elem with modifiers class name', function() { 21 | expect(buildBemClassName('button', 'box', { mod1 : 'val1', mod2 : 'val2' })) 22 | .toBe('button__box button__box_mod1_val1 button__box_mod2_val2'); 23 | }); 24 | 25 | it('should build block with mixed elem class name', function() { 26 | expect(buildBemClassName('button', null, [{ block : 'mixed', elem : 'elem' }])) 27 | .toBe('button mixed__elem'); 28 | }); 29 | 30 | it('should throw error if mix contains block only', function() { 31 | expect(function() { 32 | buildBemClassName('button', null, [{ block : 'mixed' }]) 33 | }).toThrowError('render: both block and elem should be specified in mix'); 34 | }); 35 | 36 | it('should throw error if mix only contains elem', function() { 37 | expect(function() { 38 | buildBemClassName('button', null, [{ elem : 'mixed' }]) 39 | }).toThrowError('render: both block and elem should be specified in mix'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /spec/createClass.spec.js: -------------------------------------------------------------------------------- 1 | var createClass = require('../lib/createClass'), 2 | ReactElement = require('react/lib/ReactElement'); 3 | 4 | describe('createClass', function() { 5 | it('should return constructor', function() { 6 | expect(typeof createClass({ render : function() {}})).toBe('function'); 7 | }); 8 | 9 | it('should throw error if "render" method isn\'t specified', function() { 10 | expect(function() { 11 | createClass({}); 12 | }).toThrowError('createClass: "render" method should be specified'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /spec/render.spec.js: -------------------------------------------------------------------------------- 1 | var bemReact = require('../lib/bemReact'), 2 | react = require('react'); 3 | 4 | describe('render', function() { 5 | var Block; 6 | beforeEach(function() { 7 | Block = bemReact.createClass({ 8 | render : function() { 9 | return { 10 | block : 'test', 11 | tag : 'span' 12 | }; 13 | } 14 | }); 15 | }); 16 | 17 | it('should accept bem component json', function() { 18 | expect(bemReact.renderToStaticMarkup({ block : Block })) 19 | .toBe(''); 20 | }); 21 | 22 | it('should accept React\'s element in bem component json', function() { 23 | var ReactComponent = react.createClass({ 24 | render : function() { 25 | return react.createElement('div', this.props); 26 | } 27 | }); 28 | 29 | expect(bemReact.renderToStaticMarkup({ block : ReactComponent, props : { disabled : true } })) 30 | .toBe('
'); 31 | }); 32 | 33 | it('should throw error if block isn\'t specified in input', function() { 34 | expect(function() { 35 | bemReact.renderToStaticMarkup({ foo : 'bar' }); 36 | }).toThrowError('render: invalid bem component json'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*spec.js" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /spec/tag.spec.js: -------------------------------------------------------------------------------- 1 | var bemReact = require('../lib/bemReact'); 2 | 3 | describe('tag', function() { 4 | it('for block should be
by default', function() { 5 | var Block = bemReact.createClass({ 6 | render : function() { 7 | return { 8 | block : 'test' 9 | }; 10 | } 11 | }); 12 | 13 | expect(bemReact.renderToStaticMarkup({ block : Block })) 14 | .toBe('
'); 15 | }); 16 | 17 | it('should use "tag" field', function() { 18 | var Block = bemReact.createClass({ 19 | render : function() { 20 | return { 21 | block : 'test', 22 | tag : 'span' 23 | }; 24 | } 25 | }); 26 | 27 | expect(bemReact.renderToStaticMarkup({ block : Block })) 28 | .toBe(''); 29 | }); 30 | }); 31 | --------------------------------------------------------------------------------