Skip to content

Commit 9ac27cb

Browse files
petehuntzpao
authored andcommitted
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.
1 parent a6749a6 commit 9ac27cb

8 files changed

+565
-366
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* Copyright 2013 Facebook, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* @typechecks
17+
* @providesModule ReactCSSTransitionGroup
18+
* @jsx React.DOM
19+
*/
20+
21+
"use strict";
22+
23+
var React = require('React');
24+
25+
var ReactTransitionGroup = require('ReactTransitionGroup');
26+
var ReactCSSTransitionGroupChild = require('ReactCSSTransitionGroupChild');
27+
28+
var ReactCSSTransitionGroup = React.createClass({
29+
propTypes: {
30+
transitionName: React.PropTypes.string.isRequired,
31+
transitionEnter: React.PropTypes.bool,
32+
transitionLeave: React.PropTypes.bool
33+
},
34+
35+
getDefaultProps: function() {
36+
return {
37+
transitionEnter: true,
38+
transitionLeave: true
39+
};
40+
},
41+
42+
_wrapChild: function(child) {
43+
// We need to provide this childFactory so that
44+
// ReactCSSTransitionGroupChild can receive updates to name, enter, and
45+
// leave while it is leaving.
46+
return (
47+
<ReactCSSTransitionGroupChild
48+
name={this.props.transitionName}
49+
enter={this.props.transitionEnter}
50+
leave={this.props.transitionLeave}>
51+
{child}
52+
</ReactCSSTransitionGroupChild>
53+
);
54+
},
55+
56+
render: function() {
57+
return this.transferPropsTo(
58+
<ReactTransitionGroup childFactory={this._wrapChild}>
59+
{this.props.children}
60+
</ReactTransitionGroup>
61+
);
62+
}
63+
});
64+
65+
module.exports = ReactCSSTransitionGroup;

src/addons/transitions/ReactTransitionableChild.js renamed to src/addons/transitions/ReactCSSTransitionGroupChild.js

Lines changed: 21 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,19 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*
16-
* @providesModule ReactTransitionableChild
16+
* @typechecks
17+
* @providesModule ReactCSSTransitionGroupChild
1718
*/
1819

1920
"use strict";
2021

2122
var React = require('React');
23+
2224
var CSSCore = require('CSSCore');
2325
var ReactTransitionEvents = require('ReactTransitionEvents');
2426

27+
var onlyChild = require('onlyChild');
28+
2529
// We don't remove the element from the DOM until we receive an animationend or
2630
// transitionend event. If the user screws up and forgets to add an animation
2731
// their node will be stuck in the DOM forever, so we detect if an animation
@@ -31,6 +35,7 @@ var NO_EVENT_TIMEOUT = 5000;
3135

3236
var noEventListener = null;
3337

