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.
This commit is contained in:
Pete Hunt
2014-02-12 12:29:58 -08:00
committed by Paul O’Shannessy
parent a6749a686f
commit 9ac27cb551
8 changed files with 565 additions and 366 deletions
@@ -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 (
<ReactCSSTransitionGroupChild
name={this.props.transitionName}
enter={this.props.transitionEnter}
leave={this.props.transitionLeave}>
{child}
</ReactCSSTransitionGroupChild>
);
},
render: function() {
return this.transferPropsTo(
<ReactTransitionGroup childFactory={this._wrapChild}>
{this.props.children}
</ReactTransitionGroup>
);
}
});
module.exports = ReactCSSTransitionGroup;
@@ -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;
@@ -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;
+137 -72
View File
@@ -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));
}
});
@@ -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(
<ReactCSSTransitionGroup transitionName="yolo">
<span key="one" id="one" />
</ReactCSSTransitionGroup>,
container
);
expect(a.getDOMNode().childNodes.length).toBe(1);
setTimeout.mock.calls.length = 0;
React.renderComponent(
<ReactCSSTransitionGroup transitionName="yolo">
<span key="two" id="two" />
</ReactCSSTransitionGroup>,
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(
<ReactCSSTransitionGroup transitionName="yolo">
<span key="one" id="one" />
</ReactCSSTransitionGroup>,
container
);
expect(a.getDOMNode().childNodes.length).toBe(1);
React.renderComponent(
<ReactCSSTransitionGroup transitionName="yolo">
<span key="two" id="two" />
</ReactCSSTransitionGroup>,
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(
<ReactCSSTransitionGroup
transitionName="yolo"
transitionEnter={false}
transitionLeave={false}>
<span key="one" id="one" />
</ReactCSSTransitionGroup>,
container
);
expect(a.getDOMNode().childNodes.length).toBe(1);
React.renderComponent(
<ReactCSSTransitionGroup
transitionName="yolo"
transitionEnter={false}
transitionLeave={false}>
<span key="two" id="two" />
</ReactCSSTransitionGroup>,
container
);
expect(a.getDOMNode().childNodes.length).toBe(1);
React.renderComponent(
<ReactCSSTransitionGroup
transitionName="yolo"
transitionEnter={false}
transitionLeave={true}>
<span key="three" id="three" />
</ReactCSSTransitionGroup>,
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(
<ReactCSSTransitionGroup transitionName="yolo">
</ReactCSSTransitionGroup>,
container
);
});
it('should work with a null child', function () {
React.renderComponent(
<ReactCSSTransitionGroup transitionName="yolo">
{[null]}
</ReactCSSTransitionGroup>,
container
);
});
});
@@ -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 = <div key="one">{oneone}{onetwo}</div>;
var two = <div key="two" />;
var component = <div>{one}{two}</div>;
expect(ReactTransitionKeySet.getChildMapping(component.props.children))
.toEqual({
'.$one': one,
'.$two': two
});
});
it('should support getKeySet', function() {
var oneone = <div key="oneone" />;
var onetwo = <div key="onetwo" />;
var one = <div key="one">{oneone}{onetwo}</div>;
var two = <div key="two" />;
var component = <div>{one}{two}</div>;
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
});
@@ -36,102 +36,177 @@ describe('ReactTransitionGroup', function() {
container = document.createElement('div');
});
it('should warn after time with no transitionend', function() {
var a = React.renderComponent(
<ReactTransitionGroup transitionName="yolo">
<span key="one" id="one" />
</ReactTransitionGroup>,
container
);
expect(a.getDOMNode().childNodes.length).toBe(1);
setTimeout.mock.calls.length = 0;
it('should handle willEnter correctly', function() {
var log = [];
React.renderComponent(
<ReactTransitionGroup transitionName="yolo">
<span key="two" id="two" />
</ReactTransitionGroup>,
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 <span />;
}
});
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(<Child key={i} />);
}
return <ReactTransitionGroup>{children}</ReactTransitionGroup>;
}
});
expect(a.getDOMNode().childNodes.length).toBe(2);
expect(console.warn.mock.calls.length).toBe(1);
var instance = React.renderComponent(<Component />, 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(
<ReactTransitionGroup transitionName="yolo">
<span key="one" id="one" />
</ReactTransitionGroup>,
container
);
expect(a.getDOMNode().childNodes.length).toBe(1);
React.renderComponent(
<ReactTransitionGroup transitionName="yolo">
<span key="two" id="two" />
</ReactTransitionGroup>,
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 <span />;
}
});
var Component = React.createClass({
getInitialState: function() {
return {count: 1};
},
render: function() {
var children = [];
for (var i = 0; i < this.state.count; i++) {
children.push(<Child key={i} />);
}
return <ReactTransitionGroup>{children}</ReactTransitionGroup>;
}
});
var instance = React.renderComponent(<Component />, 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(
<ReactTransitionGroup
transitionName="yolo"
transitionEnter={false}
transitionLeave={false}>
<span key="one" id="one" />
</ReactTransitionGroup>,
container
);
expect(a.getDOMNode().childNodes.length).toBe(1);
React.renderComponent(
<ReactTransitionGroup
transitionName="yolo"
transitionEnter={false}
transitionLeave={false}>
<span key="two" id="two" />
</ReactTransitionGroup>,
container
);
expect(a.getDOMNode().childNodes.length).toBe(1);
React.renderComponent(
<ReactTransitionGroup
transitionName="yolo"
transitionEnter={false}
transitionLeave={true}>
<span key="three" id="three" />
</ReactTransitionGroup>,
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(
<ReactTransitionGroup transitionName="yolo">
</ReactTransitionGroup>,
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 <span />;
}
});
it('should work with a null child', function () {
React.renderComponent(
<ReactTransitionGroup transitionName="yolo">
{[null]}
</ReactTransitionGroup>,
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(<Child key={i} />);
}
return <ReactTransitionGroup>{children}</ReactTransitionGroup>;
}
});
var instance = React.renderComponent(<Component />, 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'
]);
});
});
@@ -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(
<ReactTransitionableChild><span id="test" /></ReactTransitionableChild>,
container
);
expect(ac.getDOMNode().id).toBe('test');
ac = React.renderComponent(<ReactTransitionableChild />, 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(
<ReactTransitionableChild
runNextTick={runNextTick}
name="myanim"
enter={true}>
<span id="test" />
</ReactTransitionableChild>,
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(
<ReactTransitionableChild
runNextTick={runNextTick}
name="myanim"
enter={true}
leave={true}>
<span id="test" />
</ReactTransitionableChild>,
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(
<ReactTransitionableChild
runNextTick={runNextTick}
name="myanim"
enter={true}
leave={true}
/>,
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);
});
});