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