├── .npmignore ├── .gitignore ├── .travis.yml ├── .babelrc ├── src ├── index.js ├── example │ ├── index.html │ ├── main.css │ └── index.js ├── TransitionSwitch.js └── TransitionSwitch.test.js ├── webpack.config.js ├── LICENCE ├── webpack.config.example.js ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build 3 | /.idea 4 | /npm-debug.log 5 | /example 6 | /coverage 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build 3 | /.idea 4 | /npm-debug.log 5 | /example 6 | /lib 7 | /coverage 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | script: npm run build:prod 5 | after_success: 'npm run test:ci' -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"], 3 | "plugins": ["transform-class-properties", "transform-decorators-legacy"] 4 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Arnaud on 03/01/2017. 3 | */ 4 | import {TransitionSwitch} from './TransitionSwitch'; 5 | 6 | export {TransitionSwitch}; -------------------------------------------------------------------------------- /src/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Router V4 Transition Example 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/example/main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | font-family: Arial; 4 | } 5 | 6 | .example-app { 7 | width: 80%; 8 | margin: auto; 9 | margin-top: 20px; 10 | } 11 | 12 | .example-app__menu { 13 | text-align: center; 14 | } 15 | 16 | .example-app__menu > * { 17 | display: inline-block; 18 | margin-right: 20px; 19 | text-decoration: none; 20 | color: #555555; 21 | text-transform: uppercase; 22 | font-weight: bold; 23 | } 24 | 25 | .example-app__transition { 26 | position: absolute; 27 | width: 60%; 28 | margin-left: 10%; 29 | margin-top: 50px; 30 | background-color: #f8f8f8; 31 | height: 200px; 32 | border-left: 10px solid #555555; 33 | padding-left: 30px; 34 | padding-top: 20px; 35 | color: #555555; 36 | text-transform: uppercase; 37 | font-size: .8em; 38 | font-weight: bold; 39 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | 4 | module.exports = { 5 | devtool: 'cheap-module-source-map', 6 | entry: { 7 | bundle: path.resolve(__dirname, 'src/index.js') 8 | }, 9 | output: { 10 | path: path.resolve(__dirname, 'lib'), 11 | filename: "react-router-v4-transition.js", 12 | library: 'ReactRouterV4Transition', 13 | libraryTarget: 'umd' 14 | }, 15 | externals: [ 16 | 'react', 17 | 'react-dom', 18 | 'react-router', 19 | 'prop-types' 20 | ], 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.js$/, 25 | exclude: /node_modules/, 26 | loader: 'babel-loader' 27 | } 28 | ] 29 | }, 30 | resolve: { 31 | extensions: ['.js'], 32 | modules: [ 33 | path.resolve(__dirname, 'src'), 34 | path.resolve(__dirname, 'node_modules') 35 | ] 36 | } 37 | }; -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Arnaud Boeglin 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. -------------------------------------------------------------------------------- /webpack.config.example.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | var CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | 5 | module.exports = { 6 | watch: true, 7 | devtool: 'cheap-module-source-map', 8 | entry: { 9 | bundle: path.resolve(__dirname, 'src/example/index.js') 10 | }, 11 | output: { 12 | path: path.resolve(__dirname, 'example'), 13 | filename: "bundle.js" 14 | }, 15 | plugins: [ 16 | new CopyWebpackPlugin([ 17 | {from: 'src/example/index.html', to: 'index.html'}, 18 | {from: 'src/example/main.css', to: 'main.css'} 19 | ]) 20 | ], 21 | devServer: { 22 | contentBase: path.join(__dirname, "/example"), 23 | compress: true, 24 | port: 8080 25 | }, 26 | module: { 27 | rules: [ 28 | { 29 | test: /\.js$/, 30 | exclude: /node_modules/, 31 | loader: 'babel-loader' 32 | } 33 | ] 34 | }, 35 | resolve: { 36 | extensions: ['.js'], 37 | modules: [ 38 | path.resolve(__dirname, 'src'), 39 | path.resolve(__dirname, 'node_modules') 40 | ] 41 | } 42 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Arnaud Boeglin", 3 | "license": "MIT", 4 | "name": "react-router-v4-transition", 5 | "version": "1.0.0", 6 | "description": "", 7 | "main": "lib/react-router-v4-transition.js", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/aboeglin/react-router-v4-transition.git" 11 | }, 12 | "dependencies": {}, 13 | "peerDependencies": { 14 | "react": "^15.0.0", 15 | "react-dom": "^15.0.0", 16 | "react-router": "^4.0.0", 17 | "prop-types": "^15.0.0" 18 | }, 19 | "devDependencies": { 20 | "babel-core": "^6.25.0", 21 | "babel-loader": "^7.1.1", 22 | "babel-plugin-transform-class-properties": "^6.24.1", 23 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 24 | "babel-polyfill": "^6.26.0", 25 | "babel-preset-es2015": "^6.18.0", 26 | "babel-preset-react": "^6.24.1", 27 | "babel-preset-stage-0": "^6.24.1", 28 | "copy-webpack-plugin": "^4.0.1", 29 | "coveralls": "^2.13.1", 30 | "enzyme": "^3.3.0", 31 | "enzyme-adapter-react-16": "^1.1.1", 32 | "gsap": "^1.20.2", 33 | "jest-cli": "^22.2.2", 34 | "prop-types": "^15.0.0", 35 | "react": "^16.2.0", 36 | "react-dom": "^16.2.0", 37 | "react-router": "^4.1.1", 38 | "react-router-dom": "^4.1.1", 39 | "react-test-renderer": "^16.2.0", 40 | "sinon": "^2.3.6", 41 | "webpack": "^2.2.1" 42 | }, 43 | "scripts": { 44 | "test": "jest", 45 | "test:watch": "npm test -- --watch", 46 | "test:coverage": "jest --coverage", 47 | "test:ci": "jest --coverage && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", 48 | "build:dev": "webpack", 49 | "build:prod": "webpack --progress -p", 50 | "prepublish": "webpack --progress -p", 51 | "build:example": "webpack --config webpack.config.example.js", 52 | "start:server": "webpack-dev-server --content-base ./example --port 8080 --history-api-fallback index.html" 53 | }, 54 | "jest": { 55 | "verbose": true, 56 | "coveragePathIgnorePatterns": [ 57 | "/node_modules", 58 | "/lib" 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Router Transition 2 | [![Build Status](https://travis-ci.org/aboeglin/react-router-v4-transition.png?branch=master)](https://travis-ci.org/aboeglin/react-router-v4-transition) [![Coverage Status](https://coveralls.io/repos/github/aboeglin/react-router-v4-transition/badge.svg?branch=master)](https://coveralls.io/github/aboeglin/react-router-v4-transition?branch=master) [![npm version](https://badge.fury.io/js/react-router-v4-transition.svg)](https://badge.fury.io/js/react-router-v4-transition) 3 | 4 | Transitions for React Router v4. The API is composed of a component, TransitionSwitch, that should be used as the Switch 5 | component from react-router v4 to switch from a route to another one with a transition. That transition can be any action 6 | you need to do between routes, like animation, or fetching data. 7 | 8 | ## API Description 9 | 10 | ### 1) The component: 11 | ```javascript 12 | 13 | 14 | home path 15 | 16 | 17 | other path 18 | 19 | 20 | other home 21 | 22 | 23 | another path 24 | 25 | 26 | ``` 27 | 28 | TransitionSwitch allows you to perform transitions on route change. Given its name, it works like the router v4 Switch. It 29 | means that only one route will be visible at all times. Except if parallel is set to true, which means that the entering 30 | transition won't wait for the leaving transition to be finished. 31 | NB: parallel may be renamed in the future. 32 | 33 | ### 2) The transitions: 34 | Like a switch, the children must be Route elements. The children of these route elements will be given hooks to perform 35 | the transition. These hooks are : 36 | 37 | ```javascript 38 | class Transition extends React.Component { 39 | 40 | componentWillAppear(callback) { 41 | //do something when the component will appear 42 | 43 | callback(); 44 | } 45 | 46 | componentDidAppear() { 47 | //do something when the component appeared 48 | } 49 | 50 | componentWillEnter(callback) { 51 | //do something when the component will enter 52 | 53 | callback(); 54 | } 55 | 56 | componentDidEnter() { 57 | //do something when the component entered 58 | } 59 | 60 | componentWillLeave(callback) { 61 | //do something when the component will leave 62 | 63 | callback(); 64 | } 65 | 66 | componentDidLeave() { 67 | //do something when the component has left 68 | } 69 | 70 | } 71 | 72 | ``` 73 | The callbacks must be called after the transition is complete, in the case of animation, a good place is in the 74 | callback provided by the animation library. The interface is very much the same as react-trasition-group v1. 75 | This means that componentWillAppear is called for the first time when the TransitionSwitch is mounted. 76 | 77 | ## Sample App 78 | 79 | In case you want to quickly try it, there's a webpack setup and very rough sample app. 80 | In order to build it you should run : 81 | ``` 82 | npm run build:example 83 | npm run start:server 84 | ``` 85 | The app will be running at localhost:8080, the build command watches for changes in case you want to play with it, the 86 | sources are located in src/example. 87 | 88 | -------------------------------------------------------------------------------- /src/example/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Arnaud on 10/07/2017. 3 | */ 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import {BrowserRouter, Link, Route} from 'react-router-dom'; 7 | import {TweenLite} from 'gsap'; 8 | 9 | import {TransitionSwitch, TransitionRoute} from '../'; 10 | 11 | /** 12 | * Example App to showcase the use of react-router-v4-transition. 13 | * 14 | * It uses gsap to animate the elements, but any other library could be used in place. 15 | */ 16 | class ExampleApp extends React.Component { 17 | 18 | render() { 19 | return( 20 |
21 | 28 |
29 | 30 | 31 | home path 32 | 33 | 34 | { 35 | return ( 36 | use render 37 | ); 38 | }}/> 39 | 40 | other path 41 | 42 | 43 | other home 44 | 45 | 46 | another path 47 | 48 | 49 |
50 |
51 | ); 52 | } 53 | } 54 | 55 | let d = 1; 56 | class Transition extends React.Component { 57 | 58 | constructor(props) { 59 | super(props); 60 | } 61 | 62 | componentWillAppear(cb) { 63 | TweenLite.fromTo(ReactDOM.findDOMNode(this), d, {x: -100, opacity: 0}, {x: 0, opacity:1, onComplete: () => cb()}); 64 | } 65 | 66 | // componentDidAppear() { 67 | // //do stuff on appear 68 | // } 69 | 70 | componentWillEnter(cb) { 71 | TweenLite.fromTo(ReactDOM.findDOMNode(this), d, {x: 100, opacity: 0}, {x: 0, opacity:1, onComplete: () => cb()}); 72 | } 73 | 74 | componentDidEnter() { 75 | //do stuff on enter 76 | } 77 | 78 | componentWillLeave(cb) { 79 | // if(this.mounted) 80 | TweenLite.to(ReactDOM.findDOMNode(this), d, {x: -100, opacity:0, onComplete: () => cb()}); 81 | } 82 | 83 | componentDidLeave() { 84 | //do stuff on leave 85 | } 86 | 87 | render() { 88 | return ( 89 |
{this.props.children}
90 | ); 91 | } 92 | 93 | } 94 | 95 | class ATransition extends React.Component { 96 | constructor(props) { 97 | super(props); 98 | } 99 | 100 | componentWillAppear(cb) { 101 | TweenLite.fromTo(ReactDOM.findDOMNode(this), d, {x: -100, opacity: 0}, {x: 0, opacity:1, onComplete: () => cb()}); 102 | } 103 | 104 | // componentDidAppear() { 105 | // //do stuff on appear 106 | // } 107 | 108 | componentWillEnter(cb) { 109 | TweenLite.fromTo(ReactDOM.findDOMNode(this), d, {x: 100, opacity: 0}, {x: 0, opacity:1, onComplete: () => cb()}); 110 | } 111 | 112 | componentDidEnter() { 113 | //do stuff on enter 114 | } 115 | 116 | componentWillLeave(cb) { 117 | // if(this.mounted) 118 | TweenLite.to(ReactDOM.findDOMNode(this), d, {x: -100, opacity:0, onComplete: () => cb()}); 119 | } 120 | 121 | componentDidLeave() { 122 | //do stuff on leave 123 | } 124 | 125 | render() { 126 | return ( 127 |
A Transition
128 | ); 129 | } 130 | } 131 | 132 | ReactDOM.render( 133 | 134 | 135 | , 136 | document.getElementById('app') 137 | ); 138 | -------------------------------------------------------------------------------- /src/TransitionSwitch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Arnaud on 07/07/2017. 3 | */ 4 | import React from 'react'; 5 | import PropTypes from 'prop-types'; 6 | import {matchPath, Route} from 'react-router'; 7 | 8 | const routePropType = PropTypes.shape({ 9 | type: PropTypes.oneOf([Route]) 10 | }); 11 | 12 | /** 13 | * @class TransitionSwitch 14 | * 15 | * TransitionSwitch offers a way to get easy route transitions with 16 | * the router v4. 17 | */ 18 | export class TransitionSwitch extends React.Component { 19 | 20 | static propTypes = { 21 | parallel: PropTypes.bool, 22 | children: PropTypes.oneOfType([ 23 | PropTypes.arrayOf(routePropType), 24 | routePropType 25 | ]), 26 | location: PropTypes.object 27 | }; 28 | 29 | static defaultProps = { 30 | parallel: false 31 | }; 32 | 33 | static contextTypes = { 34 | router: PropTypes.shape({ 35 | route: PropTypes.object.isRequired 36 | }).isRequired 37 | }; 38 | 39 | constructor(props, context) { 40 | super(props, context); 41 | 42 | this.enteringRouteChildRef = null; 43 | this.leavingRouteChildRef = null; 44 | 45 | this.state = { 46 | enteringRouteKey: null, 47 | leavingRouteKey: null, 48 | match: null 49 | } 50 | } 51 | 52 | componentWillMount() { 53 | //We need to initialize given route properties before we mount it 54 | this.updateChildren(this.props, this.context); 55 | } 56 | 57 | componentWillReceiveProps(nextProps, nextContext) { 58 | this.updateChildren(nextProps, nextContext) 59 | } 60 | 61 | componentWillUpdate() { 62 | this.prevContext = {...this.context}; 63 | } 64 | 65 | /** 66 | * Update internal state of the children to render 67 | * 68 | * props and context must be given, because it may mostly be called before the render method, 69 | * therefore we need to access props and context before they are applied and can't refer to this. 70 | * 71 | * @param props props object to use for the update 72 | * @param context context object to use for the update ( eg: router info ) 73 | */ 74 | updateChildren(props, context) { 75 | let found = false; 76 | 77 | React.Children.map(props.children, child => child).forEach(child => { 78 | let pathData = { 79 | path: child.props.path, 80 | exact: child.props.exact, 81 | strict: child.props.strict 82 | }; 83 | 84 | let location = this.getLocation(props, context); 85 | 86 | let match = matchPath(location.pathname, pathData); 87 | 88 | if(!found && match) { 89 | found = true; 90 | 91 | //In case it's the current child we do nothing 92 | if(this.state.enteringRouteKey) { 93 | if(this.state.enteringRouteKey == child.key) 94 | return 95 | } 96 | 97 | //If it's not parallel, it would happen when a route change occurs while transitioning. 98 | //In that case we keep the original leaving element, and we just replace the entering element 99 | if(!this.state.leavingRouteKey || this.props.parallel) { 100 | this.leavingRouteChildRef = this.enteringRouteChildRef; 101 | this.enteringRouteChildRef = null; 102 | } 103 | 104 | this.setState({ 105 | ...this.state, 106 | leavingRouteKey: this.state.leavingRouteKey && !this.props.parallel ? this.state.leavingRouteKey : this.state.enteringRouteKey, 107 | enteringRouteKey: child.key, 108 | match: match 109 | }); 110 | } 111 | }); 112 | 113 | //In case we didn't find a match, the enteringChild will leave: 114 | if(!found && this.state.enteringRouteKey) { 115 | this.leavingRouteChildRef = this.enteringRouteChildRef; 116 | this.enteringRouteChildRef = null; 117 | 118 | this.setState({ 119 | ...this.state, 120 | leavingRouteKey: this.state.enteringRouteKey, 121 | enteringRouteKey: null, 122 | match: null 123 | }); 124 | } 125 | } 126 | 127 | render() { 128 | let enteringChild = null; 129 | let leavingChild = null; 130 | 131 | const props = { 132 | match: this.state.match, 133 | location: this.getLocation(this.props, this.context), 134 | history: this.context.router.history, 135 | staticContext: this.context.router.staticContext 136 | }; 137 | 138 | React.Children.map(this.props.children, child => child).forEach(child => { 139 | 140 | if(child.key == this.state.enteringRouteKey) { 141 | let component = null; 142 | 143 | if(child.props.component) 144 | component = React.createElement(child.props.component); 145 | else if(child.props.render) 146 | component = child.props.render(props); 147 | else 148 | component = child.props.children; 149 | 150 | enteringChild = React.cloneElement(component, { 151 | ref: ref => { 152 | if (ref) 153 | this.enteringRouteChildRef = ref 154 | }, 155 | key: `child-${child.key}`, 156 | ...props 157 | }); 158 | } 159 | else if(child.key == this.state.leavingRouteKey) { 160 | let component = null; 161 | 162 | if(child.props.component) 163 | component = React.createElement(child.props.component); 164 | else if(child.props.render) 165 | component = child.props.render(props); 166 | else 167 | component = child.props.children; 168 | 169 | leavingChild = React.cloneElement(component, { 170 | ref: ref => { 171 | if (ref) 172 | this.leavingRouteChildRef = ref 173 | }, 174 | key: `child-${child.key}`, 175 | ...props 176 | }); 177 | } 178 | 179 | }); 180 | 181 | // If it's not parallel, we only render the enteringRoute when the leavingRoute did leave 182 | if(!this.props.parallel) { 183 | if(this.state.leavingRouteKey) 184 | enteringChild = null; 185 | } 186 | 187 | return ( 188 |
189 | {enteringChild} 190 | {leavingChild} 191 |
192 | ); 193 | } 194 | 195 | componentDidMount() { 196 | if(this.enteringRouteChildRef && this.enteringRouteChildRef.componentWillAppear) { 197 | this.enteringRouteChildRef.componentWillAppear(() => this.enteringChildAppeared()); 198 | } 199 | else { 200 | this.enteringChildAppeared(); 201 | } 202 | } 203 | 204 | componentDidUpdate(prevProps, prevState, prevContext = this.prevContext) { 205 | let prevLocation = this.getLocation(prevProps, prevContext); 206 | let location = this.getLocation(this.props, this.context); 207 | let prevMatch = this.getMatch(prevProps, prevContext); 208 | let match = this.getMatch(this.props, this.context); 209 | 210 | //If it's not parallel, we check if the leaving route has left and call the entering transition 211 | if(!this.props.parallel && this.enteringRouteChildRef && this.enteringRouteChildRef.componentWillEnter) { 212 | if(prevState.enteringRouteKey == this.state.enteringRouteKey && this.state.leavingRouteKey == null && prevState.leavingRouteKey != null) 213 | this.enteringRouteChildRef.componentWillEnter(() => this.enteringChildEntered()); 214 | } 215 | 216 | //If the location didn't change we do nothing and let the eventual active transitions run 217 | if(prevLocation.pathname == location.pathname && prevMatch.isExact == match.isExact) 218 | return; 219 | 220 | if(this.state.enteringRouteKey && this.enteringRouteChildRef && this.enteringRouteChildRef.componentWillEnter) { 221 | if(this.props.parallel) { 222 | this.enteringRouteChildRef.componentWillEnter(() => this.enteringChildEntered()); 223 | } 224 | } 225 | else { 226 | this.enteringChildEntered(); 227 | } 228 | 229 | //If there's a ref and there wasn't a leaving route in the previous state 230 | if(this.leavingRouteChildRef && this.leavingRouteChildRef.componentWillLeave) { 231 | if(this.leavingRouteChildRef && (!prevState.leavingRouteKey || this.props.parallel)) { 232 | this.leavingRouteChildRef.componentWillLeave(() => this.leavingChildLeaved()); 233 | } 234 | } 235 | else { 236 | this.leavingChildLeaved(); 237 | } 238 | 239 | } 240 | 241 | getLocation(props, context) { 242 | return props.location || context.router.route.location; 243 | } 244 | 245 | getMatch(props, context) { 246 | return props.match || context.router.route.match; 247 | } 248 | 249 | enteringChildAppeared() { 250 | if(this.enteringRouteChildRef && this.enteringRouteChildRef.componentDidAppear) 251 | this.enteringRouteChildRef.componentDidAppear(); 252 | } 253 | 254 | enteringChildEntered() { 255 | if(this.enteringRouteChildRef && this.enteringRouteChildRef.componentDidEnter) 256 | this.enteringRouteChildRef.componentDidEnter(); 257 | } 258 | 259 | leavingChildLeaved() { 260 | if(this.leavingRouteChildRef && this.leavingRouteChildRef.componentDidLeave) 261 | this.leavingRouteChildRef.componentDidLeave(); 262 | 263 | this.leavingRouteChildRef = null; 264 | this.setState({ 265 | ...this.state, 266 | leavingRouteKey: null 267 | }); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/TransitionSwitch.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Arnaud on 08/07/2017. 3 | */ 4 | import Enzyme, {mount} from 'enzyme'; 5 | import React from 'react'; 6 | import {MemoryRouter, Route} from 'react-router'; 7 | import {TransitionSwitch} from './'; 8 | import PropTypes from 'prop-types'; 9 | import sinon from 'sinon'; 10 | import Adapter from 'enzyme-adapter-react-16'; 11 | 12 | Enzyme.configure({adapter: new Adapter()}); 13 | 14 | describe('TransitionSwitch', () => { 15 | const spies = []; 16 | 17 | beforeAll(() => { 18 | /** 19 | * We're gonna spy on hooks a lot, so let's set'em up once 20 | */ 21 | spies.push(sinon.spy(Transition.prototype, 'componentWillAppear')); 22 | spies.push(sinon.spy(Transition.prototype, 'componentDidAppear')); 23 | spies.push(sinon.spy(Transition.prototype, 'componentWillEnter')); 24 | spies.push(sinon.spy(Transition.prototype, 'componentDidEnter')); 25 | spies.push(sinon.spy(Transition.prototype, 'componentWillLeave')); 26 | spies.push(sinon.spy(Transition.prototype, 'componentDidLeave')); 27 | 28 | spies.push(sinon.spy(InstantTransition.prototype, 'componentWillAppear')); 29 | spies.push(sinon.spy(InstantTransition.prototype, 'componentDidAppear')); 30 | spies.push(sinon.spy(InstantTransition.prototype, 'componentWillEnter')); 31 | spies.push(sinon.spy(InstantTransition.prototype, 'componentDidEnter')); 32 | spies.push(sinon.spy(InstantTransition.prototype, 'componentWillLeave')); 33 | spies.push(sinon.spy(InstantTransition.prototype, 'componentDidLeave')); 34 | }); 35 | 36 | beforeEach(() => { 37 | //We need to reset the spies before each test 38 | spies.forEach(spy => spy.reset()) 39 | }); 40 | 41 | it('should only mount the first matching element', () => { 42 | const wrapper = mount(); 43 | 44 | expect(wrapper.find(Transition).length).toBe(1); 45 | }); 46 | 47 | it('should call componentDidAppear after transition', () => { 48 | jest.useFakeTimers(); 49 | const wrapper = mount(); 50 | 51 | expect(Transition.prototype.componentDidAppear.called).toBe(false); 52 | jest.runAllTimers(); 53 | wrapper.update(); 54 | expect(Transition.prototype.componentDidAppear.calledOnce).toBe(true); 55 | }); 56 | 57 | it('should call transition hooks', () => { 58 | jest.useFakeTimers(); 59 | const wrapper = mount(); 60 | const routerWrapper = wrapper.find(MemoryRouter); 61 | 62 | //WILL APPEAR 63 | jest.runAllTimers(); 64 | //DID APPEAR 65 | routerWrapper.instance().history.push('/otherPath'); 66 | //WILL LEAVE 67 | jest.runAllTimers(); 68 | //DID LEAVE 69 | //WILL ENTER 70 | jest.runAllTimers(); 71 | //DID ENTER 72 | wrapper.update(); 73 | 74 | expect(Transition.prototype.componentWillAppear.calledOnce).toBe(true); 75 | expect(Transition.prototype.componentDidAppear.calledOnce).toBe(true); 76 | expect(Transition.prototype.componentWillEnter.calledOnce).toBe(true); 77 | expect(Transition.prototype.componentDidEnter.calledOnce).toBe(true); 78 | expect(Transition.prototype.componentWillLeave.calledOnce).toBe(true); 79 | expect(Transition.prototype.componentDidLeave.calledOnce).toBe(true); 80 | }); 81 | 82 | it('should switch even if no hook is defined', () => { 83 | jest.useFakeTimers(); 84 | 85 | const wrapper = mount(); 86 | const routerWrapper = wrapper.find(MemoryRouter); 87 | 88 | expect(wrapper.find(TransitionWithoutHooks).length).toBe(1); 89 | 90 | routerWrapper.instance().history.push('/'); 91 | routerWrapper.instance().history.push('/noHook'); 92 | jest.runAllTimers(); //It runs the leaving transition of the route at path "/" 93 | wrapper.update(); 94 | 95 | expect(wrapper.find(TransitionWithoutHooks).length).toBe(1); 96 | }); 97 | 98 | it('should run the leaving transition and render null if the route is not found', () => { 99 | jest.useFakeTimers(); 100 | const wrapper = mount(); 101 | const routerWrapper = wrapper.find(MemoryRouter); 102 | 103 | routerWrapper.instance().history.push('/'); //We go to "/" 104 | routerWrapper.instance().history.push('/404'); //We go to a non existing route 105 | wrapper.update(); 106 | 107 | expect(wrapper.find(TransitionSwitch).instance().state.enteringRouteKey).toBe(null); 108 | expect(wrapper.find(TransitionSwitch).instance().state.leavingRouteKey).not.toBe(null); 109 | 110 | jest.runAllTimers(); //We run the leaving transition 111 | wrapper.update(); 112 | expect(wrapper.find(TransitionSwitch).instance().state.leavingRouteKey).toBe(null); 113 | }); 114 | 115 | it('should do nothing if there is no route change', () => { 116 | jest.useFakeTimers(); 117 | 118 | const wrapper = mount(); 119 | const routerWrapper = wrapper.find(MemoryRouter); 120 | 121 | //WILL APPEAR 122 | jest.runAllTimers(); //It runs the appearing animation of "/" 123 | //DID APPEAR 124 | routerWrapper.instance().history.push('/'); //pushes the same route 125 | wrapper.update(); 126 | 127 | expect(Transition.prototype.componentWillAppear.calledOnce).toBe(true); 128 | expect(Transition.prototype.componentDidAppear.calledOnce).toBe(true); 129 | expect(Transition.prototype.componentWillLeave.notCalled).toBe(true); 130 | expect(Transition.prototype.componentWillEnter.notCalled).toBe(true); 131 | }); 132 | 133 | it('should run parallel transitions', () => { 134 | jest.useFakeTimers(); 135 | const wrapper = mount(); 136 | const routerWrapper = wrapper.find(MemoryRouter); 137 | 138 | //WILL APPEAR 139 | jest.runAllTimers(); //It runs the appearing animation of "/" 140 | wrapper.update(); 141 | //DID APPEAR 142 | expect(wrapper.find(Transition).length).toBe(1); 143 | expect(Transition.prototype.componentWillAppear.calledOnce).toBe(true); 144 | expect(Transition.prototype.componentDidAppear.calledOnce).toBe(true); 145 | 146 | routerWrapper.instance().history.push('/otherPath'); 147 | wrapper.update(); 148 | //WILL LEAVE 149 | //WILL ENTER 150 | expect(Transition.prototype.componentWillLeave.calledOnce).toBe(true); 151 | expect(Transition.prototype.componentWillEnter.calledOnce).toBe(true); 152 | expect(wrapper.find(Transition).length).toBe(2); 153 | 154 | jest.runAllTimers(); 155 | wrapper.update(); 156 | //DID LEAVE 157 | //DID ENTER 158 | expect(Transition.prototype.componentDidLeave.calledOnce).toBe(true); 159 | expect(Transition.prototype.componentDidEnter.calledOnce).toBe(true); 160 | expect(wrapper.find(Transition).length).toBe(1); 161 | 162 | }); 163 | 164 | it('should pass route props', () => { 165 | //Should have match, location, history 166 | const wrapper = mount(); 167 | 168 | let props = wrapper.find(Transition).props() 169 | expect(props.match).not.toBe(undefined); 170 | expect(props.location).not.toBe(undefined); 171 | expect(props.history).not.toBe(undefined); 172 | }); 173 | 174 | it('should call hooks on instant transition', () => { 175 | 176 | const wrapper = mount(); 177 | const routerWrapper = wrapper.find(MemoryRouter); 178 | 179 | expect(InstantTransition.prototype.componentWillAppear.calledOnce).toBe(true); 180 | expect(InstantTransition.prototype.componentDidAppear.calledOnce).toBe(true); 181 | 182 | routerWrapper.instance().history.push('/otherPath'); 183 | wrapper.update(); 184 | 185 | expect(InstantTransition.prototype.componentWillLeave.calledOnce).toBe(true); 186 | expect(InstantTransition.prototype.componentDidLeave.calledOnce).toBe(true); 187 | expect(InstantTransition.prototype.componentWillEnter.calledOnce).toBe(true); 188 | expect(InstantTransition.prototype.componentDidEnter.calledOnce).toBe(true); 189 | 190 | }); 191 | 192 | }); 193 | 194 | class TestAppParallel extends React.Component { 195 | 196 | render() { 197 | return( 198 | 199 | 200 | 201 | root path 202 | 203 | 204 | other path 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | ); 215 | } 216 | } 217 | 218 | class TestApp extends React.Component { 219 | 220 | render() { 221 | return( 222 | 223 | 224 | 225 | root path 226 | 227 | 228 | other path 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | ); 239 | } 240 | } 241 | 242 | class TestAppWithInstantTransition extends React.Component { 243 | 244 | render() { 245 | return( 246 | 247 | 248 | 249 | root path 250 | 251 | 252 | other path 253 | 254 | 255 | 256 | ); 257 | } 258 | } 259 | 260 | class TestAppMountWithNoHook extends React.Component { 261 | 262 | render() { 263 | return( 264 | 265 | 266 | 267 | root path 268 | 269 | 270 | other path 271 | 272 | 273 | 274 | 275 | 276 | 277 | ); 278 | } 279 | } 280 | 281 | class TransitionWithoutHooks extends React.Component { 282 | render() { 283 | return ( 284 |
{this.props.children}
285 | ); 286 | } 287 | } 288 | 289 | class Transition extends React.Component { 290 | 291 | componentWillAppear(cb) { 292 | setTimeout(() => { 293 | cb(); 294 | }, 2000); 295 | } 296 | 297 | componentDidAppear() { 298 | //do stuff on appear 299 | } 300 | 301 | componentWillEnter(cb) { 302 | setTimeout(() => { 303 | cb(); 304 | }, 1000); 305 | } 306 | 307 | componentDidEnter() { 308 | //do stuff 309 | } 310 | 311 | componentWillLeave(cb) { 312 | setTimeout(() => { 313 | cb(); 314 | }, 1000); 315 | } 316 | 317 | componentDidLeave() { 318 | //do stuff 319 | } 320 | 321 | render() { 322 | return ( 323 |
{this.props.children}
324 | ); 325 | } 326 | 327 | } 328 | 329 | class InstantTransition extends React.Component { 330 | 331 | componentWillAppear(cb) { 332 | cb(); 333 | } 334 | 335 | componentDidAppear() { 336 | //do stuff on appear 337 | } 338 | 339 | componentWillEnter(cb) { 340 | cb(); 341 | } 342 | 343 | componentDidEnter() { 344 | //do stuff 345 | } 346 | 347 | componentWillLeave(cb) { 348 | cb(); 349 | } 350 | 351 | componentDidLeave() { 352 | //do stuff 353 | } 354 | 355 | render() { 356 | return ( 357 |
{this.props.children}
358 | ); 359 | } 360 | 361 | } 362 | --------------------------------------------------------------------------------