├── 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 |
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 | [](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 |
--------------------------------------------------------------------------------