├── .gitignore ├── .jshintrc ├── .npmignore ├── LICENSE ├── README.md ├── demos ├── index.css ├── index.js └── index.jsx ├── index.html ├── package.json ├── src ├── clone-with-classes.js ├── index.js ├── is-in.js └── merge-children.js └── test └── merge-children.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "browser": true, 4 | "globalstrict": true 5 | } 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | example 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Nick Collings 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TransitionManager 2 | 3 | A robust transition component for React projects which can handle the stress. It can cope with multiple, simultaneous transitions being queued up in quick succession as well as changes to these part way through. 4 | 5 | ## Why? 6 | 7 | I was looking for a transition component that allows for smooth page changes which can be tied to the browser history. Since the user can navigate the history at any rate they wish, the transition component would need to be able to juggle multiple transitions at a time. It seemed clear that any transitioning elements needed to be cached in some stateful manner in order to keep tabs on them between renders. Unfortunately none of the existing transition components seemed to support this idea, so subsequent renders would not be aware of any transitioning elements and they would be removed prematurely. 8 | 9 | ## How it works 10 | 11 | Inspired by React's own [`ReactCSSTransitionGroup`](https://facebook.github.io/react/docs/animation.html) component, `TransitionManager` allows you to simply declare the children you want to see, leaving the nasty diffing logic to React. Additionally, to cope with transitionends not firing in certain cases, it employs a setTimeout safety net just as the clever [`TimeoutTransitionGroup`](https://github.com/Khan/react-components/blob/master/js/timeout-transition-group.jsx) from [Khan Academy](https://www.khanacademy.org/). It will then keep track of all children in its internal state, whether they are entering, leaving or persisting, whilst adding classes to them to trigger the appropriate css transitions. Children are only removed after the timeout is complete and all timeouts are allowed to run to their conclusion regardless of the number of render calls taking place inbetween. 12 | 13 | ## Transition cycle 14 | 15 | Children will receive the following classes as props at each stage of their transition cycle. Please note, you will need to apply these classes to the children manually.* 16 | 17 | * `add` for new elements 18 | * prepare them for entry transition 19 | * `add show` for entering elements 20 | * `show` is added on the next tick in order to trigger the enter css transition 21 | * `add show hide` for leaving elements 22 | * `hide` is added in order to trigger the leave css transition 23 | * element is removed from the dom after timeout duration 24 | 25 | \* this is subject to change in a future version as this would best be automated by `TransitionManager`. 26 | 27 | ## Usage 28 | 29 | ### Example Parent 30 | 31 | Using a similar api to `TimeoutTransitionGroup`, you need to pass in a duration value in ms for the leave timeouts. All children must have a unique `key` so `TransitionManager` can keep tabs on each child. E.g. 32 | 33 | `app.js` 34 | ```js 35 | import TransitionManager from 'react-transition-manager'; 36 | const App = React.createClass({ 37 | render() { 38 | let page; 39 | switch(this.props.pageId) { 40 | case 'home': 41 | page = ; 42 | break; 43 | case 'about': 44 | page = ; 45 | break; 46 | case 'contact': 47 | page = ; 48 | break; 49 | } 50 | return ( 51 | 52 | {page} 53 | 54 | ); 55 | } 56 | }); 57 | export default App; 58 | ``` 59 | 60 | * all passed props (`id`, `className` etc) will be applied to the rendered dom 61 | * `component` attribute allows for overriding the default `` element type. 62 | 63 | ### Example Child 64 | 65 | The classes are passed down to the child components in the `className` property. These need to be applied during the render method to take affect. 66 | 67 | Additionally the current `transitionState` is also passed as prop in case it's needed in the render logic. 68 | 69 | `home.js` 70 | ```js 71 | var Home = React.createClass({ 72 | render: function() { 73 | return ( 74 |
75 | Page is currently {this.props.transitionState} 76 |
77 | ); 78 | } 79 | }); 80 | export default Home; 81 | ``` 82 | 83 | ## Demos 84 | 85 | [collingo.com/react-transition-manager](http://collingo.com/react-transition-manager/) 86 | 87 | ## License 88 | [MIT](http://opensource.org/licenses/MIT) 89 | -------------------------------------------------------------------------------- /demos/index.css: -------------------------------------------------------------------------------- 1 | .fade .stage, 2 | .translate .stage { 3 | position: relative; 4 | height: 200px; 5 | width: 200px; 6 | border: 1px solid black; 7 | margin-left: 200px; 8 | } 9 | .fade .page, 10 | .translate .page { 11 | display: block; 12 | position: absolute; 13 | width: 200px; 14 | height:110px; 15 | text-align: center; 16 | padding-top: 90px; 17 | background: #5c5; 18 | } 19 | .translate .page { 20 | transition: transform 1s ease-in-out; 21 | -webkit-transform: translate3d(0,0,0); 22 | transform: translate3d(0,0,0); 23 | -webkit-backface-visibility: hidden; /* Chrome, Safari, Opera */ 24 | backface-visibility: hidden; 25 | } 26 | .translate .page-1, 27 | .translate .page-4, 28 | .translate .page-7 { 29 | background: #c55; 30 | } 31 | .translate .page-2, 32 | .translate .page-5, 33 | .translate .page-8 { 34 | background: #55c; 35 | } 36 | .translate .page.add { 37 | -webkit-transform: translate3d(200px,0,0); 38 | transform: translate3d(200px,0,0); 39 | } 40 | .translate .page.show { 41 | -webkit-transform: translate3d(0px,0,0); 42 | transform: translate3d(0px,0,0); 43 | } 44 | .translate .page.hide { 45 | -webkit-transform: translate3d(-200px,0,0); 46 | transform: translate3d(-200px,0,0); 47 | } 48 | .translate.final .stage { 49 | overflow: hidden; 50 | margin: 0; 51 | } 52 | .fade .stage { 53 | margin: 0; 54 | } 55 | .fade .page { 56 | transition: opacity 1s ease-in-out; 57 | } 58 | .fade .page.add { 59 | opacity: 0; 60 | } 61 | .fade .page.show { 62 | transition-delay: 0.5s; 63 | opacity: 1; 64 | } 65 | .fade .page.hide { 66 | transition-delay: 0s; 67 | opacity: 0; 68 | } 69 | -------------------------------------------------------------------------------- /demos/index.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import TransitionManager from '../src/index'; 6 | 7 | const noOfPages = 9; 8 | const Demo = React.createClass({ 9 | displayName: "Demo", 10 | getInitialState() { 11 | return { 12 | page: 0 13 | }; 14 | }, 15 | render() { 16 | const pageSlug = `page-${this.state.page}`; 17 | return ( 18 |
19 | 20 | 21 | 22 |
{`Page ${this.state.page}`}
23 |
24 |
25 | ); 26 | }, 27 | onClickPrev() { 28 | this.setState({ 29 | page: (noOfPages + this.state.page - 1) % noOfPages 30 | }); 31 | }, 32 | onClickNext() { 33 | this.setState({ 34 | page: (noOfPages + this.state.page + 1) % noOfPages 35 | }); 36 | } 37 | }); 38 | const DemoPage = React.createClass({ 39 | displayName: "DemoPage", 40 | render() { 41 | return ( 42 |
43 |

Transition Manager

44 |

See readme on Github for usage instructions.

45 |

See demos directory in repo for the code behind these demos.

46 |

Demos

47 |

All three demos use the same JSX and only vary in style.

48 |
49 |

No styles

50 |

Click next/previous quickly and you should see the DOM filling up with new pages. Multiple renders are taking place during this time but transitioning elements are not removed until their transition timeout is complete.

51 | 52 |
53 |
54 |

Translate 1

55 |

A simple transition in the form of a translate from right to left. Overflow is visible to help visualise the transition.

56 | 57 |
58 |
59 |

Translate 2

60 |

Overflow hidden to complete the effect.

61 | 62 |
63 |
64 |

Fade

65 |

Making use of opacity and transition-delay to create a fade effect.

66 | 67 |
68 |
69 | ); 70 | } 71 | }); 72 | 73 | ReactDOM.render( 74 | , 75 | document.body 76 | ); 77 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Transition Manager for React | Collingo 6 | 7 | 8 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-transition-manager", 3 | "version": "1.3.1", 4 | "description": "A robust transition component for React projects", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "compile": "babel -d ./lib ./src", 8 | "test": "mocha --compilers js:babel/register", 9 | "pages": "browserify -o demos/index.js demos/index.jsx", 10 | "prepublish": "npm run compile" 11 | }, 12 | "browserify": { 13 | "transform": [ 14 | "babelify" 15 | ] 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/collingo/react-transition-manager.git" 20 | }, 21 | "keywords": [ 22 | "react-component" 23 | ], 24 | "author": "Nick Collings", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/collingo/react-transition-manager/issues" 28 | }, 29 | "homepage": "https://github.com/collingo/react-transition-manager", 30 | "dependencies": { 31 | "classnames": "^2.1.2", 32 | "lodash": "^3.9.3", 33 | "react": "^15.3.2", 34 | "react-dom": "^15.3.2" 35 | }, 36 | "devDependencies": { 37 | "babel": "^5.8.38", 38 | "babelify": "^6.4.0", 39 | "browserify": "^10.2.6", 40 | "chai": "^3.0.0", 41 | "mocha": "^2.2.5", 42 | "proxyquire": "^1.5.0", 43 | "sinon": "^1.15.4", 44 | "sinon-chai": "^2.8.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/clone-with-classes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classnames from 'classnames/dedupe'; 3 | 4 | function getClasses(state) { 5 | let classes = 'add'; 6 | switch(state) { 7 | case 'show': 8 | classes += ' show'; 9 | break; 10 | case 'shown': 11 | classes += ' show shown'; 12 | break; 13 | case 'hide': 14 | classes += ' show shown hide'; 15 | break; 16 | } 17 | return classes; 18 | } 19 | 20 | export default function cloneWithClasses(element, state) { 21 | let currentClasses = element.props.className ? element.props.className.split(' ') : []; 22 | let newClasses = classnames.apply(null, currentClasses.concat(getClasses(state))); 23 | const newElement = React.cloneElement(element, { 24 | className: newClasses, 25 | transitionState: state 26 | }); 27 | return newElement; 28 | } 29 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import filter from 'lodash/collection/filter'; 5 | import remove from 'lodash/array/remove'; 6 | import findIndex from 'lodash/array/findIndex'; 7 | 8 | import cloneWithClasses from './clone-with-classes'; 9 | import isIn from './is-in'; 10 | import mergeChildren from './merge-children'; 11 | 12 | const TransitionManager = React.createClass({ 13 | displayName: 'TransitionManager', 14 | getInitialState() { 15 | const children = this.getChildren(this.props.children); 16 | return { 17 | adding: [], 18 | entering: [], 19 | removing: [], 20 | leaving: [], 21 | children: children.map(child => cloneWithClasses(child, 'shown')) 22 | }; 23 | }, 24 | getDefaultProps() { 25 | return { 26 | component: 'span' 27 | }; 28 | }, 29 | getChildren(children) { 30 | return children ? [].concat(children) : []; 31 | }, 32 | componentWillReceiveProps(newProps) { 33 | const state = this.state; 34 | const targetChildren = this.getChildren(newProps.children); 35 | const currentRemoving = state.removing; 36 | const currentLeaving = state.leaving; 37 | const currentAdding = state.adding; 38 | const currentEntering = state.entering; 39 | const currentChildren = state.children; 40 | const targetRemoving = filter(currentChildren, child => !isIn(child, targetChildren) && !isIn(child, currentLeaving)); 41 | const targetLeaving = filter(currentChildren, child => !isIn(child, targetChildren) && !isIn(child, targetRemoving)); 42 | const targetAdding = filter(targetChildren, child => (isIn(child, currentAdding) && !isIn(child, targetRemoving) && !isIn(child, targetLeaving)) || !isIn(child, currentChildren)); 43 | const targetEntering = filter(targetChildren, child => (isIn(child, currentEntering) && !isIn(child, targetRemoving) && !isIn(child, targetLeaving)) || isIn(child, currentLeaving)); 44 | const persisting = filter(currentChildren, child => !isIn(child, targetAdding) && !isIn(child, targetEntering) && !isIn(child, targetRemoving) && !isIn(child, targetLeaving)); 45 | const children = mergeChildren(currentChildren, targetChildren, persisting, targetEntering, targetLeaving); 46 | this.setState({ 47 | adding: targetAdding, 48 | entering: targetEntering, 49 | removing: targetRemoving, 50 | leaving: targetLeaving, 51 | children: children.map(child => isIn(child, targetEntering) ? cloneWithClasses(child, 'add') : child) 52 | }); 53 | }, 54 | render() { 55 | return React.createElement( 56 | this.props.component, 57 | this.props, 58 | this.state.children.map(child => React.cloneElement(child, { 59 | ref: child.key 60 | })) 61 | ); 62 | }, 63 | timers: { 64 | adding: {}, 65 | entering: {}, 66 | removing: {}, 67 | leaving: {} 68 | }, 69 | componentDidUpdate() { 70 | this.state.adding.forEach((child) => { 71 | const key = child.key; 72 | 73 | // if doesn't exist, start an add timeout 74 | if(!this.timers.adding[key]) { 75 | this.timers.adding[key] = setTimeout(this.childAdded(key), 100); 76 | } 77 | }); 78 | 79 | this.state.entering.forEach((child) => { 80 | const key = child.key; 81 | 82 | // remove any existing leave timeouts 83 | if(this.timers.leaving[key]) { 84 | clearTimeout(this.timers.leaving[key]); 85 | delete this.timers.leaving[key]; 86 | } 87 | 88 | // if doesn't exist, start an enter timeout 89 | if(!this.timers.entering[key]) { 90 | this.timers.entering[key] = setTimeout(this.childEntered(key), this.props.duration); 91 | } 92 | }); 93 | 94 | this.state.removing.forEach((child) => { 95 | const key = child.key; 96 | 97 | // if doesn't exist, start an add timeout 98 | if(!this.timers.removing[key]) { 99 | this.timers.removing[key] = setTimeout(this.childRemoved(key), 100); 100 | } 101 | }); 102 | 103 | this.state.leaving.forEach((child) => { 104 | const key = child.key; 105 | 106 | // remove any existing enter timeouts 107 | if(this.timers.entering[key]) { 108 | clearTimeout(this.timers.entering[key]); 109 | delete this.timers.entering[key]; 110 | } 111 | 112 | // if doesn't exist, start a leave timeout 113 | if(!this.timers.leaving[key]) { 114 | this.refs[key].componentWillLeave && this.refs[key].componentWillLeave(); 115 | this.timers.leaving[key] = setTimeout(this.childLeft(key), this.props.duration); 116 | } 117 | }); 118 | }, 119 | childAdded(key) { 120 | return () => { 121 | const state = this.state; 122 | if(isIn({ key: key }, state.adding)) { 123 | const component = remove(state.adding, { 124 | key: key 125 | })[0]; 126 | const newComponent = cloneWithClasses(component, 'show'); 127 | 128 | clearTimeout(this.timers.adding[key]); 129 | delete this.timers.adding[key]; 130 | state.entering.push(newComponent); 131 | state.children.splice(findIndex(state.children, 'key', key), 1, newComponent); 132 | this.setState(state); 133 | } 134 | }; 135 | }, 136 | childEntered(key) { 137 | return () => { 138 | const state = this.state; 139 | if(isIn({ key: key }, state.entering)) { 140 | const component = remove(state.entering, { 141 | key: key 142 | })[0]; 143 | const newComponent = cloneWithClasses(component, 'shown'); 144 | 145 | clearTimeout(this.timers.entering[key]); 146 | delete this.timers.entering[key]; 147 | state.children.splice(findIndex(state.children, 'key', key), 1, newComponent); 148 | this.setState(state); 149 | } 150 | }; 151 | }, 152 | childRemoved(key) { 153 | return () => { 154 | const state = this.state; 155 | if(isIn({ key: key }, state.removing)) { 156 | const component = remove(state.removing, { 157 | key: key 158 | })[0]; 159 | const newComponent = cloneWithClasses(component, 'hide'); 160 | 161 | clearTimeout(this.timers.removing[key]); 162 | delete this.timers.removing[key]; 163 | state.leaving.push(newComponent); 164 | state.children.splice(findIndex(state.children, 'key', key), 1, newComponent); 165 | this.setState(state); 166 | } 167 | }; 168 | }, 169 | childLeft(key) { 170 | return () => { 171 | const state = this.state; 172 | if(isIn({ key: key }, state.leaving)) { 173 | remove(state.leaving, { 174 | key: key 175 | }); 176 | remove(state.children, { 177 | key: key 178 | }); 179 | 180 | clearTimeout(this.timers.leaving[key]); 181 | delete this.timers.leaving[key]; 182 | this.setState(state); 183 | } 184 | }; 185 | } 186 | }); 187 | 188 | export default TransitionManager; 189 | -------------------------------------------------------------------------------- /src/is-in.js: -------------------------------------------------------------------------------- 1 | import find from 'lodash/collection/find'; 2 | 3 | function isIn(item, array) { 4 | return !!find(array, 'key', item.key); 5 | } 6 | 7 | export default isIn; 8 | -------------------------------------------------------------------------------- /src/merge-children.js: -------------------------------------------------------------------------------- 1 | import cloneWithClasses from './clone-with-classes'; 2 | import isIn from './is-in'; 3 | 4 | function mergeChildren(currentChildren, targetChildren, persisting, targetEntering, targetLeaving) { 5 | let targetIndex = 0; 6 | let currentIndex = 0; 7 | let targetChild = targetChildren[targetIndex]; 8 | let currentChild = currentChildren[currentIndex]; 9 | let children = []; 10 | while(targetChild || currentChild) { 11 | while(targetChild && !isIn(targetChild, persisting)) { 12 | let state = isIn(targetChild, targetEntering) ? 'show' : 'add'; 13 | children.push(cloneWithClasses(targetChild, state)); 14 | targetChild = targetChildren[++targetIndex]; 15 | } 16 | while(currentChild && !isIn(currentChild, persisting)) { 17 | if(!isIn(currentChild, children)) { 18 | let state = isIn(currentChild, targetLeaving) ? 'hide' : 'remove'; 19 | children.push(cloneWithClasses(currentChild, state)); 20 | } 21 | currentChild = currentChildren[++currentIndex]; 22 | } 23 | if(targetChild) { 24 | children.push(cloneWithClasses(targetChild, 'shown')); 25 | targetChild = targetChildren[++targetIndex]; 26 | currentChild = currentChildren[++currentIndex]; 27 | } 28 | } 29 | return children; 30 | } 31 | 32 | export default mergeChildren; 33 | -------------------------------------------------------------------------------- /test/merge-children.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import chai, {expect} from 'chai'; 4 | import sinon from 'sinon'; 5 | import sinonChai from 'sinon-chai'; 6 | import proxyquire from 'proxyquire'; 7 | 8 | chai.use(sinonChai); 9 | 10 | function cloneWithClasses(item, state) { 11 | item.classes = state; 12 | return item; 13 | } 14 | 15 | describe('mergeChildren', () => { 16 | 17 | let sandbox; 18 | let mergeChildren; 19 | let cloneWithClassesSpy; 20 | let child1; 21 | let child2; 22 | let child3; 23 | let child4; 24 | let child5; 25 | let result; 26 | 27 | beforeEach(() => { 28 | sandbox = sinon.sandbox.create(); 29 | cloneWithClassesSpy = sandbox.spy(cloneWithClasses); 30 | mergeChildren = proxyquire('../src/merge-children', { 31 | './clone-with-classes': cloneWithClassesSpy 32 | }); 33 | child1 = { key: 1 }; 34 | child2 = { key: 2 }; 35 | child3 = { key: 3 }; 36 | child4 = { key: 4 }; 37 | child5 = { key: 5 }; 38 | }); 39 | 40 | afterEach(() => { 41 | sandbox.restore(); 42 | }); 43 | 44 | describe('when no children', () => { 45 | beforeEach(() => { 46 | result = mergeChildren([], [], []); 47 | }); 48 | it('should return an empty array', () => { 49 | expect(Array.isArray(result)).to.be.true; 50 | expect(result.length).to.equal(0); 51 | }); 52 | }); 53 | 54 | describe('when no changes', () => { 55 | beforeEach(() => { 56 | result = mergeChildren([child1], [child1], [child1]); 57 | }); 58 | it('should return same children', () => { 59 | expect(result.length).to.equal(1); 60 | expect(result[0].key).to.equal(child1.key); 61 | }); 62 | it('should add "shown" classes to child', () => { 63 | expect(result[0].classes).to.equal("shown"); 64 | }); 65 | }); 66 | 67 | describe('when adding', () => { 68 | beforeEach(() => { 69 | result = mergeChildren([child1], [child1, child2], [child1]); 70 | }); 71 | it('should return two children', () => { 72 | expect(result.length).to.equal(2); 73 | }); 74 | it('should be in the correct order', () => { 75 | expect(result[0].key).to.equal(child1.key); 76 | expect(result[1].key).to.equal(child2.key); 77 | }); 78 | it('should add "add" classes to new child', () => { 79 | expect(result[1].classes).to.equal("add"); 80 | }); 81 | }); 82 | 83 | describe('when removing', () => { 84 | beforeEach(() => { 85 | result = mergeChildren([child1], [], []); 86 | }); 87 | it('should return same children', () => { 88 | expect(result.length).to.equal(1); 89 | expect(result[0].key).to.equal(child1.key); 90 | }); 91 | it('should add "hide" classes to old child', () => { 92 | expect(result[0].classes).to.equal("hide"); 93 | }); 94 | }); 95 | 96 | describe('when switching', () => { 97 | beforeEach(() => { 98 | result = mergeChildren([child1, child2], [child2, child1], [child1, child2]); 99 | }); 100 | it('should return same children', () => { 101 | expect(result.length).to.equal(2); 102 | }); 103 | it('should order children correctly', () => { 104 | expect(result[0].key).to.equal(child2.key); 105 | expect(result[1].key).to.equal(child1.key); 106 | }); 107 | it('should maintain "shown" classes to both children', () => { 108 | expect(result[0].classes).to.equal("shown"); 109 | expect(result[1].classes).to.equal("shown"); 110 | }); 111 | }); 112 | 113 | describe('when replacing', () => { 114 | beforeEach(() => { 115 | result = mergeChildren([child1], [child2], []); 116 | }); 117 | it('should return all the children', () => { 118 | expect(result.length).to.equal(2); 119 | }); 120 | it('should order children correctly', () => { 121 | expect(result[0].key).to.equal(child2.key); 122 | expect(result[1].key).to.equal(child1.key); 123 | }); 124 | it('should add "add" classes to new child', () => { 125 | expect(result[0].classes).to.equal("add"); 126 | }); 127 | it('should add "hide" classes to old child', () => { 128 | expect(result[1].classes).to.equal("hide"); 129 | }); 130 | }); 131 | 132 | describe('when changes are split by a persisting child', () => { 133 | beforeEach(() => { 134 | result = mergeChildren([child1, child2, child3], [child4, child2, child5], [child2]); 135 | }); 136 | it('should return all the children', () => { 137 | expect(result.length).to.equal(5); 138 | }); 139 | it('should order children correctly', () => { 140 | expect(result[0].key).to.equal(child4.key); 141 | expect(result[1].key).to.equal(child1.key); 142 | expect(result[2].key).to.equal(child2.key); 143 | expect(result[3].key).to.equal(child5.key); 144 | expect(result[4].key).to.equal(child3.key); 145 | }); 146 | }); 147 | 148 | }); 149 | --------------------------------------------------------------------------------