diff --git a/.idea/workspace.xml b/.idea/workspace.xml index ceb44ed6f..b4fba5c4d 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -2,8 +2,18 @@ + + + + + + + + + + @@ -24,108 +34,101 @@ - - + + - + - - + + - - + + - - + + - - + + - - + + - + - - + + - + - - + + - + - - + + - + - - + + - - - - - - - - - + + - - + + - - + + - - + + - - + + @@ -139,11 +142,8 @@ @@ -170,7 +176,7 @@ - - + + + + - - - @@ -413,27 +433,10 @@ - - - - - - - - - - - - - - - - - @@ -481,7 +484,6 @@ - @@ -489,14 +491,7 @@ - - - - - - - - + @@ -520,7 +515,6 @@ - @@ -560,7 +554,6 @@ - @@ -568,14 +561,7 @@ - - - - - - - - + @@ -599,7 +585,6 @@ - @@ -624,13 +609,6 @@ - - - - - - - @@ -638,20 +616,6 @@ - - - - - - - - - - - - - - @@ -680,13 +644,6 @@ - - - - - - - @@ -726,7 +683,6 @@ - @@ -734,7 +690,6 @@ - @@ -754,41 +709,58 @@ - + - - - - - - - - - + + + + + + + + + + + + + + + + + + - + - + + + + + + + + + - + - + - + @@ -796,31 +768,55 @@ - + - + + + + + + + + + - + - + - + - + - - + + + + + + + + + + + + + + + + + + diff --git a/Example/.idea/workspace.xml b/Example/.idea/workspace.xml index 993eaae5e..ac4905646 100644 --- a/Example/.idea/workspace.xml +++ b/Example/.idea/workspace.xml @@ -1,9 +1,7 @@ - - - + - - - - - - - + + + + + + @@ -48,7 +41,9 @@ - + + + @@ -69,6 +64,7 @@ + @@ -85,41 +81,11 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + @@ -215,38 +181,10 @@ @@ -326,16 +264,16 @@ - + - + - + @@ -366,22 +304,30 @@ - + - + + + + + + + - + - - + + + + - + @@ -389,18 +335,30 @@ - + - + + + + + + + + + + + + - + + @@ -415,30 +373,32 @@ - - - - - - - - - + + + + + + - + - - + - + - + + + + + + + @@ -453,7 +413,12 @@ - + + + + + + @@ -475,7 +440,12 @@ - + + + + + + @@ -511,7 +481,12 @@ - + + + + + + @@ -540,7 +515,12 @@ - + + + + + + @@ -548,7 +528,12 @@ - + + + + + + @@ -556,7 +541,12 @@ - + + + + + + @@ -567,13 +557,6 @@ - - - - - - - @@ -609,14 +592,6 @@ - - - - - - - - @@ -633,20 +608,6 @@ - - - - - - - - - - - - - - @@ -674,7 +635,12 @@ - + + + + + + @@ -682,7 +648,9 @@ - + + + @@ -694,22 +662,6 @@ - - - - - - - - - - - - - - - - @@ -723,31 +675,16 @@ + - - - - - - - - - - - - - - - - - - + + diff --git a/Example/components/Home.js b/Example/components/Home.js new file mode 100644 index 000000000..50e2f211e --- /dev/null +++ b/Example/components/Home.js @@ -0,0 +1,38 @@ +'use strict'; + +var React = require('react-native'); +var {View, Text, StyleSheet} = React; +var Button = require('react-native-button'); +var Actions = require('react-native-router-flux').Actions; + +class Register extends React.Component { + render(){ + return ( + + Home + + + ); + } +} + +var styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#F5FCFF', + }, + welcome: { + fontSize: 20, + textAlign: 'center', + margin: 10, + }, + instructions: { + textAlign: 'center', + color: '#333333', + marginBottom: 5, + }, +}); + +module.exports = Register; \ No newline at end of file diff --git a/Example/components/Register.js b/Example/components/Register.js index 1fe22124a..6bfd07b85 100644 --- a/Example/components/Register.js +++ b/Example/components/Register.js @@ -10,6 +10,7 @@ class Register extends React.Component { return ( Register page + ); diff --git a/Example/index.ios.js b/Example/index.ios.js index 05e228c5f..dfd2b1535 100644 --- a/Example/index.ios.js +++ b/Example/index.ios.js @@ -8,6 +8,7 @@ var Login = require('./components/Login'); var {Router, Route, Container, Actions, Animations, Schema} = require('react-native-router-flux'); var {NavBar, NavBarModal} = require('./components/NavBar'); var Error = require('./components/Error'); +var Home = require('./components/Home'); var TabView = require('./components/TabView'); var TabIcon = require('./components/TabIcon'); var TabBarFlux = require('./components/TabBarFlux'); @@ -25,6 +26,7 @@ class Example extends React.Component { + diff --git a/Example/package.json b/Example/package.json index c1c8157a3..e911c0a75 100644 --- a/Example/package.json +++ b/Example/package.json @@ -9,7 +9,7 @@ "react-native": "^0.11.4", "react-native-button": "^1.2.1", "react-native-navbar": "^0.8.0", - "react-native-router-flux": "^0.2.4", + "react-native-router-flux": "^0.3.0", "react-native-tabs": "^0.0.5" } } diff --git a/README.md b/README.md index f6d6f0a67..80c54e330 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,15 @@ React Native Router using Flux architecture ## Why I need to use it? -- Use Flux actions to push/pop screens with easy syntax like Actions.login for navigation to login screen +- Use Flux actions to replace/push/pop screens with easy syntax like Actions.login for navigation to login screen - Forget about passing navigator object to all React elements, use actions from anywhere in your UI code. - Configure all of your screens ("routes") once (define animations, nav bars, etc.), at one place and then just use short actions commands. For example if you use some special animation for Login screen, you don't need to code it anywhere where an user should be redirected to login screen. - Use route "schemas" to define common property for some screens. For example some screens are "modal" (i.e. have animation from bottom and have Cancel/Close nav button), so you could define group for them to avoid any code repeatition. - Use popup with Flux actions (see Error popup within Example project) - Hide nav bar for some screens easily - Use tab bars for some screens with Flux actions (see demo) +- Simplify processing of data flow in your app (see Getting Started, 4.1) +- Define your custom Flux actions (like fetch or validation actions) with the component too, so you will have all app actions in the one place. ## Example ![demo-2](https://cloud.githubusercontent.com/assets/1321329/9466261/de64558e-4b33-11e5-8ada-0fcd49442769.gif) @@ -25,6 +27,7 @@ var Login = require('./components/Login'); var {Router, Route, Container, Actions, Animations, Schema} = require('react-native-router-flux'); var {NavBar, NavBarModal} = require('./components/NavBar'); var Error = require('./components/Error'); +var Home = require('./components/Home'); var TabView = require('./components/TabView'); var TabIcon = require('./components/TabIcon'); var TabBarFlux = require('./components/TabBarFlux'); @@ -42,6 +45,7 @@ class Example extends React.Component { + @@ -99,4 +103,38 @@ module.exports = Launch; ## Getting started 1. `npm install react-native-router-flux --save` -2. Define Route for each screen. +2. In top-level index.js: +2.1 Define Route for each app screen. Its 'type' attribute is 'push' by default, but you also could define 'replace', so navigator will replace current route with new route. +'component' attribute is React component class which will be created for this route and all route attributes will be passed to it. +'name' is unique name of Route. +2.2 If some your Routes have common attributes, you may define Schema element and just use 'schema' attribute for 'route' +2.3 If you want to define some your custom actions, just add 'Action' element inside Router. That action will not be processed by the component, it will call Actions.custom({name:ACTION_NAME, ...params}) so you could handle it in your stores. It allows to add Fetch actions (which downloads web content), etc. +3. In any app screen: +3.1 var {Actions} = require('react-native-router-flux'); +3.2 Actions.ACTION_NAME(PARAMS) will call appropriate action and params will be passed to next screen. +4. In your Flux stores (optional): +4.1 You may subscribe to any push/replace/pop 'page' actions in your store. +It could be necessary if you want to process user data somehow. For example, if some component manages user form and have "Save" button which should store that data and pop the screen, you may use Actions.pop(this.state) in that component and then subscribe to Actions.pop actions within store: +``` +class SearchFilterStore { + constructor(){ + this.bindAction(Actions.pop, this.onSet); + } + + onSet(data){ + this.waitFor(PageStore.dispatchToken); + var route = PageStore.getState().currentRoute; + + if (route == 'yourFORM'){ + // save data + + this.saveData(data); + return true; + } + return false; + } +} +module.exports = alt.createStore(SearchFilterStore, 'SearchFilterStore'); +``` + +Here PageStore.getState().currentRoute is used to check current page, so the store will process only data for needed route. \ No newline at end of file diff --git a/__mocks__/react-native.js b/__mocks__/react-native.js index 3c1128bf8..c48d5fb31 100644 --- a/__mocks__/react-native.js +++ b/__mocks__/react-native.js @@ -29,6 +29,11 @@ class Navigator extends React.Component { immediatelyResetRouteStack(routes){ this._currentRoutes = routes; } + replace(route){ + this._currentRoutes.pop(); + this._currentRoutes.push(route); + this.setState({route: route}); + } popToRoute(route){ while (this._currentRoutes[this._currentRoutes.length-1] != route){ this._currentRoutes.pop(); diff --git a/__tests__/router-tests.js b/__tests__/router-tests.js index 74ed48d60..24734a6ef 100644 --- a/__tests__/router-tests.js +++ b/__tests__/router-tests.js @@ -1,6 +1,7 @@ jest.setMock('../Animations', {FlatFloatFromRight:{}, FlatFloatFromBottom:{}, None:{}}); jest.setMock('alt/components/AltNativeContainer'); var React = require('react/addons'); +var alt = require('../alt'); var TestUtils = React.addons.TestUtils; jest.dontMock('../store'); jest.dontMock('../actions'); @@ -10,19 +11,40 @@ var Actions = require('../actions'); var {Router, Route, Schema, Action} = require('../index'); +class Store { + constructor(){ + this.bindAction(Actions.custom, this.onCustom); + this.data = undefined; + } + + onCustom(data){ + this.setState({data}) + } + +} +var TestStore = alt.createStore(Store ,"TestStore"); + describe('Router', function() { - it('route', function () { - var router = TestUtils.renderIntoDocument( - - - - - - - - - + beforeEach(function(){ + this.router = TestUtils.renderIntoDocument( + + + + + + + + + + + + + ); + }); + + it('route', function () { + var router = this.router; expect(router.refs.nav.props.initialRoute.name).toEqual('launch'); var len = router.refs.nav.getCurrentRoutes().length; expect(len).toEqual(1); @@ -55,7 +77,7 @@ describe('Router', function() { var navBar = TestUtils.findRenderedDOMComponentWithTag( router, 'navBar2'); - expect(React.findDOMNode(signinComponent).data).toEqual('Hello world2!'); + expect(signinComponent.props.data).toEqual('Hello world2!'); expect(navBar.props.customProp).toEqual("b"); expect(navBar.props.ownProp).toEqual("c"); expect(navBar.props.data).toEqual('Hello world2!'); @@ -76,23 +98,71 @@ describe('Router', function() { expect(navBar.props.customProp).toEqual("a"); expect(navBar.props.ownProp).toEqual(undefined); - Actions.pop(2); + Actions.home({data:"Hello world home!", id:111, customProp:'bb'}); len = router.refs.nav.getCurrentRoutes().length; - expect(len).toEqual(2); - - expect(router.refs.nav.getCurrentRoutes()[len-1].name).toEqual('signin'); - expect(router.refs.nav.getCurrentRoutes()[len-1].passProps.data).toEqual("Hello world!"); - expect(router.refs.nav.getCurrentRoutes()[len-1].passProps.id).toEqual(undefined); - - Actions.pop(); - len = router.refs.nav.getCurrentRoutes().length; - expect(len).toEqual(1); + expect(len).toEqual(4); + var homeComponent = TestUtils.findRenderedDOMComponentWithTag( + router, 'homeComponent'); - expect(router.refs.nav.getCurrentRoutes()[len-1].name).toEqual('launch'); - launchComponent = TestUtils.findRenderedDOMComponentWithTag( - router, 'launchComponent'); + expect(homeComponent.props.data).toEqual('Hello world home!'); + navBar = TestUtils.findRenderedDOMComponentWithTag( + router, 'navBar2'); + // + //expect(navBar.props.customProp).toEqual("bb"); + //expect(navBar.props.ownProp).toEqual("c"); + //expect(navBar.props.id).toEqual(111); + //expect(navBar.props.data).toEqual("Hello world home!"); + // + //expect(navBar.props.customProp).toEqual("bb"); + //expect(navBar.props.ownProp).toEqual("c"); + // + //Actions.pop(2); + //len = router.refs.nav.getCurrentRoutes().length; + //expect(len).toEqual(2); + // + //expect(router.refs.nav.getCurrentRoutes()[len-1].name).toEqual('signin'); + //expect(router.refs.nav.getCurrentRoutes()[len-1].passProps.data).toEqual("Hello world!"); + //expect(router.refs.nav.getCurrentRoutes()[len-1].passProps.id).toEqual(undefined); + // + //Actions.pop(); + //len = router.refs.nav.getCurrentRoutes().length; + //expect(len).toEqual(1); + // + //expect(router.refs.nav.getCurrentRoutes()[len-1].name).toEqual('launch'); + //launchComponent = TestUtils.findRenderedDOMComponentWithTag( + // router, 'launchComponent'); + // + //expect(launchComponent.props.customProp).toEqual("a"); + // - expect(launchComponent.props.customProp).toEqual("a"); }); + + //it('custom actions', function(){ + // var router = this.router; + // expect(router.refs.nav.props.initialRoute.name).toEqual('launch'); + // var len = router.refs.nav.getCurrentRoutes().length; + // expect(len).toEqual(1); + // var launchComponent = TestUtils.findRenderedDOMComponentWithTag( + // router, 'launchComponent'); + // + // expect(launchComponent.props.customProp).toEqual("a"); + // var state = TestStore.getState(); + // expect(state.data).toEqual(undefined); + // + // + // // no changes within current component should be + // Actions.custom1({url: 'hello world'}); + // + // len = router.refs.nav.getCurrentRoutes().length; + // expect(len).toEqual(1); + // var launchComponent = TestUtils.findRenderedDOMComponentWithTag( + // router, 'launchComponent'); + // + // expect(launchComponent.props.customProp).toEqual("a"); + // state = TestStore.getState(); + // expect(state.data.name).toEqual("custom1"); + // expect(state.data.data.url).toEqual('hello world'); + // + //}); }); \ No newline at end of file diff --git a/actions.js b/actions.js index feb7c7cca..5884e34e8 100644 --- a/actions.js +++ b/actions.js @@ -40,6 +40,9 @@ class Actions { custom(data){ this.dispatch(filterParam(data)); } + replace(data){ + this.dispatch(filterParam(data)); + } } module.exports = alt.createActions(Actions); \ No newline at end of file diff --git a/index.js b/index.js index 7eff5b6e2..39685bedc 100644 --- a/index.js +++ b/index.js @@ -65,7 +65,8 @@ class Router extends React.Component { data={data:data}; } var args = {name: name, data:data}; - RouterActions.push(args); + var action = child.props.type || 'push'; + RouterActions[action](args); }); } self.routes[name] = child.props; @@ -89,7 +90,7 @@ class Router extends React.Component { } onChange(page){ - if (page.mode=='push'){ + if (page.mode=='push' || page.mode=='replace'){ if (!page.name){ console.error("Page name is not defined for action"); return; @@ -109,8 +110,11 @@ class Router extends React.Component { } this.setState({modal: element}); } else { - //console.log("PUSH"); - this.refs.nav.push(this.getRoute(route, page.data)) + if (page.mode == 'replace'){ + this.refs.nav.replace(this.getRoute(route, page.data)) + } else { + this.refs.nav.push(this.getRoute(route, page.data)) + } } } if (page.mode=='pop'){ diff --git a/package.json b/package.json index 9d92cc1ec..3f20f6c4c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-router-flux", - "version": "0.2.5", + "version": "0.3.0", "description": "React Native Router using Flux architecture", "main": "index.js", "scripts": { diff --git a/store.js b/store.js index 412296f4d..d4db09e50 100644 --- a/store.js +++ b/store.js @@ -23,6 +23,7 @@ class RouterStore { this.bindAction(actions.pop, this.onPop); this.bindAction(actions.dismiss, this.onDismiss); this.bindAction(actions.reset, this.onReset); + this.bindAction(actions.replace, this.onReplace); } onInit(initial){ @@ -39,6 +40,14 @@ class RouterStore { this.setState(data); } + onReplace(data){ + data.mode = 'replace'; + this.routes.pop(); + this.routes.push(data.name); + this.currentRoute = this.routes[this.routes.length-1]; + this.setState(data); + } + onPop(data){ if (!data){ data = {};