diff --git a/docs/examples/.eslintrc b/docs/examples/.eslintrc index b6dc693611..358c61473e 100644 --- a/docs/examples/.eslintrc +++ b/docs/examples/.eslintrc @@ -20,6 +20,7 @@ "CarouselItem", "Col", "DropdownButton", + "FormControls", "Glyphicon", "Grid", "Input", diff --git a/docs/examples/InputTypes.js b/docs/examples/InputTypes.js index 036f6d6199..63a418e8a7 100644 --- a/docs/examples/InputTypes.js +++ b/docs/examples/InputTypes.js @@ -15,7 +15,6 @@ const inputTypeInstance = ( - diff --git a/docs/examples/StaticText.js b/docs/examples/StaticText.js new file mode 100644 index 0000000000..1b08ab93d9 --- /dev/null +++ b/docs/examples/StaticText.js @@ -0,0 +1,9 @@ +const staticTextExample = ( +
+ + + Bob + +); + +React.render(staticTextExample, mountNode); diff --git a/docs/src/ComponentsPage.js b/docs/src/ComponentsPage.js index e268bc27f5..863e71b78d 100644 --- a/docs/src/ComponentsPage.js +++ b/docs/src/ComponentsPage.js @@ -577,8 +577,10 @@ const ComponentsPage = React.createClass({ The helper method getInputDOMNode() returns the internal input element. If you don't want the form-group class applied apply the prop named standalone.

Types

-

Supports select, textarea, static as well as standard HTML input types. getValue() returns an array for multiple select.

+

Supports select, textarea, as well as standard HTML input types. getValue() returns an array for multiple select.

+

Static text can be added to your form controls through the use of the FormControls.Static component.

+

Button Input Types

Form buttons are encapsulated by ButtonInput. Pass in type="reset" or type="submit" to suit your needs. Styling is the same as Button.

diff --git a/docs/src/ReactPlayground.js b/docs/src/ReactPlayground.js index 6561e87287..097fd47fb5 100644 --- a/docs/src/ReactPlayground.js +++ b/docs/src/ReactPlayground.js @@ -13,6 +13,7 @@ import * as modCarousel from '../../src/Carousel'; import * as modCarouselItem from '../../src/CarouselItem'; import * as modCol from '../../src/Col'; import * as modDropdownButton from '../../src/DropdownButton'; +import * as modFormControls from '../../src/FormControls'; import * as modGlyphicon from '../../src/Glyphicon'; import * as modGrid from '../../src/Grid'; import * as modInput from '../../src/Input'; @@ -64,6 +65,7 @@ const Carousel = modCarousel.default; const CarouselItem = modCarouselItem.default; const Col = modCol.default; const DropdownButton = modDropdownButton.default; +const FormControls = modFormControls.default; const Glyphicon = modGlyphicon.default; const Grid = modGrid.default; const Input = modInput.default; diff --git a/docs/src/Samples.js b/docs/src/Samples.js index 3fc3a163a5..b15e400a85 100644 --- a/docs/src/Samples.js +++ b/docs/src/Samples.js @@ -89,6 +89,7 @@ export default { TableResponsive: require('fs').readFileSync(__dirname + '/../examples/TableResponsive.js', 'utf8'), Input: require('fs').readFileSync(__dirname + '/../examples/Input.js', 'utf8'), InputTypes: require('fs').readFileSync(__dirname + '/../examples/InputTypes.js', 'utf8'), + StaticText: require('fs').readFileSync(__dirname + '/../examples/StaticText.js', 'utf8'), ButtonInput: require('fs').readFileSync(__dirname + '/../examples/ButtonInput.js', 'utf8'), InputAddons: require('fs').readFileSync(__dirname + '/../examples/InputAddons.js', 'utf8'), InputSizes: require('fs').readFileSync(__dirname + '/../examples/InputSizes.js', 'utf8'), diff --git a/src/ButtonInput.js b/src/ButtonInput.js index 4df4c6a9f1..8667df0178 100644 --- a/src/ButtonInput.js +++ b/src/ButtonInput.js @@ -2,13 +2,7 @@ import React from 'react'; import Button from './Button'; import FormGroup from './FormGroup'; import InputBase from './InputBase'; - -function valueValidation({children, value}, propName, componentName) { - if (children && value) { - return new Error('Both value and children cannot be passed to ButtonInput'); - } - return React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number]).call(null, {children, value}, propName, componentName); -} +import childrenValueValidation from './utils/childrenValueInputValidation'; class ButtonInput extends InputBase { renderFormGroup(children) { @@ -23,18 +17,20 @@ class ButtonInput extends InputBase { } } +ButtonInput.types = ['button', 'reset', 'submit']; + ButtonInput.defaultProps = { type: 'button' }; ButtonInput.propTypes = { - type: React.PropTypes.oneOf(['button', 'reset', 'submit']), + type: React.PropTypes.oneOf(ButtonInput.types), bsStyle(props) { //defer to Button propTypes of bsStyle return null; }, - children: valueValidation, - value: valueValidation + children: childrenValueValidation, + value: childrenValueValidation }; export default ButtonInput; diff --git a/src/FormControls/Static.js b/src/FormControls/Static.js new file mode 100644 index 0000000000..3f0f4a40a3 --- /dev/null +++ b/src/FormControls/Static.js @@ -0,0 +1,26 @@ +import React from 'react'; +import classNames from 'classnames'; +import InputBase from '../InputBase'; +import childrenValueValidation from '../utils/childrenValueInputValidation'; + +class Static extends InputBase { + getValue() { + const {children, value} = this.props; + return children ? children : value; + } + + renderInput() { + return ( +

+ {this.getValue()} +

+ ); + } +} + +Static.propTypes = { + value: childrenValueValidation, + children: childrenValueValidation +}; + +export default Static; diff --git a/src/FormControls/index.js b/src/FormControls/index.js new file mode 100644 index 0000000000..5a7c16286e --- /dev/null +++ b/src/FormControls/index.js @@ -0,0 +1,5 @@ +import Static from './Static'; + +export default { + Static +}; diff --git a/src/Input.js b/src/Input.js index 61456c70c3..fa516a4ee4 100644 --- a/src/Input.js +++ b/src/Input.js @@ -1,15 +1,17 @@ import React from 'react'; import InputBase from './InputBase'; import ButtonInput from './ButtonInput'; +import FormControls from './FormControls'; import deprecationWarning from './utils/deprecationWarning'; -const buttonTypes = ['button', 'reset', 'submit']; - class Input extends InputBase { render() { - if (buttonTypes.indexOf(this.props.type) > -1) { + if (ButtonInput.types.indexOf(this.props.type) > -1) { deprecationWarning(`Input type=${this.props.type}`, 'ButtonInput'); return ; + } else if (this.props.type === 'static') { + deprecationWarning('Input type=static', 'StaticText'); + return ; } return super.render(); diff --git a/src/index.js b/src/index.js index 27a3d931f3..0f05aed32f 100644 --- a/src/index.js +++ b/src/index.js @@ -17,6 +17,7 @@ import DropdownButton from './DropdownButton'; import DropdownMenu from './DropdownMenu'; import DropdownStateMixin from './DropdownStateMixin'; import FadeMixin from './FadeMixin'; +import FormControls from './FormControls'; import Glyphicon from './Glyphicon'; import Grid from './Grid'; import Input from './Input'; @@ -71,6 +72,7 @@ export default { DropdownMenu, DropdownStateMixin, FadeMixin, + FormControls, Glyphicon, Grid, Input, diff --git a/src/utils/CustomPropTypes.js b/src/utils/CustomPropTypes.js index c56a0a4e8a..5a6ae4e0fc 100644 --- a/src/utils/CustomPropTypes.js +++ b/src/utils/CustomPropTypes.js @@ -22,7 +22,17 @@ let CustomPropTypes = { * @param componentName * @returns {Error|undefined} */ - keyOf: createKeyOfChecker + keyOf: createKeyOfChecker, + /** + * Checks if only one of the listed properties is in use. An error is given + * if multiple have a value + * + * @param props + * @param propName + * @param componentName + * @returns {Error|undefined} + */ + singlePropFrom: createSinglePropFromChecker }; /** @@ -80,4 +90,22 @@ function createKeyOfChecker(obj) { return createChainableTypeChecker(validate); } +function createSinglePropFromChecker(arrOfProps) { + function validate(props, propName, componentName) { + const usedPropCount = arrOfProps + .map(listedProp => props[listedProp]) + .reduce((acc, curr) => acc + (curr !== undefined ? 1 : 0), 0); + + if (usedPropCount > 1) { + const [first, ...others] = arrOfProps; + const message = `${others.join(', ')} and ${first}`; + return new Error( + `Invalid prop '${propName}', only one of the following ` + + `may be provided: ${message}` + ); + } + } + return validate; +} + export default CustomPropTypes; diff --git a/src/utils/childrenValueInputValidation.js b/src/utils/childrenValueInputValidation.js new file mode 100644 index 0000000000..075a4c42b0 --- /dev/null +++ b/src/utils/childrenValueInputValidation.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { singlePropFrom } from './CustomPropTypes'; + +const propList = ['children', 'value']; +const typeList = [React.PropTypes.number, React.PropTypes.string]; + +export default function valueValidation(props, propName, componentName) { + let error = singlePropFrom(propList)(props, propName, componentName); + if (!error) { + const oneOfType = React.PropTypes.oneOfType(typeList); + error = oneOfType(props, propName, componentName); + } + return error; +} diff --git a/test/ButtonInputSpec.js b/test/ButtonInputSpec.js index 5118181938..7f5e66ac66 100644 --- a/test/ButtonInputSpec.js +++ b/test/ButtonInputSpec.js @@ -55,11 +55,11 @@ describe('ButtonInput', () =>{ }); it('throws a warning if given both children and a value property', function () { - ReactTestUtils.renderIntoDocument( - button - ); + const testData = { value: 5, children: 'button' }; + const result = ButtonInput.propTypes.value(testData, 'value', 'ButtonInput'); - shouldWarn('Both value and children'); + result.should.be.instanceOf(Error); + result.message.should.have.string('value and children'); }); it('does not throw an error for strings and numbers', function () { diff --git a/test/CustomPropTypesSpec.js b/test/CustomPropTypesSpec.js index 9a0b94deb2..d28137f585 100644 --- a/test/CustomPropTypesSpec.js +++ b/test/CustomPropTypesSpec.js @@ -47,4 +47,24 @@ describe('CustomPropTypes', function () { assert.isUndefined(validate('bar')); }); }); + + describe('singlePropFrom', function () { + function validate(testProps) { + const propList = ['children', 'value']; + + return CustomPropTypes.singlePropFrom(propList)(testProps, 'value', 'Component'); + } + + it('Should return undefined if only one listed prop in used', function () { + const testProps = {value: 5}; + + assert.isUndefined(validate(testProps)); + }); + + it('Should return error if multiple of the listed properties have values', function () { + const testProps = {value: 5, children: 5}; + + validate(testProps).should.be.instanceOf(Error); + }); + }); }); diff --git a/test/FormControlsSpec.js b/test/FormControlsSpec.js new file mode 100644 index 0000000000..cc56ecd618 --- /dev/null +++ b/test/FormControlsSpec.js @@ -0,0 +1,37 @@ +import React from 'react'; +import ReactTestUtils from 'react/lib/ReactTestUtils'; +import FormControls from '../src/FormControls'; + +describe('Form Controls', function () { + describe('Static', function () { + it('renders a p element wrapped around the given value', function () { + const instance = ReactTestUtils.renderIntoDocument( + + ); + + const result = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'p'); + result.props.children.should.equal('v'); + }); + + it('getValue() pulls from either value or children', function () { + let instance = ReactTestUtils.renderIntoDocument( + + ); + + instance.getValue().should.equal('v'); + + instance = ReactTestUtils.renderIntoDocument( + 5 + ); + + instance.getValue().should.equal('5'); + }); + + it('throws an error if both value and children are provided', function () { + const testData = { value: 'blah', children: 'meh' }; + const result = FormControls.Static.propTypes.children(testData, 'children', 'Static'); + + result.should.be.instanceOf(Error); + }); + }); +}); diff --git a/test/InputSpec.js b/test/InputSpec.js index 065ff1824c..3a73cba5cf 100644 --- a/test/InputSpec.js +++ b/test/InputSpec.js @@ -65,13 +65,12 @@ describe('Input', function () { shouldWarn('deprecated'); }); - it('renders a p element when type=static', function () { - let instance = ReactTestUtils.renderIntoDocument( + it('throws a warning when type=static', function () { + ReactTestUtils.renderIntoDocument( ); - assert.ok(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'p')); - assert.equal(instance.getValue(), 'v'); + shouldWarn('deprecated'); }); it('renders an input element of given type when type is anything else', function () {