From 671d9e2aef4f30383720863d753374d03666df64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jani=20Ev=C3=A4kallio?= Date: Mon, 21 Mar 2016 12:39:56 +0000 Subject: [PATCH 01/12] Bump react native version --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 975a8fe..ea71f08 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,11 @@ }, "peerDependencies": { "rx": ">=3", - "react-native": "0.18.0" + "react-native": "~0.21.0" }, "devDependencies": { "rx": ">=3", - "react-native": "0.18.0", + "react-native": "~0.21.0", "babel": "5.8.x", "eslint": "1.0.x", "eslint-config-cycle": "3.0.x", From e951d59c9b25436e827947e4c2e109574b950150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jani=20Ev=C3=A4kallio?= Date: Mon, 21 Mar 2016 14:11:51 +0000 Subject: [PATCH 02/12] Pass children as individual args to cloneElement to avoid key-warning --- src/driver.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/driver.js b/src/driver.js index b40a183..e1c99e7 100644 --- a/src/driver.js +++ b/src/driver.js @@ -28,17 +28,17 @@ function makeReactNativeDriver(appKey) { } } } - let newChildren = vtree.props.children + let children = vtree.props.children if (Array.isArray(vtree.props.children)) { - newChildren = vtree.props.children.map(augmentVTreeWithHandlers) - wasTouched = true + return React.cloneElement(vtree, newProps, + ...children.map(augmentVTreeWithHandlers)) } else if (isChildReactElement(vtree.props.children)) { - newChildren = augmentVTreeWithHandlers(vtree.props.children) - wasTouched = true + return React.cloneElement(vtree, newProps, + augmentVTreeWithHandlers(children)) + } else if (wasTouched) { + return React.cloneElement(vtree, newProps, children) } - return wasTouched ? - React.cloneElement(vtree, newProps, newChildren) : - vtree + return vtree } function componentFactory() { From 9e7155692482dfffb298c2ef3e274c87b82fc03b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jani=20Ev=C3=A4kallio?= Date: Fri, 22 Apr 2016 21:18:03 +0200 Subject: [PATCH 03/12] Update readme with running instructions --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 008cb29..8505791 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,22 @@ # Cycle React Native Driver ## Experimental + +## 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 From 1646f6a5a744a85e2c09d98201b026a646cff0b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jani=20Ev=C3=A4kallio?= Date: Fri, 22 Apr 2016 22:03:56 +0200 Subject: [PATCH 04/12] Updgrade to RN 0.24 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ea71f08..f6987dc 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,11 @@ }, "peerDependencies": { "rx": ">=3", - "react-native": "~0.21.0" + "react-native": "~0.24.0" }, "devDependencies": { "rx": ">=3", - "react-native": "~0.21.0", + "react-native": "~0.24.0", "babel": "5.8.x", "eslint": "1.0.x", "eslint-config-cycle": "3.0.x", From b1c9ffb1c203c2618dd7de328476b6583336d91c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jani=20Ev=C3=A4kallio?= Date: Sat, 23 Apr 2016 19:28:05 +0200 Subject: [PATCH 05/12] Cycle ListView implementation --- src/ListView.js | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/ListView.js 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} + /> + ); + } +}); From 4cfb568d48fa1105042a85e1379bc0eb22646ae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jani=20Ev=C3=A4kallio?= Date: Sat, 23 Apr 2016 19:26:36 +0200 Subject: [PATCH 06/12] Refactor driver for global touch event hooks --- src/driver.js | 67 ++++++++++++++++++--------------------------------- 1 file changed, 23 insertions(+), 44 deletions(-) diff --git a/src/driver.js b/src/driver.js index e1c99e7..6199169 100644 --- a/src/driver.js +++ b/src/driver.js @@ -2,51 +2,37 @@ import React from 'react-native' import Rx from 'rx' const {AppRegistry, View} = React +let handlers = {} + +export function registerHandler(selector, 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]; +}; + +export function findHandler(selector, evType) { + 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 children = vtree.props.children - if (Array.isArray(vtree.props.children)) { - return React.cloneElement(vtree, newProps, - ...children.map(augmentVTreeWithHandlers)) - } else if (isChildReactElement(vtree.props.children)) { - return React.cloneElement(vtree, newProps, - augmentVTreeWithHandlers(children)) - } else if (wasTouched) { - return React.cloneElement(vtree, newProps, children) - } - return 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() { @@ -63,14 +49,7 @@ function makeReactNativeDriver(appKey) { 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); }, } }, From 42c351202f6a9971e1af7763d33d86635f15ab4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jani=20Ev=C3=A4kallio?= Date: Sat, 23 Apr 2016 19:26:46 +0200 Subject: [PATCH 07/12] Custom Touchable components --- src/Touchable.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/Touchable.js diff --git a/src/Touchable.js b/src/Touchable.js new file mode 100644 index 0000000..e4347a0 --- /dev/null +++ b/src/Touchable.js @@ -0,0 +1,44 @@ +'use strict'; + +import React from 'react-native'; +import {findHandler} from './driver'; +const { + View, + PropTypes +} = React; + +function createTouchableClass(className) { + return React.createClass({ + displayName: 'Cycle' + className, + propTypes: { + selector: PropTypes.string.isRequired, + }, + setNativeProps(props) { + this._touchable.setNativeProps(props); + }, + render() { + const TouchableClass = React[className]; + const {selector, ...props} = this.props; + return ( + this._touchable = view} + onPress={findHandler(selector, 'press')} + onPressIn={findHandler(selector, 'pressIn')} + onPressOut={findHandler(selector, 'pressOut')} + onLongPress={findHandler(selector, 'longPress')} + > + + {this.props.children} + + + ); + } + }); +} + +export default { + TouchableOpacity: createTouchableClass('TouchableOpacity'), + TouchableWithoutFeedback: createTouchableClass('TouchableWithoutFeedback'), + TouchableHighlight: createTouchableClass('TouchableHighlight'), + TouchableNativeFeedback: createTouchableClass('TouchableNativeFeedback') +}; From 54d91c8106a803cf40c2e40aa8ced9e7b2f8de13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jani=20Ev=C3=A4kallio?= Date: Sat, 23 Apr 2016 19:53:21 +0200 Subject: [PATCH 08/12] Add support for event payload --- src/Touchable.js | 27 ++++++++++++++++++++++----- src/driver.js | 4 +--- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/Touchable.js b/src/Touchable.js index e4347a0..9ef4d5c 100644 --- a/src/Touchable.js +++ b/src/Touchable.js @@ -7,11 +7,19 @@ const { 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); @@ -19,14 +27,23 @@ function createTouchableClass(className) { 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(selector, ACTION_TYPES[name])]) + .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} - onPress={findHandler(selector, 'press')} - onPressIn={findHandler(selector, 'pressIn')} - onPressOut={findHandler(selector, 'pressOut')} - onLongPress={findHandler(selector, 'longPress')} - > + {...handlers} + > {this.props.children} diff --git a/src/driver.js b/src/driver.js index 6199169..d59f841 100644 --- a/src/driver.js +++ b/src/driver.js @@ -8,9 +8,7 @@ export function registerHandler(selector, 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) + handlers[selector][evType].onNext(...args) } return handlers[selector][evType]; From 8398083b498393107bb9bd477e7773e49efc21e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jani=20Ev=C3=A4kallio?= Date: Sat, 23 Apr 2016 22:14:14 +0200 Subject: [PATCH 09/12] Back navigation --- src/Touchable.js | 2 +- src/driver.js | 37 +++++++++++++++++++++++++++++-------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/Touchable.js b/src/Touchable.js index 9ef4d5c..164b1ff 100644 --- a/src/Touchable.js +++ b/src/Touchable.js @@ -30,7 +30,7 @@ function createTouchableClass(className) { // find all defined touch handlers const handlers = Object.keys(ACTION_TYPES) - .map(name => [name, findHandler(selector, ACTION_TYPES[name])]) + .map(name => [name, findHandler(ACTION_TYPES[name], selector)]) .filter(([_, handler]) => !!handler) .reduce((memo, [name, handler]) => { // pass payload to event handler if defined diff --git a/src/driver.js b/src/driver.js index d59f841..f37fb21 100644 --- a/src/driver.js +++ b/src/driver.js @@ -2,19 +2,36 @@ import React from 'react-native' import Rx from 'rx' const {AppRegistry, View} = React -let handlers = {} +const BACK_ACTION = '@@back'; +const backHandler = new Rx.Subject -export function registerHandler(selector, evType) { - handlers[selector] = handlers[selector] || {}; - handlers[selector][evType] = handlers[selector][evType] || new Rx.Subject(); - handlers[selector][evType].send = function sendIntoSubject(...args) { - handlers[selector][evType].onNext(...args) +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(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 } @@ -43,7 +60,7 @@ function makeReactNativeDriver(appKey) { } let response = { - select: function select(selector) { + select(selector) { return { observable: Rx.Observable.empty(), events: function events(evType) { @@ -51,6 +68,10 @@ function makeReactNativeDriver(appKey) { }, } }, + + navigateBack() { + return findHandler(BACK_ACTION); + } } AppRegistry.registerComponent(appKey, componentFactory) From 2e53c8137bcea798b28c369b60ba8379b133c24c Mon Sep 17 00:00:00 2001 From: Justin Woo Date: Sat, 23 Apr 2016 21:56:50 +0200 Subject: [PATCH 10/12] Add Animated helper components --- src/Animated.js | 67 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/Animated.js diff --git a/src/Animated.js b/src/Animated.js new file mode 100644 index 0000000..5915b05 --- /dev/null +++ b/src/Animated.js @@ -0,0 +1,67 @@ +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).reduce((acc, key) => { + return {...acc, + [key]: this.state.currentValue.interpolate(animate[key]) + } + }, {}); + + 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') +}; + From 5b236be7abcc2fc7ec297d631de4f0a10520518d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jani=20Ev=C3=A4kallio?= Date: Sun, 24 Apr 2016 11:26:06 +0200 Subject: [PATCH 11/12] Update README.md --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 8505791..2620f8a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,16 @@ # 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 @@ -19,6 +29,7 @@ Good luck! ## Usage + ```js let {Rx, run} = require('@cycle/core'); let React = require('react-native'); @@ -44,3 +55,4 @@ run(main, { RN: makeReactNativeDriver('MyMobileApp'), }); ``` + From 3851c1e9e8c3e44b28b9bd25db938ec452b2eb35 Mon Sep 17 00:00:00 2001 From: Ossi Hanhinen Date: Sun, 24 Apr 2016 11:30:56 +0200 Subject: [PATCH 12/12] Implement transform styles --- src/Animated.js | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/Animated.js b/src/Animated.js index 5915b05..871f335 100644 --- a/src/Animated.js +++ b/src/Animated.js @@ -37,11 +37,24 @@ function createAnimationDongle(className) { const AnimatedClass = Animated[className]; const { animate } = this.props; - const animatedStyle = Object.keys(animate).reduce((acc, key) => { - return {...acc, - [key]: this.state.currentValue.interpolate(animate[key]) - } - }, {}); + 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} @@ -58,10 +71,8 @@ function createAnimationDongle(className) { }); }; - export default { View: createAnimationDongle('View'), Text: createAnimationDongle('Text'), Image: createAnimationDongle('Image') }; -