Skip to content

Commit

Permalink
Feat #18 - wait parameter is boosted - code coverage (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
Fabien JUIF committed Oct 5, 2016
1 parent 5dd88a3 commit 0d862e8
Show file tree
Hide file tree
Showing 7 changed files with 283 additions and 163 deletions.
118 changes: 38 additions & 80 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,100 +1,58 @@
# react-loader
## what is this?
This is a higher order component (`HOC`).
This HOC purpose is to call a `load` callback passes in `props` of a component only once (at `componentWillMount`).
This is convenient to load data from a `backend` for instance.
# hoc-react-loader
[![CircleCI](https://circleci.com/gh/Zenika/react-loader.svg?&style=shield&circle-token=07eae4d9bdbe138c04d32753312ba543a4e08f34)](https://circleci.com/gh/Zenika/react-loader/tree/master) [![NPM Version](https://badge.fury.io/js/hoc-react-loader.svg)](https://www.npmjs.com/package/hoc-react-loader) [![Coverage Status](https://coveralls.io/repos/github/Zenika/react-loader/badge.svg?branch=master)](https://coveralls.io/github/Zenika/react-loader?branch=master)

It shows a loading component when it's waiting for the props to be defined.
This loading component can be changed easely.
This is a higher order component ("HOC"). Its purpose is to call a `load` callback passed through the `props` of a component only once (at `componentWillMount`). This is convenient to load data from a backend for instance. The component shows a loading indicator when it's waiting for the props to be defined. The loading indicator can be changed easily.

## try it
## Demos
You can test some examples [here](https://zenika.github.io/react-loader/).

## install
## Installation
`npm i --save hoc-react-loader`

## use
You have to wrap your component, and give a `load` props to that resulted component.
## Usage
### With `this.props`
```es6
import loader from 'hoc-react-loader'

You can also add an optional configuration object as second parameter.
const Component = ({ data }) => <div>Component {JSON.stringify(data)}</div>

Parameter | Needed | Default value | Description
----------|--------|---------------|-------------
`Loader` | no | `Dots` | A component that will be display depending on `prop` value.
`prop` | no | `loaded` | A prop name that determine when to display the `Loader` component.
`wait` | no | `true` | Set to `false` if you don't want to wait for the `prop` to be set.
export default loader(Component, { wait: ['data'] })
```
In this case, the loader waits for `this.props.data` to be truthy, then mounts its child component and calls `this.props.load` if it exists. This is useful when the parent has control over the injected data, or when the `Component` is connected with `redux`. `this.props.load` should be injected by the parent component or injected by a `Container` (redux).

### Simple example with `redux` :
The `wait` parameter should be an array of props to waits. All these props should become truthy at some point.

**Component.js**
```(javascript)
import React from 'react'
export default ({ text }) => <div>{text}</div>
```
Since the `Loader` is not specified, the default `Loader` is displayed while waiting for all the props. Here's an exemple with a specified loader:
```es6
import loader from 'hoc-react-loader'

const MyLoader = () => <div>Waiting...</div>
const Component = ({ data }) => <div>Component {data}</div>

**Container.js**
```(javascript)
import { connect } from 'react-redux'
import reactLoader from 'hoc-react-loader'
import { fetchText } from '%%your_actions%%'
import Component from './Component'
const mapStateToProps = ({ text }) => {
return {
text,
}
}
const mapDispatchToProps = (dispatch) => {
return {
load: () => dispatch(fetchText()),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(reactLoader(Component))
export default loader(Component, { wait: ['data'], Loader: MyLoader })
```

The `fetchText` may be an [redux-thunk](https://github.com/gaearon/redux-thunk) action that fetch a text to a `backend`, and update the state : `state.text`.
### Don't wait
```es6
import loader from 'hoc-react-loader'

### Advanced example with `redux` :
const Component = ({ data }) => <div>Component {JSON.stringify(data)}</div>

**Component.js**
```(javascript)
import React from 'react'
export default ({ text }) => <div>{text}</div>
export default loader(Component, { wait: false })
```
In this example, the loader component doesn't wait for props. `this.props.load` is called once, but the `Loader` component isn't displayed.

**Loader.js**
```(javascript)
import React from 'react'
export default () => <div>loading...</div>
```
### Load as a parameter
```es6
import loader from 'hoc-react-loader'

const Component = ({ data }) => <div>Component {JSON.stringify(data)}</div>

**Container.js**
```(javascript)
import { connect } from 'react-redux'
import reactLoader from 'hoc-react-loader'
import { fetchText } from '%%your_actions%%'
import Component from './Component'
import Loader from './Loader'
const mapStateToProps = ({ text, isTextFetched }) => {
return {
text,
fetched: isTextFetched,
}
}
const mapDispatchToProps = (dispatch) => {
return {
load: () => dispatch(fetchText()),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(reactLoader(Component, {
Loader,
prop: 'fetched'
}))
export default loader(Component, { load: () => console.log('here') })
```
In this case, the loader calls `this.props.load` if it exists *AND* the `load` parameter, resulting in `here` to be printed.

The default `wait` parameter value is `false`. It means that in this example the `Loader` isn't displayed.

The `Loader` component will displayed instead of `Component` as long as `prop` value is false.
### Wait as a function
The `wait` parameter can also be a function. Then the `context` and `props` are given to it, and it should return the array of props to wait for.
52 changes: 36 additions & 16 deletions build/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,25 @@ function _possibleConstructorReturn(self, call) { if (!self) { throw new Referen

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; }

// http://stackoverflow.com/a/7356528
var isFunction = function isFunction(functionToCheck) {
var getType = {};
return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
};
var getDisplayName = function getDisplayName(c) {
return c.displayName || c.name || 'Component';
};

exports.default = function (ComposedComponent, config) {
exports.default = function (ComposedComponent) {
var _class, _temp2;

var _ref = config || {};
var _ref = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];

var Loader = _ref.Loader;
var _ref$prop = _ref.prop;
var prop = _ref$prop === undefined ? 'loaded' : _ref$prop;
var _ref$wait = _ref.wait;
var wait = _ref$wait === undefined ? true : _ref$wait;

var wait = _ref$wait === undefined ? ['loaded'] : _ref$wait;
var _ref$load = _ref.load;
var load = _ref$load === undefined ? undefined : _ref$load;

return _temp2 = _class = function (_Component) {
_inherits(_class, _Component);
Expand All @@ -53,11 +57,25 @@ exports.default = function (ComposedComponent, config) {
return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref2 = _class.__proto__ || Object.getPrototypeOf(_class)).call.apply(_ref2, [this].concat(args))), _this), _this.state = {
props: {}
}, _this.isLoaded = function () {
return Boolean(_this.props[prop]);
}, _this.isLoadAFunction = function () {
return typeof _this.props.load === 'function';
// Wait is an array
// Implicitly meaning that this is an array of props
if (Array.isArray(wait)) {
return wait.map(function (w) {
return Boolean(_this.props[w]);
}).reduce(function (allProps, currentProp) {
return allProps && currentProp;
});
}

// Wait is a function
if (isFunction(wait)) {
return wait(_this.props, _this.context);
}

// Anything else
return Boolean(wait);
}, _this.omitLoadInProps = function (props) {
var isLoadAFunction = _this.isLoadAFunction();
var isLoadAFunction = isFunction(props.load);

if (isLoadAFunction) {
_this.setState({
Expand All @@ -78,19 +96,21 @@ exports.default = function (ComposedComponent, config) {
_createClass(_class, [{
key: 'componentWillMount',
value: function componentWillMount() {
// Load from props
if (this.omitLoadInProps(this.props)) {
this.props.load();
}

// Load from hoc argument
if (isFunction(load)) {
load();
}
}
}, {
key: 'render',
value: function render() {
if (wait && !this.isLoaded()) {
if (Loader) {
return _react2.default.createElement(Loader, this.state.props);
}

return null;
if (!this.isLoaded()) {
return _react2.default.createElement(Loader, this.state.props);
}

return _react2.default.createElement(ComposedComponent, this.state.props);
Expand Down
4 changes: 3 additions & 1 deletion circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ dependencies:
- docker run --rm -v $(pwd)/examples:/usr/src/app zenika/alpine-node npm install

test:
pre:
- ./misc/rebuild_refs.sh
override:
- docker run --rm -v $(pwd):/usr/src/app zenika/alpine-node npm run test
- docker run --rm -v $(pwd):/usr/src/app zenika/alpine-node npm run lint
- docker run --rm -e COVERALLS_REPO_TOKEN=${COVERALLS_REPO_TOKEN} -v $(pwd):/usr/src/app zenika/alpine-node npm run coveralls
- docker run --rm -v $(pwd)/examples:/usr/src/app zenika/alpine-node npm run lint
9 changes: 9 additions & 0 deletions misc/rebuild_refs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash

IFS=$'\n'
for f in $(git show-ref --heads); do
hash=$(echo ${f} | cut -d' ' -f1)
file=$(echo ${f} | cut -d' ' -f2)

echo ${hash} > ".git/${file}"
done
11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hoc-react-loader",
"version": "1.1.0",
"version": "2.0.1",
"description": "High order component to call a load function if present.",
"main": "build/index.js",
"dependencies": {},
Expand All @@ -16,8 +16,10 @@
"babel-preset-es2017": "^1.6.1",
"babel-preset-react": "^6.11.1",
"babel-preset-stage-0": "^6.5.0",
"blanket": "^1.2.3",
"chai": "^3.5.0",
"chai-spies": "^0.7.1",
"coveralls": "^2.11.14",
"cross-env": "^2.0.0",
"enzyme": "^2.4.1",
"eslint": "^3.2.2",
Expand All @@ -28,15 +30,18 @@
"jsdom": "^9.4.2",
"lodash": "^4.15.0",
"mocha": "^3.0.2",
"nyc": "^8.3.0",
"react": "^15.3.0",
"react-addons-test-utils": "^15.3.0",
"react-dom": "^15.3.0",
"webpack": "^1.13.1"
},
"scripts": {
"lint": "eslint src/index.jsx",
"lint": "find src -iname \"*.jsx\" -exec eslint {} +; find src -iname \"*.js\" -exec eslint {} +;",
"build": "cross-env BABEL_ENV=cjs babel --ignore \"*.spec.js\" ./src/ --out-dir build",
"test": "NODE_ENV=test mocha --recursive --compilers js:babel-register --require ./misc/testSetup.js \"src/**/*.spec.js\" "
"test": "mocha --recursive --compilers js:babel-register --require ./misc/testSetup.js \"src/**/*.spec.js\" ",
"coverage": "nyc --extension .jsx npm test",
"coveralls": "nyc npm test && nyc report --reporter=text-lcov | coveralls"
},
"repository": {
"type": "git",
Expand Down
51 changes: 33 additions & 18 deletions src/core.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import React, { Component, PropTypes } from 'react'

// http://stackoverflow.com/a/7356528
const isFunction = (functionToCheck) => {
const getType = {}
return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]'
}
const getDisplayName = (c) => c.displayName || c.name || 'Component'

export default (
ComposedComponent,
config,
) => {
const {
{
Loader,
prop = 'loaded',
wait = true,
} = config || {}

wait = ['loaded'],
load = undefined,
} = {},
) => {
return class extends Component {
static displayName = `Loader(${getDisplayName(ComposedComponent)})`
static propTypes = {
Expand All @@ -23,15 +26,25 @@ export default (
}

isLoaded = () => {
return Boolean(this.props[prop])
}
// Wait is an array
// Implicitly meaning that this is an array of props
if (Array.isArray(wait)) {
return wait
.map(w => Boolean(this.props[w]))
.reduce((allProps, currentProp) => allProps && currentProp)
}

isLoadAFunction = () => {
return (typeof this.props.load === 'function')
// Wait is a function
if (isFunction(wait)) {
return wait(this.props, this.context)
}

// Anything else
return Boolean(wait)
}

omitLoadInProps = (props) => {
const isLoadAFunction = this.isLoadAFunction()
const isLoadAFunction = isFunction(props.load)

if (isLoadAFunction) {
this.setState({
Expand All @@ -48,22 +61,24 @@ export default (
}

componentWillMount() {
// Load from props
if (this.omitLoadInProps(this.props)) {
this.props.load()
}

// Load from hoc argument
if (isFunction(load)) {
load()
}
}

componentWillReceiveProps = (nextProps) => {
this.omitLoadInProps(nextProps)
}

render() {
if (wait && !this.isLoaded()) {
if (Loader) {
return <Loader {...this.state.props} />
}

return null
if (!this.isLoaded()) {
return <Loader {...this.state.props} />
}

return <ComposedComponent {...this.state.props} />
Expand Down
Loading

0 comments on commit 0d862e8

Please sign in to comment.