├── demo ├── build.js ├── server.js └── public │ ├── index.html │ └── index.js ├── Procfile ├── .babelrc ├── .gitignore ├── src ├── __tests__ │ ├── setup.js │ └── react-count-to-test.js └── react-count-to.js ├── .eslintrc ├── Dockerfile ├── .travis.yml ├── LICENSE ├── README.md ├── package.json └── dist ├── __tests__ └── react-count-to-test.js └── react-count-to.js /demo/build.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | demo/public/build.js 2 | node_modules 3 | .idea 4 | -------------------------------------------------------------------------------- /src/__tests__/setup.js: -------------------------------------------------------------------------------- 1 | global.requestAnimationFrame = callback => setTimeout(callback, 0); 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "jest": true 5 | }, 6 | "rules": { 7 | react/prefer-es6-class: 0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:5.11 2 | 3 | COPY . /src 4 | 5 | WORKDIR /src 6 | 7 | RUN npm install 8 | RUN npm run demo 9 | 10 | EXPOSE 3000 11 | 12 | CMD ["node", "server"] 13 | 14 | -------------------------------------------------------------------------------- /demo/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | 4 | app.use(express.static(__dirname + '/public')); 5 | 6 | var host = process.env.IP || '0.0.0.0', 7 | port = process.env.PORT || 3000; 8 | 9 | var server = app.listen(port, host, function() { 10 | console.log('Listening at http://%s:%s', host, port); 11 | }); 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8.12 4 | before_deploy: 5 | - npm run demo 6 | deploy: 7 | provider: heroku 8 | api_key: 9 | secure: o6AcQLLmRhoKlcKviGrXKntVWNsCuDDoq8XxtuK8Pqwhotlk6qsfWOjPFf286qNJYT0VzQ7AiqwdCuuT8xFyPl4RndptLC6XMgq409wPfR5JJWZWC5sr1bx3rHTWn/PbI4vZcPuNmHffebqDRKx4LqJz4ULVJS/VbXe9P65UVU4= 10 | skip_cleanup: true 11 | -------------------------------------------------------------------------------- /demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Count To 5 | 6 | 7 | 8 | Fork me on GitHub 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Michele Bertoli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /demo/public/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import request from 'superagent'; 4 | import CountTo from '../../dist/react-count-to'; 5 | 6 | class App extends Component { 7 | constructor() { 8 | super(); 9 | 10 | this.state = { 11 | isLoading: true, 12 | to: 0, 13 | }; 14 | 15 | this.onComplete = this.onComplete.bind(this); 16 | this.callback = this.callback.bind(this); 17 | this.renderCountTo = this.renderCountTo.bind(this); 18 | this.renderLoading = this.renderLoading.bind(this); 19 | } 20 | 21 | componentDidMount() { 22 | request 23 | .get('https://api.github.com/repos/facebook/react') 24 | .end(this.callback); 25 | } 26 | 27 | onComplete() { 28 | console.log('completed!'); 29 | } 30 | 31 | callback(err, res) { 32 | this.setState({ 33 | isLoading: false, 34 | to: res.body.stargazers_count, 35 | }); 36 | } 37 | 38 | renderLoading() { 39 | return ( 40 | Loading... 41 | ); 42 | } 43 | 44 | renderCountTo() { 45 | return ( 46 | 47 | ); 48 | } 49 | 50 | render() { 51 | return ( 52 |
53 |

How many stars does React.js have?

54 | {this.state.isLoading ? this.renderLoading() : this.renderCountTo()} 55 |
56 | ); 57 | } 58 | } 59 | 60 | ReactDOM.render(, document.getElementById('count-to')); 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/MicheleBertoli/react-count-to.svg?branch=master)](https://travis-ci.org/MicheleBertoli/react-count-to) 2 | 3 | React Count To 4 | ============== 5 | 6 | Animated counter component for [React.js](http://facebook.github.io/react/) 7 | 8 | Installation 9 | ------------ 10 | 11 | ```sh 12 | $ npm install react-count-to --save 13 | ``` 14 | 15 | Demo 16 | ---- 17 | 18 | **Live** 19 | 20 | [http://react-count-to.herokuapp.com](http://react-count-to.herokuapp.com) 21 | 22 | **Docker** (thanks to [Cirpo](https://github.com/cirpo)) 23 | 24 | - `docker build -t react-count-to .` 25 | - `docker run -p 3000:3000 -it react-count-to` 26 | - connect to [http://localhost:3000](http://localhost:3000) and enjoy 27 | 28 | Usage 29 | ----- 30 | 31 | ```javascript 32 | import CountTo from 'react-count-to'; 33 | 34 | 35 | ``` 36 | 37 | or by passing function as a children 38 | 39 | ```javascript 40 | import CountTo from 'react-count-to'; 41 | 42 | const fn = value => {value}; 43 | 44 | {fn} 45 | ``` 46 | 47 | Attributes 48 | ---------- 49 | 50 | - **from** (optional): Counting from (default: 0). 51 | - **to**: Counting to. 52 | - **speed**: Duration (in milliseconds). 53 | - **delay** (optional): Delay (in milliseconds) between each refresh (default: 100). 54 | - **onComplete** (optional): A callback triggered when counting is done. 55 | - **digits** (optional): The number of digits to appear after the decimal point (default: 0). 56 | - **className** (optional): HTML class attribute for counter element. 57 | - **tagName** (optional): Element name that will be displayed (default: 'span'). 58 | - **children** (optional): Function invoked on every update with value as parameter. Must return valid React element or null. 59 | - **easing** (optional): Function returning easing value based on input progress value from 0.0 to 1.0 (default: identity function). 60 | 61 | Test 62 | ---- 63 | 64 | ```sh 65 | $ npm test 66 | ``` 67 | 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-count-to", 3 | "version": "0.12.0", 4 | "description": "Animated counter component for React.js", 5 | "main": "dist/react-count-to.js", 6 | "scripts": { 7 | "build": "babel ./src --out-dir ./dist --ignore __tests__", 8 | "lint": "eslint ./src/*", 9 | "pretest": "npm run lint", 10 | "test": "jest", 11 | "demo": "browserify ./demo/public/index.js -o ./demo/public/build.js", 12 | "start": "node ./demo/server", 13 | "prepublish": "npm run build" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/MicheleBertoli/react-count-to.git" 18 | }, 19 | "keywords": [ 20 | "React.js", 21 | "react-component" 22 | ], 23 | "author": "Michele Bertoli", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/MicheleBertoli/react-count-to/issues" 27 | }, 28 | "homepage": "https://github.com/MicheleBertoli/react-count-to", 29 | "devDependencies": { 30 | "babel-cli": "^6.9.0", 31 | "babel-jest": "^23.6.0", 32 | "babel-preset-es2015": "^6.9.0", 33 | "babel-preset-react": "^6.5.0", 34 | "babelify": "^7.3.0", 35 | "browserify": "^13.0.1", 36 | "eslint": "^2.10.2", 37 | "eslint-config-airbnb": "^9.0.1", 38 | "eslint-plugin-import": "^1.8.0", 39 | "eslint-plugin-jsx-a11y": "^1.2.2", 40 | "eslint-plugin-react": "^5.1.1", 41 | "express": "^4.16.4", 42 | "jest-cli": "^23.6.0", 43 | "react": "^16.0.0", 44 | "react-dom": "^16.0.0", 45 | "superagent": "^3.8.3" 46 | }, 47 | "peerDependencies": { 48 | "react": "^16.0.0", 49 | "react-dom": "^16.0.0" 50 | }, 51 | "browserify": { 52 | "transform": [ 53 | [ 54 | "babelify" 55 | ] 56 | ] 57 | }, 58 | "jest": { 59 | "roots": [ 60 | "./src" 61 | ], 62 | "unmockedModulePathPatterns": [ 63 | "/node_modules/react/", 64 | "/node_modules/react-dom/", 65 | "/node_modules/prop-types/" 66 | ], 67 | "setupFiles": [ 68 | "/src/__tests__/setup.js" 69 | ], 70 | "testMatch": [ 71 | "**/?(*)+(spec|test).js" 72 | ] 73 | }, 74 | "dependencies": { 75 | "prop-types": "^15.5.8" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/react-count-to.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const propTypes = { 5 | from: PropTypes.number, 6 | to: PropTypes.number.isRequired, 7 | speed: PropTypes.number.isRequired, 8 | delay: PropTypes.number, 9 | onComplete: PropTypes.func, 10 | digits: PropTypes.number, 11 | className: PropTypes.string, 12 | tagName: PropTypes.string, 13 | children: PropTypes.func, 14 | easing: PropTypes.func, 15 | }; 16 | 17 | const defaultProps = { 18 | from: 0, 19 | delay: 100, 20 | digits: 0, 21 | tagName: 'span', 22 | easing: t => t, 23 | }; 24 | 25 | class CountTo extends PureComponent { 26 | constructor(props) { 27 | super(props); 28 | 29 | const { from } = props; 30 | 31 | this.state = { 32 | counter: from, 33 | }; 34 | 35 | this.start = this.start.bind(this); 36 | this.clear = this.clear.bind(this); 37 | this.next = this.next.bind(this); 38 | this.updateCounter = this.updateCounter.bind(this); 39 | } 40 | 41 | componentDidMount() { 42 | this.start(); 43 | } 44 | 45 | componentWillReceiveProps(nextProps) { 46 | const { from, to } = this.props; 47 | 48 | if (nextProps.to !== to || nextProps.from !== from) { 49 | this.start(nextProps); 50 | } 51 | } 52 | 53 | componentWillUnmount() { 54 | this.clear(); 55 | } 56 | 57 | start(props = this.props) { 58 | this.clear(); 59 | const { from } = props; 60 | this.setState({ 61 | counter: from, 62 | }, () => { 63 | const { speed, delay } = this.props; 64 | const now = Date.now(); 65 | this.endDate = now + speed; 66 | this.scheduleNextUpdate(now, delay); 67 | this.raf = requestAnimationFrame(this.next); 68 | }); 69 | } 70 | 71 | next() { 72 | const now = Date.now(); 73 | const { speed, onComplete, delay } = this.props; 74 | 75 | if (now >= this.nextUpdate) { 76 | const progress = Math.max(0, Math.min(1, 1 - (this.endDate - now) / speed)); 77 | this.updateCounter(progress); 78 | this.scheduleNextUpdate(now, delay); 79 | } 80 | 81 | if (now < this.endDate) { 82 | this.raf = requestAnimationFrame(this.next); 83 | } else if (onComplete) { 84 | onComplete(); 85 | } 86 | } 87 | 88 | scheduleNextUpdate(now, delay) { 89 | this.nextUpdate = Math.min(now + delay, this.endDate); 90 | } 91 | 92 | updateCounter(progress) { 93 | const { from, to, easing } = this.props; 94 | const delta = to - from; 95 | const counter = from + delta * easing(progress); 96 | this.setState({ 97 | counter, 98 | }); 99 | } 100 | 101 | clear() { 102 | cancelAnimationFrame(this.raf); 103 | } 104 | 105 | render() { 106 | const { className, digits, tagName: Tag, children: fn } = this.props; 107 | const { counter } = this.state; 108 | const value = counter.toFixed(digits); 109 | 110 | if (fn) { 111 | return fn(value); 112 | } 113 | 114 | return ( 115 | 116 | {value} 117 | 118 | ); 119 | } 120 | } 121 | 122 | CountTo.propTypes = propTypes; 123 | CountTo.defaultProps = defaultProps; 124 | 125 | export default CountTo; 126 | -------------------------------------------------------------------------------- /dist/__tests__/react-count-to-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | jest.dontMock('../react-count-to'); 4 | 5 | describe('CountTo', function () { 6 | 7 | var React = require('react/addons'); 8 | var TestUtils = React.addons.TestUtils; 9 | var CountTo = require('../react-count-to'); 10 | 11 | var countTo = undefined; 12 | 13 | describe('with `to` and `speed` props', function () { 14 | 15 | it('starts from 0, ends to 1', function () { 16 | countTo = TestUtils.renderIntoDocument(React.createElement(CountTo, { to: 1, speed: 1 })); 17 | var span = TestUtils.findRenderedDOMComponentWithTag(countTo, 'span'); 18 | expect(span.getDOMNode().textContent).toEqual('0'); 19 | jest.runAllTimers(); 20 | expect(span.getDOMNode().textContent).toEqual('1'); 21 | }); 22 | }); 23 | 24 | describe('with `from` prop', function () { 25 | 26 | it('starts from 1', function () { 27 | countTo = TestUtils.renderIntoDocument(React.createElement(CountTo, { from: 1, to: 1, speed: 1 })); 28 | var span = TestUtils.findRenderedDOMComponentWithTag(countTo, 'span'); 29 | expect(span.getDOMNode().textContent).toEqual('1'); 30 | }); 31 | }); 32 | 33 | describe('with `delay` prop', function () { 34 | 35 | it('sets increment to 1', function () { 36 | countTo = TestUtils.renderIntoDocument(React.createElement(CountTo, { to: 1, speed: 1, delay: 1 })); 37 | expect(countTo.increment).toEqual(1); 38 | }); 39 | }); 40 | 41 | describe('with `onComplete` prop', function () { 42 | 43 | it('calls onComplete', function () { 44 | var onComplete = jest.genMockFunction(); 45 | countTo = TestUtils.renderIntoDocument(React.createElement(CountTo, { to: 1, speed: 1, onComplete: onComplete })); 46 | jest.runAllTimers(); 47 | expect(onComplete).toBeCalled(); 48 | }); 49 | }); 50 | 51 | describe('with negative values', function () { 52 | 53 | it('starts from -1, ends to 1', function () { 54 | countTo = TestUtils.renderIntoDocument(React.createElement(CountTo, { from: -1, to: 1, speed: 1 })); 55 | var span = TestUtils.findRenderedDOMComponentWithTag(countTo, 'span'); 56 | expect(span.getDOMNode().textContent).toEqual('-1'); 57 | jest.runAllTimers(); 58 | expect(span.getDOMNode().textContent).toEqual('1'); 59 | }); 60 | 61 | it('starts from 1, ends to -1', function () { 62 | countTo = TestUtils.renderIntoDocument(React.createElement(CountTo, { from: 1, to: -1, speed: 1 })); 63 | var span = TestUtils.findRenderedDOMComponentWithTag(countTo, 'span'); 64 | expect(span.getDOMNode().textContent).toEqual('1'); 65 | jest.runAllTimers(); 66 | expect(span.getDOMNode().textContent).toEqual('-1'); 67 | }); 68 | 69 | it('starts sfrom -1, ends to -2', function () { 70 | countTo = TestUtils.renderIntoDocument(React.createElement(CountTo, { from: -1, to: -2, speed: 1 })); 71 | var span = TestUtils.findRenderedDOMComponentWithTag(countTo, 'span'); 72 | expect(span.getDOMNode().textContent).toEqual('-1'); 73 | jest.runAllTimers(); 74 | expect(span.getDOMNode().textContent).toEqual('-2'); 75 | }); 76 | }); 77 | 78 | describe('with decimal values', function () { 79 | 80 | it('starts from -0.5, ends to 0.5', function () { 81 | countTo = TestUtils.renderIntoDocument(React.createElement(CountTo, { from: -0.5, to: 0.5, speed: 1, digits: 1 })); 82 | var span = TestUtils.findRenderedDOMComponentWithTag(countTo, 'span'); 83 | expect(span.getDOMNode().textContent).toEqual('-0.5'); 84 | jest.runAllTimers(); 85 | expect(span.getDOMNode().textContent).toEqual('0.5'); 86 | }); 87 | }); 88 | 89 | describe('when receive new props', function () { 90 | 91 | it('starts from 0, ends to 1', function () { 92 | var Parent = React.createClass({ 93 | displayName: 'Parent', 94 | 95 | getInitialState: function getInitialState() { 96 | return { 97 | to: 1 98 | }; 99 | }, 100 | render: function render() { 101 | return React.createElement(CountTo, { to: this.state.to, speed: 1 }); 102 | } 103 | }); 104 | var parent = TestUtils.renderIntoDocument(React.createElement(Parent, null)); 105 | var span = TestUtils.findRenderedDOMComponentWithTag(parent, 'span'); 106 | expect(span.getDOMNode().textContent).toEqual('0'); 107 | jest.runAllTimers(); 108 | expect(span.getDOMNode().textContent).toEqual('1'); 109 | parent.setState({ 110 | to: 2 111 | }); 112 | expect(span.getDOMNode().textContent).toEqual('0'); 113 | jest.runAllTimers(); 114 | expect(span.getDOMNode().textContent).toEqual('2'); 115 | }); 116 | }); 117 | }); -------------------------------------------------------------------------------- /dist/react-count-to.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _react = require('react'); 10 | 11 | var _react2 = _interopRequireDefault(_react); 12 | 13 | var _propTypes = require('prop-types'); 14 | 15 | var _propTypes2 = _interopRequireDefault(_propTypes); 16 | 17 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 18 | 19 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 20 | 21 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 22 | 23 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 24 | 25 | var propTypes = { 26 | from: _propTypes2.default.number, 27 | to: _propTypes2.default.number.isRequired, 28 | speed: _propTypes2.default.number.isRequired, 29 | delay: _propTypes2.default.number, 30 | onComplete: _propTypes2.default.func, 31 | digits: _propTypes2.default.number, 32 | className: _propTypes2.default.string, 33 | tagName: _propTypes2.default.string, 34 | children: _propTypes2.default.func, 35 | easing: _propTypes2.default.func 36 | }; 37 | 38 | var defaultProps = { 39 | from: 0, 40 | delay: 100, 41 | digits: 0, 42 | tagName: 'span', 43 | easing: function easing(t) { 44 | return t; 45 | } 46 | }; 47 | 48 | var CountTo = function (_PureComponent) { 49 | _inherits(CountTo, _PureComponent); 50 | 51 | function CountTo(props) { 52 | _classCallCheck(this, CountTo); 53 | 54 | var _this = _possibleConstructorReturn(this, (CountTo.__proto__ || Object.getPrototypeOf(CountTo)).call(this, props)); 55 | 56 | var from = props.from; 57 | 58 | 59 | _this.state = { 60 | counter: from 61 | }; 62 | 63 | _this.start = _this.start.bind(_this); 64 | _this.clear = _this.clear.bind(_this); 65 | _this.next = _this.next.bind(_this); 66 | _this.updateCounter = _this.updateCounter.bind(_this); 67 | return _this; 68 | } 69 | 70 | _createClass(CountTo, [{ 71 | key: 'componentDidMount', 72 | value: function componentDidMount() { 73 | this.start(); 74 | } 75 | }, { 76 | key: 'componentWillReceiveProps', 77 | value: function componentWillReceiveProps(nextProps) { 78 | var _props = this.props, 79 | from = _props.from, 80 | to = _props.to; 81 | 82 | 83 | if (nextProps.to !== to || nextProps.from !== from) { 84 | this.start(nextProps); 85 | } 86 | } 87 | }, { 88 | key: 'componentWillUnmount', 89 | value: function componentWillUnmount() { 90 | this.clear(); 91 | } 92 | }, { 93 | key: 'start', 94 | value: function start() { 95 | var _this2 = this; 96 | 97 | var props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.props; 98 | 99 | this.clear(); 100 | var from = props.from; 101 | 102 | this.setState({ 103 | counter: from 104 | }, function () { 105 | var _props2 = _this2.props, 106 | speed = _props2.speed, 107 | delay = _props2.delay; 108 | 109 | var now = Date.now(); 110 | _this2.endDate = now + speed; 111 | _this2.scheduleNextUpdate(now, delay); 112 | _this2.raf = requestAnimationFrame(_this2.next); 113 | }); 114 | } 115 | }, { 116 | key: 'next', 117 | value: function next() { 118 | var now = Date.now(); 119 | var _props3 = this.props, 120 | speed = _props3.speed, 121 | onComplete = _props3.onComplete, 122 | delay = _props3.delay; 123 | 124 | 125 | if (now >= this.nextUpdate) { 126 | var progress = Math.max(0, Math.min(1, 1 - (this.endDate - now) / speed)); 127 | this.updateCounter(progress); 128 | this.scheduleNextUpdate(now, delay); 129 | } 130 | 131 | if (now < this.endDate) { 132 | this.raf = requestAnimationFrame(this.next); 133 | } else if (onComplete) { 134 | onComplete(); 135 | } 136 | } 137 | }, { 138 | key: 'scheduleNextUpdate', 139 | value: function scheduleNextUpdate(now, delay) { 140 | this.nextUpdate = Math.min(now + delay, this.endDate); 141 | } 142 | }, { 143 | key: 'updateCounter', 144 | value: function updateCounter(progress) { 145 | var _props4 = this.props, 146 | from = _props4.from, 147 | to = _props4.to, 148 | easing = _props4.easing; 149 | 150 | var delta = to - from; 151 | var counter = from + delta * easing(progress); 152 | this.setState({ 153 | counter: counter 154 | }); 155 | } 156 | }, { 157 | key: 'clear', 158 | value: function clear() { 159 | cancelAnimationFrame(this.raf); 160 | } 161 | }, { 162 | key: 'render', 163 | value: function render() { 164 | var _props5 = this.props, 165 | className = _props5.className, 166 | digits = _props5.digits, 167 | Tag = _props5.tagName, 168 | fn = _props5.children; 169 | var counter = this.state.counter; 170 | 171 | var value = counter.toFixed(digits); 172 | 173 | if (fn) { 174 | return fn(value); 175 | } 176 | 177 | return _react2.default.createElement( 178 | Tag, 179 | { className: className }, 180 | value 181 | ); 182 | } 183 | }]); 184 | 185 | return CountTo; 186 | }(_react.PureComponent); 187 | 188 | CountTo.propTypes = propTypes; 189 | CountTo.defaultProps = defaultProps; 190 | 191 | exports.default = CountTo; -------------------------------------------------------------------------------- /src/__tests__/react-count-to-test.js: -------------------------------------------------------------------------------- 1 | jest.unmock('../react-count-to'); 2 | jest.useFakeTimers(); 3 | 4 | import React, { Component } from 'react'; 5 | import { findDOMNode } from 'react-dom'; 6 | import TestUtils from 'react-dom/test-utils'; 7 | import CountTo from '../react-count-to'; 8 | 9 | describe('CountTo', () => { 10 | let countTo; 11 | 12 | beforeEach(() => { 13 | global.Date.now = jest.fn() 14 | .mockReturnValueOnce(0) 15 | .mockReturnValueOnce(1) 16 | .mockReturnValueOnce(2) 17 | .mockReturnValueOnce(3) 18 | .mockReturnValueOnce(4); 19 | }); 20 | 21 | afterEach(() => { 22 | jest.clearAllTimers(); 23 | }); 24 | 25 | describe('with `to` and `speed` props', () => { 26 | it('starts from 0, ends to 1', () => { 27 | countTo = TestUtils.renderIntoDocument( 28 | 29 | ); 30 | const span = TestUtils.findRenderedDOMComponentWithTag(countTo, 'span'); 31 | expect(findDOMNode(span).textContent).toEqual('0'); 32 | jest.runOnlyPendingTimers(); 33 | expect(findDOMNode(span).textContent).toEqual('1'); 34 | }); 35 | }); 36 | 37 | describe('with `from` prop', () => { 38 | it('starts from 1', () => { 39 | countTo = TestUtils.renderIntoDocument( 40 | 41 | ); 42 | const span = TestUtils.findRenderedDOMComponentWithTag(countTo, 'span'); 43 | expect(findDOMNode(span).textContent).toEqual('1'); 44 | }); 45 | }); 46 | 47 | describe('with `onComplete` prop', () => { 48 | it('calls onComplete', () => { 49 | const onComplete = jest.fn(); 50 | countTo = TestUtils.renderIntoDocument( 51 | 52 | ); 53 | jest.runOnlyPendingTimers(); 54 | expect(onComplete).toBeCalled(); 55 | }); 56 | }); 57 | 58 | describe('with negative values', () => { 59 | it('starts from -1, ends to 1', () => { 60 | countTo = TestUtils.renderIntoDocument( 61 | 62 | ); 63 | const span = TestUtils.findRenderedDOMComponentWithTag(countTo, 'span'); 64 | expect(findDOMNode(span).textContent).toEqual('-1'); 65 | jest.runOnlyPendingTimers(); 66 | expect(findDOMNode(span).textContent).toEqual('1'); 67 | }); 68 | 69 | it('starts from 1, ends to -1', () => { 70 | countTo = TestUtils.renderIntoDocument( 71 | 72 | ); 73 | const span = TestUtils.findRenderedDOMComponentWithTag(countTo, 'span'); 74 | expect(findDOMNode(span).textContent).toEqual('1'); 75 | jest.runOnlyPendingTimers(); 76 | expect(findDOMNode(span).textContent).toEqual('-1'); 77 | }); 78 | 79 | it('starts from -1, ends to -2', () => { 80 | countTo = TestUtils.renderIntoDocument( 81 | 82 | ); 83 | const span = TestUtils.findRenderedDOMComponentWithTag(countTo, 'span'); 84 | expect(findDOMNode(span).textContent).toEqual('-1'); 85 | jest.runOnlyPendingTimers(); 86 | expect(findDOMNode(span).textContent).toEqual('-2'); 87 | }); 88 | }); 89 | 90 | describe('with decimal values', () => { 91 | it('starts from -0.5, ends to 0.5', () => { 92 | countTo = TestUtils.renderIntoDocument( 93 | 94 | ); 95 | const span = TestUtils.findRenderedDOMComponentWithTag(countTo, 'span'); 96 | expect(findDOMNode(span).textContent).toEqual('-0.5'); 97 | jest.runOnlyPendingTimers(); 98 | expect(findDOMNode(span).textContent).toEqual('0.5'); 99 | }); 100 | }); 101 | 102 | describe('when receive new props', () => { 103 | class Parent extends Component { 104 | constructor() { 105 | super(); 106 | this.state = { 107 | from: 0, 108 | to: 1, 109 | }; 110 | } 111 | render() { 112 | return ; 113 | } 114 | } 115 | 116 | it('starts from 0, restarts from 0', () => { 117 | const parent = TestUtils.renderIntoDocument( 118 | 119 | ); 120 | const span = TestUtils.findRenderedDOMComponentWithTag(parent, 'span'); 121 | expect(findDOMNode(span).textContent).toEqual('0'); 122 | jest.runOnlyPendingTimers(); 123 | expect(findDOMNode(span).textContent).toEqual('1'); 124 | parent.setState({ 125 | to: 2, 126 | }); 127 | expect(findDOMNode(span).textContent).toEqual('0'); 128 | jest.runOnlyPendingTimers(); 129 | expect(findDOMNode(span).textContent).toEqual('2'); 130 | }); 131 | 132 | it('starts from 0, restarts from 2', () => { 133 | const parent = TestUtils.renderIntoDocument( 134 | 135 | ); 136 | const span = TestUtils.findRenderedDOMComponentWithTag(parent, 'span'); 137 | expect(findDOMNode(span).textContent).toEqual('0'); 138 | jest.runOnlyPendingTimers(); 139 | expect(findDOMNode(span).textContent).toEqual('1'); 140 | parent.setState({ 141 | from: 2, 142 | to: 3, 143 | }); 144 | expect(findDOMNode(span).textContent).toEqual('2'); 145 | jest.runOnlyPendingTimers(); 146 | expect(findDOMNode(span).textContent).toEqual('3'); 147 | }); 148 | }); 149 | 150 | describe('with `tagName` prop', () => { 151 | it('starts from 0, ends to 1', () => { 152 | countTo = TestUtils.renderIntoDocument( 153 | 154 | ); 155 | const div = TestUtils.findRenderedDOMComponentWithTag(countTo, 'div'); 156 | expect(findDOMNode(div).textContent).toEqual('0'); 157 | jest.runOnlyPendingTimers(); 158 | expect(findDOMNode(div).textContent).toEqual('1'); 159 | }); 160 | }); 161 | 162 | describe('with child function', () => { 163 | it('starts from 0, ends to 1', () => { 164 | const fn = jest.fn().mockImplementation(value => {value}); 165 | countTo = TestUtils.renderIntoDocument( 166 | {fn} 167 | ); 168 | jest.runOnlyPendingTimers(); 169 | expect(fn.mock.calls.length).toBe(2); 170 | expect(fn).lastCalledWith('1'); 171 | const span = TestUtils.findRenderedDOMComponentWithTag(countTo, 'span'); 172 | expect(findDOMNode(span).textContent).toEqual('1'); 173 | }); 174 | }); 175 | 176 | describe('easing prop', () => { 177 | beforeEach(() => { 178 | global.Date.now = jest.fn() 179 | .mockReturnValueOnce(0) 180 | .mockReturnValueOnce(100) 181 | .mockReturnValueOnce(200) 182 | .mockReturnValueOnce(300); 183 | }); 184 | 185 | it('does not modify behaviour by default', () => { 186 | countTo = TestUtils.renderIntoDocument( 187 | 188 | ); 189 | const span = TestUtils.findRenderedDOMComponentWithTag(countTo, 'span'); 190 | expect(findDOMNode(span).textContent).toEqual('0'); 191 | jest.runOnlyPendingTimers(); 192 | expect(findDOMNode(span).textContent).toEqual('5'); 193 | jest.runOnlyPendingTimers(); 194 | expect(findDOMNode(span).textContent).toEqual('10'); 195 | }); 196 | 197 | it('applies easing to the value', () => { 198 | const easing = jest.fn() 199 | .mockReturnValueOnce(0.2) 200 | .mockReturnValueOnce(0.8) 201 | .mockReturnValueOnce(1); 202 | countTo = TestUtils.renderIntoDocument( 203 | 204 | ); 205 | const span = TestUtils.findRenderedDOMComponentWithTag(countTo, 'span'); 206 | expect(findDOMNode(span).textContent).toEqual('0'); 207 | jest.runOnlyPendingTimers(); 208 | expect(findDOMNode(span).textContent).toEqual('2'); 209 | jest.runOnlyPendingTimers(); 210 | expect(findDOMNode(span).textContent).toEqual('8'); 211 | jest.runOnlyPendingTimers(); 212 | expect(findDOMNode(span).textContent).toEqual('10'); 213 | }); 214 | }); 215 | 216 | describe('with `delay` prop', () => { 217 | beforeEach(() => { 218 | global.Date.now = jest.fn().mockReturnValueOnce(0); 219 | }); 220 | 221 | it('does not update state before given delay', () => { 222 | countTo = TestUtils.renderIntoDocument( 223 | 224 | ); 225 | const span = TestUtils.findRenderedDOMComponentWithTag(countTo, 'span'); 226 | expect(findDOMNode(span).textContent).toEqual('0'); 227 | global.Date.now.mockReturnValueOnce(4); 228 | jest.runOnlyPendingTimers(); 229 | expect(findDOMNode(span).textContent).toEqual('0'); 230 | global.Date.now.mockReturnValueOnce(5); 231 | jest.runOnlyPendingTimers(); 232 | expect(findDOMNode(span).textContent).toEqual('5'); 233 | global.Date.now.mockReturnValueOnce(6); 234 | jest.runOnlyPendingTimers(); 235 | expect(findDOMNode(span).textContent).toEqual('5'); 236 | }); 237 | 238 | it('finishes after `speed` ms despite `delay` prop', () => { 239 | countTo = TestUtils.renderIntoDocument( 240 | 241 | ); 242 | const span = TestUtils.findRenderedDOMComponentWithTag(countTo, 'span'); 243 | expect(findDOMNode(span).textContent).toEqual('0'); 244 | global.Date.now.mockReturnValueOnce(6); 245 | jest.runOnlyPendingTimers(); 246 | expect(findDOMNode(span).textContent).toEqual('6'); 247 | global.Date.now.mockReturnValueOnce(10); 248 | jest.runOnlyPendingTimers(); 249 | expect(findDOMNode(span).textContent).toEqual('10'); 250 | }); 251 | }); 252 | }); 253 | --------------------------------------------------------------------------------