reactjsreact-motion

Animating exit of an element in React Motion (which receives these elements from a parent component)


Background I am trying to create a container for a collection of elements, each of which can be removed from the collection. When an element is removed, I want to animate its exit, and I am trying to achieve this using React Motion.

Here's a diagram of the above:

enter image description here

Problem I thought of using React Motion's TransitionMotion component, and got stuck trying to write a function that needs to be passed to it. Here is my — incorrect — code:

class Container extends Component {

  state = {
    elementStyles: {}
  }

  componentDidMount() {
    this.getElementStyles();
  }

  componentDidUpdate() {
    this.getElementStyles();
  }

  getElementStyles() {
    if (!this.props.children.length || this.hasElementStyles()) return;

    // this assumes all elements passed to the container are of equal dimensions
    let firstChild = this.refs.scroller.firstChild;

    let elementStyles = {
      width: firstChild.offsetWidth,
      height: firstChild.offsetHeight,
      opacity: 1
    };

    this.setState({
      elementStyles
    });
  }


  hasElementStyles() {
    return !isEmpty(this.state.elementStyles); // lodash to the rescue
  }

  willLeave() {
    return { width: spring(0), height: spring(0), opacity: spring(0) }
  }

  getChildrenArray() {
    return Children.toArray(this.props.children); // that's React's util function
  }

  getModifiedChild(element, props) {
    if (!element) return;

    return React.cloneElement(
      element,
      {style: props.style}
    );
  }

  getInitialTransitionStyles() {
    let elements = this.getChildrenArray();
    let result = elements.map((element, index) => ({
      key: element.key,
      style: this.state.elementStyles
    }));
    return result;
  }

  render() {
    if (this.hasElementStyles()) {

      return (
        <TransitionMotion
          willLeave={this.willLeave}
          styles={this.getInitialTransitionStyles()}
        >
            { interpolatedStyles => {
              let children = this.getChildrenArray();
              return <div ref="scroller" className="container">
                { interpolatedStyles.map((style, index) => {
                    return this.getModifiedChild(children[index], style);
                  })
                }
              </div>
            }}
        </TransitionMotion>
      );
    } else {
      return (
        <div ref="scroller" className="container">
          { this.props.children }
        </div>
      );
    }
  }

}

Notice this line inside the map function in the TransitionMotion component: return this.getModifiedChild(children[index], style). It is wrong, because once an element is removed from the collection, this.props.children will change, and indices of the children array will no longer correspond to the indices of the styles array calculated from those children.

So I what I need is either some clever way to track the props.children before and after an element has been removed from the collection (and I can't think of one; the best I could come up with was using a find function on the array return this.getModifiedChild(children.find(child => child.key === style.key), style);, but that will result in so many loops within loops I am scared even to think about it), or to use some completely different approach, which I am not aware of. Could you please help?


Solution

  • This solution's delete animation slides the deleted item (and the rest of the "cards" in the row) horizontally left beneath the card on the left (using it's lower z-index).

    The initial render sets up the < Motion/> tag and the cards display as a row.

    Each card shares a delete button method which setStates the index of the array element and starts a timer that slices the array. The setState triggers the render which animates the cards before the timer does the slice.

    class CardRow extends React.PureComponent {
    
        constructor(props) {
            super(props);
            this.state = {
                cardToRemove: 99,
            };
        }
    
        findCard = (_id) => {
            return this.myArray.find((_card) => {
                return _card.id === _id;
            });
        };
    
        removeCardClick = (evt) => {
            const _card = this.findCard(evt.target.id);
    
            setTimeout(() => {  // wait for the render...
                this.myArray.splice(this.myArray.indexOf(_card), 1);
                this.setState({cardToRemove: 99});
            }, 200);
    
            this.setState({cardToRemove: this.myArray.indexOf(_card)});
        };
    
        render() {
            let items = this.myArray.map((item, index) => {
    
                let itemLeft = 200 * index;     // card width 200px
                if (this.state.cardToRemove <= index) {
                    itemLeft = 200 * (index - 1);
                }
    
                // .cardContainer {position: fixed}
                return <div key={item.id}>
                    <Motion style={{left: spring(itemLeft)}}>
                        {({left}) =>
                            <div className="cardContainer" style={{left: left}}>
                                <div className="card">
                                    <button className="Button" id={item.id} onClick={this.removeCardClick}>Del</button>
                                </div>
                            </div>
                        }
                    </Motion>
                </div>;
            });
    
            items = items.reverse();
    
            return <div>
                {items}
            </div>;
        }
    }

    That's about it! Thanks and enjoy,