38+
3439
if (__DEV__) {
3540
noEventListener = function() {
3641
console.warn(
@@ -42,20 +47,8 @@ if (__DEV__) {
4247
};
4348
}
4449

45-
/**
46-
* This component is simply responsible for watching when its single child
47-
* changes to undefined and animating the old child out. It does this by
48-
* recording its old child in savedChildren when it detects this event is about
49-
* to occur.
50-
*/
51-
var ReactTransitionableChild = React.createClass({
52-
/**
53-
* Perform an actual DOM transition. This takes care of a few things:
54-
* - Adding the second CSS class to trigger the transition
55-
* - Listening for the finish event
56-
* - Cleaning up the css (unless noReset is true)
57-
*/
58-
transition: function(animationType, noReset, finishCallback) {
50+
var ReactCSSTransitionGroupChild = React.createClass({
51+
transition: function(animationType, finishCallback) {
5952
var node = this.getDOMNode();
6053
var className = this.props.name + '-' + animationType;
6154
var activeClassName = className + '-active';
@@ -66,13 +59,8 @@ var ReactTransitionableChild = React.createClass({
6659
clearTimeout(noEventTimeout);
6760
}
6861

69-
// If this gets invoked after the component is unmounted it's OK.
70-
if (!noReset) {
71-
// Usually this means you're about to remove the node if you want to
72-
// leave it in its animated state.
73-
CSSCore.removeClass(node, className);
74-
CSSCore.removeClass(node, activeClassName);
75-
}
62+
CSSCore.removeClass(node, className);
63+
CSSCore.removeClass(node, activeClassName);
7664

7765
ReactTransitionEvents.removeEndEventListener(node, endListener);
7866

@@ -126,39 +114,25 @@ var ReactTransitionableChild = React.createClass({
126114
}
127115
},
128116

129-
componentWillReceiveProps: function(nextProps) {
130-
if (!nextProps.children && this.props.children) {
131-
this.savedChildren = this.props.children;
132-
} else if (nextProps.children && !this.props.children) {
133-
// We're being told to re-add the child. Let's stop leaving!
134-
if (this.isMounted()) {
135-
var node = this.getDOMNode();
136-
var className = this.props.name;
137-
CSSCore.removeClass(node, className + '-leave');
138-
CSSCore.removeClass(node, className + '-leave-active');
139-
if (this.props.enter) {
140-
CSSCore.addClass(node, className + '-enter');
141-
CSSCore.addClass(node, className + '-enter-active');
142-
}
143-
}
144-
}
145-
},
146-
147-
componentDidMount: function() {
117+
componentWillEnter: function(done) {
148118
if (this.props.enter) {
149-
this.transition('enter');
119+
this.transition('enter', done);
120+
} else {
121+
done();
150122
}
151123
},
152124

153-
componentDidUpdate: function(prevProps, prevState, prevContext) {
154-
if (prevProps.children && !this.props.children) {
155-
this.transition('leave', true, this.props.onDoneLeaving);
125+
componentWillLeave: function(done) {
126+
if (this.props.leave) {
127+
this.transition('leave', done);
128+
} else {
129+
done();
156130
}
157131
},
158132

159133
render: function() {
160-
return this.props.children || this.savedChildren;
134+
return onlyChild(this.props.children);
161135
}
162136
});
163137

164-
module.exports = ReactTransitionableChild;
138+
module.exports = ReactCSSTransitionGroupChild;

src/addons/transitions/ReactTransitionKeySet.js renamed to src/addons/transitions/ReactTransitionChildMapping.js

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@
1414
* limitations under the License.
1515
*
1616
* @typechecks static-only
17-
* @providesModule ReactTransitionKeySet
17+
* @providesModule ReactTransitionChildMapping
1818
*/
1919

2020
"use strict";
2121

2222
var ReactChildren = require('ReactChildren');
2323

24-
var ReactTransitionKeySet = {
24+
var ReactTransitionChildMapping = {
2525
/**
2626
* Given `this.props.children`, return an object mapping key to child. Just
2727
* simple syntactic sugar around ReactChildren.map().
@@ -35,40 +35,35 @@ var ReactTransitionKeySet = {
3535
});
3636
},
3737

38-
/**
39-
* Simple syntactic sugar to get an object with keys of all of `children`.
40-
* Does not have references to the children themselves.
41-
*
42-
* @param {*} children `this.props.children`
43-
* @return {object} Mapping of key to the value "true"
44-
*/
45-
getKeySet: function(children) {
46-
return ReactChildren.map(children, function() {
47-
return true;
48-
});
49-
},
50-
5138
/**
5239
* When you're adding or removing children some may be added or removed in the
53-
* same render pass. We want to show *both* since we want to simultaneously
40+
* same render pass. We want ot show *both* since we want to simultaneously
5441
* animate elements in and out. This function takes a previous set of keys
5542
* and a new set of keys and merges them with its best guess of the correct
5643
* ordering. In the future we may expose some of the utilities in
5744
* ReactMultiChild to make this easy, but for now React itself does not
5845
* directly have this concept of the union of prevChildren and nextChildren
5946
* so we implement it here.
6047
*
61-
* @param {object} prev prev child keys as returned from
62-
* `ReactTransitionKeySet.getKeySet()`.
63-
* @param {object} next next child keys as returned from
64-
* `ReactTransitionKeySet.getKeySet()`.
48+
* @param {object} prev prev children as returned from
49+
* `ReactTransitionChildMapping.getChildMapping()`.
50+
* @param {object} next next children as returned from
51+
* `ReactTransitionChildMapping.getChildMapping()`.
6552
* @return {object} a key set that contains all keys in `prev` and all keys
6653
* in `next` in a reasonable order.
6754
*/
68-
mergeKeySets: function(prev, next) {
55+
mergeChildMappings: function(prev, next) {
6956
prev = prev || {};
7057
next = next || {};
7158

59+
function getValueForKey(key) {
60+
if (next.hasOwnProperty(key)) {
61+
return next[key];
62+
} else {
63+
return prev[key];
64+
}
65+
}
66+
7267
// For each key of `next`, the list of keys to insert before that key in
7368
// the combined list
7469
var nextKeysPending = {};
@@ -86,23 +81,26 @@ var ReactTransitionKeySet = {
8681
}
8782

8883
var i;
89-
var keySet = {};
84+
var childMapping = {};
9085
for (var nextKey in next) {
9186
if (nextKeysPending[nextKey]) {
9287
for (i = 0; i < nextKeysPending[nextKey].length; i++) {
93-
keySet[nextKeysPending[nextKey][i]] = true;
88+
var pendingNextKey = nextKeysPending[nextKey][i];
89+
childMapping[nextKeysPending[nextKey][i]] = getValueForKey(
90+
pendingNextKey
91+
);
9492
}
9593
}
96-
keySet[nextKey] = true;
94+
childMapping[nextKey] = getValueForKey(nextKey);
9795
}
9896

9997
// Finally, add the keys which didn't appear before any key in `next`
10098
for (i = 0; i < pendingKeys.length; i++) {
101-
keySet[pendingKeys[i]] = true;
99+
childMapping[pendingKeys[i]] = getValueForKey(pendingKeys[i]);
102100
}
103101

104-
return keySet;
102+
return childMapping;
105103
}
106104
};
107105

108-
module.exports = ReactTransitionKeySet;
106+
module.exports = ReactTransitionChildMapping;

0 commit comments

Comments
 (0)