diff --git a/dist/components/Field.js b/dist/components/Field.js index c940da7..a67b8f0 100644 --- a/dist/components/Field.js +++ b/dist/components/Field.js @@ -18,6 +18,8 @@ var _utilities = require('../helpers/utilities'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } @@ -118,6 +120,8 @@ var Field = function (_React$Component) { }, { key: 'render', value: function render() { + var _this2 = this; + var childCount = _react2.default.Children.count(this.props.children); var inputProps = { name: this.props.name, @@ -143,8 +147,16 @@ var Field = function (_React$Component) { 'div', null, _react2.default.Children.map(this.props.children, function (child) { - return (0, _utilities.mapPropsToChild)(child, 'input', function () { - return inputProps; + return (0, _utilities.mapPropsToChild)(child, { + input: function input() { + return inputProps; + }, + valid: function valid() { + return (0, _utilities.makePropsForStatus)('valid', _defineProperty({}, _this2.props.name, { valid: _this2.state.valid })); + }, + pristine: function pristine() { + return (0, _utilities.makePropsForStatus)('pristine', _defineProperty({}, _this2.props.name, { pristine: _this2.state.pristine })); + } }); }) ); diff --git a/dist/components/Form.js b/dist/components/Form.js index e7656ed..bec9c82 100644 --- a/dist/components/Form.js +++ b/dist/components/Form.js @@ -79,8 +79,15 @@ var Form = function (_React$Component) { 'form', { onSubmit: this.onSubmit }, _react2.default.Children.map(this.props.children, function (child) { - return (0, _utilities.mapPropsToChild)(child, 'Field', function (grandChild) { - return (0, _utilities.makeFieldProps)(grandChild, _this3.onFieldChange, _this3.state); + return (0, _utilities.mapPropsToChild)(child, { Field: function Field(grandChild) { + return (0, _utilities.makeFieldProps)(grandChild, _this3.onFieldChange, _this3.state); + }, + pristine: function pristine() { + return (0, _utilities.makePropsForStatus)('pristine', _this3.state); + }, + valid: function valid() { + return (0, _utilities.makePropsForStatus)('valid', _this3.state); + } }); }) ); diff --git a/dist/helpers/utilities.js b/dist/helpers/utilities.js index d8e6cea..d254bda 100644 --- a/dist/helpers/utilities.js +++ b/dist/helpers/utilities.js @@ -3,13 +3,17 @@ Object.defineProperty(exports, "__esModule", { value: true }); + +var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + exports.assembleValidators = assembleValidators; exports.updateValidators = updateValidators; exports.isValid = isValid; +exports.getValuesOf = getValuesOf; exports.buildStateForField = buildStateForField; exports.addFieldsToState = addFieldsToState; -exports.getValuesOf = getValuesOf; exports.makeFieldProps = makeFieldProps; +exports.makePropsForStatus = makePropsForStatus; exports.mapPropsToChild = mapPropsToChild; var _react = require('react'); @@ -77,16 +81,24 @@ function isValid(value, validators) { }, true); } +function getValuesOf() { + var obj = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + return Object.keys(obj).map(function (key) { + return obj[key]; + }); +} + function buildStateForField(fieldProps) { - var value = fieldProps.value, - valid = fieldProps.valid, - pristine = fieldProps.pristine; + var value = fieldProps.value; - var newState = { value: '', valid: false, pristine: true }; + var newState = { + value: '', + valid: isValid(value, getValuesOf(assembleValidators(fieldProps))), + pristine: true + }; if (value !== undefined) Object.assign(newState, { value: value }); - if (valid !== undefined) Object.assign(newState, { valid: valid }); - if (pristine !== undefined) Object.assign(newState, { pristine: pristine }); return newState; } @@ -108,14 +120,6 @@ function addFieldsToState(component, child) { } } -function getValuesOf() { - var obj = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - return Object.keys(obj).map(function (key) { - return obj[key]; - }); -} - function makeFieldProps(child, onChange, state) { if (typeof child.type === 'function' && child.type.name === 'Field') { var name = child.props.name; @@ -128,15 +132,40 @@ function makeFieldProps(child, onChange, state) { return null; } -function mapPropsToChild(child, type, propFunction) { - if (child.type === type || typeof child.type === 'function' && child.type.name === type) { - return _react2.default.cloneElement(child, propFunction(child)); +function makePropsForStatus(status, state) { + return Object.keys(state).reduce(function (props, field) { + if (Object.prototype.hasOwnProperty.call(state[field], status)) { + return _extends({}, props, _defineProperty({}, field + '_' + status, state[field][status])); + } + return props; + }, {}); +} + +function mapPropsToChild(child, childPropsMap) { + var type = typeof child.type === 'function' ? child.type.name : child.type; + var childProps = {}; + var newChildren = void 0; + + if (child.props) { + if (childPropsMap.valid && child.props.valid) { + Object.assign(childProps, childPropsMap.valid()); + } + if (childPropsMap.pristine && child.props.pristine) { + Object.assign(childProps, childPropsMap.pristine()); + } + if (child.props.children) { + newChildren = _react2.default.Children.map(child.props.children, function (nestedChild) { + return mapPropsToChild(nestedChild, childPropsMap); + }); + } } - if (child.props && child.props.children) { - var newChildren = _react2.default.Children.map(child.props.children, function (nestedChild) { - return mapPropsToChild(nestedChild, type, propFunction); - }); - return _react2.default.cloneElement(child, null, newChildren); + + if (childPropsMap.Field && type === 'Field') { + return _react2.default.cloneElement(child, _extends({}, childPropsMap.Field(child), childProps), newChildren); } - return child; + if (childPropsMap.input && type === 'input') { + return _react2.default.cloneElement(child, _extends({}, childPropsMap.input(child), childProps), newChildren); + } + + return Object.keys(childProps).length || newChildren ? _react2.default.cloneElement(child, childProps, newChildren) : child; } \ No newline at end of file diff --git a/readme.md b/readme.md index 2ca58cd..620fe1e 100644 --- a/readme.md +++ b/readme.md @@ -28,6 +28,7 @@ React-formulize can be used to both quickly compose forms or add validation to e 2. A `Field` component can wrap (nested JSX) an `input` element (or a fragment containing an `input`) and control its underlying state automatically. 3. Pass validator props to the `Field` components. A `Field` component will keep track of its own validity. 4. Pass an `onSubmit` handler to `Form` in order to interact with the submission event. The callback will be passed a clone of the `Form`'s state. + 5. Pass `valid` and `pristine` props to any nested child components in either a `Form` or `Field` component. These components will receive information about the `Form`'s status in the format of `${fieldName}_${statusType}` (e.g. name_valid & email_pristine). #### Example: Composing A New Form With Custom Input Component(s) ```javascript @@ -112,8 +113,10 @@ The `Form` component is a stateful higher-order-component which wraps presentati The `Form` component will behave as follows with respect to its children: 1. Any `Field` tag will be passed the state associated with the `Field`'s name (`Form.state[child.props.name]`). - 2. Any other component or element will be rendered with the props it would otherwise be passed. - 3. Upon submission, `Form` will pass its `onSubmit` callback a clone of its current state. + 2. Any component with a `valid` prop will be passed props stating the validity for all `Field`s in the `Form` (e.g. name_valid). + 3. Any component with a `pristine` prop will be passed a props stating the pristine state for all `Field`s in the `Form` (e.g. email_pristine). + 4. Any other component or element will be rendered with the props it would otherwise be passed. + 5. Upon submission, `Form` will pass its `onSubmit` callback a clone of its current state. *Note:* The `Form` component should be passed an `onSubmit` handler if you want to interact with the submission event! @@ -184,7 +187,9 @@ The `Field` component will behave as follows with respect to its children: 2. Any `input` tag will be passed `name`, `type`, `value`, and `onChange` props. 3. If only a single direct child is passed to `Field`, it will be passed all of the relevant input props. 4. If multiple `input` tags are nested in a single `Field`, they would all share a single state (not recommended). - + 5. Any component with a `valid` prop will be passed a prop stating the `Field`'s validity (e.g. name_valid). + 6. Any component with a `pristine` prop will be passed a prop stating the `Field`'s pristine state (e.g. email_pristine). + *Note:* Only one input element should be nested inside of a `Field` tag (see #4 above). ### Props diff --git a/src/components/Field.jsx b/src/components/Field.jsx index 2f44b22..da34e41 100644 --- a/src/components/Field.jsx +++ b/src/components/Field.jsx @@ -6,6 +6,7 @@ import { updateValidators, getValuesOf, mapPropsToChild, + makePropsForStatus, } from '../helpers/utilities'; const Field = class extends React.Component { @@ -112,7 +113,13 @@ const Field = class extends React.Component { return (
{React.Children - .map(this.props.children, child => mapPropsToChild(child, 'input', () => inputProps))} + .map(this.props.children, child => mapPropsToChild(child, { + input: () => inputProps, + valid: () => makePropsForStatus('valid', { [this.props.name]: { valid: this.state.valid } }), + pristine: () => makePropsForStatus('pristine', { + [this.props.name]: { pristine: this.state.pristine }, + }), + }))}
); } diff --git a/src/components/Form.jsx b/src/components/Form.jsx index d8f3ee6..1748c90 100644 --- a/src/components/Form.jsx +++ b/src/components/Form.jsx @@ -1,5 +1,10 @@ import React from 'react'; -import { addFieldsToState, mapPropsToChild, makeFieldProps } from '../helpers/utilities'; +import { + addFieldsToState, + mapPropsToChild, + makeFieldProps, + makePropsForStatus, +} from '../helpers/utilities'; const Form = class extends React.Component { constructor(props) { @@ -37,9 +42,12 @@ const Form = class extends React.Component { .map(this.props.children, child => mapPropsToChild( child, - 'Field', - grandChild => makeFieldProps(grandChild, this.onFieldChange, this.state), - ))} + { Field: grandChild => makeFieldProps(grandChild, this.onFieldChange, this.state), + pristine: () => makePropsForStatus('pristine', this.state), + valid: () => makePropsForStatus('valid', this.state), + }, + ), + )} ); } diff --git a/src/helpers/utilities.jsx b/src/helpers/utilities.jsx index 28dc85a..9236533 100644 --- a/src/helpers/utilities.jsx +++ b/src/helpers/utilities.jsx @@ -36,13 +36,19 @@ export function isValid(value, validators) { }, true); } +export function getValuesOf(obj = {}) { + return Object.keys(obj).map(key => obj[key]); +} + export function buildStateForField(fieldProps) { - const { value, valid, pristine } = fieldProps; - const newState = { value: '', valid: false, pristine: true }; + const { value } = fieldProps; + const newState = { + value: '', + valid: isValid(value, getValuesOf(assembleValidators(fieldProps))), + pristine: true, + }; if (value !== undefined) Object.assign(newState, { value }); - if (valid !== undefined) Object.assign(newState, { valid }); - if (pristine !== undefined) Object.assign(newState, { pristine }); return newState; } @@ -63,10 +69,6 @@ export function addFieldsToState(component, child, mounted = false) { } } -export function getValuesOf(obj = {}) { - return Object.keys(obj).map(key => obj[key]); -} - export function makeFieldProps(child, onChange, state) { if (typeof child.type === 'function' && child.type.name === 'Field') { const name = child.props.name; @@ -79,14 +81,40 @@ export function makeFieldProps(child, onChange, state) { return null; } -export function mapPropsToChild(child, type, propFunction) { - if (child.type === type || (typeof child.type === 'function' && child.type.name === type)) { - return React.cloneElement(child, propFunction(child)); +export function makePropsForStatus(status, state) { + return Object.keys(state).reduce((props, field) => { + if (Object.prototype.hasOwnProperty.call(state[field], status)) { + return { ...props, [`${field}_${status}`]: state[field][status] }; + } + return props; + }, {}); +} + +export function mapPropsToChild(child, childPropsMap) { + const type = (typeof child.type === 'function') ? child.type.name : child.type; + const childProps = {}; + let newChildren; + + if (child.props) { + if (childPropsMap.valid && child.props.valid) { + Object.assign(childProps, childPropsMap.valid()); + } + if (childPropsMap.pristine && child.props.pristine) { + Object.assign(childProps, childPropsMap.pristine()); + } + if (child.props.children) { + newChildren = React.Children + .map(child.props.children, nestedChild => mapPropsToChild(nestedChild, childPropsMap)); + } + } + + if (childPropsMap.Field && type === 'Field') { + return React.cloneElement(child, { ...childPropsMap.Field(child), ...childProps }, newChildren); } - if (child.props && child.props.children) { - const newChildren = React.Children.map(child.props.children, nestedChild => ( - mapPropsToChild(nestedChild, type, propFunction))); - return React.cloneElement(child, null, newChildren); + if (childPropsMap.input && type === 'input') { + return React.cloneElement(child, { ...childPropsMap.input(child), ...childProps }, newChildren); } - return child; + + return (Object.keys(childProps).length || newChildren) ? + React.cloneElement(child, childProps, newChildren) : child; } diff --git a/tests/components/Field.spec.js b/tests/components/Field.spec.js index 6f4fa32..133da79 100644 --- a/tests/components/Field.spec.js +++ b/tests/components/Field.spec.js @@ -128,6 +128,57 @@ describe(' Higher-Order-Component', () => { }); }); + describe('Passing down status', () => { + const TestComponent = () => Test!; + let wrapper; + let first; + let second; + let third; + + beforeEach('Set up a basic form with testComponents in different configurations', () => { + wrapper = mount( + + + Hi There! +
+ +
+ + + Hi There! + +
, + ); + first = wrapper.find(TestComponent).first(); + second = wrapper.find(TestComponent).at(1); + third = wrapper.find(TestComponent).last(); + }); + + it('passes validity information down to components with a `valid` prop', () => { + expect(first.props()).to.have.property('name_valid', false); + + expect(second.props()).to.not.have.property('name_valid'); + }); + + it('passes pristine information down to components with a `pristine` prop', () => { + expect(second.props()).to.have.property('name_pristine', true); + + updateInput(wrapper, 'secondValue'); + expect(second.props()).to.have.property('name_pristine', false); + + expect(first.props()).to.not.have.property('name_pristine'); + }); + + it('passes valid and pristine info down to components with flags', () => { + expect(third.props()).to.have.property('name_valid', false); + expect(third.props()).to.have.property('name_pristine', true); + + updateInput(wrapper, 'Test Name'); + expect(third.props()).to.have.property('name_pristine', false); + expect(third.props()).to.have.property('name_valid', true); + }); + }); + describe('`Field` lifecycle method tests', () => { let wrapper; let shouldUpdateSpy; diff --git a/tests/components/Form.spec.js b/tests/components/Form.spec.js index 79cf905..2863e03 100644 --- a/tests/components/Form.spec.js +++ b/tests/components/Form.spec.js @@ -40,7 +40,6 @@ describe('
Higher-Order-Component', () => { expect(inputs.last().getDOMNode().value).to.equal('user@company.com'); }); - it('passes appropriate props down through `props.children`', () => { wrapper = mount(( @@ -91,58 +90,133 @@ describe(' Higher-Order-Component', () => { }); }); - it('invoked an onSubmit callback upon form submission', () => { - const onSubmitSpy = sinon.spy(); - wrapper = mount( - - -