Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CycleConf hacking progress #6

Merged
merged 14 commits into from
May 14, 2016
Merged
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -32,3 +55,4 @@ run(main, {
RN: makeReactNativeDriver('MyMobileApp'),
});
```

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
78 changes: 78 additions & 0 deletions src/Animated.js
Original file line number Diff line number Diff line change
@@ -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 (
<AnimatedClass style={style} {...extraProps}>
{this.props.children}
</AnimatedClass>
);
}
});
};

export default {
View: createAnimationDongle('View'),
Text: createAnimationDongle('Text'),
Image: createAnimationDongle('Image')
};
41 changes: 41 additions & 0 deletions src/ListView.js
Original file line number Diff line number Diff line change
@@ -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 (
<ListView
ref={listView => this._listView = listView}
dataSource={this.state.dataSource}
{...listViewProps}
/>
);
}
});
61 changes: 61 additions & 0 deletions src/Touchable.js
Original file line number Diff line number Diff line change
@@ -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 (
<TouchableClass
ref={view => this._touchable = view}
{...handlers}
>
<View {...props}>
{this.props.children}
</View>
</TouchableClass>
);
}
});
}

export default {
TouchableOpacity: createTouchableClass('TouchableOpacity'),
TouchableWithoutFeedback: createTouchableClass('TouchableWithoutFeedback'),
TouchableHighlight: createTouchableClass('TouchableHighlight'),
TouchableNativeFeedback: createTouchableClass('TouchableNativeFeedback')
};
88 changes: 43 additions & 45 deletions src/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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)
Expand Down