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:
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?
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,