├── .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 [](http://badge.fury.io/js/bem-react) [](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 |
--------------------------------------------------------------------------------