├── Procfile ├── .gitignore ├── Dockerfile ├── server.js ├── .travis.yml ├── .eslintrc ├── demo ├── index.html └── index.js ├── LICENSE ├── README.md ├── package.json ├── src ├── react-count-to.js └── __tests__ │ └── react-count-to-test.js └── dist ├── react-count-to.js └── __tests__ └── react-count-to-test.js /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | demo/build.js 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:0.10 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 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | 4 | app.use(express.static(__dirname + '/demo')); 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 | - 0.10.33 4 | before_deploy: 5 | - npm run demo 6 | deploy: 7 | provider: heroku 8 | api_key: 9 | secure: QBOU8ISenmRT376Be1gga0i+Oc7OPyYEmLTUtDZeHj6GyE3bGSJ0CK1GfzfEUg84JKfP7EMzdCf5G+SYs2pkbgyM6rQOBih2qMmxS4id4HCIExThi7Bod1Si5PsEtv0c2DvZWoDsoxUjSxgII7scln+U0/iH8MYLVbB97M8hyWw= 10 | skip_cleanup: true 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "jasmine": true 5 | }, 6 | "parser": "babel-eslint", 7 | "rules": { 8 | "strict": 0, 9 | "quotes": [2, "single"], 10 | "react/jsx-uses-react": 2, 11 | "react/jsx-uses-vars": 2, 12 | "react/react-in-jsx-scope": 2, 13 | "no-var": 2, 14 | "max-len": [2, 80], 15 | "semi": 2 16 | }, 17 | "plugins": [ 18 | "react" 19 | ], 20 | "ecmaFeatures": { 21 | "jsx": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /demo/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/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import request from 'superagent'; 3 | import CountTo from '../dist/react-count-to'; 4 | 5 | const App = React.createClass({ 6 | 7 | getInitialState() { 8 | return { 9 | isLoading: true, 10 | to: 0 11 | }; 12 | }, 13 | 14 | componentDidMount() { 15 | request 16 | .get('https://api.github.com/repos/facebook/react') 17 | .end(this.callback); 18 | }, 19 | 20 | callback(err, res) { 21 | this.setState({ 22 | isLoading: false, 23 | to: res.body.stargazers_count 24 | }); 25 | }, 26 | 27 | onComplete() { 28 | console.log('completed!'); 29 | }, 30 | 31 | renderLoading() { 32 | return ( 33 | Loading... 34 | ); 35 | }, 36 | 37 | renderCountTo() { 38 | return ( 39 | 40 | ); 41 | }, 42 | 43 | render() { 44 | return ( 45 |
46 |

How many stars does React.js have?

47 | {this.state.isLoading ? this.renderLoading() : this.renderCountTo()} 48 |
49 | ); 50 | } 51 | 52 | }); 53 | 54 | React.render(, document.getElementById('count-to')); 55 | -------------------------------------------------------------------------------- /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 [Alessandro Cinelli](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 | Attributes 38 | ---------- 39 | 40 | - **from** (optional): Counting from (default: 0). 41 | - **to**: Counting to. 42 | - **speed**: Duration (in milliseconds). 43 | - **delay** (optional): Delay (in milliseconds) between each refresh (default: 100). 44 | - **onComplete** (optional): A callback triggered when counting is done. 45 | - **digits** (optional): The number of digits to appear after the decimal point (default: 0). 46 | 47 | Test 48 | ---- 49 | 50 | ```sh 51 | $ npm test 52 | ``` 53 | 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-count-to", 3 | "version": "0.4.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", 8 | "pretest": "eslint ./src/*", 9 | "test": "jest", 10 | "demo": "browserify ./demo/index.js -o ./demo/build.js", 11 | "start": "node server" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/MicheleBertoli/react-count-to.git" 16 | }, 17 | "keywords": [ 18 | "React.js", 19 | "react-component" 20 | ], 21 | "author": "Michele Bertoli", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/MicheleBertoli/react-count-to/issues" 25 | }, 26 | "homepage": "https://github.com/MicheleBertoli/react-count-to", 27 | "devDependencies": { 28 | "babel": "^5.1.11", 29 | "babel-eslint": "^4.1.3", 30 | "babel-jest": "^5.0.1", 31 | "babelify": "^6.0.2", 32 | "browserify": "^9.0.8", 33 | "eslint": "^1.6.0", 34 | "eslint-plugin-react": "^3.5.1", 35 | "express": "^4.12.3", 36 | "jest-cli": "^0.4.0", 37 | "react": "^0.13.2", 38 | "superagent": "^1.2.0" 39 | }, 40 | "browserify": { 41 | "transform": [ 42 | [ 43 | "babelify" 44 | ] 45 | ] 46 | }, 47 | "jest": { 48 | "testPathDirs": [ 49 | "./src" 50 | ], 51 | "scriptPreprocessor": "/node_modules/babel-jest", 52 | "unmockedModulePathPatterns": [ 53 | "/node_modules/react" 54 | ] 55 | }, 56 | "engines": { 57 | "node": "0.10.33" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/react-count-to.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const CountTo = React.createClass({ 4 | 5 | propTypes: { 6 | from: React.PropTypes.number, 7 | to: React.PropTypes.number.isRequired, 8 | speed: React.PropTypes.number.isRequired, 9 | delay: React.PropTypes.number, 10 | onComplete: React.PropTypes.func, 11 | digits: React.PropTypes.number, 12 | className: React.PropTypes.string 13 | }, 14 | 15 | getInitialState() { 16 | return { 17 | counter: this.props.from || 0 18 | }; 19 | }, 20 | 21 | componentDidMount() { 22 | this.start(this.props); 23 | }, 24 | 25 | componentWillReceiveProps(nextProps) { 26 | this.start(nextProps); 27 | }, 28 | 29 | componentWillUnmount() { 30 | this.clear(); 31 | }, 32 | 33 | start(props) { 34 | this.clear(); 35 | this.setState(this.getInitialState(), () => { 36 | const delay = this.props.delay || 100; 37 | this.loopsCounter = 0; 38 | this.loops = Math.ceil(props.speed / delay); 39 | this.increment = (props.to - this.state.counter) / this.loops; 40 | this.interval = setInterval(this.next.bind(this, props), delay); 41 | }); 42 | }, 43 | 44 | next(props) { 45 | if (this.loopsCounter < this.loops) { 46 | this.loopsCounter++; 47 | this.setState({ 48 | counter: this.state.counter + this.increment 49 | }); 50 | } else { 51 | this.clear(); 52 | if (props.onComplete) { 53 | props.onComplete(); 54 | } 55 | } 56 | }, 57 | 58 | clear() { 59 | clearInterval(this.interval); 60 | }, 61 | 62 | render() { 63 | return ( 64 | 65 | {this.state.counter.toFixed(this.props.digits)} 66 | 67 | ); 68 | } 69 | 70 | }); 71 | 72 | export default CountTo; 73 | -------------------------------------------------------------------------------- /dist/react-count-to.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _interopRequireWildcard = function (obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }; 4 | 5 | Object.defineProperty(exports, '__esModule', { 6 | value: true 7 | }); 8 | 9 | var _React = require('react'); 10 | 11 | var _React2 = _interopRequireWildcard(_React); 12 | 13 | var CountTo = _React2['default'].createClass({ 14 | displayName: 'CountTo', 15 | 16 | propTypes: { 17 | from: _React2['default'].PropTypes.number, 18 | to: _React2['default'].PropTypes.number.isRequired, 19 | speed: _React2['default'].PropTypes.number.isRequired, 20 | delay: _React2['default'].PropTypes.number, 21 | onComplete: _React2['default'].PropTypes.func, 22 | digits: _React2['default'].PropTypes.number 23 | }, 24 | 25 | getInitialState: function getInitialState() { 26 | return { 27 | counter: this.props.from || 0 28 | }; 29 | }, 30 | 31 | componentDidMount: function componentDidMount() { 32 | this.start(this.props); 33 | }, 34 | 35 | componentWillReceiveProps: function componentWillReceiveProps(nextProps) { 36 | this.start(nextProps); 37 | }, 38 | 39 | componentWillUnmount: function componentWillUnmount() { 40 | this.clear(); 41 | }, 42 | 43 | start: function start(props) { 44 | var _this = this; 45 | 46 | this.clear(); 47 | this.setState(this.getInitialState(), function () { 48 | var delay = _this.props.delay || 100; 49 | _this.loopsCounter = 0; 50 | _this.loops = Math.ceil(props.speed / delay); 51 | _this.increment = (props.to - _this.state.counter) / _this.loops; 52 | _this.interval = setInterval(_this.next.bind(_this, props), delay); 53 | }); 54 | }, 55 | 56 | next: function next(props) { 57 | if (this.loopsCounter < this.loops) { 58 | this.loopsCounter++; 59 | this.setState({ 60 | counter: this.state.counter + this.increment 61 | }); 62 | } else { 63 | this.clear(); 64 | if (props.onComplete) { 65 | props.onComplete(); 66 | } 67 | } 68 | }, 69 | 70 | clear: function clear() { 71 | clearInterval(this.interval); 72 | }, 73 | 74 | render: function render() { 75 | return _React2['default'].createElement( 76 | 'span', 77 | null, 78 | this.state.counter.toFixed(this.props.digits) 79 | ); 80 | } 81 | 82 | }); 83 | 84 | exports['default'] = CountTo; 85 | module.exports = exports['default']; -------------------------------------------------------------------------------- /src/__tests__/react-count-to-test.js: -------------------------------------------------------------------------------- 1 | jest.dontMock('../react-count-to'); 2 | 3 | describe('CountTo', () => { 4 | 5 | const React = require('react/addons'); 6 | const TestUtils = React.addons.TestUtils; 7 | const CountTo = require('../react-count-to'); 8 | 9 | let countTo; 10 | 11 | describe('with `to` and `speed` props', () => { 12 | 13 | it('starts from 0, ends to 1', () => { 14 | countTo = TestUtils.renderIntoDocument( 15 | 16 | ); 17 | const 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 | 25 | describe('with `from` prop', () => { 26 | 27 | it('starts from 1', () => { 28 | countTo = TestUtils.renderIntoDocument( 29 | 30 | ); 31 | const span = TestUtils.findRenderedDOMComponentWithTag(countTo, 'span'); 32 | expect(span.getDOMNode().textContent).toEqual('1'); 33 | }); 34 | 35 | }); 36 | 37 | describe('with `delay` prop', () => { 38 | 39 | it('sets increment to 1', () => { 40 | countTo = TestUtils.renderIntoDocument( 41 | 42 | ); 43 | expect(countTo.increment).toEqual(1); 44 | }); 45 | 46 | }); 47 | 48 | describe('with `onComplete` prop', () => { 49 | 50 | it('calls onComplete', () => { 51 | const onComplete = jest.genMockFunction(); 52 | countTo = TestUtils.renderIntoDocument( 53 | 54 | ); 55 | jest.runAllTimers(); 56 | expect(onComplete).toBeCalled(); 57 | }); 58 | 59 | }); 60 | 61 | describe('with negative values', () => { 62 | 63 | it('starts from -1, ends to 1', () => { 64 | countTo = TestUtils.renderIntoDocument( 65 | 66 | ); 67 | const span = TestUtils.findRenderedDOMComponentWithTag(countTo, 'span'); 68 | expect(span.getDOMNode().textContent).toEqual('-1'); 69 | jest.runAllTimers(); 70 | expect(span.getDOMNode().textContent).toEqual('1'); 71 | }); 72 | 73 | it('starts from 1, ends to -1', () => { 74 | countTo = TestUtils.renderIntoDocument( 75 | 76 | ); 77 | const span = TestUtils.findRenderedDOMComponentWithTag(countTo, 'span'); 78 | expect(span.getDOMNode().textContent).toEqual('1'); 79 | jest.runAllTimers(); 80 | expect(span.getDOMNode().textContent).toEqual('-1'); 81 | }); 82 | 83 | it('starts sfrom -1, ends to -2', () => { 84 | countTo = TestUtils.renderIntoDocument( 85 | 86 | ); 87 | const span = TestUtils.findRenderedDOMComponentWithTag(countTo, 'span'); 88 | expect(span.getDOMNode().textContent).toEqual('-1'); 89 | jest.runAllTimers(); 90 | expect(span.getDOMNode().textContent).toEqual('-2'); 91 | }); 92 | 93 | }); 94 | 95 | describe('with decimal values', () => { 96 | 97 | it('starts from -0.5, ends to 0.5', () => { 98 | countTo = TestUtils.renderIntoDocument( 99 | 100 | ); 101 | const span = TestUtils.findRenderedDOMComponentWithTag(countTo, 'span'); 102 | expect(span.getDOMNode().textContent).toEqual('-0.5'); 103 | jest.runAllTimers(); 104 | expect(span.getDOMNode().textContent).toEqual('0.5'); 105 | }); 106 | 107 | }); 108 | 109 | describe('when receive new props', () => { 110 | 111 | it('starts from 0, ends to 1', () => { 112 | const Parent = React.createClass({ 113 | getInitialState() { 114 | return { 115 | to: 1 116 | }; 117 | }, 118 | render() { 119 | return ; 120 | } 121 | }); 122 | const parent = TestUtils.renderIntoDocument( 123 | 124 | ); 125 | const span = TestUtils.findRenderedDOMComponentWithTag(parent, 'span'); 126 | expect(span.getDOMNode().textContent).toEqual('0'); 127 | jest.runAllTimers(); 128 | expect(span.getDOMNode().textContent).toEqual('1'); 129 | parent.setState({ 130 | to: 2 131 | }); 132 | expect(span.getDOMNode().textContent).toEqual('0'); 133 | jest.runAllTimers(); 134 | expect(span.getDOMNode().textContent).toEqual('2'); 135 | }); 136 | 137 | }); 138 | 139 | }); 140 | -------------------------------------------------------------------------------- /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 | }); --------------------------------------------------------------------------------