@samitouri / QOS-React-2 / commits / 9ac27cb551

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.

Pete Hunt committed Feb 12, 2014 at 12:21 UTC 9ac27cb551b85293d03d334363acd7cf9c77813a
8 files changed +565 -366
src/addons/transitions/ReactCSSTransitionGroup.js new
+65
@@ -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/ReactCSSTransitionGroupChild.js renamed
+21 -47
@@ -13,15 +13,19 @@
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 *
16 - * @providesModule ReactTransitionableChild
16 + * @typechecks
17 + * @providesModule ReactCSSTransitionGroupChild
18 */
19
20 "use strict";
21
22 var React = require('React');
23 +
24 var CSSCore = require('CSSCore');
25 var ReactTransitionEvents = require('ReactTransitionEvents');
26
27 +var onlyChild = require('onlyChild');
28 +
29 // We don't remove the element from the DOM until we receive an animationend or
30 // transitionend event. If the user screws up and forgets to add an animation
31 // their node will be stuck in the DOM forever, so we detect if an animation
@@ -31,6 +35,7 @@ var NO_EVENT_TIMEOUT = 5000;
35
36 var noEventListener = null;
37
38 +
39 if (__DEV__) {
40 noEventListener = function() {
41 console.warn(
@@ -42,20 +47,8 @@ if (__DEV__) {
47 };
48 }
49
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) {
52 var node = this.getDOMNode();
53 var className = this.props.name + '-' + animationType;
54 var activeClassName = className + '-active';
@@ -66,13 +59,8 @@ var ReactTransitionableChild = React.createClass({
59 clearTimeout(noEventTimeout);
60 }
61
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);
64
65 ReactTransitionEvents.removeEndEventListener(node, endListener);
66
@@ -126,39 +114,25 @@ var ReactTransitionableChild = React.createClass({
114 }
115 },
116
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) {
118 if (this.props.enter) {
149 - this.transition('enter');
119 + this.transition('enter', done);
120 + } else {
121 + done();
122 }
123 },
124
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();
130 }
131 },
132
133 render: function() {
160 - return this.props.children || this.savedChildren;
134 + return onlyChild(this.props.children);
135 }
136 });
137
164 -module.exports = ReactTransitionableChild;
138 +module.exports = ReactCSSTransitionGroupChild;
src/addons/transitions/ReactTransitionChildMapping.js renamed
+25 -27
@@ -14,14 +14,14 @@
14 * limitations under the License.
15 *
16 * @typechecks static-only
17 - * @providesModule ReactTransitionKeySet
17 + * @providesModule ReactTransitionChildMapping
18 */
19
20 "use strict";
21
22 var ReactChildren = require('ReactChildren');
23
24 -var ReactTransitionKeySet = {
24 +var ReactTransitionChildMapping = {
25 /**
26 * Given `this.props.children`, return an object mapping key to child. Just
27 * simple syntactic sugar around ReactChildren.map().
@@ -35,22 +35,9 @@ var ReactTransitionKeySet = {
35 });
36 },
37
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 -
38 /**
39 * 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
41 * animate elements in and out. This function takes a previous set of keys
42 * and a new set of keys and merges them with its best guess of the correct
43 * ordering. In the future we may expose some of the utilities in
@@ -58,17 +45,25 @@ var ReactTransitionKeySet = {
45 * directly have this concept of the union of prevChildren and nextChildren
46 * so we implement it here.
47 *
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()`.
52 * @return {object} a key set that contains all keys in `prev` and all keys
53 * in `next` in a reasonable order.
54 */
68 - mergeKeySets: function(prev, next) {
55 + mergeChildMappings: function(prev, next) {
56 prev = prev || {};
57 next = next || {};
58
59 + function getValueForKey(key) {
60 + if (next.hasOwnProperty(key)) {
61 + return next[key];
62 + } else {
63 + return prev[key];
64 + }
65 + }
66 +
67 // For each key of `next`, the list of keys to insert before that key in
68 // the combined list
69 var nextKeysPending = {};
@@ -86,23 +81,26 @@ var ReactTransitionKeySet = {
81 }
82
83 var i;
89 - var keySet = {};
84 + var childMapping = {};
85 for (var nextKey in next) {
86 if (nextKeysPending[nextKey]) {
87 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 + );
92 }
93 }
96 - keySet[nextKey] = true;
94 + childMapping[nextKey] = getValueForKey(nextKey);
95 }
96
97 // Finally, add the keys which didn't appear before any key in `next`
98 for (i = 0; i < pendingKeys.length; i++) {
101 - keySet[pendingKeys[i]] = true;
99 + childMapping[pendingKeys[i]] = getValueForKey(pendingKeys[i]);
100 }
101
104 - return keySet;
102 + return childMapping;
103 }
104 };
105
108 -module.exports = ReactTransitionKeySet;
106 +module.exports = ReactTransitionChildMapping;
src/addons/transitions/ReactTransitionGroup.js
+133 -68
@@ -19,103 +19,168 @@
19 "use strict";
20
21 var React = require('React');
22 -var ReactTransitionableChild = require('ReactTransitionableChild');
23 -var ReactTransitionKeySet = require('ReactTransitionKeySet');
22 +var ReactTransitionChildMapping = require('ReactTransitionChildMapping');
23 +
24 +var cloneWithProps = require('cloneWithProps');
25 +var emptyFunction = require('emptyFunction');
26 +var merge = require('merge');
27
28 var ReactTransitionGroup = React.createClass({
29
30 propTypes: {
28 - transitionName: React.PropTypes.string.isRequired,
29 - transitionEnter: React.PropTypes.bool,
30 - transitionLeave: React.PropTypes.bool,
31 - onTransition: React.PropTypes.func,
32 - component: React.PropTypes.func
31 + component: React.PropTypes.func,
32 + childFactory: React.PropTypes.func
33 },
34
35 getDefaultProps: function() {
36 return {
37 - transitionEnter: true,
38 - transitionLeave: true,
39 - component: React.DOM.span
37 + component: React.DOM.span,
38 + childFactory: emptyFunction.thatReturnsArgument
39 + };
40 + },
41 +
42 + getInitialState: function() {
43 + return {
44 + children: ReactTransitionChildMapping.getChildMapping(this.props.children)
45 };
46 },
47
48 + componentWillReceiveProps: function(nextProps) {
49 + var nextChildMapping = ReactTransitionChildMapping.getChildMapping(
50 + nextProps.children
51 + );
52 + var prevChildMapping = this.state.children;
53 +
54 + this.setState({
55 + children: ReactTransitionChildMapping.mergeChildMappings(
56 + prevChildMapping,
57 + nextChildMapping
58 + )
59 + });
60 +
61 + var key;
62 +
63 + for (key in nextChildMapping) {
64 + if (!prevChildMapping.hasOwnProperty(key) &&
65 + !this.currentlyTransitioningKeys[key]) {
66 + this.keysToEnter.push(key);
67 + }
68 + }
69 +
70 + for (key in prevChildMapping) {
71 + if (!nextChildMapping.hasOwnProperty(key) &&
72 + !this.currentlyTransitioningKeys[key]) {
73 + this.keysToLeave.push(key);
74 + }
75 + }
76 +
77 + // If we want to someday check for reordering, we could do it here.
78 + },
79 +
80 componentWillMount: function() {
44 - // _transitionGroupCurrentKeys stores the union of previous *and* next keys.
45 - // If this were a component we'd store it as state, however, since this must
46 - // be a mixin, we need to keep the result of the union of keys in each
47 - // call to animateChildren() which happens in render(), so we can't
48 - // call setState() in there.
49 - this._transitionGroupCurrentKeys = {};
81 + this.currentlyTransitioningKeys = {};
82 + this.keysToEnter = [];
83 + this.keysToLeave = [];
84 },
85
86 componentDidUpdate: function() {
53 - if (this.props.onTransition) {
54 - this.props.onTransition();
87 + var keysToEnter = this.keysToEnter;
88 + this.keysToEnter = [];
89 + keysToEnter.forEach(this.performEnter);
90 +
91 + var keysToLeave = this.keysToLeave;
92 + this.keysToLeave = [];
93 + keysToLeave.forEach(this.performLeave);
94 + },
95 +
96 + performEnter: function(key) {
97 + this.currentlyTransitioningKeys[key] = true;
98 +
99 + var component = this.refs[key];
100 +
101 + if (component.componentWillEnter) {
102 + component.componentWillEnter(
103 + this._handleDoneEntering.bind(this, key)
104 + );
105 + } else {
106 + this._handleDoneEntering(key);
107 }
108 },
109
58 - /**
59 - * Render some children in a transitionable way.
60 - */
61 - renderTransitionableChildren: function(sourceChildren) {
62 - var children = {};
63 - var childMapping = ReactTransitionKeySet.getChildMapping(sourceChildren);
64 -
65 - var prevKeys = this._transitionGroupCurrentKeys;
66 - var currentKeys = ReactTransitionKeySet.mergeKeySets(
67 - prevKeys,
68 - ReactTransitionKeySet.getKeySet(sourceChildren)
110 + _handleDoneEntering: function(key) {
111 + var component = this.refs[key];
112 + if (component.componentDidEnter) {
113 + component.componentDidEnter();
114 + }
115 +
116 + delete this.currentlyTransitioningKeys[key];
117 +
118 + var currentChildMapping = ReactTransitionChildMapping.getChildMapping(
119 + this.props.children
120 );
121
71 - for (var key in currentKeys) {
72 - // Here is how we keep the nodes in the DOM. ReactTransitionableChild
73 - // knows how to hold onto its child if it changes to undefined. Here, we
74 - // may look up an old key in the new children, and it may switch to
75 - // undefined. React's reconciler will keep the ReactTransitionableChild
76 - // instance alive such that we can animate it.
77 - if (childMapping[key] || (this.props.transitionLeave && prevKeys[key])) {
78 - children[key] = ReactTransitionableChild({
79 - name: this.props.transitionName,
80 - enter: this.props.transitionEnter,
81 - onDoneLeaving: this._handleDoneLeaving.bind(this, key)
82 - }, childMapping[key]);
83 - } else {
84 - // If there's no leave transition and the child has been removed from
85 - // the source children list, we want to remove it immediately from the
86 - // _transitionGroupCurrentKeys cache because _handleDoneLeaving won't
87 - // be called. In normal cases, this prevents a small memory leak; in
88 - // the case of switching transitionLeave from false to true, it
89 - // prevents a confusing bug where ReactTransitionableChild.render()
90 - // returns nothing, throwing an error.
91 - delete currentKeys[key];
92 - }
122 + if (!currentChildMapping.hasOwnProperty(key)) {
123 + // This was removed before it had fully entered. Remove it.
124 + this.performLeave(key);
125 }
126 + },
127
95 - this._transitionGroupCurrentKeys = currentKeys;
128 + performLeave: function(key) {
129 + this.currentlyTransitioningKeys[key] = true;
130
97 - return children;
131 + var component = this.refs[key];
132 + if (component.componentWillLeave) {
133 + component.componentWillLeave(this._handleDoneLeaving.bind(this, key));
134 + } else {
135 + // Note that this is somewhat dangerous b/c it calls setState()
136 + // again, effectively mutating the component before all the work
137 + // is done.
138 + this._handleDoneLeaving(key);
139 + }
140 },
141
142 _handleDoneLeaving: function(key) {
101 - // When the leave animation finishes, we should blow away the actual DOM
102 - // node.
103 - delete this._transitionGroupCurrentKeys[key];
104 - this.forceUpdate();
143 + var component = this.refs[key];
144 +
145 + if (component.componentDidLeave) {
146 + component.componentDidLeave();
147 + }
148 +
149 + delete this.currentlyTransitioningKeys[key];
150 +
151 + var currentChildMapping = ReactTransitionChildMapping.getChildMapping(
152 + this.props.children
153 + );
154 +
155 + if (currentChildMapping.hasOwnProperty(key)) {
156 + // This entered again before it fully left. Add it again.
157 + this.performEnter(key);
158 + } else {
159 + var newChildren = merge(this.state.children);
160 + delete newChildren[key];
161 + this.setState({children: newChildren});
162 + }
163 },
164
165 render: function() {
108 - return this.transferPropsTo(
109 - this.props.component(
110 - {
111 - transitionName: null,
112 - transitionEnter: null,
113 - transitionLeave: null,
114 - component: null
115 - },
116 - this.renderTransitionableChildren(this.props.children)
117 - )
118 - );
166 + // TODO: we could get rid of the need for the wrapper node
167 + // by cloning a single child
168 + var childrenToRender = {};
169 + for (var key in this.state.children) {
170 + var child = this.state.children[key];
171 + if (child) {
172 + // You may need to apply reactive updates to a child as it is leaving.
173 + // The normal React way to do it won't work since the child will have
174 + // already been removed. In case you need this behavior you can provide
175 + // a childFactory function to wrap every child, even the ones that are
176 + // leaving.
177 + childrenToRender[key] = cloneWithProps(
178 + this.props.childFactory(child),
179 + {ref: key}
180 + );
181 + }
182 + }
183 + return this.transferPropsTo(this.props.component(null, childrenToRender));
184 }
185 });
186
src/addons/transitions/__tests__/ReactCSSTransitionGroup-test.js new
+137
@@ -0,0 +1,137 @@
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 + * @jsx React.DOM
17 + * @emails react-core
18 + */
19 +
20 +"use strict";
21 +
22 +var React;
23 +var ReactCSSTransitionGroup;
24 +var mocks;
25 +
26 +// Most of the real functionality is covered in other unit tests, this just
27 +// makes sure we're wired up correctly.
28 +describe('ReactCSSTransitionGroup', function() {
29 + var container;
30 +
31 + beforeEach(function() {
32 + React = require('React');
33 + ReactCSSTransitionGroup = require('ReactCSSTransitionGroup');
34 + mocks = require('mocks');
35 +
36 + container = document.createElement('div');
37 + });
38 +
39 + it('should warn after time with no transitionend', function() {
40 + var a = React.renderComponent(
41 + <ReactCSSTransitionGroup transitionName="yolo">
42 + <span key="one" id="one" />
43 + </ReactCSSTransitionGroup>,
44 + container
45 + );
46 + expect(a.getDOMNode().childNodes.length).toBe(1);
47 +
48 + setTimeout.mock.calls.length = 0;
49 +
50 + React.renderComponent(
51 + <ReactCSSTransitionGroup transitionName="yolo">
52 + <span key="two" id="two" />
53 + </ReactCSSTransitionGroup>,
54 + container
55 + );
56 + expect(a.getDOMNode().childNodes.length).toBe(2);
57 + expect(a.getDOMNode().childNodes[0].id).toBe('two');
58 + expect(a.getDOMNode().childNodes[1].id).toBe('one');
59 +
60 + console.warn = mocks.getMockFunction();
61 + setTimeout.mock.calls[2][0]();
62 +
63 + expect(a.getDOMNode().childNodes.length).toBe(2);
64 + expect(console.warn.mock.calls.length).toBe(1);
65 + });
66 +
67 + it('should keep both sets of DOM nodes around', function() {
68 + var a = React.renderComponent(
69 + <ReactCSSTransitionGroup transitionName="yolo">
70 + <span key="one" id="one" />
71 + </ReactCSSTransitionGroup>,
72 + container
73 + );
74 + expect(a.getDOMNode().childNodes.length).toBe(1);
75 + React.renderComponent(
76 + <ReactCSSTransitionGroup transitionName="yolo">
77 + <span key="two" id="two" />
78 + </ReactCSSTransitionGroup>,
79 + container
80 + );
81 + expect(a.getDOMNode().childNodes.length).toBe(2);
82 + expect(a.getDOMNode().childNodes[0].id).toBe('two');
83 + expect(a.getDOMNode().childNodes[1].id).toBe('one');
84 + });
85 +
86 + it('should switch transitionLeave from false to true', function() {
87 + var a = React.renderComponent(
88 + <ReactCSSTransitionGroup
89 + transitionName="yolo"
90 + transitionEnter={false}
91 + transitionLeave={false}>
92 + <span key="one" id="one" />
93 + </ReactCSSTransitionGroup>,
94 + container
95 + );
96 + expect(a.getDOMNode().childNodes.length).toBe(1);
97 + React.renderComponent(
98 + <ReactCSSTransitionGroup
99 + transitionName="yolo"
100 + transitionEnter={false}
101 + transitionLeave={false}>
102 + <span key="two" id="two" />
103 + </ReactCSSTransitionGroup>,
104 + container
105 + );
106 + expect(a.getDOMNode().childNodes.length).toBe(1);
107 + React.renderComponent(
108 + <ReactCSSTransitionGroup
109 + transitionName="yolo"
110 + transitionEnter={false}
111 + transitionLeave={true}>
112 + <span key="three" id="three" />
113 + </ReactCSSTransitionGroup>,
114 + container
115 + );
116 + expect(a.getDOMNode().childNodes.length).toBe(2);
117 + expect(a.getDOMNode().childNodes[0].id).toBe('three');
118 + expect(a.getDOMNode().childNodes[1].id).toBe('two');
119 + });
120 + return;
121 + it('should work with no children', function () {
122 + React.renderComponent(
123 + <ReactCSSTransitionGroup transitionName="yolo">
124 + </ReactCSSTransitionGroup>,
125 + container
126 + );
127 + });
128 +
129 + it('should work with a null child', function () {
130 + React.renderComponent(
131 + <ReactCSSTransitionGroup transitionName="yolo">
132 + {[null]}
133 + </ReactCSSTransitionGroup>,
134 + container
135 + );
136 + });
137 +});
src/addons/transitions/__tests__/ReactTransitionChildMapping-test.js renamed
+18 -29
@@ -20,12 +20,12 @@
20 "use strict";
21
22 var React;
23 -var ReactTransitionKeySet;
23 +var ReactTransitionChildMapping;
24
25 -describe('ReactTransitionKeySet', function() {
25 +describe('ReactTransitionChildMapping', function() {
26 beforeEach(function() {
27 React = require('React');
28 - ReactTransitionKeySet = require('ReactTransitionKeySet');
28 + ReactTransitionChildMapping = require('ReactTransitionChildMapping');
29 });
30
31 it('should support getChildMapping', function() {
@@ -34,26 +34,15 @@ describe('ReactTransitionKeySet', function() {
34 var one = <div key="one">{oneone}{onetwo}</div>;
35 var two = <div key="two" />;
36 var component = <div>{one}{two}</div>;
37 - expect(ReactTransitionKeySet.getChildMapping(component.props.children))
38 - .toEqual({
39 - '.$one': one,
40 - '.$two': two
41 - });
42 - });
43 -
44 - it('should support getKeySet', function() {
45 - var oneone = <div key="oneone" />;
46 - var onetwo = <div key="onetwo" />;
47 - var one = <div key="one">{oneone}{onetwo}</div>;
48 - var two = <div key="two" />;
49 - var component = <div>{one}{two}</div>;
50 - expect(ReactTransitionKeySet.getKeySet(component.props.children)).toEqual({
51 - '.$one': true,
52 - '.$two': true
37 + expect(
38 + ReactTransitionChildMapping.getChildMapping(component.props.children)
39 + ).toEqual({
40 + '.$one': one,
41 + '.$two': two
42 });
43 });
44
56 - it('should support mergeKeySets for adding keys', function() {
45 + it('should support mergeChildMappings for adding keys', function() {
46 var prev = {
47 one: true,
48 two: true
@@ -63,14 +52,14 @@ describe('ReactTransitionKeySet', function() {
52 two: true,
53 three: true
54 };
66 - expect(ReactTransitionKeySet.mergeKeySets(prev, next)).toEqual({
55 + expect(ReactTransitionChildMapping.mergeChildMappings(prev, next)).toEqual({
56 one: true,
57 two: true,
58 three: true
59 });
60 });
61
73 - it('should support mergeKeySets for removing keys', function() {
62 + it('should support mergeChildMappings for removing keys', function() {
63 var prev = {
64 one: true,
65 two: true,
@@ -80,14 +69,14 @@ describe('ReactTransitionKeySet', function() {
69 one: true,
70 two: true
71 };
83 - expect(ReactTransitionKeySet.mergeKeySets(prev, next)).toEqual({
72 + expect(ReactTransitionChildMapping.mergeChildMappings(prev, next)).toEqual({
73 one: true,
74 two: true,
75 three: true
76 });
77 });
78
90 - it('should support mergeKeySets for adding and removing', function() {
79 + it('should support mergeChildMappings for adding and removing', function() {
80 var prev = {
81 one: true,
82 two: true,
@@ -98,7 +87,7 @@ describe('ReactTransitionKeySet', function() {
87 two: true,
88 four: true
89 };
101 - expect(ReactTransitionKeySet.mergeKeySets(prev, next)).toEqual({
90 + expect(ReactTransitionChildMapping.mergeChildMappings(prev, next)).toEqual({
91 one: true,
92 two: true,
93 three: true,
@@ -119,7 +108,7 @@ describe('ReactTransitionKeySet', function() {
108 three: true,
109 five: true
110 };
122 - expect(ReactTransitionKeySet.mergeKeySets(prev, next)).toEqual({
111 + expect(ReactTransitionChildMapping.mergeChildMappings(prev, next)).toEqual({
112 one: true,
113 two: true,
114 three: true,
@@ -128,7 +117,7 @@ describe('ReactTransitionKeySet', function() {
117 });
118 });
119
131 - it('should support mergeKeySets with undefined input', function () {
120 + it('should support mergeChildMappings with undefined input', function () {
121 var prev = {
122 one: true,
123 two: true
@@ -136,7 +125,7 @@ describe('ReactTransitionKeySet', function() {
125
126 var next = undefined;
127
139 - expect(ReactTransitionKeySet.mergeKeySets(prev, next)).toEqual({
128 + expect(ReactTransitionChildMapping.mergeChildMappings(prev, next)).toEqual({
129 one: true,
130 two: true
131 });
@@ -148,7 +137,7 @@ describe('ReactTransitionKeySet', function() {
137 four: true
138 };
139
151 - expect(ReactTransitionKeySet.mergeKeySets(prev, next)).toEqual({
140 + expect(ReactTransitionChildMapping.mergeChildMappings(prev, next)).toEqual({
141 three: true,
142 four: true
143 });
src/addons/transitions/__tests__/ReactTransitionGroup-test.js
+166 -91
@@ -36,102 +36,177 @@ describe('ReactTransitionGroup', function() {
36 container = document.createElement('div');
37 });
38
39 - it('should warn after time with no transitionend', function() {
40 - var a = React.renderComponent(
41 - <ReactTransitionGroup transitionName="yolo">
42 - <span key="one" id="one" />
43 - </ReactTransitionGroup>,
44 - container
45 - );
46 - expect(a.getDOMNode().childNodes.length).toBe(1);
47 -
48 - setTimeout.mock.calls.length = 0;
49 -
50 - React.renderComponent(
51 - <ReactTransitionGroup transitionName="yolo">
52 - <span key="two" id="two" />
53 - </ReactTransitionGroup>,
54 - container
55 - );
56 - expect(a.getDOMNode().childNodes.length).toBe(2);
57 - expect(a.getDOMNode().childNodes[0].id).toBe('two');
58 - expect(a.getDOMNode().childNodes[1].id).toBe('one');
59 -
60 - console.warn = mocks.getMockFunction();
61 - setTimeout.mock.calls[2][0]();
62 -
63 - expect(a.getDOMNode().childNodes.length).toBe(2);
64 - expect(console.warn.mock.calls.length).toBe(1);
65 - });
39
67 - it('should keep both sets of DOM nodes around', function() {
68 - var a = React.renderComponent(
69 - <ReactTransitionGroup transitionName="yolo">
70 - <span key="one" id="one" />
71 - </ReactTransitionGroup>,
72 - container
73 - );
74 - expect(a.getDOMNode().childNodes.length).toBe(1);
75 - React.renderComponent(
76 - <ReactTransitionGroup transitionName="yolo">
77 - <span key="two" id="two" />
78 - </ReactTransitionGroup>,
79 - container
80 - );
81 - expect(a.getDOMNode().childNodes.length).toBe(2);
82 - expect(a.getDOMNode().childNodes[0].id).toBe('two');
83 - expect(a.getDOMNode().childNodes[1].id).toBe('one');
84 - });
40 + it('should handle willEnter correctly', function() {
41 + var log = [];
42 +
43 + var Child = React.createClass({
44 + componentDidMount: function() {
45 + log.push('didMount');
46 + },
47 + componentWillEnter: function(cb) {
48 + log.push('willEnter');
49 + cb();
50 + },
51 + componentDidEnter: function() {
52 + log.push('didEnter');
53 + },
54 + componentWillLeave: function(cb) {
55 + log.push('willLeave');
56 + cb();
57 + },
58 + componentDidLeave: function() {
59 + log.push('didLeave');
60 + },
61 + componentWillUnmount: function() {
62 + log.push('willUnmount');
63 + },
64 + render: function() {
65 + return <span />;
66 + }
67 + });
68
86 - it('should switch transitionLeave from false to true', function() {
87 - var a = React.renderComponent(
88 - <ReactTransitionGroup
89 - transitionName="yolo"
90 - transitionEnter={false}
91 - transitionLeave={false}>
92 - <span key="one" id="one" />
93 - </ReactTransitionGroup>,
94 - container
95 - );
96 - expect(a.getDOMNode().childNodes.length).toBe(1);
97 - React.renderComponent(
98 - <ReactTransitionGroup
99 - transitionName="yolo"
100 - transitionEnter={false}
101 - transitionLeave={false}>
102 - <span key="two" id="two" />
103 - </ReactTransitionGroup>,
104 - container
105 - );
106 - expect(a.getDOMNode().childNodes.length).toBe(1);
107 - React.renderComponent(
108 - <ReactTransitionGroup
109 - transitionName="yolo"
110 - transitionEnter={false}
111 - transitionLeave={true}>
112 - <span key="three" id="three" />
113 - </ReactTransitionGroup>,
114 - container
115 - );
116 - expect(a.getDOMNode().childNodes.length).toBe(2);
117 - expect(a.getDOMNode().childNodes[0].id).toBe('three');
118 - expect(a.getDOMNode().childNodes[1].id).toBe('two');
69 + var Component = React.createClass({
70 + getInitialState: function() {
71 + return {count: 1};
72 + },
73 + render: function() {
74 + var children = [];
75 + for (var i = 0; i < this.state.count; i++) {
76 + children.push(<Child key={i} />);
77 + }
78 + return <ReactTransitionGroup>{children}</ReactTransitionGroup>;
79 + }
80 + });
81 +
82 + var instance = React.renderComponent(<Component />, container);
83 + expect(log).toEqual(['didMount']);
84 +
85 + instance.setState({count: 2}, function() {
86 + expect(log).toEqual(['didMount', 'didMount', 'willEnter', 'didEnter']);
87 + instance.setState({count: 1}, function() {
88 + expect(log).toEqual([
89 + "didMount", "didMount", "willEnter", "didEnter",
90 + "willLeave", "didLeave", "willUnmount"
91 + ]);
92 + });
93 + });
94 });
95
121 - it('should work with no children', function () {
122 - React.renderComponent(
123 - <ReactTransitionGroup transitionName="yolo">
124 - </ReactTransitionGroup>,
125 - container
126 - );
96 + it('should handle enter/leave/enter/leave correctly', function() {
97 + var log = [];
98 + var cb;
99 +
100 + var Child = React.createClass({
101 + componentDidMount: function() {
102 + log.push('didMount');
103 + },
104 + componentWillEnter: function(_cb) {
105 + log.push('willEnter');
106 + cb = _cb;
107 + },
108 + componentDidEnter: function() {
109 + log.push('didEnter');
110 + },
111 + componentWillLeave: function(cb) {
112 + log.push('willLeave');
113 + cb();
114 + },
115 + componentDidLeave: function() {
116 + log.push('didLeave');
117 + },
118 + componentWillUnmount: function() {
119 + log.push('willUnmount');
120 + },
121 + render: function() {
122 + return <span />;
123 + }
124 + });
125 +
126 + var Component = React.createClass({
127 + getInitialState: function() {
128 + return {count: 1};
129 + },
130 + render: function() {
131 + var children = [];
132 + for (var i = 0; i < this.state.count; i++) {
133 + children.push(<Child key={i} />);
134 + }
135 + return <ReactTransitionGroup>{children}</ReactTransitionGroup>;
136 + }
137 + });
138 +
139 + var instance = React.renderComponent(<Component />, container);
140 + expect(log).toEqual(['didMount']);
141 + instance.setState({count: 2});
142 + expect(log).toEqual(['didMount', 'didMount', 'willEnter']);
143 + for (var i = 0; i < 5; i++) {
144 + instance.setState({count: 2});
145 + expect(log).toEqual(['didMount', 'didMount', 'willEnter']);
146 + instance.setState({count: 1});
147 + }
148 + cb();
149 + expect(log).toEqual([
150 + 'didMount', 'didMount', 'willEnter',
151 + 'didEnter', 'willLeave', 'didLeave', 'willUnmount'
152 + ]);
153 });
154
129 - it('should work with a null child', function () {
130 - React.renderComponent(
131 - <ReactTransitionGroup transitionName="yolo">
132 - {[null]}
133 - </ReactTransitionGroup>,
134 - container
135 - );
155 + it('should handle enter/leave/enter correctly', function() {
156 + var log = [];
157 + var cb;
158 +
159 + var Child = React.createClass({
160 + componentDidMount: function() {
161 + log.push('didMount');
162 + },
163 + componentWillEnter: function(_cb) {
164 + log.push('willEnter');
165 + cb = _cb;
166 + },
167 + componentDidEnter: function() {
168 + log.push('didEnter');
169 + },
170 + componentWillLeave: function(cb) {
171 + log.push('willLeave');
172 + cb();
173 + },
174 + componentDidLeave: function() {
175 + log.push('didLeave');
176 + },
177 + componentWillUnmount: function() {
178 + log.push('willUnmount');
179 + },
180 + render: function() {
181 + return <span />;
182 + }
183 + });
184 +
185 + var Component = React.createClass({
186 + getInitialState: function() {
187 + return {count: 1};
188 + },
189 + render: function() {
190 + var children = [];
191 + for (var i = 0; i < this.state.count; i++) {
192 + children.push(<Child key={i} />);
193 + }
194 + return <ReactTransitionGroup>{children}</ReactTransitionGroup>;
195 + }
196 + });
197 +
198 + var instance = React.renderComponent(<Component />, container);
199 + expect(log).toEqual(['didMount']);
200 + instance.setState({count: 2});
201 + expect(log).toEqual(['didMount', 'didMount', 'willEnter']);
202 + for (var i = 0; i < 5; i++) {
203 + instance.setState({count: 1});
204 + expect(log).toEqual(['didMount', 'didMount', 'willEnter']);
205 + instance.setState({count: 2});
206 + }
207 + cb();
208 + expect(log).toEqual([
209 + 'didMount', 'didMount', 'willEnter', 'didEnter'
210 + ]);
211 });
212 });
src/addons/transitions/__tests__/ReactTransitionableChild-test.js deleted
-104
@@ -1,104 +0,0 @@
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 - * @jsx React.DOM
17 - * @emails react-core
18 - */
19 -
20 -"use strict";
21 -
22 -var React;
23 -var ReactTransitionableChild;
24 -
25 -var mocks = require('mocks');
26 -
27 -describe('ReactTransitionableChild', function() {
28 - beforeEach(function() {
29 - React = require('React');
30 - ReactTransitionableChild = require('ReactTransitionableChild');
31 - });
32 -
33 - it('should keep the DOM node around', function() {
34 - var container = document.createElement('div');
35 - var ac = React.renderComponent(
36 - <ReactTransitionableChild><span id="test" /></ReactTransitionableChild>,
37 - container
38 - );
39 - expect(ac.getDOMNode().id).toBe('test');
40 - ac = React.renderComponent(<ReactTransitionableChild />, container);
41 - expect(ac.getDOMNode().id).toBe('test');
42 - });
43 -
44 - it('should manage enter css classes correctly', function() {
45 - var runNextTick = mocks.getMockFunction();
46 - var container = document.createElement('div');
47 - var ac = React.renderComponent(
48 - <ReactTransitionableChild
49 - runNextTick={runNextTick}
50 - name="myanim"
51 - enter={true}>
52 - <span id="test" />
53 - </ReactTransitionableChild>,
54 - container
55 - );
56 - expect(ac.getDOMNode().id).toBe('test');
57 - expect(ac.getDOMNode().className.trim()).toBe('myanim-enter');
58 - expect(runNextTick.mock.calls.length).toBe(1);
59 - runNextTick.mock.calls[0][0]();
60 - expect(ac.getDOMNode().className.trim()).toBe(
61 - 'myanim-enter myanim-enter-active'
62 - );
63 - expect(runNextTick.mock.calls.length).toBe(1);
64 - });
65 -
66 - it('should manage leave css classes correctly', function() {
67 - var runNextTick = mocks.getMockFunction();
68 - var container = document.createElement('div');
69 - var ac = React.renderComponent(
70 - <ReactTransitionableChild
71 - runNextTick={runNextTick}
72 - name="myanim"
73 - enter={true}
74 - leave={true}>
75 - <span id="test" />
76 - </ReactTransitionableChild>,
77 - container
78 - );
79 - runNextTick.mock.calls[0][0]();
80 - expect(ac.getDOMNode().className.trim()).toBe(
81 - 'myanim-enter myanim-enter-active'
82 - );
83 -
84 - // TODO: we should just trigger the CSS animation end event to
85 - // clean these up
86 - ac.getDOMNode().className = '';
87 - React.renderComponent(
88 - <ReactTransitionableChild
89 - runNextTick={runNextTick}
90 - name="myanim"
91 - enter={true}
92 - leave={true}
93 - />,
94 - container
95 - );
96 - expect(ac.getDOMNode().className.trim()).toBe('myanim-leave');
97 - expect(runNextTick.mock.calls.length).toBe(2);
98 - runNextTick.mock.calls[1][0]();
99 - expect(ac.getDOMNode().className.trim()).toBe(
100 - 'myanim-leave myanim-leave-active'
101 - );
102 - expect(runNextTick.mock.calls.length).toBe(2);
103 - });
104 -});