diff --git a/.babelrc b/.babelrc
index 86c445f..2e23624 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,3 +1,4 @@
{
+ "plugins": ["transform-object-rest-spread"],
"presets": ["es2015", "react"]
}
diff --git a/.editorconfig b/.editorconfig
index 4eb6462..f05c22d 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -9,6 +9,6 @@ end_of_line = lf
insert_final_newline = true
indent_style = tab
-[package.json]
+[{.babelrc,package.json}]
indent_style = space
indent_size = 2
diff --git a/.eslintignore b/.eslintignore
index c64162c..0d9033d 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1 +1,2 @@
/coverage/*
+/lib/*
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f4410f0
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 Drew Keller
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6c7530b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,115 @@
+# react-component-update
+
+[](https://travis-ci.org/wimpyprogrammer/react-component-update)
+[](https://codecov.io/gh/wimpyprogrammer/react-component-update)
+
+Adds convenience lifecycle events to your React components.
+
+ - `componentWillMountOrReceiveProps(nextProps)` - Combines the [`componentWillMount()`](https://facebook.github.io/react/docs/react-component.html#componentwillmount) and [`componentWillReceiveProps(nextProps)`](https://facebook.github.io/react/docs/react-component.html#componentwillreceiveprops) events. This allows you to consolidate all pre-`render()` logic.
+
+ - `componentDidMountOrUpdate(prevProps, prevState)` - Combines the [`componentDidMount()`](https://facebook.github.io/react/docs/react-component.html#componentdidmount) and [`componentDidUpdate(prevProps, prevState)`](https://facebook.github.io/react/docs/react-component.html#componentdidupdate) events. This allows you to consolidate all post-`render()` logic.
+
+## Installation
+
+Published on `npm` as [`react-component-update`](https://www.npmjs.com/package/react-component-update).
+
+npm users:
+```
+npm install --save react-component-update
+```
+
+yarn users:
+```
+yarn add react-component-update
+```
+
+`react-component-update` does not include its own version of React. It will use whatever version is already installed in your project.
+
+## Usage
+
+To extend React's `Component` class:
+
+```js
+import React from 'react';
+import { Component } from 'react-component-update';
+
+class MyReactComponent extends Component {
+ componentWillMountOrReceiveProps(nextProps) {
+ // Code that runs before every render(). For example, check that the data
+ // used by this component has already loaded, otherwise trigger an AJAX
+ // request for it. nextProps contains the props that render() will receive.
+ }
+
+ componentDidMountOrUpdate(prevProps, prevState) {
+ // Code that runs after every render(). For example, inspect the latest DOM
+ // to get the height of the rendered elements. prevProps and prevState
+ // contain the props and state that render() will receive.
+ }
+
+ render() {
+ return
;
+ }
+}
+```
+
+Or to extend React's `PureComponent` class (available in React v15.3.0+):
+```js
+import { PureComponent } from 'react-component-update';
+```
+
+For compatibility with [`create-react-class`](https://www.npmjs.com/package/create-react-class), use the `withEvents()` higher-order component.
+
+```js
+import createReactClass from 'create-react-class';
+import { withEvents } from 'react-component-update';
+
+const MyReactComponent = createReactClass(withEvents({
+ componentWillMountOrReceiveProps: function(nextProps) {
+ // Code that runs before every render().
+ },
+
+ componentDidMountOrUpdate: function(prevProps, prevState) {
+ // Code that runs after every render().
+ },
+
+ render: function() {
+ return ;
+ }
+}));
+```
+
+## Mixing with your own lifecycle events
+
+`react-component-update` implements four lifecycle events of the React base classes:
+ - `componentWillMount()`
+ - `componentDidMount()`
+ - `componentWillReceiveProps()`
+ - `componentDidUpdate()`
+
+If you extend `Component` or `PureComponent` from `react-component-update` and you also implement these events in your component, you will need to call the corresponding `super()` method like so:
+
+```js
+componentWillMount() {
+ super.componentWillMount();
+}
+
+componentDidMount() {
+ super.componentDidMount();
+}
+
+componentWillReceiveProps(nextProps) {
+ super.componentWillReceiveProps(nextProps);
+}
+
+componentDidUpdate(prevProps, prevState) {
+ super.componentDidUpdate(prevProps, prevState);
+}
+```
+
+The `super()` method can be called anywhere in your function to suit your needs.
+
+If you use the `withEvents()` higher-order component, you do not need to add any extra code to your events. The new event (ex. `componentDidMountOrUpdate()`) will always run after the related built-in event (ex. `componentDidUpdate()`).
+
+## License
+
+[MIT](/LICENSE.md)
diff --git a/package.json b/package.json
index ef42199..ed273e5 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-component-update",
- "version": "1.0.0-alpha",
+ "version": "1.0.0",
"description": "Extends the native React Component to streamline updates",
"main": "lib/index.js",
"scripts": {
@@ -17,7 +17,7 @@
"url": "git+https://github.com/wimpyprogrammer/react-component-update.git"
},
"author": "Drew Keller ",
- "license": "ISC",
+ "license": "MIT",
"bugs": {
"url": "https://github.com/wimpyprogrammer/react-component-update/issues"
},
@@ -25,10 +25,12 @@
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-jest": "^20.0.3",
+ "babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"chai": "^4.0.2",
"codecov": "^2.3.0",
+ "create-react-class": "^15.6.0",
"dirty-chai": "^2.0.0",
"enzyme": "^2.8.2",
"eslint": "^3.19.0",
@@ -44,13 +46,20 @@
"react-dom": "*",
"react-test-renderer": "*",
"rimraf": "^2.6.1",
- "sinon": "^2.3.4"
+ "sinon": "^3.2.1",
+ "sinon-chai": "^2.13.0"
},
"peerDependencies": {
"react": "*"
},
"jest": {
"coverageDirectory": "./coverage/",
- "collectCoverage": true
+ "collectCoverage": true,
+ "testMatch": [
+ "**/src/*.spec.js?(x)"
+ ]
+ },
+ "dependencies": {
+ "lodash.wrap": "^4.1.1"
}
}
diff --git a/src/component.spec.js b/src/component.spec.js
index 67d7865..68e6b57 100644
--- a/src/component.spec.js
+++ b/src/component.spec.js
@@ -1,14 +1,20 @@
/* eslint-env jest */
+/* eslint-disable react/no-multi-comp */
import chai, { expect } from 'chai';
import dirtyChai from 'dirty-chai';
import { mount } from 'enzyme';
import uniqueId from 'lodash.uniqueid';
import React from 'react';
import sinon from 'sinon';
+import sinonChai from 'sinon-chai';
import Component from './component';
chai.use(dirtyChai);
+chai.use(sinonChai);
+
+const sandbox = sinon.createSandbox();
+let component;
function getUniqueState() {
return { [uniqueId('stateVarName')]: uniqueId('stateVarValue') };
@@ -30,140 +36,311 @@ describe('Component extension', () => {
}
}
- const callbackWill = sinon.spy(TestComponent.prototype, 'componentWillMountOrReceiveProps');
- const callbackDid = sinon.spy(TestComponent.prototype, 'componentDidMountOrUpdate');
- const callbackRender = sinon.spy(TestComponent.prototype, 'render');
+ const callbackWill = sandbox.spy(TestComponent.prototype, 'componentWillMountOrReceiveProps');
+ const callbackDid = sandbox.spy(TestComponent.prototype, 'componentDidMountOrUpdate');
+ const callbackRender = sandbox.spy(TestComponent.prototype, 'render');
- afterEach(() => {
- callbackWill.reset();
- callbackDid.reset();
- callbackRender.reset();
+ beforeEach(() => {
+ component = mount();
});
+ afterEach(() => sandbox.reset());
+
describe('componentWillMountOrReceiveProps()', () => {
it('runs once on mount', () => {
- mount();
- expect(callbackWill.calledOnce).to.be.true();
+ expect(callbackWill).to.have.been.calledOnce();
});
it('runs on mount with first parameter of component props', () => {
- const component = mount();
- expect(callbackWill.firstCall.args[0]).to.equal(component.props());
+ expect(callbackWill.firstCall).to.have.been.calledWith(component.props());
+ });
+
+ it('runs on mount with "this" context of component', () => {
+ expect(callbackWill.firstCall).to.have.been.calledOn(component.getNode());
});
it('runs on mount before render()', () => {
- mount();
- expect(callbackWill.calledBefore(callbackRender)).to.be.true();
+ expect(callbackWill).to.have.been.calledBefore(callbackRender);
});
it('runs on props update', () => {
- const component = mount();
component.setProps(getUniqueProps());
- expect(callbackWill.calledTwice).to.be.true();
+ expect(callbackWill).to.have.been.calledTwice();
});
it('runs on props update when no props change', () => {
- const component = mount();
component.setProps(component.props());
- expect(callbackWill.calledTwice).to.be.true();
+ expect(callbackWill).to.have.been.calledTwice();
});
it('runs on props update with first parameter of component props', () => {
- const component = mount();
component.setProps(getUniqueProps());
- expect(callbackWill.secondCall.args[0]).to.equal(component.props());
+ expect(callbackWill.secondCall).to.have.been.calledWith(component.props());
+ });
+
+ it('runs on props update with "this" context of component', () => {
+ component.setProps(getUniqueProps());
+ expect(callbackWill.secondCall).to.have.been.calledOn(component.getNode());
});
it('runs on props update before render()', () => {
- const component = mount();
component.setProps(getUniqueProps());
- expect(callbackWill.secondCall.calledBefore(callbackRender.secondCall)).to.be.true();
+ expect(callbackWill.secondCall).to.have.been.calledBefore(callbackRender.secondCall);
});
it('does not run on state update', () => {
- const component = mount();
component.setState(getUniqueState());
- expect(callbackWill.calledOnce).to.be.true();
+ expect(callbackWill).to.have.been.calledOnce();
});
});
describe('componentDidMountOrUpdate()', () => {
it('runs once when mounted', () => {
- mount();
- expect(callbackDid.calledOnce).to.be.true();
+ expect(callbackDid).to.have.been.calledOnce();
});
it('runs on mount with first parameter of component props', () => {
- const component = mount();
- expect(callbackDid.firstCall.args[0]).to.equal(component.props());
+ expect(callbackDid.firstCall).to.have.been.calledWith(component.props());
});
it('runs on mount with second parameter of component state', () => {
- const component = mount();
- expect(callbackDid.firstCall.args[1]).to.equal(component.state());
+ expect(callbackDid.firstCall).to.have.been.calledWith(sinon.match.any, component.state());
+ });
+
+ it('runs on mount with "this" context of component', () => {
+ expect(callbackDid.firstCall).to.have.been.calledOn(component.getNode());
});
it('runs after render()', () => {
- mount();
- expect(callbackDid.calledAfter(callbackRender)).to.be.true();
+ expect(callbackDid).to.have.been.calledAfter(callbackRender);
});
it('runs on props update', () => {
- const component = mount();
component.setProps(getUniqueProps());
- expect(callbackDid.calledTwice).to.be.true();
+ expect(callbackDid).to.have.been.calledTwice();
});
it('runs on props update when no props change', () => {
- const component = mount();
component.setProps(component.props());
- expect(callbackDid.calledTwice).to.be.true();
+ expect(callbackDid).to.have.been.calledTwice();
});
it('runs on props update with first parameter of previous component props', () => {
- const component = mount();
const initialProps = component.props();
component.setProps(getUniqueProps());
- expect(callbackDid.secondCall.args[0]).to.equal(initialProps);
+ expect(callbackDid.secondCall).to.have.been.calledWith(initialProps);
});
it('runs on props update with second parameter of previous component state', () => {
- const component = mount();
const initialState = component.state();
component.setProps(getUniqueProps());
- expect(callbackDid.secondCall.args[1]).to.equal(initialState);
+ expect(callbackDid.secondCall).to.have.been.calledWith(sinon.match.any, initialState);
+ });
+
+ it('runs on props update with "this" context of component', () => {
+ component.setProps(getUniqueProps());
+ expect(callbackDid.secondCall).to.have.been.calledOn(component.getNode());
});
it('runs on state update', () => {
- const component = mount();
component.setState(getUniqueState());
- expect(callbackDid.calledTwice).to.be.true();
+ expect(callbackDid).to.have.been.calledTwice();
});
it('runs on state update when no state changes', () => {
- const component = mount();
component.setState(component.state());
- expect(callbackDid.calledTwice).to.be.true();
+ expect(callbackDid).to.have.been.calledTwice();
});
it('runs on state update with first parameter of previous component props', () => {
- const component = mount();
const initialProps = component.props();
component.setState(getUniqueState());
- expect(callbackDid.secondCall.args[0]).to.equal(initialProps);
+ expect(callbackDid.secondCall).to.have.been.calledWith(initialProps);
});
it('runs on state update with second parameter of previous component state', () => {
- const component = mount();
const initialState = component.state();
component.setState(getUniqueState());
- expect(callbackDid.secondCall.args[1]).to.equal(initialState);
+ expect(callbackDid.secondCall).to.have.been.calledWith(sinon.match.any, initialState);
+ });
+
+ it('runs on state update with "this" context of component', () => {
+ component.setState(getUniqueState());
+ expect(callbackDid.secondCall).to.have.been.calledOn(component.getNode());
});
it('runs on props update before render()', () => {
- const component = mount();
component.setProps(getUniqueProps());
- expect(callbackDid.secondCall.calledAfter(callbackRender.secondCall)).to.be.true();
+ expect(callbackDid.secondCall).to.have.been.calledAfter(callbackRender.secondCall);
+ });
+ });
+});
+
+describe('Component extension with overrides calling super()', () => {
+ const userComponentWillMountBefore = sandbox.spy();
+ const userComponentWillMountAfter = sandbox.spy();
+ const userComponentDidMountBefore = sandbox.spy();
+ const userComponentDidMountAfter = sandbox.spy();
+ const userComponentWillReceivePropsBefore = sandbox.spy();
+ const userComponentWillReceivePropsAfter = sandbox.spy();
+ const userComponentDidUpdateBefore = sandbox.spy();
+ const userComponentDidUpdateAfter = sandbox.spy();
+
+ class TestComponentWithSuper extends Component {
+ componentWillMount() {
+ userComponentWillMountBefore();
+ super.componentWillMount();
+ userComponentWillMountAfter();
+ }
+
+ componentDidMount() {
+ userComponentDidMountBefore();
+ super.componentDidMount();
+ userComponentDidMountAfter();
+ }
+
+ componentWillReceiveProps() {
+ userComponentWillReceivePropsBefore();
+ super.componentWillReceiveProps();
+ userComponentWillReceivePropsAfter();
+ }
+
+ componentDidUpdate() {
+ userComponentDidUpdateBefore();
+ super.componentDidUpdate();
+ userComponentDidUpdateAfter();
+ }
+
+ render() {
+ return null;
+ }
+ }
+
+ const callbackWill = sandbox.spy(TestComponentWithSuper.prototype, 'componentWillMountOrReceiveProps');
+ const callbackDid = sandbox.spy(TestComponentWithSuper.prototype, 'componentDidMountOrUpdate');
+
+ beforeEach(() => {
+ component = mount();
+ });
+
+ afterEach(() => sandbox.reset());
+
+ describe('componentWillMountOrReceiveProps()', () => {
+ it('runs once on mount', () => {
+ expect(callbackWill).to.have.been.calledOnce();
+ });
+
+ it('runs user code in override on mount', () => {
+ sinon.assert.callOrder(
+ userComponentWillMountBefore, callbackWill, userComponentWillMountAfter,
+ );
+ });
+
+ it('runs on props update', () => {
+ component.setProps(getUniqueProps());
+ expect(callbackWill).to.have.been.calledTwice();
+ });
+
+ it('runs user code in override on props update', () => {
+ component.setProps(getUniqueProps());
+ sinon.assert.callOrder(
+ userComponentWillReceivePropsBefore, callbackWill, userComponentWillReceivePropsAfter,
+ );
+ });
+
+ it('does not run on state update', () => {
+ component.setState(getUniqueState());
+ expect(callbackWill).to.have.been.calledOnce();
+ });
+ });
+
+ describe('componentDidMountOrUpdate()', () => {
+ it('runs once when mounted', () => {
+ expect(callbackDid).to.have.been.calledOnce();
+ });
+
+ it('runs user code in override on mount', () => {
+ sinon.assert.callOrder(
+ userComponentDidMountBefore, callbackDid, userComponentDidMountAfter,
+ );
+ });
+
+ it('runs on props update', () => {
+ component.setProps(getUniqueProps());
+ expect(callbackDid).to.have.been.calledTwice();
+ });
+
+ it('runs user code in override on props update', () => {
+ component.setProps(getUniqueProps());
+ sinon.assert.callOrder(
+ userComponentDidUpdateBefore, callbackDid, userComponentDidUpdateAfter,
+ );
+ });
+
+ it('runs on state update', () => {
+ component.setState(getUniqueState());
+ expect(callbackDid).to.have.been.calledTwice();
+ });
+
+ it('runs user code in override on state update', () => {
+ component.setState(getUniqueState());
+ sinon.assert.callOrder(
+ userComponentDidUpdateBefore, callbackDid, userComponentDidUpdateAfter,
+ );
+ });
+ });
+});
+
+describe('Component extension with overrides not calling super()', () => {
+ class TestComponentWithoutSuper extends Component {
+ componentWillMount() {}
+ componentDidMount() {}
+ componentWillReceiveProps() {}
+ componentDidUpdate() {}
+
+ render() {
+ return null;
+ }
+ }
+
+ const callbackWill = sandbox.spy(TestComponentWithoutSuper.prototype, 'componentWillMountOrReceiveProps');
+ const callbackDid = sandbox.spy(TestComponentWithoutSuper.prototype, 'componentDidMountOrUpdate');
+
+ beforeEach(() => {
+ component = mount();
+ });
+
+ afterEach(() => sandbox.reset());
+
+ describe('componentWillMountOrReceiveProps()', () => {
+ it('does not run on mount', () => {
+ expect(callbackWill).not.to.have.been.called();
+ });
+
+ it('does not run on props update', () => {
+ component.setProps(getUniqueProps());
+ expect(callbackWill).not.to.have.been.called();
+ });
+
+ it('does not run on state update', () => {
+ component.setState(getUniqueState());
+ expect(callbackWill).not.to.have.been.called();
+ });
+ });
+
+ describe('componentDidMountOrUpdate()', () => {
+ it('does not run when mounted', () => {
+ expect(callbackDid).not.to.have.been.called();
+ });
+
+ it('does not run on props update', () => {
+ component.setProps(getUniqueProps());
+ expect(callbackDid).not.to.have.been.called();
+ });
+
+ it('does not run on state update', () => {
+ component.setState(getUniqueState());
+ expect(callbackDid).not.to.have.been.called();
});
});
});
diff --git a/src/index.js b/src/index.js
index 27ded4d..06680e5 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,7 +1,9 @@
import Component from './component';
import PureComponent from './pureComponent';
+import withEvents from './withEvents';
-export default {
+module.exports = {
Component,
PureComponent,
+ withEvents,
};
diff --git a/src/pureComponent.spec.js b/src/pureComponent.spec.js
index a28e41b..820bb82 100644
--- a/src/pureComponent.spec.js
+++ b/src/pureComponent.spec.js
@@ -1,12 +1,18 @@
/* eslint-env jest */
+/* eslint-disable react/no-multi-comp */
import chai, { expect } from 'chai';
import dirtyChai from 'dirty-chai';
import { mount } from 'enzyme';
import uniqueId from 'lodash.uniqueid';
import React from 'react';
import sinon from 'sinon';
+import sinonChai from 'sinon-chai';
chai.use(dirtyChai);
+chai.use(sinonChai);
+
+const sandbox = sinon.createSandbox();
+let component;
function getUniqueState() {
return { [uniqueId('stateVarName')]: uniqueId('stateVarValue') };
@@ -20,7 +26,7 @@ function getUniqueProps() {
const descriptor = React.PureComponent ? describe : describe.skip;
descriptor('PureComponent extension', () => {
- const { default: PureComponent } = require('./pureComponent'); // eslint-disable-line global-require
+ const { PureComponent } = require('./'); // eslint-disable-line global-require
class TestComponent extends PureComponent {
constructor(props) {
@@ -33,128 +39,305 @@ descriptor('PureComponent extension', () => {
}
}
- const callbackWill = sinon.spy(TestComponent.prototype, 'componentWillMountOrReceiveProps');
- const callbackDid = sinon.spy(TestComponent.prototype, 'componentDidMountOrUpdate');
- const callbackRender = sinon.spy(TestComponent.prototype, 'render');
+ const callbackWill = sandbox.spy(TestComponent.prototype, 'componentWillMountOrReceiveProps');
+ const callbackDid = sandbox.spy(TestComponent.prototype, 'componentDidMountOrUpdate');
+ const callbackRender = sandbox.spy(TestComponent.prototype, 'render');
- afterEach(() => {
- callbackWill.reset();
- callbackDid.reset();
- callbackRender.reset();
+ beforeEach(() => {
+ component = mount();
});
+ afterEach(() => sandbox.reset());
+
describe('componentWillMountOrReceiveProps()', () => {
it('runs once on mount', () => {
- mount();
- expect(callbackWill.calledOnce).to.be.true();
+ expect(callbackWill).to.have.been.calledOnce();
});
it('runs on mount with first parameter of component props', () => {
- const component = mount();
- expect(callbackWill.firstCall.args[0]).to.equal(component.props());
+ expect(callbackWill.firstCall).to.have.been.calledWith(component.props());
+ });
+
+ it('runs on mount with "this" context of component', () => {
+ expect(callbackWill.firstCall).to.have.been.calledOn(component.getNode());
});
it('runs on mount before render()', () => {
- mount();
- expect(callbackWill.calledBefore(callbackRender)).to.be.true();
+ expect(callbackWill).to.have.been.calledBefore(callbackRender);
});
it('runs on props update', () => {
- const component = mount();
component.setProps(getUniqueProps());
- expect(callbackWill.calledTwice).to.be.true();
+ expect(callbackWill).to.have.been.calledTwice();
});
it('runs on props update when no props change', () => {
- const component = mount();
component.setProps(component.props());
- expect(callbackWill.calledTwice).to.be.true();
+ expect(callbackWill).to.have.been.calledTwice();
});
it('runs on props update with first parameter of component props', () => {
- const component = mount();
component.setProps(getUniqueProps());
- expect(callbackWill.secondCall.args[0]).to.equal(component.props());
+ expect(callbackWill.secondCall).to.have.been.calledWith(component.props());
+ });
+
+ it('runs on props update with "this" context of component', () => {
+ component.setProps(getUniqueProps());
+ expect(callbackWill.secondCall).to.have.been.calledOn(component.getNode());
});
it('runs on props update before render()', () => {
- const component = mount();
component.setProps(getUniqueProps());
- expect(callbackWill.secondCall.calledBefore(callbackRender.secondCall)).to.be.true();
+ expect(callbackWill.secondCall).to.have.been.calledBefore(callbackRender.secondCall);
});
it('does not run on state update', () => {
- const component = mount();
component.setState(getUniqueState());
- expect(callbackWill.calledOnce).to.be.true();
+ expect(callbackWill).to.have.been.calledOnce();
});
});
describe('componentDidMountOrUpdate()', () => {
it('runs once when mounted', () => {
- mount();
- expect(callbackDid.calledOnce).to.be.true();
+ expect(callbackDid).to.have.been.calledOnce();
});
it('runs on mount with first parameter of component props', () => {
- const component = mount();
- expect(callbackDid.firstCall.args[0]).to.equal(component.props());
+ expect(callbackDid.firstCall).to.have.been.calledWith(component.props());
});
it('runs on mount with second parameter of component state', () => {
- const component = mount();
- expect(callbackDid.firstCall.args[1]).to.equal(component.state());
+ expect(callbackDid.firstCall).to.have.been.calledWith(sinon.match.any, component.state());
+ });
+
+ it('runs on mount with "this" context of component', () => {
+ expect(callbackDid.firstCall).to.have.been.calledOn(component.getNode());
});
it('runs after render()', () => {
- mount();
- expect(callbackDid.calledAfter(callbackRender)).to.be.true();
+ expect(callbackDid).to.have.been.calledAfter(callbackRender);
});
it('runs on props update', () => {
- const component = mount();
component.setProps(getUniqueProps());
- expect(callbackDid.calledTwice).to.be.true();
+ expect(callbackDid).to.have.been.calledTwice();
});
it('runs on props update with first parameter of previous component props', () => {
- const component = mount();
const initialProps = component.props();
component.setProps(getUniqueProps());
- expect(callbackDid.secondCall.args[0]).to.equal(initialProps);
+ expect(callbackDid.secondCall).to.have.been.calledWith(initialProps);
});
it('runs on props update with second parameter of previous component state', () => {
- const component = mount();
const initialState = component.state();
component.setProps(getUniqueProps());
- expect(callbackDid.secondCall.args[1]).to.equal(initialState);
+ expect(callbackDid.secondCall).to.have.been.calledWith(sinon.match.any, initialState);
+ });
+
+ it('runs on props update with "this" context of component', () => {
+ component.setProps(getUniqueProps());
+ expect(callbackDid.secondCall).to.have.been.calledOn(component.getNode());
});
it('runs on state update', () => {
- const component = mount();
component.setState(getUniqueState());
- expect(callbackDid.calledTwice).to.be.true();
+ expect(callbackDid).to.have.been.calledTwice();
});
it('runs on state update with first parameter of previous component props', () => {
- const component = mount();
const initialProps = component.props();
component.setState(getUniqueState());
- expect(callbackDid.secondCall.args[0]).to.equal(initialProps);
+ expect(callbackDid.secondCall).to.have.been.calledWith(initialProps);
});
it('runs on state update with second parameter of previous component state', () => {
- const component = mount();
const initialState = component.state();
component.setState(getUniqueState());
- expect(callbackDid.secondCall.args[1]).to.equal(initialState);
+ expect(callbackDid.secondCall).to.have.been.calledWith(sinon.match.any, initialState);
+ });
+
+ it('runs on state update with "this" context of component', () => {
+ component.setState(getUniqueState());
+ expect(callbackDid.secondCall).to.have.been.calledOn(component.getNode());
});
it('runs on props update before render()', () => {
- const component = mount();
component.setProps(getUniqueProps());
- expect(callbackDid.secondCall.calledAfter(callbackRender.secondCall)).to.be.true();
+ expect(callbackDid.secondCall).to.have.been.calledAfter(callbackRender.secondCall);
+ });
+ });
+});
+
+descriptor('PureComponent extension with overrides calling super()', () => {
+ const { PureComponent } = require('./'); // eslint-disable-line global-require
+
+ const userComponentWillMountBefore = sandbox.spy();
+ const userComponentWillMountAfter = sandbox.spy();
+ const userComponentDidMountBefore = sandbox.spy();
+ const userComponentDidMountAfter = sandbox.spy();
+ const userComponentWillReceivePropsBefore = sandbox.spy();
+ const userComponentWillReceivePropsAfter = sandbox.spy();
+ const userComponentDidUpdateBefore = sandbox.spy();
+ const userComponentDidUpdateAfter = sandbox.spy();
+
+ class TestComponentWithSuper extends PureComponent {
+ componentWillMount() {
+ userComponentWillMountBefore();
+ super.componentWillMount();
+ userComponentWillMountAfter();
+ }
+
+ componentDidMount() {
+ userComponentDidMountBefore();
+ super.componentDidMount();
+ userComponentDidMountAfter();
+ }
+
+ componentWillReceiveProps() {
+ userComponentWillReceivePropsBefore();
+ super.componentWillReceiveProps();
+ userComponentWillReceivePropsAfter();
+ }
+
+ componentDidUpdate() {
+ userComponentDidUpdateBefore();
+ super.componentDidUpdate();
+ userComponentDidUpdateAfter();
+ }
+
+ render() {
+ return null;
+ }
+ }
+
+ const callbackWill = sandbox.spy(TestComponentWithSuper.prototype, 'componentWillMountOrReceiveProps');
+ const callbackDid = sandbox.spy(TestComponentWithSuper.prototype, 'componentDidMountOrUpdate');
+
+ beforeEach(() => {
+ component = mount();
+ });
+
+ afterEach(() => sandbox.reset());
+
+ describe('componentWillMountOrReceiveProps()', () => {
+ it('runs once on mount', () => {
+ expect(callbackWill).to.have.been.calledOnce();
+ });
+
+ it('runs user code in override on mount', () => {
+ sinon.assert.callOrder(
+ userComponentWillMountBefore, callbackWill, userComponentWillMountAfter,
+ );
+ });
+
+ it('runs on props update', () => {
+ component.setProps(getUniqueProps());
+ expect(callbackWill).to.have.been.calledTwice();
+ });
+
+ it('runs user code in override on props update', () => {
+ component.setProps(getUniqueProps());
+ sinon.assert.callOrder(
+ userComponentWillReceivePropsBefore, callbackWill, userComponentWillReceivePropsAfter,
+ );
+ });
+
+ it('does not run on state update', () => {
+ component.setState(getUniqueState());
+ expect(callbackWill).to.have.been.calledOnce();
+ });
+ });
+
+ describe('componentDidMountOrUpdate()', () => {
+ it('runs once when mounted', () => {
+ expect(callbackDid).to.have.been.calledOnce();
+ });
+
+ it('runs user code in override on mount', () => {
+ sinon.assert.callOrder(
+ userComponentDidMountBefore, callbackDid, userComponentDidMountAfter,
+ );
+ });
+
+ it('runs on props update', () => {
+ component.setProps(getUniqueProps());
+ expect(callbackDid).to.have.been.calledTwice();
+ });
+
+ it('runs user code in override on props update', () => {
+ component.setProps(getUniqueProps());
+ sinon.assert.callOrder(
+ userComponentDidUpdateBefore, callbackDid, userComponentDidUpdateAfter,
+ );
+ });
+
+ it('runs on state update', () => {
+ component.setState(getUniqueState());
+ expect(callbackDid).to.have.been.calledTwice();
+ });
+
+ it('runs user code in override on state update', () => {
+ component.setState(getUniqueState());
+ sinon.assert.callOrder(
+ userComponentDidUpdateBefore, callbackDid, userComponentDidUpdateAfter,
+ );
+ });
+ });
+});
+
+descriptor('Component extension with overrides not calling super()', () => {
+ const { PureComponent } = require('./'); // eslint-disable-line global-require
+
+ class TestComponentWithoutSuper extends PureComponent {
+ componentWillMount() {}
+ componentDidMount() {}
+ componentWillReceiveProps() {}
+ componentDidUpdate() {}
+
+ render() {
+ return null;
+ }
+ }
+
+ const callbackWill = sandbox.spy(TestComponentWithoutSuper.prototype, 'componentWillMountOrReceiveProps');
+ const callbackDid = sandbox.spy(TestComponentWithoutSuper.prototype, 'componentDidMountOrUpdate');
+
+ beforeEach(() => {
+ component = mount();
+ });
+
+ afterEach(() => sandbox.reset());
+
+ describe('componentWillMountOrReceiveProps()', () => {
+ it('does not run on mount', () => {
+ expect(callbackWill).not.to.have.been.called();
+ });
+
+ it('does not run on props update', () => {
+ component.setProps(getUniqueProps());
+ expect(callbackWill).not.to.have.been.called();
+ });
+
+ it('does not run on state update', () => {
+ component.setState(getUniqueState());
+ expect(callbackWill).not.to.have.been.called();
+ });
+ });
+
+ describe('componentDidMountOrUpdate()', () => {
+ it('does not run when mounted', () => {
+ expect(callbackDid).not.to.have.been.called();
+ });
+
+ it('does not run on props update', () => {
+ component.setProps(getUniqueProps());
+ expect(callbackDid).not.to.have.been.called();
+ });
+
+ it('does not run on state update', () => {
+ component.setState(getUniqueState());
+ expect(callbackDid).not.to.have.been.called();
});
});
});
diff --git a/src/withEvents.js b/src/withEvents.js
new file mode 100644
index 0000000..bcd7787
--- /dev/null
+++ b/src/withEvents.js
@@ -0,0 +1,39 @@
+import wrap from 'lodash.wrap';
+
+function noop() {}
+
+export default function withEvents(config) {
+ function willMountCustom(nativeFunc = noop, ...args) {
+ const result = nativeFunc(...args);
+ this.componentWillMountOrReceiveProps(this.props);
+ return result;
+ }
+
+ function didMountCustom(nativeFunc = noop, ...args) {
+ const result = nativeFunc(...args);
+ this.componentDidMountOrUpdate(this.props, this.state);
+ return result;
+ }
+
+ function willReceivePropsCustom(nativeFunc = noop, ...args) {
+ const result = nativeFunc(...args);
+ this.componentWillMountOrReceiveProps(...args);
+ return result;
+ }
+
+ function didUpdateCustom(nativeFunc = noop, ...args) {
+ const result = nativeFunc(...args);
+ this.componentDidMountOrUpdate(...args);
+ return result;
+ }
+
+ return {
+ componentWillMountOrReceiveProps: noop,
+ componentDidMountOrUpdate: noop,
+ ...config,
+ componentWillMount: wrap(config.componentWillMount, willMountCustom),
+ componentDidMount: wrap(config.componentDidMount, didMountCustom),
+ componentWillReceiveProps: wrap(config.componentWillReceiveProps, willReceivePropsCustom),
+ componentDidUpdate: wrap(config.componentDidUpdate, didUpdateCustom),
+ };
+}
diff --git a/src/withEvents.spec.js b/src/withEvents.spec.js
new file mode 100644
index 0000000..b1aa483
--- /dev/null
+++ b/src/withEvents.spec.js
@@ -0,0 +1,245 @@
+/* eslint-env jest */
+import chai, { expect } from 'chai';
+import createReactClass from 'create-react-class';
+import dirtyChai from 'dirty-chai';
+import { mount } from 'enzyme';
+import uniqueId from 'lodash.uniqueid';
+import React from 'react';
+import sinon from 'sinon';
+import sinonChai from 'sinon-chai';
+import withEvents from './withEvents';
+
+chai.use(dirtyChai);
+chai.use(sinonChai);
+
+const sandbox = sinon.createSandbox();
+let component;
+
+function getUniqueState() {
+ return { [uniqueId('stateVarName')]: uniqueId('stateVarValue') };
+}
+
+function getUniqueProps() {
+ return { [uniqueId('propName')]: uniqueId('propValue') };
+}
+
+describe('withEvents extension', () => {
+ const callbackWill = sandbox.spy();
+ const callbackDid = sandbox.spy();
+
+ const TestComponent = createReactClass(withEvents({
+ getInitialState: getUniqueState,
+ componentWillMountOrReceiveProps: callbackWill,
+ componentDidMountOrUpdate: callbackDid,
+ render: () => null,
+ }));
+
+ const callbackRender = sandbox.spy(TestComponent.prototype, 'render');
+
+ beforeEach(() => {
+ component = mount();
+ });
+
+ afterEach(() => sandbox.reset());
+
+ describe('componentWillMountOrReceiveProps()', () => {
+ it('runs once on mount', () => {
+ expect(callbackWill).to.have.been.calledOnce();
+ });
+
+ it('runs on mount with first parameter of component props', () => {
+ expect(callbackWill.firstCall).to.have.been.calledWith(component.props());
+ });
+
+ it('runs on mount with "this" context of component', () => {
+ expect(callbackWill.firstCall).to.have.been.calledOn(component.getNode());
+ });
+
+ it('runs on mount before render()', () => {
+ expect(callbackWill).to.have.been.calledBefore(callbackRender);
+ });
+
+ it('runs on props update', () => {
+ component.setProps(getUniqueProps());
+ expect(callbackWill).to.have.been.calledTwice();
+ });
+
+ it('runs on props update when no props change', () => {
+ component.setProps(component.props());
+ expect(callbackWill).to.have.been.calledTwice();
+ });
+
+ it('runs on props update with first parameter of component props', () => {
+ component.setProps(getUniqueProps());
+ expect(callbackWill.secondCall).to.have.been.calledWith(component.props());
+ });
+
+ it('runs on props update with "this" context of component', () => {
+ component.setProps(getUniqueProps());
+ expect(callbackWill.secondCall).to.have.been.calledOn(component.getNode());
+ });
+
+ it('runs on props update before render()', () => {
+ component.setProps(getUniqueProps());
+ expect(callbackWill.secondCall).to.have.been.calledBefore(callbackRender.secondCall);
+ });
+
+ it('does not run on state update', () => {
+ component.setState(getUniqueState());
+ expect(callbackWill).to.have.been.calledOnce();
+ });
+ });
+
+ describe('componentDidMountOrUpdate()', () => {
+ it('runs once when mounted', () => {
+ expect(callbackDid).to.have.been.calledOnce();
+ });
+
+ it('runs on mount with first parameter of component props', () => {
+ expect(callbackDid.firstCall).to.have.been.calledWith(component.props());
+ });
+
+ it('runs on mount with second parameter of component state', () => {
+ expect(callbackDid.firstCall).to.have.been.calledWith(sinon.match.any, component.state());
+ });
+
+ it('runs on mount with "this" context of component', () => {
+ expect(callbackDid.firstCall).to.have.been.calledOn(component.getNode());
+ });
+
+ it('runs after render()', () => {
+ expect(callbackDid).to.have.been.calledAfter(callbackRender);
+ });
+
+ it('runs on props update', () => {
+ component.setProps(getUniqueProps());
+ expect(callbackDid).to.have.been.calledTwice();
+ });
+
+ it('runs on props update with first parameter of previous component props', () => {
+ const initialProps = component.props();
+ component.setProps(getUniqueProps());
+ expect(callbackDid.secondCall).to.have.been.calledWith(initialProps);
+ });
+
+ it('runs on props update with second parameter of previous component state', () => {
+ const initialState = component.state();
+ component.setProps(getUniqueProps());
+ expect(callbackDid.secondCall).to.have.been.calledWith(sinon.match.any, initialState);
+ });
+
+ it('runs on props update with "this" context of component', () => {
+ component.setProps(getUniqueProps());
+ expect(callbackDid.secondCall).to.have.been.calledOn(component.getNode());
+ });
+
+ it('runs on state update', () => {
+ component.setState(getUniqueState());
+ expect(callbackDid).to.have.been.calledTwice();
+ });
+
+ it('runs on state update with first parameter of previous component props', () => {
+ const initialProps = component.props();
+ component.setState(getUniqueState());
+ expect(callbackDid.secondCall).to.have.been.calledWith(initialProps);
+ });
+
+ it('runs on state update with second parameter of previous component state', () => {
+ const initialState = component.state();
+ component.setState(getUniqueState());
+ expect(callbackDid.secondCall).to.have.been.calledWith(sinon.match.any, initialState);
+ });
+
+ it('runs on state update with "this" context of component', () => {
+ component.setState(getUniqueState());
+ expect(callbackDid.secondCall).to.have.been.calledOn(component.getNode());
+ });
+
+ it('runs on props update before render()', () => {
+ component.setProps(getUniqueProps());
+ expect(callbackDid.secondCall).to.have.been.calledAfter(callbackRender.secondCall);
+ });
+ });
+});
+
+describe('withEvents extension with overrides', () => {
+ const userComponentWillMount = sandbox.spy();
+ const userComponentDidMount = sandbox.spy();
+ const userComponentWillReceiveProps = sandbox.spy();
+ const userComponentDidUpdate = sandbox.spy();
+
+ const callbackWill = sandbox.spy();
+ const callbackDid = sandbox.spy();
+
+ const TestComponentWithOverrides = createReactClass(withEvents({
+ getInitialState: getUniqueState,
+ componentWillMount: userComponentWillMount,
+ componentDidMount: userComponentDidMount,
+ componentWillReceiveProps: userComponentWillReceiveProps,
+ componentDidUpdate: userComponentDidUpdate,
+ componentWillMountOrReceiveProps: callbackWill,
+ componentDidMountOrUpdate: callbackDid,
+ render: () => null,
+ }));
+
+ beforeEach(() => {
+ component = mount();
+ });
+
+ afterEach(() => sandbox.reset());
+
+ describe('componentWillMountOrReceiveProps()', () => {
+ it('runs once on mount', () => {
+ expect(callbackWill).to.have.been.calledOnce();
+ });
+
+ it('runs user code in override on mount', () => {
+ expect(userComponentWillMount).to.have.been.calledBefore(callbackWill);
+ });
+
+ it('runs on props update', () => {
+ component.setProps(getUniqueProps());
+ expect(callbackWill).to.have.been.calledTwice();
+ });
+
+ it('runs user code in override on props update', () => {
+ component.setProps(getUniqueProps());
+ expect(userComponentWillReceiveProps).to.have.been.calledBefore(callbackWill.secondCall);
+ });
+
+ it('does not run on state update', () => {
+ component.setState(getUniqueState());
+ expect(callbackWill).to.have.been.calledOnce();
+ });
+ });
+
+ describe('componentDidMountOrUpdate()', () => {
+ it('runs once when mounted', () => {
+ expect(callbackDid).to.have.been.calledOnce();
+ });
+
+ it('runs user code in override on mount', () => {
+ expect(userComponentDidMount).to.have.been.calledBefore(callbackDid);
+ });
+
+ it('runs on props update', () => {
+ component.setProps(getUniqueProps());
+ expect(callbackDid).to.have.been.calledTwice();
+ });
+
+ it('runs user code in override on props update', () => {
+ component.setProps(getUniqueProps());
+ expect(userComponentDidUpdate).to.have.been.calledBefore(callbackDid.secondCall);
+ });
+
+ it('runs on state update', () => {
+ component.setState(getUniqueState());
+ expect(callbackDid).to.have.been.calledTwice();
+ });
+
+ it('runs user code in override on state update', () => {
+ component.setState(getUniqueState());
+ expect(userComponentDidUpdate).to.have.been.calledBefore(callbackDid.secondCall);
+ });
+ });
+});