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