mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
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:
committed by
Paul O’Shannessy
parent
a6749a686f
commit
9ac27cb551
@@ -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;
|
||||
+21
-47
@@ -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;
|
||||
+25
-27
@@ -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;
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
+18
-29
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user