Skip to content

Accommodate interruption of transitions in ReactTransitionGroup #5989

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 101 additions & 95 deletions src/addons/transitions/ReactTransitionGroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
var React = require('React');
var ReactTransitionChildMapping = require('ReactTransitionChildMapping');

var assign = require('Object.assign');
var emptyFunction = require('emptyFunction');

var ReactTransitionGroup = React.createClass({
Expand All @@ -33,138 +32,151 @@ var ReactTransitionGroup = React.createClass({
},

getInitialState: function() {
// children - the set of children that we are trying to acheive
// childrenToRender - expresses our current state and is what we actually render
return {
children: ReactTransitionChildMapping.getChildMapping(this.props.children),
childrenToRender: {},
};
},

componentWillMount: function() {
this.currentlyTransitioningKeys = {};
this.keysToEnter = [];
this.keysToLeave = [];
this.actionsToPerform = {};
this.setState({
childrenToRender: this.updatechildrenToRender(this.state.children),
});
},

componentDidMount: function() {
var initialChildMapping = this.state.children;
for (var key in initialChildMapping) {
if (initialChildMapping[key]) {
this.performAppear(key);
}
}
this.performchildrenToRenderActions(true);
},

componentWillReceiveProps: function(nextProps) {
var nextChildMapping = ReactTransitionChildMapping.getChildMapping(
nextProps.children
);
var prevChildMapping = this.state.children;
var nextChildMapping = ReactTransitionChildMapping.getChildMapping(nextProps.children);

var nextchildrenToRender = this.updatechildrenToRender(nextChildMapping);

this.setState({
children: ReactTransitionChildMapping.mergeChildMappings(
prevChildMapping,
nextChildMapping
),
children: nextChildMapping,
childrenToRender: nextchildrenToRender,
});
},

var key;
componentDidUpdate: function() {
this.performchildrenToRenderActions();
},

for (key in nextChildMapping) {
var hasPrev = prevChildMapping && prevChildMapping.hasOwnProperty(key);
if (nextChildMapping[key] && !hasPrev &&
!this.currentlyTransitioningKeys[key]) {
this.keysToEnter.push(key);
updatechildrenToRender: function(newchildren) {
newchildren = newchildren || {};
var childrenToRender = this.state.childrenToRender;
var nextActionsToPerform = {};

// Find new children and add
for (var key in newchildren) {

if (childrenToRender[key]) {
// Already exists

// Exists but was on it's way out. Let's interrupt
if (!childrenToRender[key].shouldBeInDOM) {
childrenToRender[key].shouldBeInDOM = true;
// Queue action to be performed during componentDidUpdate
nextActionsToPerform[key] = childrenToRender[key];
}
} else {
// Is new
childrenToRender[key] = {
child: newchildren[key],
shouldBeInDOM: true,
};
// Queue action to be performed during componentDidUpdate
nextActionsToPerform[key] = childrenToRender[key];
}
}

for (key in prevChildMapping) {
var hasNext = nextChildMapping && nextChildMapping.hasOwnProperty(key);
if (prevChildMapping[key] && !hasNext &&
!this.currentlyTransitioningKeys[key]) {
this.keysToLeave.push(key);
}
}
// Find nodes that should longer exist, mark for removal
var childrenKeys = Object.keys(newchildren);
var keysForRemoval = Object.keys(childrenToRender).filter(function(k) {
return childrenKeys.indexOf(k) < 0;
});
keysForRemoval.forEach(function(keyToRemove) {
childrenToRender[keyToRemove].shouldBeInDOM = false;
// Queue action to be performed during componentDidUpdate
nextActionsToPerform[keyToRemove] = childrenToRender[keyToRemove];
});

this.actionsToPerform = nextActionsToPerform;

// If we want to someday check for reordering, we could do it here.

return childrenToRender;
},

componentDidUpdate: function() {
var keysToEnter = this.keysToEnter;
this.keysToEnter = [];
keysToEnter.forEach(this.performEnter);

var keysToLeave = this.keysToLeave;
this.keysToLeave = [];
keysToLeave.forEach(this.performLeave);

performchildrenToRenderActions: function(isInitialMount) {
for (var key in this.actionsToPerform) {
if (this.actionsToPerform[key].shouldBeInDOM) {
if (isInitialMount) {
this.performAppear(key);
} else {
this.performEnter(key);
}
} else {
this.performLeave(key);
}
}
// Reset actions since we've performed all of them.
this.actionsToPerform = {};
},

performAppear: function(key) {
this.currentlyTransitioningKeys[key] = true;

var component = this.refs[key];

if (component.componentWillAppear) {
component.componentWillAppear(
this._handleDoneAppearing.bind(this, key)
);
component.componentWillAppear(this._handleDoneAppearing.bind(this, key));
} else {
this._handleDoneAppearing(key);
}
},

_handleDoneAppearing: function(key) {
if (!this.state.childrenToRender[key].shouldBeInDOM) {
// Ignore this callback if the component should now be in the DOM
return;
}

var component = this.refs[key];

if (component.componentDidAppear) {
component.componentDidAppear();
}

delete this.currentlyTransitioningKeys[key];

var currentChildMapping = ReactTransitionChildMapping.getChildMapping(
this.props.children
);

if (!currentChildMapping || !currentChildMapping.hasOwnProperty(key)) {
// This was removed before it had fully appeared. Remove it.
this.performLeave(key);
}
},

performEnter: function(key) {
this.currentlyTransitioningKeys[key] = true;

var component = this.refs[key];

if (component.componentWillEnter) {
component.componentWillEnter(
this._handleDoneEntering.bind(this, key)
);
component.componentWillEnter(this._handleDoneEntering.bind(this, key));
} else {
this._handleDoneEntering(key);
}
},

_handleDoneEntering: function(key) {
var component = this.refs[key];
if (component.componentDidEnter) {
component.componentDidEnter();
if (!this.state.childrenToRender[key].shouldBeInDOM) {
// Ignore this callback if the component should no longer be in the DOM
return;
}

delete this.currentlyTransitioningKeys[key];

var currentChildMapping = ReactTransitionChildMapping.getChildMapping(
this.props.children
);
var component = this.refs[key];

if (!currentChildMapping || !currentChildMapping.hasOwnProperty(key)) {
// This was removed before it had fully entered. Remove it.
this.performLeave(key);
if (component.componentDidEnter) {
component.componentDidEnter();
}
},

performLeave: function(key) {
this.currentlyTransitioningKeys[key] = true;

var component = this.refs[key];

if (component.componentWillLeave) {
component.componentWillLeave(this._handleDoneLeaving.bind(this, key));
} else {
Expand All @@ -176,52 +188,46 @@ var ReactTransitionGroup = React.createClass({
},

_handleDoneLeaving: function(key) {
if (this.state.childrenToRender[key].shouldBeInDOM) {
return;
}

var component = this.refs[key];

if (component.componentDidLeave) {
component.componentDidLeave();
}

delete this.currentlyTransitioningKeys[key];

var currentChildMapping = ReactTransitionChildMapping.getChildMapping(
this.props.children
);

if (currentChildMapping && currentChildMapping.hasOwnProperty(key)) {
// This entered again before it fully left. Add it again.
this.performEnter(key);
} else {
this.setState(function(state) {
var newChildren = assign({}, state.children);
delete newChildren[key];
return {children: newChildren};
});
}
var newChildrenToRender = this.state.childrenToRender;
delete newChildrenToRender[key];
this.setState({
childrenToRender: newChildrenToRender,
});
},

render: function() {
// 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];
for (var key in this.state.childrenToRender) {
var child = this.state.childrenToRender[key].child;
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.push(React.cloneElement(
this.props.childFactory(child),
{ref: key, key: key}
this.props.childFactory(child),
{ ref: key, key: key }
));
}
}

return React.createElement(
this.props.component,
this.props,
childrenToRender
childrenToRender,
);
},
});
Expand Down
14 changes: 7 additions & 7 deletions src/addons/transitions/__tests__/ReactCSSTransitionGroup-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe('ReactCSSTransitionGroup', function() {
var container;

beforeEach(function() {
jest.resetModuleRegistry();
require('mock-modules').dumpCache();
React = require('React');
ReactDOM = require('ReactDOM');
ReactCSSTransitionGroup = require('ReactCSSTransitionGroup');
Expand Down Expand Up @@ -90,8 +90,8 @@ describe('ReactCSSTransitionGroup', function() {
container
);
expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(2);
expect(ReactDOM.findDOMNode(a).childNodes[0].id).toBe('two');
expect(ReactDOM.findDOMNode(a).childNodes[1].id).toBe('one');
expect(ReactDOM.findDOMNode(a).childNodes[0].id).toBe('one');
expect(ReactDOM.findDOMNode(a).childNodes[1].id).toBe('two');

// For some reason jst is adding extra setTimeout()s and grunt test isn't,
// so we need to do this disgusting hack.
Expand Down Expand Up @@ -125,8 +125,8 @@ describe('ReactCSSTransitionGroup', function() {
container
);
expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(2);
expect(ReactDOM.findDOMNode(a).childNodes[0].id).toBe('two');
expect(ReactDOM.findDOMNode(a).childNodes[1].id).toBe('one');
expect(ReactDOM.findDOMNode(a).childNodes[0].id).toBe('one');
expect(ReactDOM.findDOMNode(a).childNodes[1].id).toBe('two');
});

it('should switch transitionLeave from false to true', function() {
Expand Down Expand Up @@ -160,8 +160,8 @@ describe('ReactCSSTransitionGroup', function() {
container
);
expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(2);
expect(ReactDOM.findDOMNode(a).childNodes[0].id).toBe('three');
expect(ReactDOM.findDOMNode(a).childNodes[1].id).toBe('two');
expect(ReactDOM.findDOMNode(a).childNodes[0].id).toBe('two');
expect(ReactDOM.findDOMNode(a).childNodes[1].id).toBe('three');
});

it('should work with no children', function() {
Expand Down
Loading