Add support for React Native

This commit is contained in:
Nir Natan 2015-08-19 20:20:31 +03:00
parent a0f39e3435
commit 08aa36a2f1
15 changed files with 248 additions and 9 deletions

View File

@ -40,6 +40,7 @@ http://plugins.jetbrains.com/plugin/7648
* [rt-props](#rt-props)
* [rt-class](#rt-class)
* [rt-require](#rt-require)
* [rt-template](#rt-template)
* [styles](#styles)
* [event handlers](#event-handlers)
@ -66,6 +67,9 @@ In most cases, this package will be wrapped in a build task, so CLI will not be
* Browserify plugin: [react-templatify](https://www.npmjs.com/package/react-templatify)
* Webpack loader : [react-templates-loader](https://github.com/AlexanderPavlenko/react-templates-loader)
### Use React Templates for Native Apps?
You can get all the react templates functionality and more. [Click here for more info](https://github.com/wix/react-templates/blob/gh-pages/docs/native.md)
# Template directives and syntax
## Any valid HTML is a template
@ -399,7 +403,7 @@ define([
```
## properties template functions
## rt-template, and defining 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: `<rt-template prop="propName" arguments="arg1, arg2"/>`.

80
docs/native.md Normal file
View File

@ -0,0 +1,80 @@
# React Template for Native Apps
In order to use React Templates for [React Native](https://facebook.github.io/react-native/) you should set the `native` option to true.
In native mode the default `modules` option is set to `commonjs`.
## Default properties templates configuration
In native mode we define a default properties template configuration in order to easily write native templates.
```json
{
ListView: {
Row: {prop: 'renderRow', arguments: ['rowData', 'sectionID', 'rowID', 'highlightRow']},
Footer: {prop: 'renderFooter', arguments: []},
Header: {prop: 'renderHeader', arguments: []},
ScrollComponent: {prop: 'renderScrollComponent', arguments: ['props']},
SectionHeader: {prop: 'renderSectionHeader', arguments: ['sectionData', 'sectionID']},
Separator: {prop: 'renderSeparator', arguments: ['sectionID', 'rowID', 'adjacentRowHighlighted']}
}
}
```
With this configuration you can write your ListView component as follow:
##### With default arguments:
```html
<View>
<ListView dataSource="{this.state.dataSource}">
<Row>
<Text>{rowData}</Text>
</Row>
</ListView>
</View>
```
###### Compiled:
```javascript
'use strict';
var React = require('react-native');
var _ = require('lodash');
function renderRow1(rowData) {
return React.createElement(React.Text, {}, rowData);
}
module.exports = function () {
return React.createElement(React.View, {}, React.createElement(React.ListView, {
'dataSource': this.state.dataSource,
'renderRow': renderRow1.bind(this)
}));
};
```
##### With custom arguments:
```html
<View>
<ListView dataSource="{this.state.dataSource}">
<Row arguments="item">
<Text>{item}</Text>
</Row>
</ListView>
</View>
```
###### Compiled:
```javascript
'use strict';
var React = require('react-native');
var _ = require('lodash');
function renderRow1(item) {
return React.createElement(React.Text, {}, item);
}
module.exports = function () {
return React.createElement(React.View, {}, React.createElement(React.ListView, {
'dataSource': this.state.dataSource,
'renderRow': renderRow1.bind(this)
}));
};
```

View File

@ -47,6 +47,7 @@
"grunt-node-tap": "^0.1.61",
"istanbul": "^0.3.17",
"react": "^0.12.2",
"react-native": "^0.9.0",
"tape": "^4.0.1"
},
"keywords": [

View File

@ -11,6 +11,7 @@
var optionator = require('optionator');
var pkg = require('../package.json');
var reactDOMSupport = require('./reactDOMSupport');
var reactNativeSupport = require('./reactNativeSupport');
//------------------------------------------------------------------------------
// Initialization and Public Interface
@ -74,7 +75,7 @@ module.exports = optionator({
option: 'target-version',
alias: 't',
type: 'String',
default: '0.13.1',
default: reactDOMSupport.default,
description: 'React version to generate code for (' + Object.keys(reactDOMSupport).join(', ') + ')'
}, {
option: 'list-target-version',
@ -101,5 +102,16 @@ module.exports = optionator({
default: 'lodash',
type: 'String',
description: 'Dependency path for importing lodash.'
}, {
option: 'native',
alias: 'rn',
type: 'Boolean',
description: 'Renders react native templates.'
}, {
option: 'native-target-version',
alias: 'rnv',
type: 'String',
default: reactNativeSupport.default,
description: 'React native version to generate code for (' + Object.keys(reactNativeSupport).join(', ') + ')'
}]
});

View File

@ -17,7 +17,8 @@ var versions = {
'0.11.2': ver0_11_2,
'0.11.1': ver0_11_0,
'0.11.0': ver0_11_0,
'0.10.0': ver0_10_0
'0.10.0': ver0_10_0,
default: '0.13.1'
};
module.exports = versions;

10
src/reactNativeSupport.js Normal file
View File

@ -0,0 +1,10 @@
'use strict';
var ver0_9_0 = ['ActivityIndicatorIOS', 'DatePickerIOS', 'Image', 'ListView', 'MapView', 'Navigator', 'NavigatorIOS', 'PickerIOS', 'ScrollView', 'SliderIOS', 'SwitchIOS', 'TabBarIOS', 'Text', 'TextInput', 'TouchableHighlight', 'TouchableOpacity', 'TouchableWithoutFeedback', 'View', 'WebView'];
var versions = {
'0.9.0': ver0_9_0,
default: '0.9.0'
};
module.exports = versions;

19
src/reactPropTemplates.js Normal file
View File

@ -0,0 +1,19 @@
'use strict';
var native = {
'0.9.0': {
ListView: {
Row: {prop: 'renderRow', arguments: ['rowData', 'sectionID', 'rowID', 'highlightRow']},
Footer: {prop: 'renderFooter', arguments: []},
Header: {prop: 'renderHeader', arguments: []},
ScrollComponent: {prop: 'renderScrollComponent', arguments: ['props']},
SectionHeader: {prop: 'renderSectionHeader', arguments: ['sectionData', 'sectionID']},
Separator: {prop: 'renderSeparator', arguments: ['sectionID', 'rowID', 'adjacentRowHighlighted']}
}
}
};
module.exports = {
native: native,
dom: {}
};

View File

@ -7,6 +7,8 @@ var _ = require('lodash');
var esprima = require('esprima-harmony');
var escodegen = require('escodegen');
var reactDOMSupport = require('./reactDOMSupport');
var reactNativeSupport = require('./reactNativeSupport');
var reactPropTemplates = require('./reactPropTemplates');
var stringUtils = require('./stringUtils');
var rtError = require('./RTCodeError');
var RTCodeError = rtError.RTCodeError;
@ -55,7 +57,29 @@ 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'};
function getOptions(options) {
options = options || {};
var defaultOptions = {
modules: options.native ? 'commonjs': 'amd',
version: false,
force: false,
format: 'stylish',
targetVersion: reactDOMSupport.default,
reactImportPath: options.native ? 'react-native' : 'react/addons',
lodashImportPath: 'lodash',
native: false,
nativeTargetVersion: reactNativeSupport.default
};
var finalOptions = _.defaults({}, options, defaultOptions);
var defaultPropTemplates = finalOptions.native ?
reactPropTemplates.native[finalOptions.nativeTargetVersion] :
reactPropTemplates.dom[finalOptions.targetVersion];
finalOptions.propTemplates = _.defaults({}, options.propTemplates, defaultPropTemplates);
return finalOptions;
}
/**
* @param {Context} context
@ -181,7 +205,7 @@ function generateInjectedFunc(context, namePrefix, body, params) {
}
function generateTemplateProps(node, context) {
var propTemplateDefinition = context.options.templates && context.options.templates[node.name];
var propTemplateDefinition = context.options.propTemplates[node.name];
var propertiesTemplates = _(node.children)
.map(function (child, index) {
var templateProp = null;
@ -190,7 +214,7 @@ function generateTemplateProps(node, context) {
throw RTCodeError.build('rt-template must have a prop attribute', context, child);
}
var childTemplate = _.find(context.options.templates, {prop: child.attribs.prop}) || {arguments: []};
var childTemplate = _.find(context.options.propTemplates, {prop: child.attribs.prop}) || {arguments: []};
templateProp = {
prop: child.attribs.prop,
arguments: (child.attribs.arguments ? child.attribs.arguments.split(',') : childTemplate.arguments) || []
@ -300,6 +324,10 @@ function generateProps(node, context) {
* @return {string}
*/
function convertTagNameToConstructor(tagName, context) {
if (context.options.native) {
return _.includes(reactNativeSupport[context.options.nativeTargetVersion], tagName) ? 'React.' + tagName : tagName;
}
var isHtmlTag = _.includes(reactDOMSupport[context.options.targetVersion], tagName);
if (shouldUseCreateElement(context)) {
isHtmlTag = isHtmlTag || tagName.match(/^\w+(-\w+)$/);
@ -508,7 +536,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);
options = getOptions(options);
var defaultDefines = {};
defaultDefines[options.reactImportPath] = 'React';
@ -576,7 +604,7 @@ function convertRT(html, reportContext, options) {
}
function convertJSRTToJS(text, reportContext, options) {
options = _.defaults({}, options, defaultOptions);
options = getOptions(options)
options.modules = 'jsrt';
var templateMatcherJSRT = /<template>([^]*?)<\/template>/gm;
var code = text.replace(templateMatcherJSRT, function (template, html) {

View File

@ -0,0 +1,12 @@
<View>
<ListView dataSource="{this.state.dataSource}">
<Row arguments="rowData">
<Text>{rowData}</Text>
</Row>
</ListView>
<MyComp data="{[1,2,3]}">
<Row arguments="item">
<Text>{item}</Text>
</Row>
</MyComp>
</View>

View File

@ -0,0 +1,22 @@
'use strict';
var React = require('react-native');
var _ = require('lodash');
function renderRow1(rowData) {
return React.createElement(React.Text, {}, rowData);
}
function renderRow2(item) {
return React.createElement(React.Text, {}, item);
}
module.exports = function () {
return React.createElement(React.View, {}, React.createElement(React.ListView, {
'dataSource': this.state.dataSource,
'renderRow': renderRow1.bind(this)
}), React.createElement(MyComp, {
'data': [
1,
2,
3
],
'renderRow': renderRow2.bind(this)
}));
};

View File

@ -0,0 +1,7 @@
<View>
<ListView dataSource="{this.state.dataSource}">
<Row arguments="rowData">
<Text>{rowData}</Text>
</Row>
</ListView>
</View>

View File

@ -0,0 +1,12 @@
'use strict';
var React = require('react-native');
var _ = require('lodash');
function renderRow1(rowData) {
return React.createElement(React.Text, {}, rowData);
}
module.exports = function () {
return React.createElement(React.View, {}, React.createElement(React.ListView, {
'dataSource': this.state.dataSource,
'renderRow': renderRow1.bind(this)
}));
};

1
test/data/nativeView.rt Normal file
View File

@ -0,0 +1 @@
<View></View>

View File

@ -0,0 +1,6 @@
'use strict';
var React = require('react-native');
var _ = require('lodash');
module.exports = function () {
return React.createElement(React.View, {});
};

View File

@ -116,7 +116,7 @@ test('conversion test', function (t) {
test('prop template conversion test', function (t) {
var options = {
templates: {
propTemplates: {
List: {
Row: {prop: 'renderRow', arguments: ['rowData']}
}
@ -136,6 +136,30 @@ test('prop template conversion test', function (t) {
}
});
test('conversion test - native', function (t) {
var options = {
propTemplates: {
MyComp: {
Row: {prop: 'renderRow', arguments: ['rowData']}
}
},
native: true
};
var files = ['nativeView.rt', 'listViewTemplate.rt', 'listViewAndCustomTemplate.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 expected = fs.readFileSync(filename.replace(".html", ".js")).toString();
var actual = reactTemplates.convertTemplateToReact(html, options).replace(/\r/g, '').trim();
compareAndWrite(t, actual, expected, filename);
}
});
/**
* @param {*} t
* @param {string} actual