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 @@ +
+ + +
{rowData}
+
+
+
\ 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 @@ +
+ +
{arg1}
+
+
\ 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