├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── README.md ├── lib ├── container.js ├── index.js ├── middleware.js └── reducer.js ├── package.json ├── src ├── container.js ├── index.js ├── middleware.js └── reducer.js └── tests ├── container.spec.js ├── middleware.spec.js ├── reducer.spec.js ├── setup.js └── utils └── helper.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", "react", "stage-0" 4 | ] 5 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = spaces 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "es6": true 6 | }, 7 | "settings": { 8 | "react": { 9 | "pragma": "React", 10 | "version": "15.0" 11 | } 12 | }, 13 | "rules": { 14 | "indent": ["error", 4], 15 | "id-length": 0, 16 | "react/jsx-indent": [2, 4], 17 | "react/jsx-indent-props": [2, 4], 18 | "react/forbid-prop-types": 0, 19 | "jsx-a11y/href-no-hash": 0 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #### joe made this: https://goel.io/joe 2 | 3 | #####=== Node ===##### 4 | 5 | # Logs 6 | logs 7 | *.log 8 | .nyc_output 9 | coverage/ 10 | 11 | # build 12 | .out 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (http://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directory 35 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 36 | node_modules 37 | 38 | # Debug log from npm 39 | npm-debug.log 40 | 41 | #####=== OSX ===##### 42 | .DS_Store 43 | .AppleDouble 44 | .LSOverride 45 | 46 | # Icon must end with two \r 47 | Icon 48 | 49 | 50 | # Thumbnails 51 | ._* 52 | 53 | # Files that might appear on external disk 54 | .Spotlight-V100 55 | .Trashes 56 | 57 | # Directories potentially created on remote AFP share 58 | .AppleDB 59 | .AppleDesktop 60 | Network Trash Folder 61 | Temporary Items 62 | .apdisk 63 | 64 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 7 4 | cache: 5 | directories: 6 | - node_modules 7 | script: 8 | - npm run lint 9 | after_script: 10 | - NODE_ENV=test ./node_modules/.bin/nyc npm test && ./node_modules/.bin/nyc report 11 | --reporter=text-lcov | coveralls 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. Fork it! 4 | 2. Create your feature branch: `git checkout -b my-new-feature` 5 | 3. Commit your changes: `git commit -m 'Add some feature'` 6 | 4. Push to the branch: `git push origin my-new-feature` 7 | 8 | *Remember that we have a pre-push hook with steps that analyzes and prevents mistakes.* 9 | 10 | **After your pull request is merged**, you can safely delete your branch. 11 | 12 | ### [<-- Back](https://github.com/guilouro/redux-global-loader/) 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redux Global Loader 2 | 3 | [![Build Status](https://travis-ci.org/guilouro/redux-global-loader.svg?branch=master)](https://travis-ci.org/guilouro/redux-global-loader) 4 | [![Coverage Status](https://coveralls.io/repos/github/guilouro/redux-global-loader/badge.svg?branch=master)](https://coveralls.io/github/guilouro/redux-global-loader?branch=master) 5 | 6 | **Obs**: [Redux Promise Middleware](https://github.com/pburtchaell/redux-promise-middleware/) is required 7 | 8 | A redux middleware with [redux-promise-middleware's](https://github.com/pburtchaell/redux-promise-middleware/) integration that shows the Loading while there is one or more than one pending promises in the current page. The loading state will be hidden as soon as all Promises gets completed. 9 | 10 | 11 | ## Installation 12 | 13 | #### Install with npm 14 | 15 | ```sh 16 | $ npm install --save redux-global-loader 17 | ``` 18 | 19 | #### Import the middleware and include it after `promiseMiddleware()` in your `applyMiddleware` 20 | 21 | ```jsx 22 | import { globalLoaderMiddleware } from 'redux-global-loader'; 23 | 24 | composeStoreWithMiddleware = applyMiddleware( 25 | ... 26 | promiseMiddleware(), 27 | globalLoaderMiddleware, 28 | ... 29 | )(createStore); 30 | ``` 31 | 32 | #### Import the reducer `loadingAll` and include it in the `combineReducers` 33 | 34 | ```jsx 35 | import { combineReducers } from 'redux'; 36 | import { loadingAll } from 'redux-global-loader'; 37 | 38 | ... 39 | combineReducers({ 40 | ... 41 | loadingAll, 42 | ... 43 | }); 44 | ... 45 | ``` 46 | 47 | ### Usage 48 | 49 | ```jsx 50 | import { Loading } from 'redux-global-loader'; 51 | 52 | ... 53 | render() { 54 | return ( 55 | 56 | ... 57 | // Your loading component here 58 | ... 59 | 60 | ); 61 | } 62 | ... 63 | ``` 64 | 65 | ## Contributing 66 | 67 | If you want to contribute with this component: 68 | [Contributing Documentation](https://github.com/guilouro/redux-global-loader/blob/master/CONTRIBUTING.md). 69 | 70 | -------------------------------------------------------------------------------- /lib/container.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _propTypes = require('prop-types'); 8 | 9 | var _propTypes2 = _interopRequireDefault(_propTypes); 10 | 11 | var _reactRedux = require('react-redux'); 12 | 13 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 14 | 15 | var mapStateToProps = function mapStateToProps(_ref) { 16 | var loadingAll = _ref.loadingAll; 17 | return { loadingAll: loadingAll }; 18 | }; 19 | var Loading = function Loading(props) { 20 | return !Object.values(props.loadingAll).every(function (item) { 21 | return !item; 22 | }) ? props.children : false; 23 | }; 24 | 25 | Loading.propTypes = { 26 | children: _propTypes2.default.any 27 | }; 28 | 29 | exports.default = (0, _reactRedux.connect)(mapStateToProps)(Loading); -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.Loading = exports.loadingAll = exports.globalLoaderMiddleware = undefined; 7 | 8 | var _middleware = require('./middleware'); 9 | 10 | var _middleware2 = _interopRequireDefault(_middleware); 11 | 12 | var _reducer = require('./reducer'); 13 | 14 | var _reducer2 = _interopRequireDefault(_reducer); 15 | 16 | var _container = require('./container'); 17 | 18 | var _container2 = _interopRequireDefault(_container); 19 | 20 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 21 | 22 | exports.globalLoaderMiddleware = _middleware2.default; 23 | exports.loadingAll = _reducer2.default; 24 | exports.Loading = _container2.default; -------------------------------------------------------------------------------- /lib/middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | exports.default = function (store) { 8 | var dispatch = store.dispatch; 9 | 10 | 11 | return function (next) { 12 | return function (action) { 13 | ['_PENDING', '_FULFILLED', '_REJECTED'].forEach(function (item, key) { 14 | if (action.type.endsWith(item)) { 15 | dispatch({ 16 | type: '@@loadingAll/' + (!key ? 'STARTED' : 'FINISHED'), 17 | payload: action.type.replace(item, '') 18 | }); 19 | } 20 | }); 21 | 22 | next(action); 23 | }; 24 | }; 25 | }; -------------------------------------------------------------------------------- /lib/reducer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 10 | 11 | exports.default = function () { 12 | var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 13 | var action = arguments[1]; 14 | 15 | switch (action.type) { 16 | case '@@loadingAll/STARTED': 17 | return _extends({}, state, _defineProperty({}, action.payload, true)); 18 | 19 | case '@@loadingAll/FINISHED': 20 | return _extends({}, state, _defineProperty({}, action.payload, false)); 21 | 22 | default: 23 | return state; 24 | } 25 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-global-loader", 3 | "version": "1.0.2", 4 | "description": "A Redux Middleware that show a Loading when wait resolve all promise-middleware's promises", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "./node_modules/.bin/babel src -d lib", 8 | "test": "NODE_ENV=test ./node_modules/.bin/mocha --require tests/setup.js tests/*.spec.js", 9 | "test:tdd": "npm t -- --watch", 10 | "test:coverage": "NODE_ENV=test ./node_modules/.bin/nyc --reporter=html --reporter=text ./node_modules/.bin/mocha tests/setup.js tests/*.spec.js", 11 | "lint": "./node_modules/.bin/eslint src", 12 | "lint:fix": "./node_modules/.bin/eslint src --fix", 13 | "prepush": "npm run lint && npm t" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/guilouro/redux-global-loader.git" 18 | }, 19 | "nyc": { 20 | "lines": 90, 21 | "check-coverage": true, 22 | "exclude": [ 23 | "node_modules", 24 | "tests", 25 | "lib", 26 | "src/index.js" 27 | ] 28 | }, 29 | "keywords": [ 30 | "redux", 31 | "react", 32 | "middleware", 33 | "loading", 34 | "promises" 35 | ], 36 | "author": "Guilherme Louro", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/guilouro/redux-global-loader/issues" 40 | }, 41 | "homepage": "https://github.com/guilouro/redux-global-loader#readme", 42 | "devDependencies": { 43 | "babel-cli": "^6.26.0", 44 | "babel-core": "^6.26.0", 45 | "babel-eslint": "^7.2.3", 46 | "babel-preset-env": "^1.6.0", 47 | "babel-preset-react": "^6.24.1", 48 | "babel-preset-stage-0": "^6.24.1", 49 | "babel-register": "^6.26.0", 50 | "chai": "^4.1.1", 51 | "coveralls": "^2.13.1", 52 | "enzyme": "^2.9.1", 53 | "eslint": "^4.5.0", 54 | "eslint-config-airbnb": "^15.1.0", 55 | "eslint-plugin-import": "^2.7.0", 56 | "eslint-plugin-jsx-a11y": "^6.0.2", 57 | "eslint-plugin-react": "^7.2.1", 58 | "husky": "^0.14.3", 59 | "jsdom": "^11.1.0", 60 | "mocha": "^3.5.0", 61 | "nyc": "^11.1.0", 62 | "react-test-renderer": "^15.6.1", 63 | "sinon": "^3.2.1", 64 | "sinon-chai": "^2.13.0" 65 | }, 66 | "dependencies": { 67 | "react": "^15.6.1", 68 | "react-dom": "^15.6.1", 69 | "react-redux": "^5.0.6", 70 | "redux": "^3.7.2", 71 | "prop-types": "^15.5.10" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/container.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { connect } from 'react-redux'; 3 | 4 | const mapStateToProps = ({ loadingAll }) => ({ loadingAll }); 5 | const Loading = props => ( 6 | !Object.values(props.loadingAll) 7 | .every(item => !item) ? props.children : false 8 | ); 9 | 10 | Loading.propTypes = { 11 | children: PropTypes.any, 12 | }; 13 | 14 | export default connect(mapStateToProps)(Loading); 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export globalLoaderMiddleware from './middleware'; 2 | export loadingAll from './reducer'; 3 | export Loading from './container'; 4 | -------------------------------------------------------------------------------- /src/middleware.js: -------------------------------------------------------------------------------- 1 | export default (store) => { 2 | const { dispatch } = store; 3 | 4 | return next => (action) => { 5 | ['_PENDING', '_FULFILLED', '_REJECTED'].forEach((item, key) => { 6 | if (action.type.endsWith(item)) { 7 | dispatch({ 8 | type: `@@loadingAll/${!key ? 'STARTED' : 'FINISHED'}`, 9 | payload: action.type.replace(item, ''), 10 | }); 11 | } 12 | }); 13 | 14 | next(action); 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /src/reducer.js: -------------------------------------------------------------------------------- 1 | export default (state = {}, action) => { 2 | switch (action.type) { 3 | case '@@loadingAll/STARTED': 4 | return { 5 | ...state, 6 | [action.payload]: true, 7 | }; 8 | 9 | case '@@loadingAll/FINISHED': 10 | return { 11 | ...state, 12 | [action.payload]: false, 13 | }; 14 | 15 | default: 16 | return state; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /tests/container.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Loading from '../src/container'; 3 | import { mountConnected, mockStore } from './utils/helper'; 4 | import { expect } from 'chai'; 5 | 6 | describe(' - Loading', () => { 7 | it('should render when there is true in global store', () => { 8 | const loadingAll = { name1: true, name2: true }; 9 | const wrapper = mountConnected(
Lorem
, mockStore({ loadingAll })); 10 | expect(wrapper.find('div')).to.have.length(1); 11 | }); 12 | 13 | it('should render when there is any true in global store', () => { 14 | const loadingAll = { name1: true, name2: false }; 15 | const wrapper = mountConnected(
Lorem
, mockStore({ loadingAll })); 16 | expect(wrapper.find('div')).to.have.length(1); 17 | }); 18 | 19 | it('should not render when there isn`t any true in global store', () => { 20 | const loadingAll = { name1: false, name2: false }; 21 | const wrapper = mountConnected(
Lorem
, mockStore({ loadingAll })); 22 | expect(wrapper.find('div')).to.have.length(0); 23 | }); 24 | }); -------------------------------------------------------------------------------- /tests/middleware.spec.js: -------------------------------------------------------------------------------- 1 | import middleware from '../src/middleware'; 2 | import chai, { expect } from 'chai'; 3 | import sinon from 'sinon'; 4 | import sinonChai from 'sinon-chai'; 5 | 6 | chai.use(sinonChai); 7 | 8 | describe('- Middleware', () => { 9 | let store; 10 | let next; 11 | 12 | beforeEach(() => { 13 | store = { dispatch: sinon.spy() } 14 | next = sinon.spy(); 15 | }); 16 | 17 | it('should have dispatch STARTED when contains _PENDING in action', () => { 18 | const action = { type: 'NAME_PENDING' }; 19 | const expected = { payload: 'NAME', type: '@@loadingAll/STARTED' } 20 | middleware(store)(next)(action); 21 | expect(store.dispatch).to.be.calledWith(expected); 22 | }); 23 | 24 | it('should have dispatch FINISHED when contains _FULFILLED in action', () => { 25 | const action = { type: 'NAME_FULFILLED' }; 26 | const expected = { payload: 'NAME', type: '@@loadingAll/FINISHED' } 27 | middleware(store)(next)(action); 28 | expect(store.dispatch).to.be.calledWith(expected); 29 | }); 30 | 31 | it('should have dispatch FINISHED when contains _REJECTED in action', () => { 32 | const action = { type: 'NAME_REJECTED' }; 33 | const expected = { payload: 'NAME', type: '@@loadingAll/FINISHED' } 34 | middleware(store)(next)(action); 35 | expect(store.dispatch).to.be.calledWith(expected); 36 | }); 37 | 38 | }) -------------------------------------------------------------------------------- /tests/reducer.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import reducer from '../src/reducer'; 3 | 4 | describe('- Reducer', () => { 5 | it('should return the initial state', () => { 6 | const state = reducer({}, { type: '' }); 7 | expect(state).to.eql({}); 8 | }); 9 | 10 | it('should have true when action type START', () => { 11 | const state = reducer({}, { 12 | type: '@@loadingAll/STARTED', 13 | payload: 'name', 14 | }); 15 | 16 | const expectedState = { 17 | name: true, 18 | }; 19 | 20 | expect(state).to.eql(expectedState); 21 | }); 22 | 23 | it('should have false when action type FINISHED', () => { 24 | const state = reducer({}, { 25 | type: '@@loadingAll/FINISHED', 26 | payload: 'name', 27 | }); 28 | 29 | const expectedState = { 30 | name: false, 31 | }; 32 | 33 | expect(state).to.eql(expectedState); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | require('babel-register')(); 2 | 3 | var jsdom = require('jsdom'); 4 | const { JSDOM } = jsdom; 5 | 6 | const { document } = (new JSDOM('')).window; 7 | global.document = document; 8 | 9 | const exposedProperties = ['window', 'navigator', 'document']; 10 | 11 | global.window = document.defaultView; 12 | 13 | Object.keys(global.window).forEach((property) => { 14 | if (typeof global[property] === 'undefined') { 15 | exposedProperties.push(property); 16 | global[property] = global.window[property]; 17 | } 18 | }); 19 | 20 | global.navigator = { 21 | userAgent: 'node.js', 22 | }; 23 | -------------------------------------------------------------------------------- /tests/utils/helper.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createStore, combineReducers } from 'redux'; 3 | import { Provider } from 'react-redux'; 4 | import { mount } from 'enzyme'; 5 | import loadingAll from '../../src/reducer'; 6 | 7 | export const mockStore = (mock) => { 8 | const reducers = { 9 | loadingAll, 10 | }; 11 | 12 | Object.keys(mock).forEach(key => { 13 | reducers[key] = (state, action) => (mock[key]); 14 | }); 15 | 16 | return createStore( 17 | combineReducers(reducers), 18 | ); 19 | }; 20 | 21 | export const mountConnected = (component, store) => ( 22 | mount( 23 | 24 | {component} 25 | 26 | ) 27 | ); 28 | --------------------------------------------------------------------------------