Skip to content

Commit

Permalink
Merge pull request #24 from bem-sdk/yeti-or.nestedCustomProps
Browse files Browse the repository at this point in the history
Nested custom props
  • Loading branch information
Yeti-or committed Jun 19, 2017
2 parents 63a0380 + 87bfedb commit 98b3197
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 86 deletions.
1 change: 1 addition & 0 deletions .eslintignore
@@ -1,2 +1,3 @@
node_modules
demo
coverage
52 changes: 52 additions & 0 deletions lib/JSXNode.js
@@ -0,0 +1,52 @@
var pascalCase = require('pascal-case');

var reactMappings = require('./reactMappings');
var valToStr = require('./helpers').valToStr;

function JSXNode(tag, props, children) {
this.tag = tag || 'div';
this.props = props || {};
this.children = children || [];
this.bemEntity = null;
this.isJSON = false;
this.isSimple = false;
this.isText = false;
this.simpleVal = undefined;
}

var propsToStr = props => Object.keys(props).reduce((acc, k) => {
if (typeof props[k] === 'string') {
return acc + ` ${k}=${valToStr(props[k])}`
} else {
return acc + ` ${k}={${valToStr(props[k])}}`
}
}, '');

var tagToClass = tag => reactMappings[tag] ? tag : pascalCase(tag);

JSXNode.prototype.toString = function() {
if (this.isText) {
return this.simpleVal;
}
if (this.isSimple) {
return this.simpleVal;
}
if (this.isJSON) {
return valToStr(this.props);
}

var tag = tagToClass(this.tag);
var children = [].concat(this.children)
.filter(Boolean)
// remove empty text nodes
.filter(child => !(child.isText && child.simpleVal === ''));

var str = children.length ?
`<${tag}${propsToStr(this.props)}>\n${children.join('\n')}\n</${tag}>` :
`<${tag}${propsToStr(this.props)}/>`;
return str;
};


module.exports = JSXNode;
module.exports.tagToClass = tagToClass;
4 changes: 3 additions & 1 deletion lib/helpers.js
@@ -1,4 +1,3 @@

