diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..dc561e3
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,4 @@
+{
+ "extends": ["godaddy-react"]
+}
+
diff --git a/.gitignore b/.gitignore
index 434ec16..f3ef808 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,9 @@ lib-cov
# Coverage directory used by tools like istanbul
coverage
+# nyc coverage output directory
+.nyc_output/
+
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
diff --git a/README.md b/README.md
index e0a37d4..6070a2b 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,8 @@ npm install react-validation-context --save
This library revolves around the idea of "validity". A component can have one of the following validities:
-- `@typedef {(null|Boolean)} Validity`
+- `@typedef {(undefined|null|Boolean)} Validity`
+ - `undefined` - No validation state defined. This is the default.
- `null` - Validation is disabled.
- `true` - Validation passed.
- `false` - Validation failed.
@@ -34,7 +35,7 @@ The `Validates` component is used to wrap a component that can be validated, pro
### Props
-- `{String} name` - A unique identifier for the component.
+- `{String} name` - A unique identifier for the component. Required.
- `{Validity} validates` - The component's validity.
- `{Function} onValidChange` - Validity change handler.
- `{ReactElement} children` - Children. The component only accepts a single child, and will simply render as that child.
@@ -95,10 +96,6 @@ RequiredInput.propTypes = {
onValidChange: React.PropTypes.func, // validity change handler
children: React.PropTypes.node // React children
};
-
-RequiredInput.defaultProps = {
- validate: () => null // By default, validation is disabled, so return `null`
-};
```
@@ -195,11 +192,10 @@ Run the [`mocha`][mocha] unit tests via:
npm test
```
-Coverage reports are generated using [`istanbul`][istanbul] for [Cobertura][cobertura].
+Text and HTML coverage reports are generated using [`nyc`][nyc].
[react-docs-context]: https://facebook.github.io/react/docs/context.html (React context API docs)
[mocha]: http://mochajs.org/ (mocha)
-[istanbul]: https://www.npmjs.com/package/istanbul (istanbul)
-[cobertura]: https://cobertura.github.io/cobertura/ (Cobertura)
+[nyc]: https://www.npmjs.com/package/nyc (nyc)
diff --git a/package.json b/package.json
index 444864e..192e266 100644
--- a/package.json
+++ b/package.json
@@ -18,11 +18,16 @@
"*.js",
"test"
],
+ "nyc": {
+ "require": ["babel-register"],
+ "reporter": ["text", "html"]
+ },
"scripts": {
- "test": "istanbul cover --report cobertura _mocha -- --compilers js:babel-register",
- "pretest": "godaddy-js-style-lint *.js ./test",
- "prepublish": "npm run build",
- "build": "babel *.js -d lib"
+ "build": "babel *.js -d lib",
+ "lint": "eslint --fix *.js test",
+ "pretest": "eslint *.js test",
+ "test": "nyc mocha test/**/*.test.js",
+ "prepare": "npm run build"
},
"repository": {
"type": "git",
@@ -47,26 +52,28 @@
]
},
"dependencies": {
- "babel-plugin-transform-object-rest-spread": "^6.8",
- "babel-preset-react": "^6.5",
- "babel-preset-es2015": "^6.9"
+ "babel-plugin-transform-object-rest-spread": "^6.23.0",
+ "babel-preset-react": "^6.24.1",
+ "babel-preset-es2015": "^6.24.1"
},
"devDependencies": {
- "assume": "^1.4",
+ "assume": "^1.5.1",
"assume-sinon": "^1.0",
- "babel-cli": "^6.18.0",
- "babel-eslint": "^6.0",
- "babel-register": "^6.9",
- "godaddy-style": "^3.1.6",
- "istanbul": "~1.0.0-alpha.2",
- "jsdom": "^9.4.1",
- "mocha": "^2.5",
- "react": "15.x.x",
- "react-addons-test-utils": "15.x.x",
- "react-dom": "15.x.x",
- "sinon": "^1.17"
+ "babel-cli": "^6.24.1",
+ "babel-register": "^6.24.1",
+ "eslint": "^3.19.0",
+ "eslint-config-godaddy-react": "^1.1.1",
+ "eslint-plugin-json": "^1.2.0",
+ "eslint-plugin-mocha": "^4.11.0",
+ "eslint-plugin-react": "^6.10.3",
+ "mocha": "^3.4.2",
+ "nyc": "^11.0.3",
+ "prop-types": "^15.0.0",
+ "react": "^15.0.0",
+ "react-test-renderer": "^15.0.0",
+ "sinon": "^2.4.1"
},
"peerDependencies": {
- "react": "15.x.x"
+ "react": "^15.0.0"
}
}
diff --git a/test/.eslintrc.json b/test/.eslintrc.json
deleted file mode 100644
index 1da326e..0000000
--- a/test/.eslintrc.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "extends": "../node_modules/godaddy-style/dist/.eslintrc",
- "env": {
- "node": true,
- "mocha": true
- }
-}
diff --git a/test/index.js b/test/index.js
deleted file mode 100644
index f90466f..0000000
--- a/test/index.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import assume from 'assume';
-import assumeSinon from 'assume-sinon';
-
-assume.use(assumeSinon);
-
-
-import { jsdom } from 'jsdom';
-
-global.document = jsdom();
-global.window = document.defaultView;
-
-
-import './validates';
-import './validate';
-
diff --git a/test/utils.js b/test/utils.js
index 3424ff6..7126856 100644
--- a/test/utils.js
+++ b/test/utils.js
@@ -1,18 +1,22 @@
import assume from 'assume';
import sinon from 'sinon';
import React from 'react';
-import ReactDOM from 'react-dom';
-import ReactTestUtils from 'react-addons-test-utils';
+import { func, element } from 'prop-types';
+import TestRenderer from 'react-test-renderer';
+import ShallowRenderer from 'react-test-renderer/shallow';
+
+const undef = void 0;
+
+export { undef };
export function shallowRender(elem) {
- const renderer = ReactTestUtils.createRenderer();
+ const renderer = new ShallowRenderer();
renderer.render(elem);
- return { renderer, output: renderer.getRenderOutput() };
+ return renderer.getRenderOutput();
}
export function render(elem) {
- const tree = document.createElement('div');
- return { tree, output: ReactDOM.render(elem, tree) };
+ return TestRenderer.create(elem);
}
export class MockContext extends React.Component {
@@ -25,25 +29,31 @@ export class MockContext extends React.Component {
}
}
-MockContext.propTypes = { onValidChange: React.PropTypes.func, children: React.PropTypes.element };
-MockContext.childContextTypes = { onValidChange: React.PropTypes.func };
+MockContext.propTypes = {
+ onValidChange: func,
+ children: element
+};
+
+MockContext.childContextTypes = {
+ onValidChange: func
+};
-export function testRendersAsChildren(Component) {
+export function describeRenderAsChildren(Component) {
describe('#render()', function renderTests() {
it('renders as its child', function renderAsChildTest() {
const children = this is a test;
- const { output } = shallowRender({ children });
+ const output = shallowRender({ children });
assume(output).equals(children);
});
it('renders nothing if it does not have children', function renderNothingTest() {
- const { output } = shallowRender();
+ const output = shallowRender();
assume(output).equals(null);
});
});
}
-export function testValidatesHandlers(Component) {
+export function describeValidatesHandlers(Component) {
describe('validates handlers', function validatesHandlersTests() {
it('calls handlers in props and context with the initial state and undefined', function initialTest() {
const name = 'test';
@@ -51,12 +61,15 @@ export function testValidatesHandlers(Component) {
const ctxSpy = sinon.spy();
function test(validates) {
+ propsSpy.reset();
+ ctxSpy.reset();
render(
);
- let undef;
+ assume(propsSpy).is.called(1);
assume(propsSpy).is.calledWithExactly(name, validates, undef);
+ assume(ctxSpy).is.called(1);
assume(ctxSpy).is.calledWithExactly(name, validates, undef);
}
@@ -69,13 +82,13 @@ export function testValidatesHandlers(Component) {
const ctxSpy = sinon.spy();
class Fixture extends React.Component {
- constructor () {
+ constructor() {
super();
this.state = {};
}
- render () {
+ render() {
const { validates } = this.state;
return
@@ -90,7 +103,15 @@ export function testValidatesHandlers(Component) {
false,
true,
null,
- true
+ true,
+ undef,
+ true,
+ false,
+ undef,
+ false,
+ null,
+ undef,
+ null
];
let isValid, wasValid;
@@ -102,8 +123,12 @@ export function testValidatesHandlers(Component) {
wasValid = isValid;
isValid = valids[i];
+ propsSpy.reset();
+ ctxSpy.reset();
elem.setState({ validates: isValid }, function check() {
+ assume(propsSpy).is.called(1);
assume(propsSpy).is.calledWithExactly(name, isValid, wasValid);
+ assume(ctxSpy).is.called(1);
assume(ctxSpy).is.calledWithExactly(name, isValid, wasValid);
call(i + 1);
});
@@ -117,3 +142,58 @@ export function testValidatesHandlers(Component) {
});
}
+export function describeValidatesMountHandlers(Component) {
+ describe('mount/unmount', function unmountTests() {
+ it('calls the context and props handlers at mount and unmount appropriately', function handlerTest(done) {
+ const name = 'test';
+ const validates = true;
+ const propsSpy = sinon.spy();
+ const ctxSpy = sinon.spy();
+
+ class Fixture extends React.Component {
+ constructor() {
+ super();
+ this.state = { shouldMount: false };
+ }
+
+ render() {
+ const { shouldMount } = this.state;
+
+ const child = shouldMount
+ ?
+ : null;
+ return
+ {child}
+ ;
+ }
+ }
+
+ function test(elem) {
+ function didUnmount() {
+ assume(propsSpy).is.called(1);
+ assume(propsSpy).is.calledWithExactly(name, undef, validates);
+ assume(ctxSpy).is.called(1);
+ assume(ctxSpy).is.calledWithExactly(name, undef, validates);
+ done();
+ }
+
+ function didMount() {
+ assume(propsSpy).is.called(1);
+ assume(propsSpy).is.calledWithExactly(name, validates, undef);
+ assume(ctxSpy).is.called(1);
+ assume(ctxSpy).is.calledWithExactly(name, validates, undef);
+ propsSpy.reset();
+ ctxSpy.reset();
+ elem.setState({ shouldMount: false }, didUnmount);
+ }
+
+ assume(propsSpy).is.not.called();
+ assume(ctxSpy).is.not.called();
+ elem.setState({ shouldMount: true }, didMount);
+ }
+
+ render();
+ });
+ });
+}
+
diff --git a/test/validate.js b/test/validate.test.js
similarity index 63%
rename from test/validate.js
rename to test/validate.test.js
index 7a11532..70bbc06 100644
--- a/test/validate.js
+++ b/test/validate.test.js
@@ -1,16 +1,35 @@
import assume from 'assume';
import sinon from 'sinon';
+import assumeSinon from 'assume-sinon';
import React from 'react';
-import { render, testRendersAsChildren, testValidatesHandlers } from './utils';
+import {
+ undef,
+ render,
+ describeRenderAsChildren,
+ describeValidatesHandlers,
+ describeValidatesMountHandlers
+} from './utils';
import Validate from '../validate';
import Validates from '../validates';
+assume.use(assumeSinon);
+
describe('Validate', function ValidatesTests() {
- testRendersAsChildren(Validate);
- testValidatesHandlers(Validate);
+ describeRenderAsChildren(Validate);
+ describeValidatesHandlers(Validate);
+ describeValidatesMountHandlers(Validate);
describe('validate handler', function validateHandlerTests() {
+ it('defaults to not specifying a validity (i.e. returns `undefined`)', function defaultHandlerTest() {
+ function test(elem) {
+ const { validate } = elem.props;
+ assume(validate()).equals(undef);
+ }
+
+ render();
+ });
+
it('calls the handler with a map from names to validity states', function handlerCallTest() {
const validateSpy = sinon.spy();
render(
@@ -28,15 +47,15 @@ describe('Validate', function ValidatesTests() {
function testHandlerResult(props, check, done) {
const name = 'test';
class Fixture extends React.Component {
- constructor () {
+ constructor() {
super();
this.state = {};
}
- render () {
+ render() {
const { validate } = this.state;
- return ;
+ return ;
}
}
function test(elem) {
@@ -58,7 +77,7 @@ describe('Validate', function ValidatesTests() {
wasValid = isValid;
isValid = valids[i];
- const validateSpy = sinon.spy(function validate() { return isValid });
+ const validateSpy = sinon.spy(function validate() { return isValid; });
elem.setState({ validate: validateSpy }, function next() {
assume(validateSpy).is.called(1);
check(name, isValid, wasValid);
@@ -98,7 +117,6 @@ describe('Validate', function ValidatesTests() {
const onValidChangeSpy = sinon.spy();
function check(name) {
- let undef;
assume(onValidChangeSpy).is.calledWithExactly(name, validates, undef);
}
@@ -107,6 +125,37 @@ describe('Validate', function ValidatesTests() {
test(valids[i]);
});
+
+ it('calls the validate function with appropriate names if the child names change', function updatedNamesTest(done) {
+ const validateSpy = sinon.spy();
+ class Fixture extends React.Component {
+ constructor() {
+ super();
+
+ this.state = {
+ childName: 'test-child-name'
+ };
+ }
+
+ render() {
+ const { childName } = this.state;
+ return
+
+ ;
+ }
+ }
+
+ function test(elem) {
+ assume(validateSpy).is.calledWithExactly({ 'test-child-name': true });
+ validateSpy.reset();
+ elem.setState({ childName: 'test-child-name-2' }, function next() {
+ assume(validateSpy).is.calledWithExactly({ 'test-child-name-2': true });
+ done();
+ });
+ }
+
+ render();
+ });
});
});
diff --git a/test/validates.js b/test/validates.js
deleted file mode 100644
index f125b16..0000000
--- a/test/validates.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import assume from 'assume';
-import sinon from 'sinon';
-import React from 'react';
-
-import { render, MockContext, testRendersAsChildren, testValidatesHandlers } from './utils';
-import Validates from '../validates';
-
-describe('Validates', function ValidatesTests() {
- testRendersAsChildren(Validates);
- testValidatesHandlers(Validates);
-
- describe('#onValidChange()', function onValidChangeTests() {
- it('calls the handlers in props and context appropriately', function handlerTest(done) {
- const name = 'test';
- const propsSpy = sinon.spy();
- const ctxSpy = sinon.spy();
-
- function test(elem) {
- elem.onValidChange(true, true);
- assume(propsSpy).is.not.called();
- assume(ctxSpy).is.not.called();
-
- let isValid, wasValid;
- function call(nextValid) {
- wasValid = isValid;
- isValid = nextValid;
- elem.onValidChange(isValid, wasValid);
- assume(propsSpy).is.calledWithExactly(name, isValid, wasValid);
- assume(ctxSpy).is.calledWithExactly(name, isValid, wasValid);
- }
-
- [
- true,
- false,
- true,
- null,
- true
- ].forEach(call);
-
- done();
- }
-
- render(
-
- );
- });
-
- it('does nothing if no handlers are present', function noHandlersTest(done) {
- function test(elem) {
- elem.onValidChange(true, false);
- done();
- }
-
- render();
- });
- });
-});
-
diff --git a/test/validates.test.js b/test/validates.test.js
new file mode 100644
index 0000000..5b571be
--- /dev/null
+++ b/test/validates.test.js
@@ -0,0 +1,123 @@
+import assume from 'assume';
+import sinon from 'sinon';
+import assumeSinon from 'assume-sinon';
+import React from 'react';
+
+import {
+ undef,
+ render,
+ MockContext,
+ describeRenderAsChildren,
+ describeValidatesHandlers,
+ describeValidatesMountHandlers
+} from './utils';
+import Validates from '../validates';
+
+assume.use(assumeSinon);
+
+describe('Validates', function ValidatesTests() {
+ describeRenderAsChildren(Validates);
+ describeValidatesHandlers(Validates);
+ describeValidatesMountHandlers(Validates);
+
+ describe('#onValidChange()', function onValidChangeTests() {
+ it('calls the handlers in props and context appropriately', function handlerTest(done) {
+ const name = 'test';
+ const propsSpy = sinon.spy();
+ const ctxSpy = sinon.spy();
+
+ function test(elem) {
+ elem.onValidChange(true, true);
+ assume(propsSpy).is.not.called();
+ assume(ctxSpy).is.not.called();
+
+ let isValid, wasValid;
+ function call(nextValid) {
+ wasValid = isValid;
+ isValid = nextValid;
+ elem.onValidChange(isValid, wasValid);
+ assume(propsSpy).is.calledWithExactly(name, isValid, wasValid);
+ assume(ctxSpy).is.calledWithExactly(name, isValid, wasValid);
+ }
+
+ [
+ true,
+ false,
+ true,
+ null,
+ true
+ ].forEach(call);
+
+ done();
+ }
+
+ render(
+
+ );
+ });
+
+ it('does nothing if no handlers are present', function noHandlersTest(done) {
+ function test(elem) {
+ elem.onValidChange(true, false);
+ done();
+ }
+
+ render();
+ });
+
+ it('does nothing if given identical validities', function noChangeTest(done) {
+ const ctxSpy = sinon.spy();
+ const propsSpy = sinon.spy();
+ function test(elem) {
+ [
+ undef,
+ null,
+ false,
+ true
+ ].forEach(state => {
+ elem.onValidChange(state, state);
+ assume(ctxSpy).is.not.called();
+ assume(propsSpy).is.not.called();
+ });
+
+ done();
+ }
+
+ render(
+
+ );
+ });
+
+ it('calls the handlers twice appropriately if an old name is given', function newNameTest(done) {
+ const name = 'test';
+ const propsSpy = sinon.spy();
+ const ctxSpy = sinon.spy();
+
+ function test(elem) {
+ elem.onValidChange(true, true);
+ assume(propsSpy).is.not.called();
+ assume(ctxSpy).is.not.called();
+ propsSpy.reset();
+ ctxSpy.reset();
+
+ const oldName = 'old-name';
+ elem.onValidChange(true, false, oldName);
+ assume(propsSpy).is.called(2);
+ assume(ctxSpy).is.called(2);
+ [
+ [oldName, undef, false],
+ [name, true, false]
+ ].forEach(args => {
+ assume(propsSpy).is.calledWithExactly(...args);
+ assume(ctxSpy).is.calledWithExactly(...args);
+ });
+ done();
+ }
+
+ render(
+
+ );
+ });
+ });
+});
+
diff --git a/validate.js b/validate.js
index ce667d8..269ddff 100644
--- a/validate.js
+++ b/validate.js
@@ -1,46 +1,87 @@
-import React from 'react';
+import { func } from 'prop-types';
import Validates from './validates';
+const undef = void 0;
+
+/**
+ * This library revolves around the idea of "validity". A component can have one of the following validities:
+ *
+ * - `undefined` - No validation state defined. This is the default.
+ * - `null` - Validation is disabled.
+ * - `true` - Validation passed.
+ * - `false` - Validation failed.
+ *
+ * @typedef {(undefined|null|Boolean)} Validity
+ */
+
+/**
+ * The `Validate` component is used to wrap a component which has descendants that may be validated, and provides an interface for
+ * validating all of those descendants. It extends `Validates` to provide the same interface for listening for validation changes
+ * on the component itself.
+ *
+ * **NOTE**: This component is able to keep track of all conforming descendant components (not just direct children) via the React
+ * `context` api.
+ */
export default class Validate extends Validates {
- constructor (props) {
+ /**
+ * Creates a new instance of the component.
+ *
+ * @param {Object} props - The component's props.
+ */
+ constructor(props) {
super(props);
this.state = {
- validates: props.validates,
- valids: {}
+ validates: undef, // validity that results from calling the validate() function from props
+ valids: {} // set of validities for descendent components; key is component name, value is validity
};
}
+ /**
+ * Whether or not the component currently validates.
+ *
+ * @type {Validity}
+ * @private
+ */
+ get validates() {
+ // Prefer props over state.
+ const { validates = this.state.validates } = this.props;
+ return validates;
+ }
+
/**
* Get the child context.
*
* @returns {Object} The child context.
- * @private
*/
- getChildContext () {
+ getChildContext() {
return {
/**
* Child validity change handler.
*
- * @param {String} name Identifier for the field whose validity changed
- * @param {Mixed} isValid Validity. `true`/`false` if the component is valid/invalid; `null` if validation is disabled
+ * @param {String} name Identifier for the field whose validity changed.
+ * @param {Validity} isValid The field's current validity.
*/
onValidChange: (name, isValid) => {
const { valids } = this.state;
- valids[name] = isValid;
- this.setState({ valids, validates: this.props.validate(valids) });
+ const { validate } = this.props;
+
+ if (isValid === undef) {
+ delete valids[name];
+ } else {
+ valids[name] = isValid;
+ }
+ this.setState({ valids, validates: validate(valids) });
}
- }
+ };
}
/**
* React lifecycle handler called immediately before the component's initial render.
*/
- componentWillMount () {
- const { validates : isValid = this.state.validates } = this.props;
-
+ componentWillMount() {
// Update the handlers with the initial state
- this.onValidChange(isValid);
+ this.onValidChange(this.validates);
}
/**
@@ -48,40 +89,56 @@ export default class Validate extends Validates {
*
* @param {Object} nextProps Component's new props.
*/
- componentWillReceiveProps (nextProps) {
+ componentWillReceiveProps(nextProps) {
const { validate } = nextProps;
- this.setState({ validates: validate(this.state.valids) });
+ if (validate === this.props.validate) {
+ return;
+ }
+
+ const { valids } = this.state;
+
+ // Compute new validity, update state
+ this.setState({ validates: validate(valids) });
}
/**
- * React lifecycle handler called when component is about to update.
+ * React lifecycle handler called when a component finished updating.
*
- * @param {Object} nextProps Component's new props.
- * @param {Object} nextState Component's new state.
+ * @param {Object} prevProps Component's previous props.
+ * @param {Object} prevState Component's previous state.
*/
- componentWillUpdate (nextProps, nextState) {
- const { validates: wasValid = this.state.validates } = this.props;
- const { validates: isValid = nextState.validates } = nextProps;
+ componentDidUpdate(prevProps, prevState) {
+ const isValid = this.validates;
+
+ // Prefer props over state.
+ const { validates: wasValid = prevState.validates, name: prevName } = prevProps;
+ this.onValidChange(isValid, wasValid, prevName);
+ }
- this.onValidChange(isValid, wasValid);
+ /**
+ * React lifecycle handler called when component is about to be unmounted.
+ */
+ componentWillUnmount() {
+ // Update the handlers with `isValid=undefined` to notify them that the component no longer is being validated
+ this.onValidChange(undef, this.validates);
}
}
Validate.defaultProps = {
- validate: () => null // by default, validation is disabled
+ validate: () => {} // by default, no validation defined.
};
Validate.propTypes = {
- validate: React.PropTypes.func // validation function
+ validate: func.isRequired // validation function
};
-// Inherit all propTypes from Validate. In production propTypes are stipped
+// Inherit all propTypes from Validate. In production propTypes are stripped
// so be sure to check for their existence before copying them over.
if (Validates.propTypes) {
Object.keys(Validates.propTypes).forEach(k => (Validate.propTypes[k] = Validates.propTypes[k]));
}
Validate.childContextTypes = {
- onValidChange: React.PropTypes.func
+ onValidChange: func
};
diff --git a/validates.js b/validates.js
index c73a67f..08b27b8 100644
--- a/validates.js
+++ b/validates.js
@@ -1,25 +1,59 @@
import React from 'react';
+import { string, func, element, oneOf } from 'prop-types';
+const undef = void 0;
+function noop() {}
+
+/**
+ * This library revolves around the idea of "validity". A component can have one of the following validities:
+ *
+ * - `undefined` - No validation state defined. This is the default.
+ * - `null` - Validation is disabled.
+ * - `true` - Validation passed.
+ * - `false` - Validation failed.
+ *
+ * @typedef {(undefined|null|Boolean)} Validity
+ */
+
+/**
+ * It is useful to know when a component's validity changes. As such, this library attempts to provide a uniform API for
+ * validation change handlers. In general, a validity change handler has the following API:
+ *
+ * @callback onValidChange
+ * @param {String} name - The unique identifier for the component whose validity changed.
+ * @param {Validity} isValid - The current validity of the component.
+ * @param {Validity} wasValid - The previous validity of the component.
+ */
+
+/**
+ * The `Validates` component is used to wrap a component that can be validated, providing the logic for validation change
+ * handlers.
+ */
export default class Validates extends React.Component {
/**
- * If isValid !== wasValid, calls the onValidChange handlers in props and context with the specified arguments.
+ * If `isValid !== wasValid` or `prevName !== this.props.name`, calls the onValidChange handlers in props and context with the
+ * specified arguments.
*
- * @param {Mixed} isValid Validity. `true`/`false` if the component is valid/invalid; `null` if validation is disabled
- * @param {Mixed} wasValid The previous validity. May be `undefined` if this is the first update.
+ * @protected
+ * @param {Validity} isValid The current validity.
+ * @param {Validity} wasValid The previous validity.
+ * @param {Object} [prevName] The previous name that the component was using. If it is different than the current name, the
+ * props and context handlers will be called first with `undefined` to indicate the previous name no longer has validation.
*/
- onValidChange (isValid, wasValid) {
- if (isValid === wasValid) {
- return;
- }
-
- const { onValidChange: propsHandler, name } = this.props;
- const { onValidChange: ctxHandler } = this.context;
+ onValidChange(isValid, wasValid, prevName) {
+ const { onValidChange: propsHandler = noop } = this.props;
+ const { onValidChange: ctxHandler = noop } = this.context;
+ const { name } = this.props;
- if (propsHandler) {
- propsHandler(name, isValid, wasValid);
+ const nameChanged = prevName && prevName !== name;
+ const validChanged = isValid !== wasValid;
+ if (nameChanged && (undef !== wasValid)) {
+ propsHandler(prevName, undef, wasValid);
+ ctxHandler(prevName, undef, wasValid);
}
- if (ctxHandler) {
+ if (nameChanged || validChanged) {
+ propsHandler(name, isValid, wasValid);
ctxHandler(name, isValid, wasValid);
}
}
@@ -27,21 +61,29 @@ export default class Validates extends React.Component {
/**
* React lifecycle handler called immediately before the component's initial render
*/
- componentWillMount () {
+ componentWillMount() {
// Update the handlers with the initial state
this.onValidChange(this.props.validates);
}
/**
- * React lifecycle handler called when component is about to update.
+ * React lifecycle handler called when a component finished updating.
*
- * @param {Object} nextProps Component's new props.
+ * @param {Object} prevProps Component's previous props.
*/
- componentWillUpdate (nextProps) {
- const { validates: wasValid } = this.props;
- const { validates: isValid } = nextProps;
+ componentDidUpdate(prevProps) {
+ const { validates: wasValid, name: prevName } = prevProps;
+ const { validates: isValid } = this.props;
+
+ this.onValidChange(isValid, wasValid, prevName);
+ }
- this.onValidChange(isValid, wasValid);
+ /**
+ * React lifecycle handler called when component is about to be unmounted.
+ */
+ componentWillUnmount() {
+ // Update the handlers with `isValid=undefined` to notify them that the component no longer is being validated
+ this.onValidChange(undef, this.props.validates);
}
/**
@@ -49,19 +91,19 @@ export default class Validates extends React.Component {
*
* @returns {React.DOM} Rendered component.
*/
- render () {
+ render() {
return this.props.children || null;
}
}
Validates.propTypes = {
- validates: React.PropTypes.oneOf([true, false, null]),
- onValidChange: React.PropTypes.func,
- name: React.PropTypes.string.isRequired,
- children: React.PropTypes.element
+ validates: oneOf([true, false, null]),
+ onValidChange: func,
+ name: string.isRequired,
+ children: element
};
Validates.contextTypes = {
- onValidChange: React.PropTypes.func
+ onValidChange: func
};