diff --git a/README.md b/README.md
index 443123e..96c02c0 100644
--- a/README.md
+++ b/README.md
@@ -399,6 +399,81 @@ define([
```
+## properties template functions
+In cases you'd like to use a property that accepts a function and return renderable React component.
+You should use a **rt-template** tag that will let you do exactly that: ``.
+
+Templates can be used only as an immediate child of the component that it will be used in. All scope variable will be available in the template function.
+
+###### Sample:
+```html
+
+
+ {item}
+
+
+```
+###### Compiled (AMD):
+```javascript
+define([
+ 'react/addons',
+ 'lodash'
+], function (React, _) {
+ 'use strict';
+ function renderItem1(item) {
+ return React.createElement('div', {}, item);
+ }
+ return function () {
+ return React.createElement(MyComp, {
+ 'data': [
+ 1,
+ 2,
+ 3
+ ],
+ 'renderItem': renderItem1.bind(this)
+ });
+ };
+});
+```
+###### Compiled (with CommonJS flag):
+```javascript
+'use strict';
+var React = require('react/addons');
+var _ = require('lodash');
+function renderItem1(item) {
+ return React.createElement('div', {}, item);
+}
+module.exports = function () {
+ return React.createElement(MyComp, {
+ 'data': [
+ 1,
+ 2,
+ 3
+ ],
+ 'renderItem': renderItem1.bind(this)
+ });
+};
+```
+
+###### Compiled (with ES6 flag):
+```javascript
+import React from 'react/addons';
+import _ from 'lodash';
+function renderItem1(item) {
+ return React.createElement('div', {}, item);
+}
+export default function () {
+ return React.createElement(MyComp, {
+ 'data': [
+ 1,
+ 2,
+ 3
+ ],
+ 'renderItem': renderItem1.bind(this)
+ });
+};
+```
+
## Contributing
See the [Contributing page](CONTRIBUTING.md).
diff --git a/src/reactTemplates.js b/src/reactTemplates.js
index d28fef8..48eacb7 100644
--- a/src/reactTemplates.js
+++ b/src/reactTemplates.js
@@ -53,6 +53,7 @@ var classSetAttr = 'rt-class';
var classAttr = 'class';
var scopeAttr = 'rt-scope';
var propsAttr = 'rt-props';
+var templateNode = 'rt-template';
var defaultOptions = {modules: 'amd', version: false, force: false, format: 'stylish', targetVersion: '0.13.1', reactImportPath: 'react/addons', lodashImportPath: 'lodash'};
@@ -179,6 +180,55 @@ function generateInjectedFunc(context, namePrefix, body, params) {
return generatedFuncName;
}
+function generateTemplateProps(node, context) {
+ var propTemplateDefinition = context.options.templates && context.options.templates[node.name];
+ var propertiesTemplates = _(node.children)
+ .map(function (child, index) {
+ var templateProp = null;
+ if (child.name === templateNode) { // Generic explicit template tag
+ if (!_.has(child.attribs, 'prop')) {
+ throw RTCodeError.build('rt-template must have a prop attribute', context, child);
+ }
+
+ var childTemplate = _.find(context.options.templates, {prop: child.attribs.prop}) || {arguments: []};
+ templateProp = {
+ prop: child.attribs.prop,
+ arguments: (child.attribs.arguments ? child.attribs.arguments.split(',') : childTemplate.arguments) || []
+ };
+ } else if (propTemplateDefinition && propTemplateDefinition[child.name]) { // Implicit child template from configuration
+ templateProp = {
+ prop: propTemplateDefinition[child.name].prop,
+ arguments: child.attribs.arguments ? child.attribs.arguments.split(',') : propTemplateDefinition[child.name].arguments
+ };
+ }
+
+ if (templateProp) {
+ _.assign(templateProp, {childIndex: index, content: _.find(child.children, {type: 'tag'})});
+ }
+
+ return templateProp;
+ })
+ .compact()
+ .value();
+
+ return _.transform(propertiesTemplates, function (props, templateProp) {
+ var functionParams = _.values(context.boundParams).concat(templateProp.arguments);
+
+ var oldBoundParams = context.boundParams;
+ context.boundParams = context.boundParams.concat(templateProp.arguments);
+
+ var functionBody = 'return ' + convertHtmlToReact(templateProp.content, context);
+ context.boundParams = oldBoundParams;
+
+ var generatedFuncName = generateInjectedFunc(context, templateProp.prop, functionBody, functionParams);
+ var boundArguments = _.values(context.boundParams).join(',');
+ props[templateProp.prop] = generatedFuncName + '.bind(this' + (boundArguments.length ? ', ' + boundArguments : '') + ')';
+
+ // Remove the template child from the children definition.
+ node.children.splice(templateProp.childIndex, 1);
+ }, {});
+}
+
/**
* @param node
* @param {Context} context
@@ -237,6 +287,8 @@ function generateProps(node, context) {
}
});
+ _.assign(props, generateTemplateProps(node, context));
+
return '{' + _.map(props, function (val, key) {
return JSON.stringify(key) + ' : ' + val;
}).join(',') + '}';
@@ -304,7 +356,7 @@ function convertHtmlToReact(node, context) {
_.each(context.boundParams, function (boundParam) {
data.outerScopeMapping[boundParam] = boundParam;
});
-
+
// these are variables declared in the rt-scope attribute
data.innerScopeMapping = {};
_.each(node.attribs[scopeAttr].split(';'), function (scopePart) {
@@ -457,7 +509,7 @@ function convertTemplateToReact(html, options) {
function convertRT(html, reportContext, options) {
var rootNode = cheerio.load(html, {lowerCaseTags: false, lowerCaseAttributeNames: false, xmlMode: true, withStartIndices: true});
options = _.defaults({}, options, defaultOptions);
-
+
var defaultDefines = {};
defaultDefines[options.reactImportPath] = 'React';
defaultDefines[options.lodashImportPath] = '_';
@@ -490,15 +542,23 @@ function convertRT(html, reportContext, options) {
throw RTCodeError.build('Document should have a single root element', context, rootNode.root()[0]);
}
var body = convertHtmlToReact(firstTag, context);
- var requirePaths = _(defines).keys().map(function (reqName) { return '"' + reqName + '"'; }).value().join(',');
+ var requirePaths = _(defines).keys().map(function (reqName) {
+ return '"' + reqName + '"';
+ }).value().join(',');
var requireVars = _(defines).values().value().join(',');
var vars;
if (options.modules === 'typescript') {
- vars = _(defines).map(function (reqVar, reqPath) { return 'import ' + reqVar + " = require('" + reqPath + "');"; }).join('\n');
+ vars = _(defines).map(function (reqVar, reqPath) {
+ return 'import ' + reqVar + " = require('" + reqPath + "');";
+ }).join('\n');
} else if (options.modules === 'es6') {
- vars = _(defines).map(function (reqVar, reqPath) { return 'import ' + reqVar + " from '" + reqPath + "';"; }).join('\n');
+ vars = _(defines).map(function (reqVar, reqPath) {
+ return 'import ' + reqVar + " from '" + reqPath + "';";
+ }).join('\n');
} else {
- vars = _(defines).map(function (reqVar, reqPath) { return 'var ' + reqVar + " = require('" + reqPath + "');"; }).join('\n');
+ vars = _(defines).map(function (reqVar, reqPath) {
+ return 'var ' + reqVar + " = require('" + reqPath + "');";
+ }).join('\n');
}
var data = {body: body, injectedFunctions: '', requireNames: requireVars, requirePaths: requirePaths, vars: vars, name: options.name};
data.injectedFunctions = context.injectedFunctions.join('\n');
diff --git a/test/data/propTemplates/implicitTemplate.rt b/test/data/propTemplates/implicitTemplate.rt
new file mode 100644
index 0000000..0f83847
--- /dev/null
+++ b/test/data/propTemplates/implicitTemplate.rt
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/test/data/propTemplates/implicitTemplate.rt.js b/test/data/propTemplates/implicitTemplate.rt.js
new file mode 100644
index 0000000..3703a66
--- /dev/null
+++ b/test/data/propTemplates/implicitTemplate.rt.js
@@ -0,0 +1,19 @@
+define([
+ 'react/addons',
+ 'lodash'
+], function (React, _) {
+ 'use strict';
+ function renderRow1(rowData) {
+ return React.createElement('div', {}, rowData);
+ }
+ return function () {
+ return React.createElement('div', {}, React.createElement(List, {
+ 'data': [
+ 1,
+ 2,
+ 3
+ ],
+ 'renderRow': renderRow1.bind(this)
+ }));
+ };
+});
\ No newline at end of file
diff --git a/test/data/propTemplates/simpleTemplate.rt b/test/data/propTemplates/simpleTemplate.rt
new file mode 100644
index 0000000..4719430
--- /dev/null
+++ b/test/data/propTemplates/simpleTemplate.rt
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/test/data/propTemplates/simpleTemplate.rt.js b/test/data/propTemplates/simpleTemplate.rt.js
new file mode 100644
index 0000000..387ba75
--- /dev/null
+++ b/test/data/propTemplates/simpleTemplate.rt.js
@@ -0,0 +1,12 @@
+define([
+ 'react/addons',
+ 'lodash'
+], function (React, _) {
+ 'use strict';
+ function templateProp1(arg1) {
+ return React.createElement('div', {}, arg1);
+ }
+ return function () {
+ return React.createElement('div', { 'templateProp': templateProp1.bind(this) });
+ };
+});
\ No newline at end of file
diff --git a/test/data/propTemplates/templateInScope.rt b/test/data/propTemplates/templateInScope.rt
new file mode 100644
index 0000000..750dbf3
--- /dev/null
+++ b/test/data/propTemplates/templateInScope.rt
@@ -0,0 +1,5 @@
+
+
+ Name: {name} {arg1}
+
+
\ No newline at end of file
diff --git a/test/data/propTemplates/templateInScope.rt.js b/test/data/propTemplates/templateInScope.rt.js
new file mode 100644
index 0000000..7812efe
--- /dev/null
+++ b/test/data/propTemplates/templateInScope.rt.js
@@ -0,0 +1,16 @@
+define([
+ 'react/addons',
+ 'lodash'
+], function (React, _) {
+ 'use strict';
+ function templateProp1(name, arg1) {
+ return React.createElement('div', {}, 'Name: ', name, ' ', arg1);
+ }
+ function scopeName2() {
+ var name = 'boten';
+ return React.createElement('div', { 'templateProp': templateProp1.bind(this, name) });
+ }
+ return function () {
+ return scopeName2.apply(this, []);
+ };
+});
\ No newline at end of file
diff --git a/test/data/propTemplates/twoTemplates.rt b/test/data/propTemplates/twoTemplates.rt
new file mode 100644
index 0000000..c7e7af8
--- /dev/null
+++ b/test/data/propTemplates/twoTemplates.rt
@@ -0,0 +1,10 @@
+
+
+
+
+ {arg1 + inner1 + inner2}
+
+
{arg1}
+
+
+
\ No newline at end of file
diff --git a/test/data/propTemplates/twoTemplates.rt.js b/test/data/propTemplates/twoTemplates.rt.js
new file mode 100644
index 0000000..b96cfad
--- /dev/null
+++ b/test/data/propTemplates/twoTemplates.rt.js
@@ -0,0 +1,15 @@
+define([
+ 'react/addons',
+ 'lodash'
+], function (React, _) {
+ 'use strict';
+ function templateProp21(arg1, inner1, inner2) {
+ return React.createElement('div', {}, arg1 + inner1 + inner2);
+ }
+ function templateProp2(arg1) {
+ return React.createElement('div', { 'templateProp2': templateProp21.bind(this, arg1) }, React.createElement('div', {}, arg1));
+ }
+ return function () {
+ return React.createElement('div', { 'templateProp': templateProp2.bind(this) });
+ };
+});
\ No newline at end of file
diff --git a/test/src/test.js b/test/src/test.js
index d392a73..a15820d 100644
--- a/test/src/test.js
+++ b/test/src/test.js
@@ -114,6 +114,28 @@ test('conversion test', function (t) {
}
});
+test('prop template conversion test', function (t) {
+ var options = {
+ templates: {
+ List: {
+ Row: {prop: 'renderRow', arguments: ['rowData']}
+ }
+ }
+ };
+
+ var files = ['propTemplates/simpleTemplate.rt', 'propTemplates/templateInScope.rt', 'propTemplates/implicitTemplate.rt', 'propTemplates/twoTemplates.rt'];
+ t.plan(files.length);
+ files.forEach(check);
+
+ function check(testFile) {
+ var filename = path.join(dataPath, testFile);
+ var html = readFileNormalized(filename);
+ var expected = readFileNormalized(filename + '.js');
+ var actual = reactTemplates.convertTemplateToReact(html, options).replace(/\r/g, '').trim();
+ compareAndWrite(t, actual, expected, filename);
+ }
+});
+
/**
* @param {*} t
* @param {string} actual