react transition group
Created: 2018-03-30 11:15:53 -0700 Modified: 2018-03-30 14:29:21 -0700
Using this to animate width
Section titled Using this to animate widthThis talks about a specific case that I had in Bot Land where I wanted to animate the width on a mission’s progress bar and then do something only when that was complete.
At first, I thought to use CSS transitions, but that could be cumbersome for several reasons:
- Differentiating between various transitions that happen
- Having to add/remove event handlers myself
If you do want to use the CSS way of handling animations, I’ve got some code here that I am going to delete from my codebase since I use React now:
/** * Adds a transitionend handler in a browser-agnostic way to a DOM element. * @param {DOMElement} element */export function addTransitionEndToElement(element, handler) { const transitionEndEventName = getTransitionEndEventName();
if (_.isString(transitionEndEventName)) { element.addEventListener(transitionEndEventName, handler, false); } else { setTimeout(handler, 500); }}
export function removeTransitionEndFromElement(element, handler) { const transitionEndEventName = getTransitionEndEventName();
if (_.isString(transitionEndEventName)) { element.removeEventListener(transitionEndEventName, handler, false); }}
/** * @return {?string} the name of the transitionend event on this browser */function getTransitionEndEventName () { var i, undefined, el = document.createElement('div'), transitions = { 'transition':'transitionend', 'OTransition':'otransitionend', // oTransitionEnd in very old Opera 'MozTransition':'transitionend', 'WebkitTransition':'webkitTransitionEnd', };
for (i in transitions) { if (transitions.hasOwnProperty(i) && el.style[i] !== undefined) { return transitions[i]; } }
return null;}
Here’s the solution I came up with in React:
import React, { Component } from 'react'; // eslint-disable-line no-unused-varsimport {Row} from 'jsxstyle';import onlyUpdateForKeys from 'recompose/onlyUpdateForKeys';import Transition from 'react-transition-group/Transition';
const _ = require('lodash');const classNames = require('classnames');const styles = require('../../../stylesheets/main.less');
class ProgressBar extends Component {
constructor(props) { super(props);
this.state = { /** * This is simply needed to re-render the react-transition-group * component; it NEEDS to change from false to true if the animation * is going to take place. * @type {boolean} */ transitionInProp: false, }; }
renderBackground(state) { // When animationDuration is specified, it means that we don't want to // rely on a CSS class passed in for "transition" values and that we'll // add our own here. This boolean has a big enough impact where I should // ideally split this class into two classes: one that uses react- // transition-group for animation, and one that doesn't. const isAnimatingViaReactTransitionGroup = !_.isNil(this.props.animationDuration); const fillPercent = _.round(_.clamp(this.props.fillPercent, 0, 1), 2) * 100.0;
const endingFillPercent = _.round(_.clamp(_.defaultTo(this.props.endingFillPercent, this.props.fillPercent), 0, 1), 2) * 100.0;
const defaultStyle = { width: `${fillPercent}%`, };
if (isAnimatingViaReactTransitionGroup) { defaultStyle.transition = `width ${this.props.animationDuration}ms ease-in`; }
const transitionStyles = { entering: { width: `${endingFillPercent}%` }, entered: { width: `${endingFillPercent}%` }, };
const styleToUse = { ...defaultStyle, ...transitionStyles[state], };
const finalStyle = isAnimatingViaReactTransitionGroup ? styleToUse : defaultStyle;
return ( <span style={finalStyle} className={this.props.backgroundStyle}/> ); }
componentDidMount() { setTimeout(() => { this.setState({ transitionInProp: true, }); }, 500); }
onFinishAnimatingProgressBar() { if (_.isFunction(this.props.onFinishAnimatingProgressBar)) { this.props.onFinishAnimatingProgressBar(); } }
render() { const foregroundCss = classNames( styles.progressBarForeground, this.props.foregroundStyle );
return ( <Row className={this.props.containerStyle}> <Transition in={this.state.transitionInProp} timeout={this.props.animationDuration} onEntered={this.onFinishAnimatingProgressBar.bind(this)} > {this.renderBackground.bind(this)} </Transition> <Row alignItems='center' className={foregroundCss}> {this.props.children} </Row> </Row> ); }}
const enhance = onlyUpdateForKeys([ 'onFinishAnimatingProgressBar', 'fillPercent', 'endingFillPercent',
/** * The amount of ms it takes to animate CSS properties. * @type {number} */ 'animationDuration', 'containerStyle', 'backgroundStyle', 'foregroundStyle', 'children',]);
export default enhance(ProgressBar);
Things to note:
- I need to have the starting and ending values at the same time, otherwise I can’t apply the animation correctly since it needs to be done through JS (since the mission progress is dynamic).
- Note: if you always wanted to animate between static values, then you could still use this solution and just manually type “0%” and “100%” or something like that.
- I have a setTimeout on the “transitionInProp” so that the animations don’t start until the component has been mounted for at least 500 ms. I had tried instead to just change “endingFillPercent” after the component had already mounted, but that turned out to be a terrible idea because a CSS transition would have already kicked off (internally in react-transition-group) as soon as “transitionInProp” is true, so the “onEntered” callback would get hit way before the progress bar finished filling up if you changed the prop mid-animation.