├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── LoaderHandler.js ├── README.md ├── circle.yml ├── demo.gif ├── index.js ├── package.json ├── scripts └── test-helper.js └── tests ├── LoaderHandler.test.js └── index.test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "node": true, 5 | "es6": true, 6 | "mocha": true 7 | }, 8 | "plugins": [ 9 | "react", 10 | "react-native" 11 | ], 12 | "root": true, 13 | "rules": { 14 | "jsx-quotes": 2, 15 | "no-unused-vars": [2, {"vars": "all", "args": "none"}], 16 | "react/jsx-boolean-value": 2, 17 | "react/jsx-curly-spacing": 2, 18 | "react/jsx-no-duplicate-props": 2, 19 | "react/jsx-no-undef": 2, 20 | "react/jsx-sort-prop-types": 2, 21 | "react/jsx-sort-props": 2, 22 | "react/jsx-uses-react": 2, 23 | "react/jsx-uses-vars": 2, 24 | "react/no-danger": 2, 25 | "react/no-did-mount-set-state": 2, 26 | "react/no-did-update-set-state": 2, 27 | "react/no-direct-mutation-state": 2, 28 | "react/no-multi-comp": 2, 29 | "react/no-unknown-property": 2, 30 | "react/prefer-es6-class": 1, 31 | "react/react-in-jsx-scope": 2, 32 | "react/require-extension": 2, 33 | "react/self-closing-comp": 2, 34 | "react/sort-comp": 2, 35 | "react/wrap-multilines": 2, 36 | "react-native/no-unused-styles": 2 37 | }, 38 | "extends": "eslint:recommended" 39 | } 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | /Example/node_modules 22 | *.ipa 23 | *.xcuserstate 24 | project.xcworkspace 25 | 26 | # Android/IJ 27 | # 28 | .idea 29 | .gradle 30 | local.properties 31 | 32 | # node.js 33 | # 34 | node_modules/ 35 | npm-debug.log 36 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | .eslintrc 3 | demo.gif 4 | circle.yml 5 | tests/ 6 | scripts/ 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Durgaprasad 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 | -------------------------------------------------------------------------------- /LoaderHandler.js: -------------------------------------------------------------------------------- 1 | import { DeviceEventEmitter } from 'react-native'; 2 | 3 | const loaderHandler = { 4 | hideLoader () { 5 | DeviceEventEmitter.emit('changeLoadingEffect', {isVisible: false}); 6 | }, 7 | showLoader (title) { 8 | DeviceEventEmitter.emit('changeLoadingEffect', {title, isVisible: true}); 9 | } 10 | }; 11 | 12 | module.exports = loaderHandler; 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-Native Busy Indicator 2 | [![npm version](https://badge.fury.io/js/react-native-busy-indicator.svg)](https://badge.fury.io/js/react-native-busy-indicator) 3 | [![Circle CI](https://circleci.com/gh/RealOrangeOne/react-native-busy-indicator.svg)](https://circleci.com/gh/RealOrangeOne/react-native-busy-indicator) 4 | [![NPM downloads](http://img.shields.io/npm/dm/react-native-busy-indicator.svg?style=flat-square)](https://www.npmjs.com/package/react-native-busy-indicator) 5 | 6 | ## Install 7 | ```shell 8 | npm install react-native-busy-indicator --save 9 | ``` 10 | 11 | ## Usage 12 | Place the indicator component as close to the root as possible, outside your other view components 13 | 14 | ```js 15 | const BusyIndicator = require('react-native-busy-indicator'); 16 | 17 | const YourComponent = React.createClass({ 18 | render() { 19 | return ( 20 | 21 | ... 22 | 23 | 24 | ); 25 | } 26 | ``` 27 | 28 | ### Show / Hide loader 29 | Toggling the component can be done from any file, provided the component has already been placed and rendered. 30 | 31 | ```js 32 | const loaderHandler = require('react-native-busy-indicator/LoaderHandler'); 33 | 34 | loaderHandler.showLoader("Loading"); // Show indicator with message 'Loading' 35 | loaderHandler.showLoader("Loading More"); // Show indicator with message 'Loading More' 36 | 37 | loaderHandler.hideLoader(); // Hide the loader 38 | ``` 39 | 40 | ## Component Properties 41 | 42 | | Prop | Type | Description | 43 | |---|---|---| 44 | |**`color`**|`number`| color of the indicator. Default gray| 45 | |**`overlayWidth`**|`number`|overlay width| 46 | |**`overlayHeight`**|`number`|overlay height| 47 | |**`overlayColor`**|`string`|overlay color| 48 | |**`text`**|`string`|text. Default: `Please wait...`| 49 | |**`textColor`**|`string`| text color| 50 | |**`textFontSize`**|`number`|text font size| 51 | |**`textNumberOfLines`**|`number`| total number of lines does not exceed this number. Default: 1| 52 | |**`Size`**|`small/large`| Size of the spinner. Default: small| 53 | 54 | 55 | ## Demo 56 | 57 | ![Demo](https://raw.githubusercontent.com/RealOrangeOne/react-native-busy-indicator/master/demo.gif) 58 | 59 | __Note__: The spinner moves much smoother than shown in recording! 60 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 5.11.1 4 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealOrangeOne/react-native-busy-indicator/9680b3b59502155e8b315651bc1dbb7d2b2b6d00/demo.gif -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | StyleSheet, 5 | View, 6 | Text, 7 | DeviceEventEmitter, 8 | ActivityIndicator 9 | } from 'react-native'; 10 | 11 | import PropTypes from 'prop-types'; 12 | 13 | const styles = StyleSheet.create({ 14 | container: { 15 | position: 'absolute', 16 | backgroundColor: 'rgba(0,0,0,0.3)', 17 | justifyContent: 'center', 18 | alignItems: 'center', 19 | top: 0, 20 | bottom: 0, 21 | left: 0, 22 | right: 0, 23 | flex: 1 24 | }, 25 | 26 | progressBar: { 27 | margin: 10, 28 | justifyContent: 'center', 29 | alignItems: 'center', 30 | padding: 10 31 | }, 32 | 33 | nocontainer: { 34 | position: 'absolute', 35 | top: 0, 36 | left: 0, 37 | width: 0.001, 38 | height: 0.001 39 | }, 40 | 41 | overlay: { 42 | alignItems: 'center', 43 | justifyContent: 'center', 44 | borderRadius: 10, 45 | padding: 10 46 | }, 47 | 48 | text: { 49 | marginTop: 8 50 | } 51 | }); 52 | 53 | class BusyIndicator extends React.Component { 54 | constructor(props) { 55 | super(props); 56 | this.state = { 57 | isVisible: props.startVisible 58 | }; 59 | } 60 | 61 | componentDidMount () { 62 | this.emitter = DeviceEventEmitter.addListener('changeLoadingEffect', this.changeLoadingEffect.bind(this)); 63 | } 64 | 65 | componentWillUnmount() { 66 | this.emitter.remove(); 67 | } 68 | 69 | changeLoadingEffect(state) { 70 | this.setState({ 71 | isVisible: state.isVisible, 72 | text: state.title ? state.title : this.props.text 73 | }); 74 | } 75 | 76 | render() { 77 | if (!this.state.isVisible) { 78 | return (); 79 | } 80 | 81 | const customStyles = StyleSheet.create({ 82 | overlay: { 83 | backgroundColor: this.props.overlayColor, 84 | width: this.props.overlayWidth, 85 | height: this.props.overlayHeight 86 | }, 87 | 88 | text: { 89 | color: this.props.textColor, 90 | fontSize: this.props.textFontSize 91 | } 92 | }); 93 | 94 | return ( 95 | 96 | 97 | 101 | 102 | 105 | {this.state.text} 106 | 107 | 108 | 109 | ); 110 | } 111 | } 112 | 113 | BusyIndicator.propTypes = { 114 | color: PropTypes.string, 115 | overlayColor: PropTypes.string, 116 | overlayHeight: PropTypes.number, 117 | overlayWidth: PropTypes.number, 118 | size: PropTypes.oneOf(['small', 'large']), 119 | startVisible: PropTypes.bool, 120 | text: PropTypes.string, 121 | textColor: PropTypes.string, 122 | textFontSize: PropTypes.number, 123 | textNumberOfLines: PropTypes.number 124 | }; 125 | 126 | BusyIndicator.defaultProps = { 127 | isDismissible: false, 128 | overlayWidth: 120, 129 | overlayHeight: 100, 130 | overlayColor: '#333333', 131 | color: '#f5f5f5', 132 | size: 'small', 133 | startVisible: false, 134 | text: 'Please wait...', 135 | textColor: '#f5f5f5', 136 | textFontSize: 14, 137 | textNumberOfLines: 1 138 | }; 139 | 140 | module.exports = BusyIndicator; 141 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-busy-indicator", 3 | "version": "1.1.0", 4 | "main": "index.js", 5 | "description": "A simple busy indicator which can be trigger from any child or navigated page in react native application", 6 | "scripts": { 7 | "lint": "eslint index.js LoaderHandler.js scripts/ tests/", 8 | "mocha": "mocha --require babel-core/register scripts/test-helper.js --bail 'tests/**/*.test.js'", 9 | "test": "npm run lint && npm run mocha" 10 | }, 11 | "keywords": [ 12 | "react", 13 | "native", 14 | "busy", 15 | "indicator", 16 | "loading", 17 | "indicator", 18 | "ios", 19 | "android", 20 | "react-component", 21 | "react-native" 22 | ], 23 | "devDependencies": { 24 | "babel-cli": "=6.11.4", 25 | "babel-eslint": "=4.1.7", 26 | "babel-polyfill": "=6.9.1", 27 | "babel-preset-react-native": "=1.8.0", 28 | "chai": "=3.5.0", 29 | "enzyme": "=2.4.1", 30 | "eslint": "=1.10.3", 31 | "eslint-plugin-react": "=3.14.0", 32 | "eslint-plugin-react-native": "=1.0.2", 33 | "jsdom": "=9.4.1", 34 | "mocha": "=2.5.3", 35 | "react": "=15.1.0", 36 | "react-dom": "=15.1.0", 37 | "react-native": "=0.26.0", 38 | "react-native-mock": "=0.2.5", 39 | "sinon": "=1.17.4", 40 | "sinon-chai": "=2.8.0", 41 | "prop-types": "^15.6.0" 42 | }, 43 | "repository": { 44 | "type": "git", 45 | "url": "https://github.com/RealOrangeOne/react-native-busy-indicator" 46 | }, 47 | "author": "Durgaprasad Budhwani", 48 | "license": "MIT", 49 | "peerDependencies": { 50 | "react-native": ">=0.25.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /scripts/test-helper.js: -------------------------------------------------------------------------------- 1 | import jsdom from 'jsdom'; 2 | import chai from 'chai'; 3 | import sinonChai from 'sinon-chai'; 4 | import { assert } from 'sinon'; 5 | 6 | require('react-native-mock/mock'); 7 | require('babel-polyfill'); 8 | 9 | 10 | // Setup mocks 11 | // Jsdom document & window 12 | const doc = jsdom.jsdom(''); 13 | const win = doc.defaultView; 14 | 15 | // Add to global 16 | global.document = doc; 17 | global.window = win; 18 | 19 | 20 | // Add window keys to global window 21 | Object.keys(global.window).forEach((key) => { 22 | if (!(key in global)) { 23 | global[key] = global.window[key]; 24 | } 25 | }); 26 | 27 | global.window.localStorage = { getItem: () => null }; 28 | 29 | chai.expect(); 30 | chai.use(sinonChai); 31 | 32 | assert.expose(chai.expect, { prefix: '' }); 33 | -------------------------------------------------------------------------------- /tests/LoaderHandler.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { DeviceEventEmitter } from 'react-native'; 3 | import LoaderHandler from '../LoaderHandler'; 4 | import sinon from 'sinon'; 5 | 6 | describe('Loader Handler', function () { 7 | let eventHandler; 8 | 9 | beforeEach(function () { 10 | eventHandler = sinon.spy(); 11 | DeviceEventEmitter.addListener('changeLoadingEffect', eventHandler); 12 | }); 13 | 14 | it('Should expose the right functions', function () { 15 | expect(LoaderHandler.showLoader).to.be.a('function'); 16 | expect(LoaderHandler.hideLoader).to.be.a('function'); 17 | }); 18 | 19 | it('Should emit event when showing', function () { 20 | LoaderHandler.showLoader('Loading'); 21 | expect(eventHandler).to.have.been.calledWithExactly({ isVisible: true, title: 'Loading' }); 22 | }); 23 | 24 | it('Should emit event when hiding', function () { 25 | LoaderHandler.hideLoader(); 26 | expect(eventHandler).to.have.been.calledWithExactly({ isVisible: false }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line no-unused-vars 2 | import { expect } from 'chai'; 3 | import { shallow, mount } from 'enzyme'; 4 | import BusyIndicator from '../index'; 5 | 6 | describe('Main Component', function () { 7 | it('Should render empty initially', function () { 8 | const instance = shallow(); 9 | expect(instance.find('View')).to.have.lengthOf(1); 10 | expect(instance.find('ActivityIndicator')).to.have.lengthOf(0); 11 | }); 12 | 13 | it('Should render properly if starting visible', function () { 14 | const instance = shallow(); 15 | expect(instance.find('ActivityIndicator')).to.have.lengthOf(1); 16 | }); 17 | 18 | describe('Update Event', function () { 19 | it('Should show if event handler called', function () { 20 | const instance = shallow(); 21 | expect(instance.find('ActivityIndicator')).to.have.lengthOf(0); 22 | instance.instance().changeLoadingEffect({ isVisible: true }); 23 | instance.update(); 24 | expect(instance.find('ActivityIndicator')).to.have.lengthOf(1); 25 | }); 26 | 27 | it('Should create event emitter on mount', function () { 28 | const instance = mount(); 29 | expect(instance.instance().emitter).to.exist; 30 | }); 31 | }); 32 | 33 | it('Should show correct text', function () { 34 | const instance = shallow(); 35 | instance.setState({ text: 'Loading...' }); 36 | instance.update(); 37 | expect(instance.find('Text').prop('children')).to.equal('Loading...'); 38 | }); 39 | }); 40 | --------------------------------------------------------------------------------