├── .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 | [](https://travis-ci.org/aboeglin/react-router-v4-transition) [](https://coveralls.io/github/aboeglin/react-router-v4-transition?branch=master) [](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 |
--------------------------------------------------------------------------------