diff --git a/README.md b/README.md
index 008cb29..2620f8a 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,35 @@
# Cycle React Native Driver
## Experimental
+This fork of [cyclejs/cycle-react-native](https://github.com/cyclejs/cycle-react-native) was created at the CycleConf 2016 hackathon by @ohanhi, @justinwoo, @chadrien, @sectore, @ozzee and @jevakallio. It contains a collection of ideas and hacks to test feasibility of the CycleJS architecture on React Native.
+
+## Features
+
+ * Custom `Touchable*` components for event delegation to support lazily created elements (e.g. navigation scenes, ListView rows)
+ * Custom `Animated` component to run animations declaratively
+ * Custom `ListView` component to manage "infinite scrolling" with `ListView.DataSource`
+ * Navigation support with `NavigationExperimental`
+
+See [jevakallio/cycle-react-native-example](https://github.com/jevakallio/cycle-react-native-example) for example of use.
+
+## Running on iOS
+
+Start by installing React Native [prerequisites](https://facebook.github.io/react-native/docs/getting-started.html) (XCode, react-native-cli, watchman).
+
+Then:
```
-npm install @cycle/react-native@1.0.0-experimental.12
+git clone git@github.com:jevakallio/cycle-react-native.git && cd cycle-react-native
+npm install
+react-native run-ios
```
+## Running on Android
+
+Good luck!
+
## Usage
+
```js
let {Rx, run} = require('@cycle/core');
let React = require('react-native');
@@ -32,3 +55,4 @@ run(main, {
RN: makeReactNativeDriver('MyMobileApp'),
});
```
+
diff --git a/package.json b/package.json
index 975a8fe..f6987dc 100644
--- a/package.json
+++ b/package.json
@@ -11,11 +11,11 @@
},
"peerDependencies": {
"rx": ">=3",
- "react-native": "0.18.0"
+ "react-native": "~0.24.0"
},
"devDependencies": {
"rx": ">=3",
- "react-native": "0.18.0",
+ "react-native": "~0.24.0",
"babel": "5.8.x",
"eslint": "1.0.x",
"eslint-config-cycle": "3.0.x",
diff --git a/src/Animated.js b/src/Animated.js
new file mode 100644
index 0000000..871f335
--- /dev/null
+++ b/src/Animated.js
@@ -0,0 +1,78 @@
+import React from 'react-native';
+const {
+ Animated,
+ PropTypes
+} = React;
+
+function createAnimationDongle(className) {
+ return React.createClass({
+ displayName: 'Cycle' + className,
+
+ propTypes: {
+ },
+
+ getInitialState() {
+ const currentValue = new Animated.Value(
+ this.props.initialValue || 0
+ );
+
+ return {
+ currentValue
+ }
+ },
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.value !== this.state.currentValue._value) {
+ this.runAnimation(nextProps);
+ }
+ },
+
+ runAnimation({animation, options = {}, value}) {
+ animation(this.state.currentValue, {...options,
+ toValue: value
+ }).start();
+ },
+
+ render() {
+ const AnimatedClass = Animated[className];
+ const { animate } = this.props;
+
+ const animatedStyle = Object.keys(animate)
+ .filter(key => key !== 'transform')
+ .reduce((acc, key) => {
+ return {...acc,
+ [key]: this.state.currentValue.interpolate(animate[key])
+ }
+ }, {});
+
+ if (animate.transform) {
+ const transforms = animate.transform.map(obj => {
+ const key = Object.keys(obj)[0];
+ return {
+ [key]: this.state.currentValue.interpolate(obj[key])
+ };
+ });
+
+ animatedStyle.transform = transforms;
+ }
+
+ const style = {...(this.props.style || {}), ...animatedStyle}
+
+ const extraProps = {
+ source: this.props.source
+ };
+
+ return (
+
+ {this.props.children}
+
+ );
+ }
+ });
+};
+
+export default {
+ View: createAnimationDongle('View'),
+ Text: createAnimationDongle('Text'),
+ Image: createAnimationDongle('Image')
+};
diff --git a/src/ListView.js b/src/ListView.js
new file mode 100644
index 0000000..f4e564c
--- /dev/null
+++ b/src/ListView.js
@@ -0,0 +1,41 @@
+'use strict';
+
+import React from 'react-native';
+
+const {
+ PropTypes,
+ ListView
+} = React;
+
+export default React.createClass({
+ displayName: 'CycleListView',
+ propTypes: {
+ items: PropTypes.array.isRequired
+ },
+
+ getInitialState() {
+ const dataSource = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
+ return {dataSource: dataSource.cloneWithRows(this.props.items)};
+ },
+
+ componentWillReceiveProps({items}) {
+ if (items !== this.props.items) {
+ this.setState({dataSource: this.state.dataSource.cloneWithRows(items)});
+ }
+ },
+
+ getScrollResponder() {
+ return this._listView.getScrollResponder();
+ },
+
+ render() {
+ const {items, ...listViewProps} = this.props;
+ return (
+ this._listView = listView}
+ dataSource={this.state.dataSource}
+ {...listViewProps}
+ />
+ );
+ }
+});
diff --git a/src/Touchable.js b/src/Touchable.js
new file mode 100644
index 0000000..164b1ff
--- /dev/null
+++ b/src/Touchable.js
@@ -0,0 +1,61 @@
+'use strict';
+
+import React from 'react-native';
+import {findHandler} from './driver';
+const {
+ View,
+ PropTypes
+} = React;
+
+const ACTION_TYPES = {
+ onPress: 'press',
+ onPressIn: 'pressIn',
+ onPressOut: 'pressOut',
+ onLongPress: 'longPress'
+};
+
+function createTouchableClass(className) {
+ return React.createClass({
+ displayName: 'Cycle' + className,
+ propTypes: {
+ selector: PropTypes.string.isRequired,
+ payload: PropTypes.any
+ },
+ setNativeProps(props) {
+ this._touchable.setNativeProps(props);
+ },
+ render() {
+ const TouchableClass = React[className];
+ const {selector, ...props} = this.props;
+
+ // find all defined touch handlers
+ const handlers = Object.keys(ACTION_TYPES)
+ .map(name => [name, findHandler(ACTION_TYPES[name], selector)])
+ .filter(([_, handler]) => !!handler)
+ .reduce((memo, [name, handler]) => {
+ // pass payload to event handler if defined
+ memo[name] = () => handler(this.props.payload || null);
+ return memo;
+ }, {});
+
+
+ return (
+ this._touchable = view}
+ {...handlers}
+ >
+
+ {this.props.children}
+
+
+ );
+ }
+ });
+}
+
+export default {
+ TouchableOpacity: createTouchableClass('TouchableOpacity'),
+ TouchableWithoutFeedback: createTouchableClass('TouchableWithoutFeedback'),
+ TouchableHighlight: createTouchableClass('TouchableHighlight'),
+ TouchableNativeFeedback: createTouchableClass('TouchableNativeFeedback')
+};
diff --git a/src/driver.js b/src/driver.js
index b40a183..f37fb21 100644
--- a/src/driver.js
+++ b/src/driver.js
@@ -2,51 +2,52 @@ import React from 'react-native'
import Rx from 'rx'
const {AppRegistry, View} = React
+const BACK_ACTION = '@@back';
+const backHandler = new Rx.Subject
+
+let handlers = {
+ [BACK_ACTION]: createHandler()
+};
+
+function createHandler() {
+ const handler = new Rx.Subject();
+ handler.send = function sendIntoSubject(...args) {
+ handler.onNext(...args)
+ }
+ return handler;
+}
+
+export function getBackHandler() {
+ return handlers[BACK_ACTION];
+}
+
+export function registerHandler(selector, evType) {
+ handlers[selector] = handlers[selector] || {};
+ handlers[selector][evType] = handlers[selector][evType] || createHandler();
+ return handlers[selector][evType];
+};
+
+export function findHandler(evType, selector) {
+ if (evType === BACK_ACTION && !selector) {
+ return handlers[BACK_ACTION];
+ }
+
+ if (handlers[selector].hasOwnProperty(evType)) {
+ return handlers[selector][evType].send
+ }
+}
+
function isChildReactElement(child) {
return !!child && typeof child === `object` && child._isReactElement
}
function makeReactNativeDriver(appKey) {
return function reactNativeDriver(vtree$) {
- let handlers = {}
-
- function augmentVTreeWithHandlers(vtree, index = null) {
- if (typeof vtree === `string` || typeof vtree === `number`) {
- return vtree
- }
- let newProps = {}
- if (!vtree.props.selector && !!index) {
- newProps.selector = index
- }
- let wasTouched = false
- if (handlers[vtree.props.selector]) {
- for (let evType in handlers[vtree.props.selector]) {
- if (handlers[vtree.props.selector].hasOwnProperty(evType)) {
- let handlerFnName = `on${evType.charAt(0).toUpperCase()}${evType.slice(1)}`
- newProps[handlerFnName] = handlers[vtree.props.selector][evType].send
- wasTouched = true
- }
- }
- }
- let newChildren = vtree.props.children
- if (Array.isArray(vtree.props.children)) {
- newChildren = vtree.props.children.map(augmentVTreeWithHandlers)
- wasTouched = true
- } else if (isChildReactElement(vtree.props.children)) {
- newChildren = augmentVTreeWithHandlers(vtree.props.children)
- wasTouched = true
- }
- return wasTouched ?
- React.cloneElement(vtree, newProps, newChildren) :
- vtree
- }
-
function componentFactory() {
return React.createClass({
componentWillMount() {
- vtree$.subscribe(rawVTree => {
- let replacedVTree = augmentVTreeWithHandlers(rawVTree)
- this.setState({vtree: replacedVTree})
+ vtree$.subscribe(newVTree => {
+ this.setState({vtree: newVTree})
})
},
getInitialState() {
@@ -59,21 +60,18 @@ function makeReactNativeDriver(appKey) {
}
let response = {
- select: function select(selector) {
+ select(selector) {
return {
observable: Rx.Observable.empty(),
events: function events(evType) {
- handlers[selector] = handlers[selector] || {}
- handlers[selector][evType] = handlers[selector][evType] || new Rx.Subject()
- handlers[selector][evType].send = function sendIntoSubject(...args) {
- const props = this
- const event = {currentTarget: {props}, args}
- handlers[selector][evType].onNext(event)
- }
- return handlers[selector][evType]
+ return registerHandler(selector, evType);
},
}
},
+
+ navigateBack() {
+ return findHandler(BACK_ACTION);
+ }
}
AppRegistry.registerComponent(appKey, componentFactory)