├── .babelrc
├── .codeclimate.yml
├── .editorconfig
├── .eslintrc
├── .gitignore
├── .npmignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── gulpfile.js
├── package.json
├── src
├── actions.js
├── constants.js
├── index.js
└── reducers
│ ├── card-stack.js
│ ├── helpers.js
│ └── tab-reducer.js
└── test
├── .eslintrc
├── runner.html
├── setup
├── .globals.json
├── browser.js
├── node.js
└── setup.js
└── unit
├── actions.js
├── constants.js
├── index.js
└── reducers.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015"]
3 | }
4 |
--------------------------------------------------------------------------------
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | engines:
2 | eslint:
3 | enabled: true
4 |
5 | ratings:
6 | paths:
7 | - "src/**/**.js"
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | root = true;
4 |
5 | [*]
6 | # Ensure there's no lingering whitespace
7 | trim_trailing_whitespace = true
8 | # Ensure a newline at the end of each file
9 | insert_final_newline = true
10 |
11 | [*.js]
12 | # Unix-style newlines
13 | end_of_line = lf
14 | charset = utf-8
15 | indent_style = space
16 | indent_size = 2
17 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parserOptions": {
3 | "ecmaVersion": 6,
4 | "sourceType": "module"
5 | },
6 | "rules": {
7 | # Possible Errors
8 | comma-dangle: [2, never],
9 | no-cond-assign: 2,
10 | no-console: 0,
11 | no-constant-condition: 2,
12 | no-control-regex: 2,
13 | no-debugger: 2,
14 | no-dupe-args: 2,
15 | no-dupe-keys: 2,
16 | no-duplicate-case: 2,
17 | no-empty: 2,
18 | no-empty-character-class: 2,
19 | no-ex-assign: 2,
20 | no-extra-boolean-cast: 2,
21 | no-extra-parens: 0,
22 | no-extra-semi: 2,
23 | no-func-assign: 2,
24 | no-inner-declarations: [2, functions],
25 | no-invalid-regexp: 2,
26 | no-irregular-whitespace: 2,
27 | no-negated-in-lhs: 2,
28 | no-obj-calls: 2,
29 | no-regex-spaces: 2,
30 | no-sparse-arrays: 2,
31 | no-unexpected-multiline: 2,
32 | no-unreachable: 2,
33 | use-isnan: 2,
34 | valid-jsdoc: 0,
35 | valid-typeof: 2,
36 |
37 | # Best Practices
38 | accessor-pairs: 2,
39 | block-scoped-var: 0,
40 | complexity: [2, 6],
41 |
42 | consistent-return: 0,
43 | curly: 0,
44 | default-case: 0,
45 | dot-location: 0,
46 | dot-notation: 0,
47 | eqeqeq: 2,
48 | guard-for-in: 2,
49 | no-alert: 2,
50 | no-caller: 2,
51 | no-case-declarations: 2,
52 | no-div-regex: 2,
53 | no-else-return: 0,
54 | no-empty-pattern: 2,
55 | no-eq-null: 2,
56 | no-eval: 2,
57 | no-extend-native: 2,
58 | no-extra-bind: 2,
59 | no-fallthrough: 2,
60 | no-floating-decimal: 0,
61 | no-implicit-coercion: 0,
62 | no-implied-eval: 2,
63 | no-invalid-this: 0,
64 | no-iterator: 2,
65 | no-labels: 0,
66 | no-lone-blocks: 2,
67 | no-loop-func: 2,
68 | no-magic-number: 0,
69 | no-multi-spaces: 0,
70 | no-multi-str: 0,
71 | no-native-reassign: 2,
72 | no-new-func: 2,
73 | no-new-wrappers: 2,
74 | no-new: 2,
75 | no-octal-escape: 2,
76 | no-octal: 2,
77 | no-proto: 2,
78 | no-redeclare: 2,
79 | no-return-assign: 2,
80 | no-script-url: 2,
81 | no-self-compare: 2,
82 | no-sequences: 0,
83 | no-throw-literal: 0,
84 | no-unused-expressions: 2,
85 | no-useless-call: 2,
86 | no-useless-concat: 2,
87 | no-void: 2,
88 | no-warning-comments: 0,
89 | no-with: 2,
90 | radix: 2,
91 | vars-on-top: 0,
92 | wrap-iife: 2,
93 | yoda: 0,
94 |
95 | # Strict
96 | strict: 0,
97 |
98 | # Variables
99 | init-declarations: 0,
100 | no-catch-shadow: 2,
101 | no-delete-var: 2,
102 | no-label-var: 2,
103 | no-shadow-restricted-names: 2,
104 | no-shadow: 0,
105 | no-undef-init: 2,
106 | no-undef: 0,
107 | no-undefined: 0,
108 | no-unused-vars: 0,
109 | no-use-before-define: 0,
110 |
111 | # Node.js and CommonJS
112 | callback-return: 2,
113 | handle-callback-err: 2,
114 | no-mixed-requires: 0,
115 | no-new-require: 0,
116 | no-path-concat: 2,
117 | no-process-exit: 2,
118 | no-restricted-modules: 0,
119 | no-sync: 0,
120 |
121 | # Stylistic Issues
122 | array-bracket-spacing: 0,
123 | block-spacing: 0,
124 | brace-style: 0,
125 | camelcase: 0,
126 | comma-spacing: 0,
127 | comma-style: 0,
128 | computed-property-spacing: 0,
129 | consistent-this: 0,
130 | eol-last: 0,
131 | func-names: 0,
132 | func-style: 0,
133 | id-length: 0,
134 | id-match: 0,
135 | indent: 0,
136 | jsx-quotes: 0,
137 | key-spacing: 0,
138 | linebreak-style: 0,
139 | lines-around-comment: 0,
140 | max-depth: 0,
141 | max-len: 0,
142 | max-nested-callbacks: 0,
143 | max-params: 0,
144 | max-statements: [2, 30],
145 | new-cap: 0,
146 | new-parens: 0,
147 | newline-after-var: 0,
148 | no-array-constructor: 0,
149 | no-bitwise: 0,
150 | no-continue: 0,
151 | no-inline-comments: 0,
152 | no-lonely-if: 0,
153 | no-mixed-spaces-and-tabs: 0,
154 | no-multiple-empty-lines: 0,
155 | no-negated-condition: 0,
156 | no-nested-ternary: 0,
157 | no-new-object: 0,
158 | no-plusplus: 0,
159 | no-restricted-syntax: 0,
160 | no-spaced-func: 0,
161 | no-ternary: 0,
162 | no-trailing-spaces: 0,
163 | no-underscore-dangle: 0,
164 | no-unneeded-ternary: 0,
165 | object-curly-spacing: 0,
166 | one-var: 0,
167 | operator-assignment: 0,
168 | operator-linebreak: 0,
169 | padded-blocks: 0,
170 | quote-props: 0,
171 | quotes: 0,
172 | require-jsdoc: 0,
173 | semi-spacing: 0,
174 | semi: 0,
175 | sort-vars: 0,
176 | space-after-keywords: 0,
177 | space-before-blocks: 0,
178 | space-before-function-paren: 0,
179 | space-before-keywords: 0,
180 | space-in-parens: 0,
181 | space-infix-ops: 0,
182 | space-return-throw-case: 0,
183 | space-unary-ops: 0,
184 | spaced-comment: 0,
185 | wrap-regex: 0,
186 |
187 | # ECMAScript 6
188 | arrow-body-style: 0,
189 | arrow-parens: 0,
190 | arrow-spacing: 0,
191 | constructor-super: 0,
192 | generator-star-spacing: 0,
193 | no-arrow-condition: 0,
194 | no-class-assign: 0,
195 | no-const-assign: 0,
196 | no-dupe-class-members: 0,
197 | no-this-before-super: 0,
198 | no-var: 0,
199 | object-shorthand: 0,
200 | prefer-arrow-callback: 0,
201 | prefer-const: 0,
202 | prefer-reflect: 0,
203 | prefer-spread: 0,
204 | prefer-template: 0,
205 | require-yield: 0
206 | },
207 | "env": {
208 | "browser": true,
209 | "node": true
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | tmp
3 | dist
4 | coverage
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # Compiled binary addons (http://nodejs.org/api/addons.html)
20 | build/Release
21 |
22 | # Dependency directory
23 | # Commenting this out is preferred by some people, see
24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
25 | node_modules
26 | bower_components
27 | coverage
28 | tmp
29 |
30 | # Users Environment Variables
31 | .lock-wscript
32 |
33 | # Source code
34 | src
35 |
36 | # babelrc (otherwise react native gets confused somehow)
37 | .babelrc
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "4"
4 | - "5"
5 | - "stable"
6 | sudo: false
7 | script: "gulp coverage"
8 | after_success:
9 | - npm install -g codeclimate-test-reporter
10 | - codeclimate-test-reporter < coverage/lcov.info
11 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ### [0.0.1](https://github.com/bakeryhq/react-native-navigation-redux-helpers/releases/tag/v0.0.1)
2 |
3 | - The first release
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 hi@thebakery.io
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Native Navigation Redux helpers
2 | [](https://travis-ci.org/bakery/react-native-navigation-redux-helpers)
3 | [](https://codeclimate.com/github/thebakeryio/react-native-navigation-redux-helpers)
4 | [](https://david-dm.org/thebakeryio/react-native-navigation-redux-helpers)
5 | [](https://david-dm.org/thebakeryio/react-native-navigation-redux-helpers#info=devDependencies)
6 |
7 | Reducers and actions to implement navigation in React Native applications (RN 0.28.0+)
8 |
9 | ## When to use this
10 |
11 | - you are using RN ExperimentalNavigation
12 | - you are using Redux
13 | - you do not want to write and re-write your own actions and reducers for navigation
14 |
15 | ## Getting started
16 |
17 | ```bash
18 | npm install --save react-native-navigation-redux-helpers
19 | ```
20 |
21 | ### Card navigation
22 |
23 | Define your card reducer
24 |
25 | ```javascript
26 | import { cardStackReducer } from 'react-native-navigation-redux-helpers';
27 |
28 | const initialState = {
29 | key: 'global',
30 | index: 0,
31 | routes: [
32 | {
33 | key: 'applicationSection1',
34 | index: 0
35 | },
36 | ],
37 | };
38 |
39 | module.exports = cardStackReducer(initialState);
40 | ```
41 |
42 | Use this reducer in NavigationCardStack in your component
43 |
44 | ```javascript
45 | import { NavigationExperimental } from 'react-native';
46 | import React, { Component } from 'react';
47 | import { connect } from 'react-redux';
48 | import { actions } from 'react-native-navigation-redux-helpers';
49 |
50 | const {
51 | popRoute,
52 | pushRoute,
53 | } = actions;
54 |
55 | const {
56 | CardStack: NavigationCardStack
57 | } = NavigationExperimental;
58 |
59 | class GlobalNavigation extends Component {
60 | render() {
61 | return (
62 |
67 | );
68 | }
69 |
70 | /* ... */
71 |
72 | onGoBack() {
73 | const { dispatch, navigation } = this.props;
74 | dispatch(popRoute(navigation.key));
75 | }
76 |
77 | onGoSomewhere() {
78 | const { dispatch, navigation } = this.props;
79 | dispatch(pushRoute({ key: 'sowhere else' }, navigation.key));
80 | }
81 | }
82 |
83 | function mapDispatchToProps(dispatch) {
84 | return {
85 | dispatch
86 | };
87 | }
88 |
89 | function mapStateToProps(state) {
90 | return {
91 | // XX: assuming you've registered the reducer above under the name 'cardNavigation'
92 | navigation: state.cardNavigation
93 | };
94 | }
95 |
96 | export default connect(mapStateToProps, mapDispatchToProps)(GlobalNavigation);
97 |
98 | ```
99 |
100 | ### Tab navigation
101 |
102 | Define your tab reducer
103 |
104 | ```javascript
105 | import { tabReducer } from 'react-native-navigation-redux-helpers';
106 |
107 | const tabs = {
108 | routes: [
109 | { key: 'feed', title: 'Items' },
110 | { key: 'notifications', title: 'Notifications' },
111 | { key: 'settings', title: 'Settings' }
112 | ],
113 | key: 'ApplicationTabs',
114 | index: 0
115 | };
116 |
117 | module.exports = tabReducer(tabs);
118 | ```
119 |
120 | And now put it to good use inside your component
121 |
122 | ```javascript
123 | import { TabBarIOS } from 'react-native';
124 | import React, { Component } from 'react';
125 | import Feed from '../Feed';
126 | import { connect } from 'react-redux';
127 | import { actions as navigationActions } from 'react-native-navigation-redux-helpers';
128 |
129 | const { jumpTo } = navigationActions;
130 |
131 | class ApplicationTabs extends Component {
132 | _renderTabContent(tab) {
133 | if (tab.key === 'feed') {
134 | return (
135 |
136 | );
137 | }
138 |
139 | /* ... */
140 | }
141 |
142 | render() {
143 | const { dispatch, navigation } = this.props;
144 | const children = navigation.routes.map( (tab, i) => {
145 | return (
146 | dispatch(jumpTo(i, navigation.key)) }
150 | selected={this.props.navigation.index === i}>
151 | { this._renderTabContent(tab) }
152 |
153 | );
154 | });
155 | return (
156 |
157 | {children}
158 |
159 | );
160 | }
161 | }
162 |
163 | function mapDispatchToProps(dispatch) {
164 | return {
165 | dispatch
166 | };
167 | }
168 |
169 | function mapStateToProps(state) {
170 | return {
171 | // XX: assuming your tab reducer is registered as 'tabs'
172 | navigation: state.tabs
173 | };
174 | }
175 | export default connect(mapStateToProps, mapDispatchToProps)(ApplicationTabs);
176 | ```
177 |
178 | ## Supported actions
179 |
180 | ### cardStackReducer
181 |
182 | - pushRoute
183 | - popRoute
184 | - jumpTo
185 | - reset
186 | - replaceAt
187 | - replaceAtIndex
188 | - jumpToIndex
189 | - back
190 | - forward
191 |
192 | ### tabReducer
193 |
194 | - jumpTo
195 | - jumpToIndex
196 |
197 | ## Complete examples
198 |
199 | - [Example using RN experimental navigation with Redux](https://github.com/thebakeryio/react-native-complex-nav)
200 | - [TodoMVC React Native](https://github.com/thebakeryio/todomvc-react-native)
201 |
202 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | const gulp = require('gulp');
2 | const loadPlugins = require('gulp-load-plugins');
3 | const del = require('del');
4 | const glob = require('glob');
5 | const path = require('path');
6 | const isparta = require('isparta');
7 | const webpack = require('webpack');
8 | const webpackStream = require('webpack-stream');
9 | const source = require('vinyl-source-stream');
10 |
11 | const Instrumenter = isparta.Instrumenter;
12 | const mochaGlobals = require('./test/setup/.globals');
13 | const manifest = require('./package.json');
14 |
15 | // Load all of our Gulp plugins
16 | const $ = loadPlugins();
17 |
18 | // Gather the library data from `package.json`
19 | const config = manifest.babelBoilerplateOptions;
20 | const mainFile = manifest.main;
21 | const destinationFolder = path.dirname(mainFile);
22 | const exportFileName = path.basename(mainFile, path.extname(mainFile));
23 |
24 | function cleanDist(done) {
25 | del([destinationFolder]).then(() => done());
26 | }
27 |
28 | function cleanTmp(done) {
29 | del(['tmp']).then(() => done());
30 | }
31 |
32 | // Lint a set of files
33 | function lint(files) {
34 | return gulp.src(files)
35 | .pipe($.eslint())
36 | .pipe($.eslint.format())
37 | .pipe($.eslint.failAfterError());
38 | }
39 |
40 | function lintSrc() {
41 | return lint('src/**/*.js');
42 | }
43 |
44 | function lintTest() {
45 | return lint('test/**/*.js');
46 | }
47 |
48 | function lintGulpfile() {
49 | return lint('gulpfile.js');
50 | }
51 |
52 | function build() {
53 | return gulp.src(path.join('src', config.entryFileName))
54 | .pipe(webpackStream({
55 | output: {
56 | filename: exportFileName + '.js',
57 | libraryTarget: 'umd',
58 | library: config.mainVarName
59 | },
60 | externals: [
61 | {
62 | 'react-native': true
63 | }
64 | ],
65 | module: {
66 | loaders: [
67 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' }
68 | ]
69 | },
70 | devtool: 'source-map'
71 | }))
72 | .pipe(gulp.dest(destinationFolder))
73 | .pipe($.filter(['**', '!**/*.js.map']))
74 | .pipe($.rename(exportFileName + '.min.js'))
75 | .pipe($.sourcemaps.init({ loadMaps: true }))
76 | .pipe($.uglify())
77 | .pipe($.sourcemaps.write('./'))
78 | .pipe(gulp.dest(destinationFolder));
79 | }
80 |
81 | function _mocha() {
82 | return gulp.src(['test/setup/node.js', 'test/unit/**/*.js'], {read: false})
83 | .pipe($.mocha({
84 | reporter: 'dot',
85 | globals: Object.keys(mochaGlobals.globals),
86 | ignoreLeaks: false
87 | }));
88 | }
89 |
90 | function _registerBabel() {
91 | // eslint-disable-line global-require
92 | require('babel-register');
93 | }
94 |
95 | function _registerTestBabel() {
96 | require('babel-register')({
97 | plugins: ['babel-plugin-rewire']
98 | });
99 | }
100 |
101 | function test() {
102 | _registerTestBabel();
103 | return _mocha();
104 | }
105 |
106 | function coverage(done) {
107 | gulp.src(['src/**/*.js'])
108 | .pipe($.babel({
109 | plugins: ['babel-plugin-rewire']
110 | }))
111 | .pipe($.istanbul({ instrumenter: Instrumenter }))
112 | .pipe($.istanbul.hookRequire())
113 | .on('finish', () => {
114 | return test()
115 | .pipe($.istanbul.writeReports())
116 | .on('end', done);
117 | });
118 | }
119 |
120 | const watchFiles = ['src/**/*', 'test/**/*', 'package.json', '**/.eslintrc', '.jscsrc'];
121 |
122 | // Run the headless unit tests as you make changes.
123 | function watch() {
124 | gulp.watch(watchFiles, ['test']);
125 | }
126 |
127 | function testBrowser() {
128 | // Our testing bundle is made up of our unit tests, which
129 | // should individually load up pieces of our application.
130 | // We also include the browser setup file.
131 | const testFiles = glob.sync('./test/unit/**/*.js');
132 | const allFiles = ['./test/setup/browser.js'].concat(testFiles);
133 |
134 | // Lets us differentiate between the first build and subsequent builds
135 | var firstBuild = true;
136 |
137 | // This empty stream might seem like a hack, but we need to specify all of our files through
138 | // the `entry` option of webpack. Otherwise, it ignores whatever file(s) are placed in here.
139 | return gulp.src('')
140 | .pipe($.plumber())
141 | .pipe(webpackStream({
142 | watch: true,
143 | entry: allFiles,
144 | output: {
145 | filename: '__spec-build.js'
146 | },
147 | // Externals isn't necessary here since these are for tests.
148 | module: {
149 | loaders: [
150 | // This is what allows us to author in future JavaScript
151 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader?plugins=babel-plugin-rewire' },
152 | // This allows the test setup scripts to load `package.json`
153 | { test: /\.json$/, exclude: /node_modules/, loader: 'json-loader' }
154 | ]
155 | },
156 | plugins: [
157 | // By default, webpack does `n=>n` compilation with entry files. This concatenates
158 | // them into a single chunk.
159 | new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })
160 | ],
161 | devtool: 'inline-source-map'
162 | }, null, function() {
163 | if (firstBuild) {
164 | $.livereload.listen({port: 35729, host: 'localhost', start: true});
165 | var watcher = gulp.watch(watchFiles, ['lint']);
166 | } else {
167 | $.livereload.reload('./tmp/__spec-build.js');
168 | }
169 | firstBuild = false;
170 | }))
171 | .pipe(gulp.dest('./tmp'));
172 | }
173 |
174 | // Remove the built files
175 | gulp.task('clean', cleanDist);
176 |
177 | // Remove our temporary files
178 | gulp.task('clean-tmp', cleanTmp);
179 |
180 | // Lint our source code
181 | gulp.task('lint-src', lintSrc);
182 |
183 | // Lint our test code
184 | gulp.task('lint-test', lintTest);
185 |
186 | // Lint this file
187 | gulp.task('lint-gulpfile', lintGulpfile);
188 |
189 | // Lint everything
190 | gulp.task('lint', ['lint-src', 'lint-test', 'lint-gulpfile']);
191 |
192 | // Build two versions of the library
193 | gulp.task('build', ['lint', 'clean'], build);
194 |
195 | // Lint and run our tests
196 | gulp.task('test', ['lint'], test);
197 |
198 | // Set up coverage and run tests
199 | gulp.task('coverage', ['lint'], coverage);
200 |
201 | // Set up a livereload environment for our spec runner `test/runner.html`
202 | gulp.task('test-browser', ['lint', 'clean-tmp'], testBrowser);
203 |
204 | // Run the headless unit tests as you make changes.
205 | gulp.task('watch', watch);
206 |
207 | // An alias of test
208 | gulp.task('default', ['test']);
209 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-navigation-redux-helpers",
3 | "version": "0.5.0",
4 | "description": "Redux helpers for React Native navigation",
5 | "main": "dist/index.js",
6 | "scripts": {
7 | "test": "gulp",
8 | "lint": "gulp lint",
9 | "test-browser": "gulp test-browser",
10 | "watch": "gulp watch",
11 | "build": "gulp build",
12 | "coverage": "gulp coverage",
13 | "prepublish": "npm run lint && npm test && npm run build"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/thebakeryio/react-native-navigation-redux-helpers.git"
18 | },
19 | "keywords": [],
20 | "author": "The Bakery",
21 | "license": "MIT",
22 | "bugs": {
23 | "url": "https://github.com/thebakeryio/react-native-navigation-redux-helpers/issues"
24 | },
25 | "homepage": "https://github.com/thebakeryio/react-native-navigation-redux-helpers",
26 | "devDependencies": {
27 | "babel-core": "^6.3.26",
28 | "babel-loader": "^6.2.0",
29 | "babel-plugin-rewire": "^1.0.0-rc-4",
30 | "babel-polyfill": "^6.3.14",
31 | "babel-preset-es2015": "^6.3.13",
32 | "babel-register": "^6.3.13",
33 | "chai": "^3.4.1",
34 | "del": "^2.2.0",
35 | "glob": "^7.0.3",
36 | "gulp": "^3.9.0",
37 | "gulp-babel": "^6.1.2",
38 | "gulp-eslint": "^3.0.1",
39 | "gulp-filter": "^4.0.0",
40 | "gulp-istanbul": "^1.1.1",
41 | "gulp-livereload": "^3.8.1",
42 | "gulp-load-plugins": "^1.1.0",
43 | "gulp-mocha": "^3.0.1",
44 | "gulp-plumber": "^1.0.1",
45 | "gulp-rename": "^1.2.2",
46 | "gulp-sourcemaps": "^2.1.1",
47 | "gulp-uglify": "^2.0.0",
48 | "isparta": "^4.0.0",
49 | "json-loader": "^0.5.3",
50 | "mocha": "^3.0.2",
51 | "sinon": "^1.17.2",
52 | "sinon-chai": "^2.8.0",
53 | "vinyl-source-stream": "^1.1.0",
54 | "webpack": "^1.12.9",
55 | "webpack-stream": "^3.1.0"
56 | },
57 | "peerDependencies": {
58 | "react-native": ">=0.28.0"
59 | },
60 | "babelBoilerplateOptions": {
61 | "entryFileName": "index.js"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/actions.js:
--------------------------------------------------------------------------------
1 | import {
2 | PUSH_ROUTE,
3 | POP_ROUTE,
4 | RESET_ROUTE,
5 | REPLACE_AT,
6 | REPLACE_AT_INDEX,
7 | JUMP_TO,
8 | JUMP_TO_INDEX,
9 | BACK,
10 | FORWARD,
11 | GET,
12 | HAS,
13 | INDEX_OF
14 | } from './constants';
15 |
16 | export function pushRoute(route, key) {
17 | if (!key) {
18 | throw new Error('pushRoute requires key argument');
19 | }
20 |
21 | return {
22 | type: PUSH_ROUTE,
23 | payload: {
24 | route,
25 | key
26 | }
27 | };
28 | }
29 |
30 | export function popRoute(key) {
31 | if (!key) {
32 | throw new Error('popRoute requires key argument');
33 | }
34 |
35 | return {
36 | type: POP_ROUTE,
37 | payload: {
38 | key
39 | }
40 | };
41 | }
42 |
43 | export function jumpTo(keyOrIndex, key) {
44 | // XX: to make this backwards compatible,
45 | // jumpTo supports both key and index first arg
46 | // JUMP_TO action is used if the first arg is a string key
47 | // otherwise JUMP_TO_INDEX is used
48 |
49 | if (!key) {
50 | throw new Error('jumpTo requires key argument');
51 | }
52 |
53 | if (typeof keyOrIndex === 'string') {
54 | return {
55 | type: JUMP_TO,
56 | payload: {
57 | routeKey: keyOrIndex,
58 | key
59 | }
60 | };
61 | }
62 |
63 | return jumpToIndex(keyOrIndex, key);
64 | }
65 |
66 | export function reset(routes, key, index) {
67 | if (!key) {
68 | throw new Error('reset requires key argument');
69 | }
70 | return {
71 | type: RESET_ROUTE,
72 | payload: {
73 | routes,
74 | index,
75 | key
76 | }
77 | }
78 | }
79 |
80 | export function replaceAt(routeKey, route, key) {
81 | if (!key) {
82 | throw new Error('Replace At requires key argument');
83 | }
84 |
85 | return {
86 | type: REPLACE_AT,
87 | payload: {
88 | routeKey,
89 | route,
90 | key
91 | }
92 | }
93 | }
94 |
95 | export function replaceAtIndex(index, route, key) {
96 | if (!key) {
97 | throw new Error('Replace At Index requires key argument');
98 | }
99 |
100 | return {
101 | type: REPLACE_AT_INDEX,
102 | payload:
103 | {
104 | index,
105 | route,
106 | key
107 | }
108 | }
109 | }
110 |
111 |
112 | export function jumpToIndex(routeIndex, key) {
113 | if (!key) {
114 | throw new Error('Jump to Index requires key argument');
115 | }
116 |
117 | return {
118 | type: JUMP_TO_INDEX,
119 | payload: {
120 | routeIndex,
121 | key
122 | }
123 | }
124 | }
125 |
126 | export function back(key) {
127 | if (!key) {
128 | throw new Error('popRoute requires key argument');
129 | }
130 |
131 | return {
132 | type: BACK,
133 | payload: {
134 | key
135 | }
136 | };
137 | }
138 |
139 | export function forward(key) {
140 | if (!key) {
141 | throw new Error('popRoute requires key argument');
142 | }
143 |
144 | return {
145 | type: FORWARD,
146 | payload: {
147 | key
148 | }
149 | };
150 | }
151 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | const prefix = 'react-native-navigation-redux-helpers/';
2 |
3 | export const JUMP_TO = `${prefix}JUMP_TO`;
4 | export const PUSH_ROUTE = `${prefix}PUSH_ROUTE`;
5 | export const POP_ROUTE = `${prefix}POP_ROUTE`;
6 | export const RESET_ROUTE = `${prefix}RESET_ROUTE`;
7 | export const REPLACE_AT = `${prefix}REPLACE_AT`;
8 | export const REPLACE_AT_INDEX = `${prefix}REPLACE_AT_INDEX`;
9 | export const JUMP_TO_INDEX = `${prefix}JUMP_TO_INDEX`;
10 | export const BACK = `${prefix}BACK`;
11 | export const FORWARD = `${prefix}FORWARD`;
12 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | PUSH_ROUTE,
3 | POP_ROUTE,
4 | RESET_ROUTE,
5 | REPLACE_AT,
6 | REPLACE_AT_INDEX,
7 | JUMP_TO,
8 | JUMP_TO_INDEX,
9 | BACK,
10 | FORWARD,
11 | GET,
12 | HAS,
13 | INDEX_OF
14 | } from './constants';
15 |
16 | import {
17 | pushRoute,
18 | popRoute,
19 | jumpTo,
20 | reset,
21 | replaceAt,
22 | replaceAtIndex,
23 | jumpToIndex,
24 | back,
25 | forward,
26 | get,
27 | has,
28 | indexOf
29 | } from './actions';
30 |
31 | import { cardStackReducer as csr } from './reducers/card-stack';
32 | import { tabReducer as tr } from './reducers/tab-reducer';
33 |
34 | export const constants = {
35 | PUSH_ROUTE,
36 | POP_ROUTE,
37 | RESET_ROUTE,
38 | REPLACE_AT,
39 | REPLACE_AT_INDEX,
40 | JUMP_TO,
41 | JUMP_TO_INDEX,
42 | BACK,
43 | FORWARD,
44 | GET,
45 | HAS,
46 | INDEX_OF
47 | };
48 |
49 | export const actions = {
50 | pushRoute,
51 | popRoute,
52 | jumpTo,
53 | reset,
54 | replaceAt,
55 | replaceAtIndex,
56 | jumpToIndex,
57 | back,
58 | forward,
59 | get,
60 | has,
61 | indexOf
62 | };
63 |
64 | export const cardStackReducer = csr;
65 | export const tabReducer = tr;
66 |
--------------------------------------------------------------------------------
/src/reducers/card-stack.js:
--------------------------------------------------------------------------------
1 | import {
2 | PUSH_ROUTE,
3 | POP_ROUTE,
4 | RESET_ROUTE,
5 | REPLACE_AT,
6 | REPLACE_AT_INDEX,
7 | JUMP_TO,
8 | JUMP_TO_INDEX,
9 | BACK,
10 | FORWARD
11 | } from '../constants';
12 |
13 | import {
14 | checkInitialState,
15 | isActionPotentiallyApplicable,
16 | getStateUtils
17 | } from './helpers';
18 |
19 | const StateUtils = getStateUtils();
20 |
21 | export function cardStackReducer(initialState) {
22 | checkInitialState(initialState);
23 |
24 | // eslint-disable-next-line complexity
25 | return function cardStackReducerFn(state = initialState, action) {
26 | if (!isActionPotentiallyApplicable(action, state.key)) {
27 | return state;
28 | }
29 |
30 | switch (action.type) {
31 | case PUSH_ROUTE:
32 | if (state.routes[state.index].key === (action.payload && action.payload.route.key)) return state;
33 | return StateUtils.push(state, action.payload.route);
34 | case POP_ROUTE:
35 | return StateUtils.pop(state);
36 | case RESET_ROUTE:
37 | return StateUtils.reset(state, action.payload.routes, action.payload.index);
38 | case REPLACE_AT:
39 | return StateUtils.replaceAt(state, action.payload.routeKey, action.payload.route);
40 | case REPLACE_AT_INDEX:
41 | return StateUtils.replaceAtIndex(state, action.payload.index, action.payload.route);
42 | case JUMP_TO:
43 | return StateUtils.jumpTo(state, action.payload.routeKey);
44 | case JUMP_TO_INDEX:
45 | return StateUtils.jumpToIndex(state, action.payload.routeIndex);
46 | case BACK:
47 | return StateUtils.back(state);
48 | case FORWARD:
49 | return StateUtils.forward(state);
50 | default:
51 | return state;
52 | }
53 | };
54 | }
55 |
--------------------------------------------------------------------------------
/src/reducers/helpers.js:
--------------------------------------------------------------------------------
1 | export function checkInitialState(initialState) {
2 | if (!initialState) {
3 | throw Error('initialState arg is required');
4 | }
5 |
6 | if (typeof initialState.key !== 'string') {
7 | throw Error('initialState must have an attribute **key** which is a string');
8 | }
9 |
10 | if (typeof initialState.index !== 'number') {
11 | throw Error('initialState must have an attribute **index** which is a number');
12 | }
13 |
14 | if (!(initialState.routes instanceof Array)) {
15 | throw Error('initialState must have an attribute **route** which is an array');
16 | }
17 | }
18 |
19 | export function isActionPotentiallyApplicable(action, navigationKey) {
20 | return action && action.payload && (action.payload.key === navigationKey);
21 | }
22 |
23 | export function getStateUtils() {
24 | try {
25 | const { NavigationExperimental } = require('react-native');
26 | return NavigationExperimental.StateUtils;
27 | } catch(e) {
28 | // no-op
29 | }
30 |
31 | return {};
32 | }
33 |
--------------------------------------------------------------------------------
/src/reducers/tab-reducer.js:
--------------------------------------------------------------------------------
1 | import { checkInitialState, isActionPotentiallyApplicable, getStateUtils } from './helpers';
2 | import { JUMP_TO, JUMP_TO_INDEX } from '../constants';
3 |
4 | const StateUtils = getStateUtils();
5 |
6 | export function tabReducer(initialState) {
7 | checkInitialState(initialState);
8 |
9 | return function tabReducerFn(state = initialState, action) {
10 | if (!isActionPotentiallyApplicable(action, state.key)) {
11 | return state;
12 | }
13 |
14 | switch(action.type) {
15 | case JUMP_TO:
16 | return StateUtils.jumpTo(state, action.payload.routeKey);
17 | case JUMP_TO_INDEX:
18 | return StateUtils.jumpToIndex(state, action.payload.routeIndex);
19 | default:
20 | return state;
21 | }
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./setup/.globals.json",
3 | "parserOptions": {
4 | "ecmaVersion": 6,
5 | "sourceType": "module"
6 | },
7 | "rules": {
8 | "strict": 0,
9 | "quotes": [2, "single"],
10 | "no-unused-expressions": 0
11 | },
12 | "env": {
13 | "browser": true,
14 | "node": true,
15 | "mocha": true
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/test/runner.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Tests
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/test/setup/.globals.json:
--------------------------------------------------------------------------------
1 | {
2 | "globals": {
3 | "expect": true,
4 | "mock": true,
5 | "sandbox": true,
6 | "spy": true,
7 | "stub": true,
8 | "useFakeServer": true,
9 | "useFakeTimers": true,
10 | "useFakeXMLHttpRequest": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/test/setup/browser.js:
--------------------------------------------------------------------------------
1 | var mochaGlobals = require('./.globals.json').globals;
2 |
3 | window.mocha.setup('bdd');
4 | window.onload = function() {
5 | window.mocha.checkLeaks();
6 | window.mocha.globals(Object.keys(mochaGlobals));
7 | window.mocha.run();
8 | require('./setup')(window);
9 | };
10 |
--------------------------------------------------------------------------------
/test/setup/node.js:
--------------------------------------------------------------------------------
1 | global.chai = require('chai');
2 | global.sinon = require('sinon');
3 | global.chai.use(require('sinon-chai'));
4 |
5 | require('babel-core/register');
6 | require('./setup')();
7 |
8 | /*
9 | Uncomment the following if your library uses features of the DOM,
10 | for example if writing a jQuery extension, and
11 | add 'simple-jsdom' to the `devDependencies` of your package.json
12 |
13 | Note that JSDom doesn't implement the entire DOM API. If you're using
14 | more advanced or experimental features, you may need to switch to
15 | PhantomJS. Setting that up is currently outside of the scope of this
16 | boilerplate.
17 | */
18 | // import simpleJSDom from 'simple-jsdom';
19 | // simpleJSDom.install();
20 |
--------------------------------------------------------------------------------
/test/setup/setup.js:
--------------------------------------------------------------------------------
1 | module.exports = function(root) {
2 | root = root ? root : global;
3 | root.expect = root.chai.expect;
4 |
5 | beforeEach(function() {
6 | // Using these globally-available Sinon features is preferrable, as they're
7 | // automatically restored for you in the subsequent `afterEach`
8 | root.sandbox = root.sinon.sandbox.create();
9 | root.stub = root.sandbox.stub.bind(root.sandbox);
10 | root.spy = root.sandbox.spy.bind(root.sandbox);
11 | root.mock = root.sandbox.mock.bind(root.sandbox);
12 | root.useFakeTimers = root.sandbox.useFakeTimers.bind(root.sandbox);
13 | root.useFakeXMLHttpRequest = root.sandbox.useFakeXMLHttpRequest.bind(root.sandbox);
14 | root.useFakeServer = root.sandbox.useFakeServer.bind(root.sandbox);
15 | });
16 |
17 | afterEach(function() {
18 | delete root.stub;
19 | delete root.spy;
20 | root.sandbox.restore();
21 | });
22 | };
23 |
--------------------------------------------------------------------------------
/test/unit/actions.js:
--------------------------------------------------------------------------------
1 | import {
2 | pushRoute,
3 | popRoute,
4 | jumpTo,
5 | reset,
6 | replaceAt,
7 | replaceAtIndex,
8 | jumpToIndex,
9 | back,
10 | forward
11 | } from '../../src/actions';
12 | import {
13 | JUMP_TO,
14 | PUSH_ROUTE,
15 | POP_ROUTE,
16 | RESET_ROUTE,
17 | REPLACE_AT,
18 | REPLACE_AT_INDEX,
19 | JUMP_TO_INDEX,
20 | BACK,
21 | FORWARD
22 | } from '../../src/constants';
23 |
24 | const navigationKey = 'nav-key';
25 |
26 | describe('actions', () => {
27 | describe('definitions', () => {
28 | it('pushRoute action is defined', () => {
29 | expect(pushRoute).to.be.ok;
30 | expect(typeof pushRoute === 'function').to.be.true;
31 | });
32 |
33 | it('popRoute action is defined', () => {
34 | expect(popRoute).to.be.ok;
35 | expect(typeof popRoute === 'function').to.be.true;
36 | });
37 |
38 | it('jumpTo action is defined', () => {
39 | expect(jumpTo).to.be.ok;
40 | expect(typeof jumpTo === 'function').to.be.true;
41 | });
42 |
43 | it('reset action is defined', () => {
44 | expect(reset).to.be.ok;
45 | expect(typeof reset === 'function').to.be.true;
46 | });
47 | });
48 |
49 | describe('all actions', () => {
50 | it('require key attribute', () => {
51 | const pushRouteFn = () => pushRoute({});
52 | const popRouteFn = () => popRoute();
53 | const jumpToFn = () => jumpTo();
54 |
55 | expect(pushRouteFn).to.throw(Error);
56 | expect(popRouteFn).to.throw(Error);
57 | expect(jumpToFn).to.throw(Error);
58 | });
59 | });
60 |
61 | describe('pushRoute', () => {
62 | it('returns a message with type set to PUSH_ROUTE and appropriate payload', () => {
63 | const route = { key: 'route', data : {} };
64 | const actionData = pushRoute(route, navigationKey);
65 |
66 | expect(actionData.type).to.equal(PUSH_ROUTE);
67 | expect(actionData.payload).to.be.ok;
68 | expect(actionData.payload.route).to.equal(route);
69 | expect(actionData.payload.key).to.equal(navigationKey);
70 | });
71 | });
72 |
73 | describe('popRoute', () => {
74 | it('returns a message with type set to POP_ROUTE and appropriate payload', () => {
75 | const actionData = popRoute(navigationKey);
76 |
77 | expect(actionData.type).to.equal(POP_ROUTE);
78 | expect(actionData.payload.key).to.equal(navigationKey);
79 | });
80 | });
81 |
82 | describe('jumpTo', () => {
83 | it('returns a message with type set to JUMP_TO_INDEX and appropriate payload with index first arg', () => {
84 | const tabIndex = 3;
85 | const actionData = jumpTo(tabIndex, navigationKey);
86 |
87 | expect(actionData.type).to.equal(JUMP_TO_INDEX);
88 | expect(actionData.payload).to.be.ok;
89 | expect(actionData.payload.routeIndex).to.equal(tabIndex);
90 | expect(actionData.payload.key).to.equal(navigationKey);
91 | });
92 |
93 | it('supports string key first argument and returns message with type JUMP_TO and proper payload', () => {
94 | const routeKey = 'key';
95 | const actionData = jumpTo(routeKey, navigationKey);
96 |
97 | expect(actionData.type).to.equal(JUMP_TO);
98 | expect(actionData.payload).to.be.ok;
99 | expect(actionData.payload.routeKey).to.equal(routeKey);
100 | expect(actionData.payload.key).to.equal(navigationKey);
101 | });
102 | });
103 |
104 | describe('reset', () => {
105 | it('returns a message with type set to RESET_ROUTE', () => {
106 | const routes = [{ key: 'route1' }];
107 | const actionData = reset(routes, navigationKey);
108 | expect(actionData.type).to.equal(RESET_ROUTE);
109 | expect(actionData.payload).to.be.ok;
110 | expect(actionData.payload.key).to.equal(navigationKey);
111 | expect(actionData.payload.routes).to.equal(routes);
112 | });
113 |
114 | it('returns a message with payload.index set to index passed as second arg', () => {
115 | const routes = [{ key: 'route1' }];
116 | const actionData = reset(routes, navigationKey, 1);
117 | expect(actionData.payload).to.be.ok;
118 | expect(actionData.payload.routes).to.equal(routes);
119 | expect(actionData.payload.index).to.equal(1);
120 | });
121 | });
122 |
123 | describe('replaceAt', () => {
124 | it('returns a message with type set to REPLACE_AT + proper payload', () => {
125 | const route = { key: 'new route' };
126 | const routeKey = 'old route';
127 | const actionData = replaceAt(routeKey, route, navigationKey);
128 | expect(actionData.type).to.equal(REPLACE_AT);
129 | expect(actionData.payload).to.be.ok;
130 | expect(actionData.payload.routeKey).to.equal(routeKey);
131 | expect(actionData.payload.route).to.equal(route);
132 | });
133 | });
134 |
135 | describe('replaceAtIndex', () => {
136 | it('returns a message with type set to REPLACE_AT_INDEX + proper payload', () => {
137 | const route = { key: 'new route' };
138 | const index = 1;
139 | const actionData = replaceAtIndex(index, route, navigationKey);
140 | expect(actionData.type).to.equal(REPLACE_AT_INDEX);
141 | expect(actionData.payload).to.be.ok;
142 | expect(actionData.payload.index).to.equal(index);
143 | expect(actionData.payload.route).to.equal(route);
144 | });
145 | });
146 |
147 | describe('jumpToIndex', () => {
148 | it('returns a message with type set to JUMP_TO_INDEX + proper payload', () => {
149 | const index = 1;
150 | const actionData = jumpToIndex(index, navigationKey);
151 | expect(actionData.type).to.equal(JUMP_TO_INDEX);
152 | expect(actionData.payload).to.be.ok;
153 | expect(actionData.payload.routeIndex).to.equal(index);
154 | });
155 | });
156 |
157 | describe('back', () => {
158 | it('returns a message with type set to BACK + proper payload', () => {
159 | const actionData = back(navigationKey);
160 | expect(actionData.type).to.equal(BACK);
161 | expect(actionData.payload).to.be.ok;
162 | expect(actionData.payload.key).to.equal(navigationKey);
163 | });
164 | });
165 |
166 | describe('forward', () => {
167 | it('returns a message with type set to FORWARD + proper payload', () => {
168 | const actionData = forward(navigationKey);
169 | expect(actionData.type).to.equal(FORWARD);
170 | expect(actionData.payload).to.be.ok;
171 | expect(actionData.payload.key).to.equal(navigationKey);
172 | });
173 | });
174 | });
175 |
--------------------------------------------------------------------------------
/test/unit/constants.js:
--------------------------------------------------------------------------------
1 | import {
2 | JUMP_TO,
3 | PUSH_ROUTE,
4 | POP_ROUTE,
5 | RESET_ROUTE,
6 | REPLACE_AT,
7 | REPLACE_AT_INDEX,
8 | JUMP_TO_INDEX,
9 | BACK,
10 | FORWARD
11 | } from '../../src/constants';
12 |
13 | describe('constants', () => {
14 | describe('definitions', () => {
15 | it('JUMP_TO is defined', () => {
16 | expect(JUMP_TO).to.be.ok;
17 | });
18 |
19 | it('PUSH_ROUTE is defined', () => {
20 | expect(PUSH_ROUTE).to.be.ok;
21 | });
22 |
23 | it('POP_ROUTE is defined', () => {
24 | expect(POP_ROUTE).to.be.ok;
25 | });
26 |
27 | it('RESET_ROUTE is defined', () => {
28 | expect(RESET_ROUTE).to.be.ok;
29 | });
30 |
31 | it('REPLACE_AT is defined', () => {
32 | expect(REPLACE_AT).to.be.ok;
33 | });
34 |
35 | it('REPLACE_AT_INDEX is defined', () => {
36 | expect(REPLACE_AT_INDEX).to.be.ok;
37 | });
38 |
39 | it('JUMP_TO_INDEX is defined', () => {
40 | expect(JUMP_TO_INDEX).to.be.ok;
41 | });
42 |
43 | it('BACK is defined', () => {
44 | expect(BACK).to.be.ok;
45 | });
46 |
47 | it('FORWARD is defined', () => {
48 | expect(FORWARD).to.be.ok;
49 | });
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/test/unit/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | constants, actions, cardStackReducer, tabReducer
3 | } from '../../src/index';
4 |
5 | const {
6 | JUMP_TO,
7 | PUSH_ROUTE,
8 | POP_ROUTE,
9 | RESET_ROUTE,
10 | REPLACE_AT,
11 | REPLACE_AT_INDEX,
12 | JUMP_TO_INDEX,
13 | BACK,
14 | FORWARD
15 | } = constants;
16 |
17 | const {
18 | pushRoute,
19 | popRoute,
20 | jumpTo,
21 | reset,
22 | replaceAt,
23 | replaceAtIndex,
24 | jumpToIndex,
25 | back,
26 | forward
27 | } = actions;
28 |
29 | describe('react-native-navigation-redux-helpers', () => {
30 | describe('definitions', () => {
31 | it('exports constants', () => {
32 | expect(constants).to.be.ok;
33 |
34 | [
35 | JUMP_TO,
36 | PUSH_ROUTE,
37 | POP_ROUTE,
38 | RESET_ROUTE,
39 | REPLACE_AT,
40 | REPLACE_AT_INDEX,
41 | JUMP_TO_INDEX,
42 | BACK,
43 | FORWARD
44 | ].forEach(c => expect(c).to.be.ok);
45 | });
46 |
47 | it('exports actions', () => {
48 | expect(actions).to.be.ok;
49 |
50 | [
51 | pushRoute,
52 | popRoute,
53 | jumpTo,
54 | reset,
55 | replaceAt,
56 | replaceAtIndex,
57 | jumpToIndex,
58 | back,
59 | forward
60 | ].forEach(a => expect(a).to.be.ok);
61 | });
62 |
63 | it('exports cardStackReducer', () => {
64 | expect(cardStackReducer).to.be.ok;
65 | });
66 |
67 | it('exports tabReducer', () => {
68 | expect(tabReducer).to.be.ok;
69 | });
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/test/unit/reducers.js:
--------------------------------------------------------------------------------
1 | import { __RewireAPI__ as cardStackReducerAPI, cardStackReducer } from '../../src/reducers/card-stack';
2 | import { __RewireAPI__ as tabReducerAPI, tabReducer } from '../../src/reducers/tab-reducer';
3 | import {
4 | pushRoute,
5 | popRoute,
6 | jumpTo,
7 | reset,
8 | replaceAt,
9 | replaceAtIndex,
10 | jumpToIndex,
11 | back,
12 | forward
13 | } from '../../src/actions';
14 |
15 | const cardStackInitialState = {
16 | key: 'key',
17 | index: 0,
18 | routes: [{
19 | key: 'route-1',
20 | title: 'Route 1'
21 | }]
22 | };
23 |
24 | const repeatedRoute = {
25 | key: 'route-1',
26 | title: 'Route 1'
27 | };
28 |
29 | const tabInitialState = {
30 | key: 'ta-key',
31 | index: 0,
32 | routes: [
33 | {
34 | key: 'route-1',
35 | title: 'Route 1'
36 | },
37 | {
38 | key: 'route-2',
39 | title: 'Route 2'
40 | }
41 | ]
42 | }
43 |
44 | describe('reducers', () => {
45 | let pushSpy, popSpy, jumpToIndexSpy, jumpToSpy,
46 | resetSpy, replaceAtSpy, replaceAtIndexSpy,
47 | backSpy, forwardSpy;
48 |
49 | const StateUtils = {
50 | push(state, action) {
51 | return 'StateUtils.push';
52 | },
53 |
54 | pop(state) {
55 | return 'StateUtils.pop';
56 | },
57 |
58 | jumpTo(state, key) {
59 | return 'StateUtils.jumpTo';
60 | },
61 |
62 | jumpToIndex(state, index) {
63 | return 'StateUtils.jumpToIndex';
64 | },
65 |
66 | reset(state, routes, index) {
67 | return 'StateUtils.reset';
68 | },
69 |
70 | replaceAt(state, key, route) {
71 | return 'StateUtils.replaceAt';
72 | },
73 |
74 | replaceAtIndex(state, index, route) {
75 | return 'StateUtils.replaceAtIndex';
76 | },
77 |
78 | back(state) {
79 | return 'StateUtils.back';
80 | },
81 |
82 | forward(state) {
83 | return 'StateUtils.forward';
84 | }
85 | };
86 |
87 | describe('definitions', () => {
88 | it('cardStackReducer is defined and it is a function', () => {
89 | expect(cardStackReducer).to.be.ok;
90 | expect(typeof cardStackReducer === 'function').to.be.true;
91 | });
92 |
93 | it('tabReducer is defined and it is a function', () => {
94 | expect(tabReducer).to.be.ok;
95 | expect(typeof tabReducer === 'function').to.be.true;
96 | });
97 | });
98 |
99 | describe('cardStackReducer', () => {
100 | let reducer;
101 |
102 | beforeEach(() => {
103 | reducer = cardStackReducer(cardStackInitialState);
104 |
105 | cardStackReducerAPI.__Rewire__('StateUtils', StateUtils);
106 | pushSpy = spy(StateUtils, 'push');
107 | popSpy = spy(StateUtils, 'pop');
108 | resetSpy = spy(StateUtils, 'reset');
109 | replaceAtSpy = spy(StateUtils, 'replaceAt');
110 | replaceAtIndexSpy = spy(StateUtils, 'replaceAtIndex');
111 | jumpToSpy = spy(StateUtils, 'jumpTo');
112 | jumpToIndexSpy = spy(StateUtils, 'jumpToIndex');
113 | backSpy = spy(StateUtils, 'back');
114 | forwardSpy = spy(StateUtils, 'forward');
115 | });
116 |
117 | afterEach(() => {
118 | StateUtils.push.restore();
119 | StateUtils.pop.restore();
120 | StateUtils.reset.restore();
121 | StateUtils.replaceAt.restore();
122 | StateUtils.replaceAtIndex.restore();
123 | StateUtils.jumpTo.restore();
124 | StateUtils.jumpToIndex.restore();
125 | StateUtils.back.restore();
126 | StateUtils.forward.restore();
127 | cardStackReducerAPI.__ResetDependency__('StateUtils');
128 | });
129 |
130 | it('throws if initial state does not look good', () => {
131 | const cardStackReducerFnNoInitialState = () => cardStackReducer();
132 | const cardStackReducerFnJustKey = () => cardStackReducer({ key: 'key' });
133 | const cardStackReducerFnNoRoutes = () => cardStackReducer({ key: 'key', index: 0 });
134 |
135 | expect(cardStackReducerFnNoInitialState).to.throw(Error);
136 | expect(cardStackReducerFnJustKey).to.throw(Error);
137 | expect(cardStackReducerFnNoRoutes).to.throw(Error);
138 | });
139 |
140 | it('returns a function', () => {
141 | expect(typeof reducer === 'function').to.be.true;
142 | });
143 |
144 | it('returns navigation state for random events', () => {
145 | expect(reducer(cardStackInitialState, null)).to.equal(cardStackInitialState);
146 | expect(reducer(cardStackInitialState, { type: 'RANDOM_EVENT' })).to.equal(cardStackInitialState);
147 | });
148 |
149 | it('calls RN\'s StateUtils.push when pushRoute action arrives and returns whatever StateUtils.push returns', () => {
150 | const action = pushRoute({ key: 'route' }, cardStackInitialState.key);
151 |
152 | const returnValue = reducer(cardStackInitialState, action);
153 |
154 | expect(pushSpy).to.have.been.calledOnce;
155 | expect(pushSpy).to.have.been.calledWith(cardStackInitialState, action.payload.route);
156 | expect(returnValue).to.equal('StateUtils.push');
157 | });
158 |
159 | it('does not call RN\'s StateUtils.push when pushRoute action has payload.key same with current route state.key and returns current nav state', () => {
160 | const action = pushRoute({ key: 'route' }, repeatedRoute.key);
161 |
162 | const returnValue = reducer(cardStackInitialState, action);
163 | expect(pushSpy.callCount).to.equal(0);
164 | expect(returnValue).to.equal(cardStackInitialState);
165 | });
166 |
167 | it('does not call RN\'s StateUtils.push when pushRoute action has payload.key different from state.key and returns current nav state', () => {
168 | const action = pushRoute({ key: 'route' }, 'nav');
169 |
170 | const returnValue = reducer(cardStackInitialState, action);
171 | expect(pushSpy.callCount).to.equal(0);
172 | expect(returnValue).to.equal(cardStackInitialState);
173 | });
174 |
175 | it('calls RN\'s StateUtils.pop when popRoute action arrives and returns whatever StateUtils.pop returns', () => {
176 | const action = popRoute(cardStackInitialState.key);
177 | const returnValue = reducer(cardStackInitialState, action);
178 |
179 | expect(popSpy).to.have.been.calledOnce;
180 | expect(popSpy).to.have.been.calledWith(cardStackInitialState);
181 | expect(returnValue).to.equal('StateUtils.pop');
182 | });
183 |
184 | it('does not call RN\'s StateUtils.pop when popRoute action has payload.key different from state.key and returns current nav state', () => {
185 | const action = popRoute('random-nav-key');
186 | const returnValue = reducer(cardStackInitialState, action);
187 |
188 | expect(popSpy.callCount).to.equal(0);
189 | expect(returnValue).to.equal(cardStackInitialState);
190 | });
191 |
192 | it('calls RN\'s StateUtils.reset when reset action arrives', () => {
193 | const routes = [{ key: 'new route'}];
194 | const action = reset(routes, cardStackInitialState.key);
195 | const returnValue = reducer(cardStackInitialState, action);
196 |
197 | expect(resetSpy).to.have.been.calledOnce;
198 | expect(resetSpy).to.have.been.calledWith(
199 | cardStackInitialState, routes);
200 | });
201 |
202 | it('calls RN\'s StateUtils.reset with index when reset action has index data', () => {
203 | const routes = [{ key: 'new route'}];
204 | const action = reset(routes, cardStackInitialState.key, 0);
205 | reducer(cardStackInitialState, action);
206 |
207 | expect(resetSpy).to.have.been.calledOnce;
208 | expect(resetSpy).to.have.been.calledWith(
209 | cardStackInitialState, routes, 0);
210 | });
211 |
212 | it('calls RN\'s StateUtils.replaceAt when replaceAt action arrives', () => {
213 | const routeKey = 'old-key';
214 | const route = { key: 'new-route' };
215 | const action = replaceAt(routeKey, route, cardStackInitialState.key);
216 | reducer(cardStackInitialState, action);
217 |
218 | expect(replaceAtSpy).to.have.been.calledOnce;
219 | expect(replaceAtSpy).to.have.been.calledWith(
220 | cardStackInitialState, routeKey, route
221 | );
222 | });
223 |
224 | it('calls RN\'s StateUtils.replaceAtIndex when replaceAtIndex action arrives', () => {
225 | const index = 1;
226 | const route = { key: 'new-route' };
227 | const action = replaceAtIndex(index, route, cardStackInitialState.key);
228 | reducer(cardStackInitialState, action);
229 |
230 | expect(replaceAtIndexSpy).to.have.been.calledOnce;
231 | expect(replaceAtIndexSpy).to.have.been.calledWith(
232 | cardStackInitialState, index, route
233 | );
234 | });
235 |
236 | it('calls RN\'s StateUtils.jumpToIndex when jumpToIndex action arrives', () => {
237 | const action = jumpToIndex(0, cardStackInitialState.key);
238 | reducer(cardStackInitialState, action);
239 |
240 | expect(jumpToIndexSpy).to.have.been.calledOnce;
241 | expect(jumpToIndexSpy).to.have.been.calledWith(cardStackInitialState, action.payload.routeIndex);
242 | });
243 |
244 | it('calls RN\'s StateUtils.jumpTo when jumpTo action arrives', () => {
245 | const routeKey = 'key';
246 | const action = jumpTo(routeKey, cardStackInitialState.key);
247 | reducer(cardStackInitialState, action);
248 |
249 | expect(jumpToSpy).to.have.been.calledOnce;
250 | expect(jumpToSpy).to.have.been.calledWith(cardStackInitialState, action.payload.routeKey);
251 | });
252 |
253 | it('calls RN\'s StateUtils.back when back action arrives', () => {
254 | const action = back(cardStackInitialState.key);
255 | reducer(cardStackInitialState, action);
256 |
257 | expect(backSpy).to.have.been.calledOnce;
258 | expect(backSpy).to.have.been.calledWith(cardStackInitialState);
259 | });
260 |
261 | it('calls RN\'s StateUtils.forward when forward action arrives', () => {
262 | const action = forward(cardStackInitialState.key);
263 | reducer(cardStackInitialState, action);
264 |
265 | expect(forwardSpy).to.have.been.calledOnce;
266 | expect(forwardSpy).to.have.been.calledWith(cardStackInitialState);
267 | });
268 | });
269 |
270 | describe('tabReducer', () => {
271 | let reducer;
272 |
273 | beforeEach(() => {
274 | reducer = tabReducer(tabInitialState);
275 | tabReducerAPI.__Rewire__('StateUtils', StateUtils);
276 | jumpToIndexSpy = spy(StateUtils, 'jumpToIndex');
277 | jumpToSpy = spy(StateUtils, 'jumpTo');
278 | });
279 |
280 | afterEach(() => {
281 | StateUtils.jumpToIndex.restore();
282 | StateUtils.jumpTo.restore();
283 | tabReducerAPI.__ResetDependency__('StateUtils');
284 | });
285 |
286 | it('throws if initial state does not look good', () => {
287 | const tabReducerFnNoInitialState = () => tabReducer();
288 | const tabReducerFnJustKey = () => tabReducer({ key: 'key' });
289 | const tabReducerFnNoRoutes = () => tabReducer({ key: 'key', index: 0 });
290 |
291 | expect(tabReducerFnNoInitialState).to.throw(Error);
292 | expect(tabReducerFnJustKey).to.throw(Error);
293 | expect(tabReducerFnNoRoutes).to.throw(Error);
294 | });
295 |
296 | it('returns a function', () => {
297 | expect(typeof reducer === 'function').to.be.true;
298 | });
299 |
300 | it('returns navigation state for random events', () => {
301 | expect(reducer(tabInitialState, null)).to.equal(tabInitialState);
302 | expect(reducer(tabInitialState, { type: 'RANDOM_EVENT' })).to.equal(tabInitialState);
303 | });
304 |
305 | it('calls RN\'s StateUtils.jumpToIndex when jumpToIndex action arrives and returns whatever StateUtils.jumpToIndex returns', () => {
306 | const action = jumpToIndex(0, tabInitialState.key);
307 | const returnValue = reducer(tabInitialState, action);
308 |
309 | expect(jumpToIndexSpy).to.have.been.calledOnce;
310 | expect(jumpToIndexSpy).to.have.been.calledWith(tabInitialState, action.payload.routeIndex);
311 | expect(returnValue).to.equal('StateUtils.jumpToIndex');
312 | });
313 |
314 | it('does not call RN\'s StateUtils.jumpToIndex when jumpToIndex action has key different from state.key and returns current nav state', () => {
315 | const action = jumpToIndex(0, 'a different key');
316 | const returnValue = reducer(tabInitialState, action);
317 |
318 | expect(jumpToIndexSpy.callCount).to.equal(0);
319 | expect(returnValue).to.equal(tabInitialState);
320 | });
321 |
322 | it('calls RN\'s StateUtils.jumpTo when jumpTo action arrives', () => {
323 | const routeKey = 'key';
324 | const action = jumpTo(routeKey, tabInitialState.key);
325 | reducer(tabInitialState, action);
326 |
327 | expect(jumpToSpy).to.have.been.calledOnce;
328 | expect(jumpToSpy).to.have.been.calledWith(tabInitialState, action.payload.routeKey);
329 | });
330 | });
331 | });
332 |
--------------------------------------------------------------------------------