From 9ac27cb551b85293d03d334363acd7cf9c77813a Mon Sep 17 00:00:00 2001 From: Pete Hunt Date: Wed, 12 Feb 2014 12:21:46 -0800 Subject: [PATCH] Rewrite ReactTransitionGroup The key idea here is that you're always rendering `this.state.children`, not `this.props.children`. When combined with `cloneWithProps()` this means we can keep them in the DOM as long as we want. We add new children and reactively update existing ones using `setState()` inside of `componentWillReceiveProps()` so `this.state.children` always has the latest versions of components. Since we may be keeping old components around that are no longer in `this.props.children` we need a way to figure out where they should be inside of the combined `this.state.children` list. `ReactTransitionChildMapping` does this for us. Based on that infrastructure we can build the interface we always wanted: enter and leave lifecycle hooks. When a component is added to the DOM, `componentWillEnter(callback)` gets called. Call the callback when you're done animating and `componentDidEnter()` will be called. When a component is about to be removed from the DOM, `componentWillLeave(callback)` gets called. Call the callback when you're done animating and `componentDidLeave()` will be called and the component will *actually* be removed from the DOM. It won't be removed until you call the callback. These also handle "concurrent" changes. If you "stack" enter/leaves of a single component before the animation has completed, it will block out all of those animations until the current animation completes, and then finally it will animate 0 or 1 times to get itself into the desired current state. This is what differentiates `componentWillEnter()` from `componentDidMount()`. The next step would be to build `componentDidReorder()`. I've built `ReactCSSTransitionGroup` which is identical to the old `ReactTransitionGroup` and codemodded the callsites. --- .../transitions/ReactCSSTransitionGroup.js | 65 +++++ ...ild.js => ReactCSSTransitionGroupChild.js} | 68 ++--- ...ySet.js => ReactTransitionChildMapping.js} | 52 ++-- .../transitions/ReactTransitionGroup.js | 209 ++++++++++----- .../__tests__/ReactCSSTransitionGroup-test.js | 137 ++++++++++ ...js => ReactTransitionChildMapping-test.js} | 47 ++-- .../__tests__/ReactTransitionGroup-test.js | 249 ++++++++++++------ .../ReactTransitionableChild-test.js | 104 -------- 8 files changed, 565 insertions(+), 366 deletions(-) create mode 100644 src/addons/transitions/ReactCSSTransitionGroup.js rename src/addons/transitions/{ReactTransitionableChild.js => ReactCSSTransitionGroupChild.js} (60%) rename src/addons/transitions/{ReactTransitionKeySet.js => ReactTransitionChildMapping.js} (71%) create mode 100644 src/addons/transitions/__tests__/ReactCSSTransitionGroup-test.js rename src/addons/transitions/__tests__/{ReactTransitionKeySet-test.js => ReactTransitionChildMapping-test.js} (62%) delete mode 100644 src/addons/transitions/__tests__/ReactTransitionableChild-test.js diff --git a/src/addons/transitions/ReactCSSTransitionGroup.js b/src/addons/transitions/ReactCSSTransitionGroup.js new file mode 100644 index 0000000000..a388088c99 --- /dev/null +++ b/src/addons/transitions/ReactCSSTransitionGroup.js @@ -0,0 +1,65 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @typechecks + * @providesModule ReactCSSTransitionGroup + * @jsx React.DOM + */ + +"use strict"; + +var React = require('React'); + +var ReactTransitionGroup = require('ReactTransitionGroup'); +var ReactCSSTransitionGroupChild = require('ReactCSSTransitionGroupChild'); + +var ReactCSSTransitionGroup = React.createClass({ + propTypes: { + transitionName: React.PropTypes.string.isRequired, + transitionEnter: React.PropTypes.bool, + transitionLeave: React.PropTypes.bool + }, + + getDefaultProps: function() { + return { + transitionEnter: true, + transitionLeave: true + }; + }, + + _wrapChild: function(child) { + // We need to provide this childFactory so that + // ReactCSSTransitionGroupChild can receive updates to name, enter, and + // leave while it is leaving. + return ( + + {child} + + ); + }, + + render: function() { + return this.transferPropsTo( + + {this.props.children} + + ); + } +}); + +module.exports = ReactCSSTransitionGroup; diff --git a/src/addons/transitions/ReactTransitionableChild.js b/src/addons/transitions/ReactCSSTransitionGroupChild.js similarity index 60% rename from src/addons/transitions/ReactTransitionableChild.js rename to src/addons/transitions/ReactCSSTransitionGroupChild.js index 23267726a6..a890152384 100644 --- a/src/addons/transitions/ReactTransitionableChild.js +++ b/src/addons/transitions/ReactCSSTransitionGroupChild.js @@ -13,15 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * @providesModule ReactTransitionableChild + * @typechecks + * @providesModule ReactCSSTransitionGroupChild */ "use strict"; var React = require('React'); + var CSSCore = require('CSSCore'); var ReactTransitionEvents = require('ReactTransitionEvents'); +var onlyChild = require('onlyChild'); + // We don't remove the element from the DOM until we receive an animationend or // transitionend event. If the user screws up and forgets to add an animation // their node will be stuck in the DOM forever, so we detect if an animation @@ -31,6 +35,7 @@ var NO_EVENT_TIMEOUT = 5000; var noEventListener = null; + if (__DEV__) { noEventListener = function() { console.warn( @@ -42,20 +47,8 @@ if (__DEV__) { }; } -/** - * This component is simply responsible for watching when its single child - * changes to undefined and animating the old child out. It does this by - * recording its old child in savedChildren when it detects this event is about - * to occur. - */ -var ReactTransitionableChild = React.createClass({ - /** - * Perform an actual DOM transition. This takes care of a few things: - * - Adding the second CSS class to trigger the transition - * - Listening for the finish event - * - Cleaning up the css (unless noReset is true) - */ - transition: function(animationType, noReset, finishCallback) { +var ReactCSSTransitionGroupChild = React.createClass({ + transition: function(animationType, finishCallback) { var node = this.getDOMNode(); var className = this.props.name + '-' + animationType; var activeClassName = className + '-active'; @@ -66,13 +59,8 @@ var ReactTransitionableChild = React.createClass({ clearTimeout(noEventTimeout); } - // If this gets invoked after the component is unmounted it's OK. - if (!noReset) { - // Usually this means you're about to remove the node if you want to - // leave it in its animated state. - CSSCore.removeClass(node, className); - CSSCore.removeClass(node, activeClassName); - } + CSSCore.removeClass(node, className); + CSSCore.removeClass(node, activeClassName); ReactTransitionEvents.removeEndEventListener(node, endListener); @@ -126,39 +114,25 @@ var ReactTransitionableChild = React.createClass({ } }, - componentWillReceiveProps: function(nextProps) { - if (!nextProps.children && this.props.children) { - this.savedChildren = this.props.children; - } else if (nextProps.children && !this.props.children) { - // We're being told to re-add the child. Let's stop leaving! - if (this.isMounted()) { - var node = this.getDOMNode(); - var className = this.props.name; - CSSCore.removeClass(node, className + '-leave'); - CSSCore.removeClass(node, className + '-leave-active'); - if (this.props.enter) { - CSSCore.addClass(node, className + '-enter'); - CSSCore.addClass(node, className + '-enter-active'); - } - } - } - }, - - componentDidMount: function() { + componentWillEnter: function(done) { if (this.props.enter) { - this.transition('enter'); + this.transition('enter', done); + } else { + done(); } }, - componentDidUpdate: function(prevProps, prevState, prevContext) { - if (prevProps.children && !this.props.children) { - this.transition('leave', true, this.props.onDoneLeaving); + componentWillLeave: function(done) { + if (this.props.leave) { + this.transition('leave', done); + } else { + done(); } }, render: function() { - return this.props.children || this.savedChildren; + return onlyChild(this.props.children); } }); -module.exports = ReactTransitionableChild; +module.exports = ReactCSSTransitionGroupChild; diff --git a/src/addons/transitions/ReactTransitionKeySet.js b/src/addons/transitions/ReactTransitionChildMapping.js similarity index 71% rename from src/addons/transitions/ReactTransitionKeySet.js rename to src/addons/transitions/ReactTransitionChildMapping.js index 03fb177700..6b7515337c 100644 --- a/src/addons/transitions/ReactTransitionKeySet.js +++ b/src/addons/transitions/ReactTransitionChildMapping.js @@ -14,14 +14,14 @@ * limitations under the License. * * @typechecks static-only - * @providesModule ReactTransitionKeySet + * @providesModule ReactTransitionChildMapping */ "use strict"; var ReactChildren = require('ReactChildren'); -var ReactTransitionKeySet = { +var ReactTransitionChildMapping = { /** * Given `this.props.children`, return an object mapping key to child. Just * simple syntactic sugar around ReactChildren.map(). @@ -35,22 +35,9 @@ var ReactTransitionKeySet = { }); }, - /** - * Simple syntactic sugar to get an object with keys of all of `children`. - * Does not have references to the children themselves. - * - * @param {*} children `this.props.children` - * @return {object} Mapping of key to the value "true" - */ - getKeySet: function(children) { - return ReactChildren.map(children, function() { - return true; - }); - }, - /** * When you're adding or removing children some may be added or removed in the - * same render pass. We want to show *both* since we want to simultaneously + * same render pass. We want ot show *both* since we want to simultaneously * animate elements in and out. This function takes a previous set of keys * and a new set of keys and merges them with its best guess of the correct * ordering. In the future we may expose some of the utilities in @@ -58,17 +45,25 @@ var ReactTransitionKeySet = { * directly have this concept of the union of prevChildren and nextChildren * so we implement it here. * - * @param {object} prev prev child keys as returned from - * `ReactTransitionKeySet.getKeySet()`. - * @param {object} next next child keys as returned from - * `ReactTransitionKeySet.getKeySet()`. + * @param {object} prev prev children as returned from + * `ReactTransitionChildMapping.getChildMapping()`. + * @param {object} next next children as returned from + * `ReactTransitionChildMapping.getChildMapping()`. * @return {object} a key set that contains all keys in `prev` and all keys * in `next` in a reasonable order. */ - mergeKeySets: function(prev, next) { + mergeChildMappings: function(prev, next) { prev = prev || {}; next = next || {}; + function getValueForKey(key) { + if (next.hasOwnProperty(key)) { + return next[key]; + } else { + return prev[key]; + } + } + // For each key of `next`, the list of keys to insert before that key in // the combined list var nextKeysPending = {}; @@ -86,23 +81,26 @@ var ReactTransitionKeySet = { } var i; - var keySet = {}; + var childMapping = {}; for (var nextKey in next) { if (nextKeysPending[nextKey]) { for (i = 0; i < nextKeysPending[nextKey].length; i++) { - keySet[nextKeysPending[nextKey][i]] = true; + var pendingNextKey = nextKeysPending[nextKey][i]; + childMapping[nextKeysPending[nextKey][i]] = getValueForKey( + pendingNextKey + ); } } - keySet[nextKey] = true; + childMapping[nextKey] = getValueForKey(nextKey); } // Finally, add the keys which didn't appear before any key in `next` for (i = 0; i < pendingKeys.length; i++) { - keySet[pendingKeys[i]] = true; + childMapping[pendingKeys[i]] = getValueForKey(pendingKeys[i]); } - return keySet; + return childMapping; } }; -module.exports = ReactTransitionKeySet; +module.exports = ReactTransitionChildMapping; diff --git a/src/addons/transitions/ReactTransitionGroup.js b/src/addons/transitions/ReactTransitionGroup.js index 98ce8619e9..e022504841 100644 --- a/src/addons/transitions/ReactTransitionGroup.js +++ b/src/addons/transitions/ReactTransitionGroup.js @@ -19,103 +19,168 @@ "use strict"; var React = require('React'); -var ReactTransitionableChild = require('ReactTransitionableChild'); -var ReactTransitionKeySet = require('ReactTransitionKeySet'); +var ReactTransitionChildMapping = require('ReactTransitionChildMapping'); + +var cloneWithProps = require('cloneWithProps'); +var emptyFunction = require('emptyFunction'); +var merge = require('merge'); var ReactTransitionGroup = React.createClass({ propTypes: { - transitionName: React.PropTypes.string.isRequired, - transitionEnter: React.PropTypes.bool, - transitionLeave: React.PropTypes.bool, - onTransition: React.PropTypes.func, - component: React.PropTypes.func + component: React.PropTypes.func, + childFactory: React.PropTypes.func }, getDefaultProps: function() { return { - transitionEnter: true, - transitionLeave: true, - component: React.DOM.span + component: React.DOM.span, + childFactory: emptyFunction.thatReturnsArgument }; }, - componentWillMount: function() { - // _transitionGroupCurrentKeys stores the union of previous *and* next keys. - // If this were a component we'd store it as state, however, since this must - // be a mixin, we need to keep the result of the union of keys in each - // call to animateChildren() which happens in render(), so we can't - // call setState() in there. - this._transitionGroupCurrentKeys = {}; + getInitialState: function() { + return { + children: ReactTransitionChildMapping.getChildMapping(this.props.children) + }; }, - componentDidUpdate: function() { - if (this.props.onTransition) { - this.props.onTransition(); - } - }, - - /** - * Render some children in a transitionable way. - */ - renderTransitionableChildren: function(sourceChildren) { - var children = {}; - var childMapping = ReactTransitionKeySet.getChildMapping(sourceChildren); - - var prevKeys = this._transitionGroupCurrentKeys; - var currentKeys = ReactTransitionKeySet.mergeKeySets( - prevKeys, - ReactTransitionKeySet.getKeySet(sourceChildren) + componentWillReceiveProps: function(nextProps) { + var nextChildMapping = ReactTransitionChildMapping.getChildMapping( + nextProps.children ); + var prevChildMapping = this.state.children; - for (var key in currentKeys) { - // Here is how we keep the nodes in the DOM. ReactTransitionableChild - // knows how to hold onto its child if it changes to undefined. Here, we - // may look up an old key in the new children, and it may switch to - // undefined. React's reconciler will keep the ReactTransitionableChild - // instance alive such that we can animate it. - if (childMapping[key] || (this.props.transitionLeave && prevKeys[key])) { - children[key] = ReactTransitionableChild({ - name: this.props.transitionName, - enter: this.props.transitionEnter, - onDoneLeaving: this._handleDoneLeaving.bind(this, key) - }, childMapping[key]); - } else { - // If there's no leave transition and the child has been removed from - // the source children list, we want to remove it immediately from the - // _transitionGroupCurrentKeys cache because _handleDoneLeaving won't - // be called. In normal cases, this prevents a small memory leak; in - // the case of switching transitionLeave from false to true, it - // prevents a confusing bug where ReactTransitionableChild.render() - // returns nothing, throwing an error. - delete currentKeys[key]; + this.setState({ + children: ReactTransitionChildMapping.mergeChildMappings( + prevChildMapping, + nextChildMapping + ) + }); + + var key; + + for (key in nextChildMapping) { + if (!prevChildMapping.hasOwnProperty(key) && + !this.currentlyTransitioningKeys[key]) { + this.keysToEnter.push(key); } } - this._transitionGroupCurrentKeys = currentKeys; + for (key in prevChildMapping) { + if (!nextChildMapping.hasOwnProperty(key) && + !this.currentlyTransitioningKeys[key]) { + this.keysToLeave.push(key); + } + } - return children; + // If we want to someday check for reordering, we could do it here. + }, + + componentWillMount: function() { + this.currentlyTransitioningKeys = {}; + this.keysToEnter = []; + this.keysToLeave = []; + }, + + componentDidUpdate: function() { + var keysToEnter = this.keysToEnter; + this.keysToEnter = []; + keysToEnter.forEach(this.performEnter); + + var keysToLeave = this.keysToLeave; + this.keysToLeave = []; + keysToLeave.forEach(this.performLeave); + }, + + performEnter: function(key) { + this.currentlyTransitioningKeys[key] = true; + + var component = this.refs[key]; + + if (component.componentWillEnter) { + component.componentWillEnter( + this._handleDoneEntering.bind(this, key) + ); + } else { + this._handleDoneEntering(key); + } + }, + + _handleDoneEntering: function(key) { + var component = this.refs[key]; + if (component.componentDidEnter) { + component.componentDidEnter(); + } + + delete this.currentlyTransitioningKeys[key]; + + var currentChildMapping = ReactTransitionChildMapping.getChildMapping( + this.props.children + ); + + if (!currentChildMapping.hasOwnProperty(key)) { + // This was removed before it had fully entered. Remove it. + this.performLeave(key); + } + }, + + performLeave: function(key) { + this.currentlyTransitioningKeys[key] = true; + + var component = this.refs[key]; + if (component.componentWillLeave) { + component.componentWillLeave(this._handleDoneLeaving.bind(this, key)); + } else { + // Note that this is somewhat dangerous b/c it calls setState() + // again, effectively mutating the component before all the work + // is done. + this._handleDoneLeaving(key); + } }, _handleDoneLeaving: function(key) { - // When the leave animation finishes, we should blow away the actual DOM - // node. - delete this._transitionGroupCurrentKeys[key]; - this.forceUpdate(); + var component = this.refs[key]; + + if (component.componentDidLeave) { + component.componentDidLeave(); + } + + delete this.currentlyTransitioningKeys[key]; + + var currentChildMapping = ReactTransitionChildMapping.getChildMapping( + this.props.children + ); + + if (currentChildMapping.hasOwnProperty(key)) { + // This entered again before it fully left. Add it again. + this.performEnter(key); + } else { + var newChildren = merge(this.state.children); + delete newChildren[key]; + this.setState({children: newChildren}); + } }, render: function() { - return this.transferPropsTo( - this.props.component( - { - transitionName: null, - transitionEnter: null, - transitionLeave: null, - component: null - }, - this.renderTransitionableChildren(this.props.children) - ) - ); + // TODO: we could get rid of the need for the wrapper node + // by cloning a single child + var childrenToRender = {}; + for (var key in this.state.children) { + var child = this.state.children[key]; + if (child) { + // You may need to apply reactive updates to a child as it is leaving. + // The normal React way to do it won't work since the child will have + // already been removed. In case you need this behavior you can provide + // a childFactory function to wrap every child, even the ones that are + // leaving. + childrenToRender[key] = cloneWithProps( + this.props.childFactory(child), + {ref: key} + ); + } + } + return this.transferPropsTo(this.props.component(null, childrenToRender)); } }); diff --git a/src/addons/transitions/__tests__/ReactCSSTransitionGroup-test.js b/src/addons/transitions/__tests__/ReactCSSTransitionGroup-test.js new file mode 100644 index 0000000000..07e5c01f52 --- /dev/null +++ b/src/addons/transitions/__tests__/ReactCSSTransitionGroup-test.js @@ -0,0 +1,137 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @jsx React.DOM + * @emails react-core + */ + +"use strict"; + +var React; +var ReactCSSTransitionGroup; +var mocks; + +// Most of the real functionality is covered in other unit tests, this just +// makes sure we're wired up correctly. +describe('ReactCSSTransitionGroup', function() { + var container; + + beforeEach(function() { + React = require('React'); + ReactCSSTransitionGroup = require('ReactCSSTransitionGroup'); + mocks = require('mocks'); + + container = document.createElement('div'); + }); + + it('should warn after time with no transitionend', function() { + var a = React.renderComponent( + + + , + container + ); + expect(a.getDOMNode().childNodes.length).toBe(1); + + setTimeout.mock.calls.length = 0; + + React.renderComponent( + + + , + container + ); + expect(a.getDOMNode().childNodes.length).toBe(2); + expect(a.getDOMNode().childNodes[0].id).toBe('two'); + expect(a.getDOMNode().childNodes[1].id).toBe('one'); + + console.warn = mocks.getMockFunction(); + setTimeout.mock.calls[2][0](); + + expect(a.getDOMNode().childNodes.length).toBe(2); + expect(console.warn.mock.calls.length).toBe(1); + }); + + it('should keep both sets of DOM nodes around', function() { + var a = React.renderComponent( + + + , + container + ); + expect(a.getDOMNode().childNodes.length).toBe(1); + React.renderComponent( + + + , + container + ); + expect(a.getDOMNode().childNodes.length).toBe(2); + expect(a.getDOMNode().childNodes[0].id).toBe('two'); + expect(a.getDOMNode().childNodes[1].id).toBe('one'); + }); + + it('should switch transitionLeave from false to true', function() { + var a = React.renderComponent( + + + , + container + ); + expect(a.getDOMNode().childNodes.length).toBe(1); + React.renderComponent( + + + , + container + ); + expect(a.getDOMNode().childNodes.length).toBe(1); + React.renderComponent( + + + , + container + ); + expect(a.getDOMNode().childNodes.length).toBe(2); + expect(a.getDOMNode().childNodes[0].id).toBe('three'); + expect(a.getDOMNode().childNodes[1].id).toBe('two'); + }); + return; + it('should work with no children', function () { + React.renderComponent( + + , + container + ); + }); + + it('should work with a null child', function () { + React.renderComponent( + + {[null]} + , + container + ); + }); +}); diff --git a/src/addons/transitions/__tests__/ReactTransitionKeySet-test.js b/src/addons/transitions/__tests__/ReactTransitionChildMapping-test.js similarity index 62% rename from src/addons/transitions/__tests__/ReactTransitionKeySet-test.js rename to src/addons/transitions/__tests__/ReactTransitionChildMapping-test.js index 8e3954a5f3..9325c4daca 100644 --- a/src/addons/transitions/__tests__/ReactTransitionKeySet-test.js +++ b/src/addons/transitions/__tests__/ReactTransitionChildMapping-test.js @@ -20,12 +20,12 @@ "use strict"; var React; -var ReactTransitionKeySet; +var ReactTransitionChildMapping; -describe('ReactTransitionKeySet', function() { +describe('ReactTransitionChildMapping', function() { beforeEach(function() { React = require('React'); - ReactTransitionKeySet = require('ReactTransitionKeySet'); + ReactTransitionChildMapping = require('ReactTransitionChildMapping'); }); it('should support getChildMapping', function() { @@ -34,26 +34,15 @@ describe('ReactTransitionKeySet', function() { var one =
{oneone}{onetwo}
; var two =
; var component =
{one}{two}
; - expect(ReactTransitionKeySet.getChildMapping(component.props.children)) - .toEqual({ - '.$one': one, - '.$two': two - }); - }); - - it('should support getKeySet', function() { - var oneone =
; - var onetwo =
; - var one =
{oneone}{onetwo}
; - var two =
; - var component =
{one}{two}
; - expect(ReactTransitionKeySet.getKeySet(component.props.children)).toEqual({ - '.$one': true, - '.$two': true + expect( + ReactTransitionChildMapping.getChildMapping(component.props.children) + ).toEqual({ + '.$one': one, + '.$two': two }); }); - it('should support mergeKeySets for adding keys', function() { + it('should support mergeChildMappings for adding keys', function() { var prev = { one: true, two: true @@ -63,14 +52,14 @@ describe('ReactTransitionKeySet', function() { two: true, three: true }; - expect(ReactTransitionKeySet.mergeKeySets(prev, next)).toEqual({ + expect(ReactTransitionChildMapping.mergeChildMappings(prev, next)).toEqual({ one: true, two: true, three: true }); }); - it('should support mergeKeySets for removing keys', function() { + it('should support mergeChildMappings for removing keys', function() { var prev = { one: true, two: true, @@ -80,14 +69,14 @@ describe('ReactTransitionKeySet', function() { one: true, two: true }; - expect(ReactTransitionKeySet.mergeKeySets(prev, next)).toEqual({ + expect(ReactTransitionChildMapping.mergeChildMappings(prev, next)).toEqual({ one: true, two: true, three: true }); }); - it('should support mergeKeySets for adding and removing', function() { + it('should support mergeChildMappings for adding and removing', function() { var prev = { one: true, two: true, @@ -98,7 +87,7 @@ describe('ReactTransitionKeySet', function() { two: true, four: true }; - expect(ReactTransitionKeySet.mergeKeySets(prev, next)).toEqual({ + expect(ReactTransitionChildMapping.mergeChildMappings(prev, next)).toEqual({ one: true, two: true, three: true, @@ -119,7 +108,7 @@ describe('ReactTransitionKeySet', function() { three: true, five: true }; - expect(ReactTransitionKeySet.mergeKeySets(prev, next)).toEqual({ + expect(ReactTransitionChildMapping.mergeChildMappings(prev, next)).toEqual({ one: true, two: true, three: true, @@ -128,7 +117,7 @@ describe('ReactTransitionKeySet', function() { }); }); - it('should support mergeKeySets with undefined input', function () { + it('should support mergeChildMappings with undefined input', function () { var prev = { one: true, two: true @@ -136,7 +125,7 @@ describe('ReactTransitionKeySet', function() { var next = undefined; - expect(ReactTransitionKeySet.mergeKeySets(prev, next)).toEqual({ + expect(ReactTransitionChildMapping.mergeChildMappings(prev, next)).toEqual({ one: true, two: true }); @@ -148,7 +137,7 @@ describe('ReactTransitionKeySet', function() { four: true }; - expect(ReactTransitionKeySet.mergeKeySets(prev, next)).toEqual({ + expect(ReactTransitionChildMapping.mergeChildMappings(prev, next)).toEqual({ three: true, four: true }); diff --git a/src/addons/transitions/__tests__/ReactTransitionGroup-test.js b/src/addons/transitions/__tests__/ReactTransitionGroup-test.js index ab0fb369ab..d7ee5343fb 100644 --- a/src/addons/transitions/__tests__/ReactTransitionGroup-test.js +++ b/src/addons/transitions/__tests__/ReactTransitionGroup-test.js @@ -36,102 +36,177 @@ describe('ReactTransitionGroup', function() { container = document.createElement('div'); }); - it('should warn after time with no transitionend', function() { - var a = React.renderComponent( - - - , - container - ); - expect(a.getDOMNode().childNodes.length).toBe(1); - setTimeout.mock.calls.length = 0; + it('should handle willEnter correctly', function() { + var log = []; - React.renderComponent( - - - , - container - ); - expect(a.getDOMNode().childNodes.length).toBe(2); - expect(a.getDOMNode().childNodes[0].id).toBe('two'); - expect(a.getDOMNode().childNodes[1].id).toBe('one'); + var Child = React.createClass({ + componentDidMount: function() { + log.push('didMount'); + }, + componentWillEnter: function(cb) { + log.push('willEnter'); + cb(); + }, + componentDidEnter: function() { + log.push('didEnter'); + }, + componentWillLeave: function(cb) { + log.push('willLeave'); + cb(); + }, + componentDidLeave: function() { + log.push('didLeave'); + }, + componentWillUnmount: function() { + log.push('willUnmount'); + }, + render: function() { + return ; + } + }); - console.warn = mocks.getMockFunction(); - setTimeout.mock.calls[2][0](); + var Component = React.createClass({ + getInitialState: function() { + return {count: 1}; + }, + render: function() { + var children = []; + for (var i = 0; i < this.state.count; i++) { + children.push(); + } + return {children}; + } + }); - expect(a.getDOMNode().childNodes.length).toBe(2); - expect(console.warn.mock.calls.length).toBe(1); + var instance = React.renderComponent(, container); + expect(log).toEqual(['didMount']); + + instance.setState({count: 2}, function() { + expect(log).toEqual(['didMount', 'didMount', 'willEnter', 'didEnter']); + instance.setState({count: 1}, function() { + expect(log).toEqual([ + "didMount", "didMount", "willEnter", "didEnter", + "willLeave", "didLeave", "willUnmount" + ]); + }); + }); }); - it('should keep both sets of DOM nodes around', function() { - var a = React.renderComponent( - - - , - container - ); - expect(a.getDOMNode().childNodes.length).toBe(1); - React.renderComponent( - - - , - container - ); - expect(a.getDOMNode().childNodes.length).toBe(2); - expect(a.getDOMNode().childNodes[0].id).toBe('two'); - expect(a.getDOMNode().childNodes[1].id).toBe('one'); + it('should handle enter/leave/enter/leave correctly', function() { + var log = []; + var cb; + + var Child = React.createClass({ + componentDidMount: function() { + log.push('didMount'); + }, + componentWillEnter: function(_cb) { + log.push('willEnter'); + cb = _cb; + }, + componentDidEnter: function() { + log.push('didEnter'); + }, + componentWillLeave: function(cb) { + log.push('willLeave'); + cb(); + }, + componentDidLeave: function() { + log.push('didLeave'); + }, + componentWillUnmount: function() { + log.push('willUnmount'); + }, + render: function() { + return ; + } + }); + + var Component = React.createClass({ + getInitialState: function() { + return {count: 1}; + }, + render: function() { + var children = []; + for (var i = 0; i < this.state.count; i++) { + children.push(); + } + return {children}; + } + }); + + var instance = React.renderComponent(, container); + expect(log).toEqual(['didMount']); + instance.setState({count: 2}); + expect(log).toEqual(['didMount', 'didMount', 'willEnter']); + for (var i = 0; i < 5; i++) { + instance.setState({count: 2}); + expect(log).toEqual(['didMount', 'didMount', 'willEnter']); + instance.setState({count: 1}); + } + cb(); + expect(log).toEqual([ + 'didMount', 'didMount', 'willEnter', + 'didEnter', 'willLeave', 'didLeave', 'willUnmount' + ]); }); - it('should switch transitionLeave from false to true', function() { - var a = React.renderComponent( - - - , - container - ); - expect(a.getDOMNode().childNodes.length).toBe(1); - React.renderComponent( - - - , - container - ); - expect(a.getDOMNode().childNodes.length).toBe(1); - React.renderComponent( - - - , - container - ); - expect(a.getDOMNode().childNodes.length).toBe(2); - expect(a.getDOMNode().childNodes[0].id).toBe('three'); - expect(a.getDOMNode().childNodes[1].id).toBe('two'); - }); + it('should handle enter/leave/enter correctly', function() { + var log = []; + var cb; - it('should work with no children', function () { - React.renderComponent( - - , - container - ); - }); + var Child = React.createClass({ + componentDidMount: function() { + log.push('didMount'); + }, + componentWillEnter: function(_cb) { + log.push('willEnter'); + cb = _cb; + }, + componentDidEnter: function() { + log.push('didEnter'); + }, + componentWillLeave: function(cb) { + log.push('willLeave'); + cb(); + }, + componentDidLeave: function() { + log.push('didLeave'); + }, + componentWillUnmount: function() { + log.push('willUnmount'); + }, + render: function() { + return ; + } + }); - it('should work with a null child', function () { - React.renderComponent( - - {[null]} - , - container - ); + var Component = React.createClass({ + getInitialState: function() { + return {count: 1}; + }, + render: function() { + var children = []; + for (var i = 0; i < this.state.count; i++) { + children.push(); + } + return {children}; + } + }); + + var instance = React.renderComponent(, container); + expect(log).toEqual(['didMount']); + instance.setState({count: 2}); + expect(log).toEqual(['didMount', 'didMount', 'willEnter']); + for (var i = 0; i < 5; i++) { + instance.setState({count: 1}); + expect(log).toEqual(['didMount', 'didMount', 'willEnter']); + instance.setState({count: 2}); + } + cb(); + expect(log).toEqual([ + 'didMount', 'didMount', 'willEnter', 'didEnter' + ]); }); }); diff --git a/src/addons/transitions/__tests__/ReactTransitionableChild-test.js b/src/addons/transitions/__tests__/ReactTransitionableChild-test.js deleted file mode 100644 index 1d5155052b..0000000000 --- a/src/addons/transitions/__tests__/ReactTransitionableChild-test.js +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Copyright 2013 Facebook, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @jsx React.DOM - * @emails react-core - */ - -"use strict"; - -var React; -var ReactTransitionableChild; - -var mocks = require('mocks'); - -describe('ReactTransitionableChild', function() { - beforeEach(function() { - React = require('React'); - ReactTransitionableChild = require('ReactTransitionableChild'); - }); - - it('should keep the DOM node around', function() { - var container = document.createElement('div'); - var ac = React.renderComponent( - , - container - ); - expect(ac.getDOMNode().id).toBe('test'); - ac = React.renderComponent(, container); - expect(ac.getDOMNode().id).toBe('test'); - }); - - it('should manage enter css classes correctly', function() { - var runNextTick = mocks.getMockFunction(); - var container = document.createElement('div'); - var ac = React.renderComponent( - - - , - container - ); - expect(ac.getDOMNode().id).toBe('test'); - expect(ac.getDOMNode().className.trim()).toBe('myanim-enter'); - expect(runNextTick.mock.calls.length).toBe(1); - runNextTick.mock.calls[0][0](); - expect(ac.getDOMNode().className.trim()).toBe( - 'myanim-enter myanim-enter-active' - ); - expect(runNextTick.mock.calls.length).toBe(1); - }); - - it('should manage leave css classes correctly', function() { - var runNextTick = mocks.getMockFunction(); - var container = document.createElement('div'); - var ac = React.renderComponent( - - - , - container - ); - runNextTick.mock.calls[0][0](); - expect(ac.getDOMNode().className.trim()).toBe( - 'myanim-enter myanim-enter-active' - ); - - // TODO: we should just trigger the CSS animation end event to - // clean these up - ac.getDOMNode().className = ''; - React.renderComponent( - , - container - ); - expect(ac.getDOMNode().className.trim()).toBe('myanim-leave'); - expect(runNextTick.mock.calls.length).toBe(2); - runNextTick.mock.calls[1][0](); - expect(ac.getDOMNode().className.trim()).toBe( - 'myanim-leave myanim-leave-active' - ); - expect(runNextTick.mock.calls.length).toBe(2); - }); -});