1

My goal is to have a save indicator that flashes a save icon when data has just been saved (not while it's being saved), as an indication to the user that their edit was successful. React seems better suited towards state than one-off "actions," but this was the best I was able to come up with:

import React, { PureComponent, PropTypes } from 'react';
import Radium from 'radium';
import moment from 'moment';
import { ContentSave as SaveIcon } from 'material-ui/svg-icons';

class SaveIndicator extends PureComponent {
  getStyles = () => {
    if (!this.props.saving) return { opacity: 0 };

    return {
      animation: 'x 700ms ease 0s 3 normal forwards',
      animationName: saveAnimation,
    };
  };

  render() {
    return <div style={styles.root}>
      <div style={{ display: 'flex' }}>
        <div style={{ marginRight: 16 }}>
          Last saved {moment(this.props.lastSaved).fromNow()}
        </div>
        <div
          style={this.getStyles()}
          onAnimationEnd={() => this.props.onIndicationComplete()}
        >
          <SaveIcon color="#666" />
        </div>
      </div>
    </div>
  }
}

const saveAnimation = Radium.keyframes({
  '0%': { opacity: 0 },
  '50%': { opacity: 1 },
  '100%': { opacity: 0 },
});

const styles = {
  root: {
    display: 'inline-block',
  },
};

SaveIndicator.defaultProps = {
  saving: false,
};

SaveIndicator.propTypes = {
  lastSaved: PropTypes.object.isRequired,
  onIndicationComplete: PropTypes.func,
  saving: PropTypes.bool,
};

export default Radium(SaveIndicator)

It works, but is there a way I could streamline this and make it even shorter?

ffxsam
  • 26,428
  • 32
  • 94
  • 144
  • Sounds awesome and maybe worth making into a component, if one doesn't already exist. Call it something like `react-temporary` :) – ZekeDroid Nov 05 '16 at 03:39

1 Answers1

0

How about this. I had a component a while back that needed something similar to what you're describing. I'll paste it in because it works fully but the strategy is kind of like this: pass in a time to start the animation. This prop triggers a function to start the animation which grabs the difference between that time and "now". It iteratively sets the state to close the gap between the initial time and now until it exceeds the passed in duration prop.

class SaveIndicator extends Component {
    static propTypes = {
        children: Types.element,
        // time in milliseconds when the save is started; can change
        indicationStartTime: Types.number.isRequired,
        // time in milliseconds that the animation will take to fade
        duration: Types.number,
        // time in milliseconds to wait between renderings
        frameRate: Types.number,
    };

    static defaultProps = {
        duration: 7000,
        frameRate: 100,
    }

    constructor(props) {
        super(props);
        this.state = { opacity: 0 };
    }

    componentDidMount() {
        this.startAnimation();
    }

    componentWillReceiveProps({ indicationStartTime }) {
        if (indicationStartTime !== this.props.indicationStartTime) {
            this.startAnimation();
        }
    }

    startAnimation() {
        const { indicationStartTime, duration, frameRate } = this.props;
        const now = new Date().getTime();
        const newOpacity = 1 - ((now - indicationStartTime) / duration);

        if (now - indicationStartTime < duration) {
            this.setState({ opacity: newOpacity }, () =>
                setTimeout(::this.startAnimation, frameRate)
            );
        } else {
            this.setState({ opacity: 0 });
        }
    }

    render() {
        const { children } = this.props;
        const { opacity } = this.state;
        const style = { opacity };

        return <div style={style}>{children}</div>;
    }
}
ZekeDroid
  • 7,089
  • 5
  • 33
  • 59
  • Small defect in the above: you'll be wanting to pass in the new prop into startAnimation, or else it fires before getting the new prop (works off the prior prop value). That means that you'll only see the animation if you fire the event twice within the duration. React documentation snippet: _"componentWillReceiveProps() is invoked before a mounted component receives new props."_ – Tyler Hoppe Jun 16 '17 at 22:37
  • It's firing during `componentWillMount` so i think it should be good – ZekeDroid Jun 19 '17 at 13:05