Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use better naming convention for variables

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

.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