function valToStr(val) {
switch(typeof val) {
case 'string':
Expand All @@ -21,6 +20,9 @@ function propToStr (key, val) {
}

function objToStr(obj) {
if (obj instanceof require('./JSXNode')) {
return obj.isText ? `'${obj}'` : obj.toString();
}
const keys = Object.keys(obj);
if (!keys.length) { return '{}'; }
return `{ ${keys.map(k => propToStr(k, obj[k])).join(', ')} }`;
Expand Down
114 changes: 38 additions & 76 deletions lib/index.js
@@ -1,50 +1,10 @@
var bn = require('@bem/naming');
var BemEntity = require('@bem/entity-name');
var pascalCase = require('pascal-case');

var reactMappings = require('./reactMappings');
var valToStr = require('./helpers').valToStr;
var styleToObj = require('./helpers').styleToObj;

var JSXNode = require('./JSXNode');
var plugins = require('./plugins');

function JSXNode(tag, props, children) {
this.tag = tag || 'div';
this.props = props || {};
this.children = children || [];
this.bemEntity = null;
this.isText = false;
this.simpleText = '';
}

var propsToStr = props => Object.keys(props).reduce((acc, k) => {
if (typeof props[k] === 'string') {
return acc + ` ${k}=${valToStr(props[k])}`
} else if (props[k] instanceof JSXNode) {
return acc + ` ${k}={${render(props[k])}}`
} else {
return acc + ` ${k}={${valToStr(props[k])}}`
}
}, '');
var tagToClass = tag => reactMappings[tag] ? tag : pascalCase(tag);

JSXNode.prototype.toString = function() {
if (this.isText) {
return this.simpleText;
}

var tag = tagToClass(this.tag);
var children = [].concat(this.children)
.filter(Boolean)
// remove empty text nodes
.filter(child => !(child.isText && child.simpleText === ''));

var str = children.length ?
`<${tag}${propsToStr(this.props)}>\n${children.join('\n')}\n</${tag}>` :
`<${tag}${propsToStr(this.props)}/>`;
return str;
};

function Transformer(options) {
this.plugins = [];
this.use(plugins.defaultPlugins.map(plugin => plugin()));
Expand All @@ -62,50 +22,40 @@ Transformer.prototype.process = function(bemjson) {

var node;

var setJsx = (json) => {
var jsx = new JSXNode();
var _blockName = json.block || node.blockName;

if (typeof json === 'string') {
jsx.isText = true;
jsx.simpleText = json;
}

if (json.tag) {
jsx.tag = json.tag;
} else if (json.block || json.elem) {
jsx.bemEntity = new BemEntity({ block: _blockName, elem: json.elem });
jsx.tag = this.bemNaming.stringify(jsx.bemEntity);
}

return jsx;
};

while((node = nodes.shift())) {
var json = node.json, i;

if (Array.isArray(json)) {
for (i = 0; i < json.length; i++) {
nodes.push({ json: json[i], id: i, tree: node.tree, blockName: node.blockName});
nodes.push({ json: json[i], id: i, tree: node.tree, blockName: node.blockName });
}
} else {
var res = undefined;
var jsx = new JSXNode();
var blockName = json.block || node.blockName;

var jsx = setJsx(json);

for (var key in json) {
if (!~['mix', 'content', 'attrs'].indexOf(key) && typeof Object(json[key]).block === 'string') {
var nestedJSX = setJsx(json[key]);

for (i = 0; i < this.plugins.length; i++) {
this.plugins[i](nestedJSX, Object.assign({ block: json[key].block }, json[key]));
switch (typeof json) {
case 'string':
jsx.isText = true;
case 'number':
case 'boolean':
jsx.isSimple = true;
jsx.simpleVal = json;
break;

default:
if (json.tag) {
jsx.tag = json.tag;
} else if (json.block || json.elem) {
jsx.bemEntity = new BemEntity({ block: blockName, elem: json.elem });
jsx.tag = this.bemNaming.stringify(jsx.bemEntity);
} else {
jsx.isJSON = true;
}

json[key] = nestedJSX;
}
break;
}


for (i = 0; i < this.plugins.length; i++) {
var plugin = this.plugins[i];
res = plugin(jsx, Object.assign({ block: blockName }, json));
Expand All @@ -118,6 +68,20 @@ Transformer.prototype.process = function(bemjson) {
}
}

// Nested JSX in custom fields
for (var key in json) {
if (!~['mix', 'content', 'attrs', 'block', 'elem', 'mods', 'elemMods', 'tag', 'js'].indexOf(key)) {
var prop = jsx.props[key];
if (typeof prop === 'object') {
if (Array.isArray(prop)) {
nodes.push({ json: prop, id: key, tree: prop, blockName: blockName });
} else {
nodes.push({ json: prop, id: key, tree: jsx.props, blockName: blockName });
}
}
}
}

if (res === undefined) {
var content = json.content;
if (content) {
Expand Down Expand Up @@ -167,9 +131,7 @@ Transformer.prototype.use = function() {
};

function render(tree) {
return Array.isArray(tree) ?
tree.join('\n') :
tree.toString();
return tree.join('\n');
}

Transformer.prototype.Transformer = Transformer;
Expand All @@ -178,6 +140,6 @@ module.exports = function(opts) {
return new Transformer(opts || {});
};

module.exports.tagToClass = tagToClass;
module.exports.tagToClass = JSXNode.tagToClass;
module.exports.plugins = plugins;
module.exports.styleToObj = styleToObj;
5 changes: 2 additions & 3 deletions lib/plugins.js
Expand Up @@ -39,9 +39,9 @@ module.exports.stylePropToObj = () => function stylePropToObj(jsx) {

module.exports.keepWhiteSpaces = () => function keepWhiteSpaces(jsx) {
if (jsx.isText) {
if (jsx.simpleText[0] === ' ' || jsx.simpleText[jsx.simpleText.length - 1] === ' ') {
if (jsx.simpleVal[0] === ' ' || jsx.simpleVal[jsx.simpleVal.length - 1] === ' ') {
// wrap to {} to keep spaces
jsx.simpleText = `{${valToStr(jsx.simpleText)}}`;
jsx.simpleVal = `{${valToStr(jsx.simpleVal)}}`;
}
}
};
Expand All @@ -64,4 +64,3 @@ module.exports.whiteList = function(options) {
}
}
};

2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -32,7 +32,7 @@
"bugs": {
"url": "https://github.com/bem-sdk/bemjson-to-jsx/issues"
},
"homepage": "https://github.com/bem-sdk/bemjson-to-jsx#readme",
"homepage": "https://bem-sdk.github.io/bemjson-to-jsx",
"dependencies": {
"@bem/entity-name": "github:bem-sdk/bem-entity-name",
"@bem/naming": "^2.0.0-6",
Expand Down
33 changes: 28 additions & 5 deletions test/index.js
Expand Up @@ -118,25 +118,48 @@ describe('transform', () => {

it('should treat mods as props', () => {
expect(
transform({ block: 'button2', mods: {theme: 'normal', size: 's'} }).JSX
transform({ block: 'button2', mods: { theme: 'normal', size: 's' } }).JSX
).to.equal(`<Button2 theme='normal' size='s'/>`);
});

it('should provide mix as obj', () => {
expect(
transform({ block: 'button2', mix: {block: 'header', elem: 'button' } }).JSX
transform({ block: 'button2', mix: { block: 'header', elem: 'button' } }).JSX
).to.equal(`<Button2 mix={{ 'block': 'header', 'elem': 'button' }}/>`);
});

it('should provide custom prop as jsx', () => {
it('should provide custom prop with block as jsx', () => {
expect(
transform({ block: 'button2', custom: {block: 'header', elem: 'button' } }).JSX
transform({ block: 'button2', custom: { block: 'header', elem: 'button' } }).JSX
).to.equal(`<Button2 custom={<HeaderButton/>}/>`);
});

it('should provide custom prop with elem as jsx', () => {
expect(
transform({ block: 'button2', custom: { elem: 'text' } }).JSX
).to.equal(`<Button2 custom={<Button2Text/>}/>`);
});

it('should provide custom prop[] as jsx', () => {
expect(
transform({ block: 'button2', custom: [42, true, { val: 42 }, 'Hello world', { block: 'header', elem: 'button' }] }).JSX
).to.equal(`<Button2 custom={[42, true, { 'val': 42 }, 'Hello world', <HeaderButton/>]}/>`);
});

it('should provide custom with nested blocks as jsx', () => {
expect(
transform({
block: 'menu2',
items: [
{ icon: { block: 'icon', mods: { type: 'kz' } } }
]
}).JSX
).to.equal(`<Menu2 items={[{ 'icon': <Icon type='kz'/> }]}/>`);
});

it('should treat strings as text', () => {
expect(
transform(['Hello I am a string', { block: 'button2', content: 'Hello I am a string'}]).JSX
transform(['Hello I am a string', { block: 'button2', content: 'Hello I am a string' }]).JSX
).to.equal(`Hello I am a string\n<Button2>\nHello I am a string\n</Button2>`);
});
});

0 comments on commit 98b3197

Please sign in to comment.