2

I'm having problems with complex animations, where one component has to finish animating before another. In this example, I'm trying to fade out a component before another component is faded in. I can't use react-motion or any third party library, and can't rely on css transitions. Here is a working example highlighting the problem. Please note that the 'Editor' and 'Display' components aren't always of the same height.

Javascript

var Editor = React.createClass({
    render: function() {
        return <input type="text" defaultValue={this.props.name} />
    }
});

var Display = React.createClass({
    render: function() {
        return <div>{this.props.name}</div>;
    }
});

var Row = React.createClass({
        getInitialState: function() {
            return {
                isEditing: false
      }
    },
    updateRow: function() {
            this.setState({isEditing: !this.state.isEditing});
    },
    render: function() {
        return (
            <div className="row" onClick={this.updateRow}>
            <React.addons.TransitionGroup>
            {
            this.state.isEditing ?
                <Fade key="e"><Editor name={this.props.name}/></Fade> :
                <Fade key="d"><Display name={this.props.name}/></Fade>
            }
            </React.addons.TransitionGroup>
        </div>);
    }
});

var Table = React.createClass({
    render: function() {
        return (

            <div className="row" onClick={this.updateRow}>
              <Row name="One" />
                <Row name="Two" />
                <Row name="Three" />
                <Row name="Four" />
            </div>);
    }
});

var Fade = React.createClass({
  componentWillEnter: function(callback) {
      var container = $(React.findDOMNode(this.refs.fade));
        container.animate({
        opacity:1
      }, callback);
  },
  componentWillLeave: function(callback) {
        var container = $(React.findDOMNode(this.refs.fade));
        container.animate({
        opacity:0
      }, callback);
  },
  render: function() {
    return(<div className="fade" ref="fade">
        {this.props.children}
    </div>)
  }
});

ReactDOM.render(
    <Table />,
    document.getElementById('container')
);

CSS

.row {
  background-color: #c9c9c9;
  border-bottom: 1px solid #dedede;
  padding: 5px;
  color: gray;
  cursor:pointer;
}

HTML

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

<script src="https://facebook.github.io/react/js/jsfiddle-integration-babel.js">
</script>


<div id="container">
</div>

1 Answer 1

7

Really, something like React Motion is the answer here, because it implements the functionality you want. However, it's certainly possible to implement it yourself. I'll overview a similar effect I created that demonstrates some techniques, then apply it to your specific code at the end.

The effect I implemented (fading children in or out one-at-a-time) was created by using a couple components:

  • StaggerIn - a component that fades its children in or out one-by-one, staggering the animations by props.delay milliseconds. This component is implemented with a TransitionGroup and wrapping the children in a StaggeringChild.
  • StaggeringChild - a component that implements React's TransitionGroup callbacks to do the actual animation.

When rendering, wrapping the children in a StaggerIn component triggers the effect:

if (this.state.active) {
  return (
    <StaggerIn delay={100}>
      <div key="one">One</div>
      <div key="two">Two</div>
      <div key="three">Three</div>
      <div key="four">Four</div>
      <div key="five">Five</div>
      <div key="six">Six</div>
      <div key="seven">Seven</div>
    </StaggerIn>
  );
} else {
  return <StaggerIn delay={100} />;
}

To make the staggering work, StaggerIn counts the number of children, and determines the appropriate delay by determining the index of each child (multiplied by the delay value):

var StaggerIn = React.createClass({
  render: function() {
    var childCount = React.Children.count(this.props.children);
    var children = React.Children.map(this.props.children, function(child, idx) {
      var inDelay = this.props.delay * idx;
      var outDelay = this.props.delay * (childCount - idx - 1);
      return (
        <StaggeringChild key={child.key}
                         animateInDelay={inDelay}
                         animateOutDelay={outDelay}>
          {child}
        </StaggeringChild>
      );
    }.bind(this));

    return (
      <React.addons.TransitionGroup>
        {children}
      </React.addons.TransitionGroup>
    );
  }
});

As mentioned, StaggerChild actually does the animation; here I'm using the TweenLite animation library in _animateIn and _animateOut, but jQuery animations and the like should work fine as well:

var StaggeringChild = React.createClass({
  getDefaultProps: function() {
    return {
      tag: "div"
    };
  },

  componentWillAppear: function(callback) {
    this._animateIn(callback);
  },

  componentWillEnter: function(callback) {
    this._animateIn(callback);
  },

  componentWillLeave: function(callback) {
    this._animateOut(callback);
  },

  _animateIn(callback) {
    var el = React.findDOMNode(this);
    TweenLite.set(el, {opacity: 0});
    setTimeout(function() {
      console.log("timed in");
      TweenLite.to(el, 1, {opacity: 1}).play().eventCallback("onComplete", callback);
    }, this.props.animateInDelay);
  },

  _animateOut(callback) {
    var el = React.findDOMNode(this);
    setTimeout(function() {
      TweenLite.to(el, 1, {opacity: 0}).play().eventCallback("onComplete", callback);
    }, this.props.animateOutDelay);
  },

  render: function() {
    var Comp = this.props.tag;
    var { tag, animateInDelay, animateOutDelay, ...props } = this.props;

    return <Comp {...props}>{this.props.children}</Comp>;
  }
});

Here's a JSFiddle showing the completed effect: http://jsfiddle.net/BinaryMuse/s2z0vmcn/


The key to making all this work is calculating the appropriate timeout value before you start to animate in or out. In your case, it's easy: you know you have exactly two items to animate, and you always want to fade out the one leaving before you fade in the one appearing.

First, let's specify a default property for a new prop called time that will specify how long the animation should take (since we'll need to know how long to wait):

var Fade = React.createClass({
  getDefaultProps: function() {
    return { time: 400 };
  },

  // ...
});

Next, we'll modify the animation methods so that leaving happens immediately, but appearing waits this.props.time milliseconds so that the leaving has time to finish first.

var Fade = React.createClass({
  // ...

  // no change to this function
  componentWillLeave: function(callback) {
    var container = $(React.findDOMNode(this.refs.fade));
    container.animate({
      opacity:0
    }, this.props.time, callback);
  },

  componentWillEnter: function(callback) {
    var container = $(React.findDOMNode(this.refs.fade));
    // hide element immediately
    container.css({opacity: 0});
    // wait until the leave animations finish before fading in
    setTimeout(function() {
      container.animate({
        opacity:1
      }, this.props.time, callback);
    }.bind(this), this.props.time);
  },

  // ...
});

With those changes, the item that's disappearing will animate out before the item that's appearing animates in. There's a bit of jumpiness because of the way the DOM works (crossfading elements is notoriously difficult) that will be left as an exercise to the reader. :)

Here's a working JSFiddle with the completed code: https://jsfiddle.net/BinaryMuse/xfz3seyc/

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks Michelle :)! I've been struggling with this problem for a while. Question, though, aren't timer delays not guaranteed? Is it possibly that the queue could have fadeOut - 400ms; animating height of parent - 600ms; fadeIn - 400ms; Won't the animation of the parent cause a delay to the fadeIn?
@user1036767 In the example I gave, I don't think so — depends on how the height of the parent is animated. If I wanted to explicitly control that, I would encapsulate the entire transition queue, including the animation of the parent's height, into a single component that is "intelligent" about all the steps (similar to the way StaggerIn encapsulates logic for the stagger animation).
Perfect, thanks for all your help! I'll mark yours as the answer.
thanks for the awesome hints, IMHO to progress on coding and deploy creatives inventions, one should learn to do it himself, then use the machine to assist the process because he want it instead of being dependent of it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.