Skip to content

Commit

Permalink
Rethink animation conditions
Browse files Browse the repository at this point in the history
One problem with the original implementation is it didn't handle
items entering/leaving the DOM very gracefully. Items on enter would
animate, often from the wrong position. Items on leave could throw
errors.

I refactored the on-update logic to skip animation on a specific
child if that child:
* Was stationary (untracked)
* Was brand new (no history)
* Was just destroyed.

I also made some small improvements, such as batching all .setState
requests instead of making several individual ones.
  • Loading branch information
joshwcomeau committed Jan 31, 2016
1 parent 2ce2745 commit 076c6eb
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 12 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-flip-move",
"version": "0.1.1",
"version": "0.1.2",
"description": "Effortless animation between DOM changes (eg. list reordering) using the FLIP technique.",
"main": "lib/index.js",
"files": [
Expand Down
56 changes: 45 additions & 11 deletions src/FlipMove.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,62 @@
/**
* React Flip Move
* Automagically animate the transition when the DOM gets reordered.
*
* How it works:
* The basic idea with this component is pretty straightforward:
*
* - We track all rendered elements by their `key` property, and we keep
* their bounding boxes (their top/left/right/bottom coordinates) in this
* component's state.
* - When the component updates, we compare its former position (held in
* state) with its new position (derived from the DOM after update).
* - If the two have moved, we use the FLIP technique to animate the
* transition between their positions.
*/

import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';


// Technique
class FlipMove extends Component {
componentWillReceiveProps(nextProps) {
this.props.children.forEach(child => {
componentWillReceiveProps() {
// Get the bounding boxes of all currently-rendered, keyed children.
// Store it in this.state.
const newState = this.props.children.reduce( (state, child) => {
// It is possible that a child does not have a `key` property;
// Ignore these children, they don't need to be moved.
if ( !child.key ) return;
if ( !child.key ) return state;

const domNode = ReactDOM.findDOMNode( this.refs[child.key] );
const boundingBox = domNode.getBoundingClientRect();

this.setState({ [child.key]: boundingBox });
});
return { ...state, [child.key]: boundingBox };
}, {});

this.setState(newState);
}

componentDidUpdate(prevProps) {
// If we haven't assigned any keys to state yet, it's the first render.
// The first render cannot possibly have any animations. No work needed.
componentDidUpdate(previousProps) {
// Re-calculate the bounding boxes of tracked elements.
// Compare to the bounding boxes stored in state.
// Animate as required =)

// On the very first render, `componentWillReceiveProps` is not called.
// This means that `this.state` will be undefined.
// That's alright, though, because there is no possible transition on
// the first render; we only animate transitions between states =)
if ( !this.state ) return;

this.props.children.forEach(child => {
if ( !child.key ) return;
previousProps.children.forEach(child => {
// We only want to animate if:
// * The child has an associated key (stationary children are supported)
// * The child still exists in the DOM.
// * The child isn't brand new.
const isStationary = !child.key;
const isBrandNew = !this.state[child.key];
const isDestroyed = !this.refs[child.key];

if ( isStationary || isBrandNew || isDestroyed ) return;

// The new box can be calculated from the current DOM state.
// The old box was stored in this.state when the component received props.
Expand Down

0 comments on commit 076c6eb

Please sign in to comment.