UNPKG

d2-ui

Version:
501 lines (426 loc) 22.4 kB
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; }; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 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 _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } 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; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } import React from 'react'; import AsyncValidatorRunner from './AsyncValidatorRunner'; import { isObject } from 'lodash'; import CircularProgres from 'material-ui/CircularProgress'; var noop = function noop() {}; var FormBuilder = function (_React$Component) { _inherits(FormBuilder, _React$Component); function FormBuilder(props) { _classCallCheck(this, FormBuilder); var _this = _possibleConstructorReturn(this, (FormBuilder.__proto__ || Object.getPrototypeOf(FormBuilder)).call(this, props)); _this.state = _this.initState(props); _this.asyncValidators = _this.createAsyncValidators(props); _this.asyncValidationRunner = props.asyncValidationRunner || new AsyncValidatorRunner(); _this.getFieldProp = _this.getFieldProp.bind(_this); _this.getStateClone = _this.getStateClone.bind(_this); return _this; } /** * Called by React when the component receives new props, but not on the initial render. * * State is calculated based on the incoming props, in such a way that existing form fields * are updated as necessary, but not overridden. See the initState function for details. * * @param props */ _createClass(FormBuilder, [{ key: 'componentWillReceiveProps', value: function componentWillReceiveProps(props) { var _this2 = this; this.asyncValidators = this.createAsyncValidators(props); var clonedState = this.getStateClone(); props.fields // Only check fields that are set on the component state .filter(function (field) { return _this2.state && _this2.state.fields && _this2.state.fields[field.name]; }) // Filter out fields where the values changed .filter(function (field) { return field.value !== _this2.state.fields[field.name].value; }) // Change field value and run validators for the field .forEach(function (field) { clonedState.fields[field.name].value = field.value; _this2.validateField(clonedState, field.name, field.value); }); this.setState(clonedState); } /** * Custom state deep copy function * * @returns {{form: {pristine: (boolean), valid: (boolean), validating: (boolean)}, fields: *}} */ }, { key: 'getStateClone', value: function getStateClone() { var _this3 = this; return { form: { pristine: this.state.form.pristine, valid: this.state.form.valid, validating: this.state.form.validating }, fields: Object.keys(this.state.fields).reduce(function (p, c) { p[c] = { pristine: _this3.state.fields[c].pristine, validating: _this3.state.fields[c].validating, valid: _this3.state.fields[c].valid, value: _this3.state.fields[c].value, error: _this3.state.fields[c].error }; return p; }, {}) }; } /** * Render the form fields. * * @returns {*} An array containing markup for each form field */ }, { key: 'renderFields', value: function renderFields() { var _this4 = this; var styles = { field: { position: 'relative' }, progress: this.props.validatingProgressStyle, validatingErrorStyle: { color: 'orange' } }; return this.props.fields.map(function (field) { var _ref = field.props || {}; var errorTextProp = _ref.errorTextProp; var props = _objectWithoutProperties(_ref, ['errorTextProp']); var fieldState = _this4.state.fields[field.name]; var changeHandler = _this4.handleFieldChange.bind(_this4, field.name); var onBlurChangeHandler = props.changeEvent === 'onBlur' ? function (e) { var stateClone = _this4.updateFieldState(_this4.getStateClone(), field.name, { value: e.target.value }); _this4.validateField(stateClone, field.name, e.target.value); _this4.setState(stateClone); } : undefined; var errorText = fieldState && fieldState.validating ? field.validatingLabelText || _this4.props.validatingLabelText : errorTextProp; return React.createElement( 'div', { key: field.name, style: Object.assign({}, styles.field, _this4.props.fieldWrapStyle) }, fieldState.validating ? React.createElement(CircularProgres, { mode: 'indeterminate', size: 0.33, style: styles.progress }) : undefined, React.createElement(field.component, _extends({ value: fieldState.value, onChange: props.changeEvent && props.changeEvent === 'onBlur' ? onBlurChangeHandler : changeHandler, onBlur: props.changeEvent && props.changeEvent === 'onBlur' ? changeHandler : undefined, errorStyle: fieldState.validating ? styles.validatingErrorStyle : undefined, errorText: fieldState.valid ? errorText : fieldState.error }, props)) ); }); } /** * Render the component * * @returns {XML} */ }, { key: 'render', value: function render() { return React.createElement( 'div', { style: this.props.style }, this.renderFields() ); } /** * Calculates initial state based on the provided props and the existing state, if any. * * @param props * @returns {{form: {pristine: (boolean), valid: (boolean), validating: (boolean)}, fields: *}} */ }, { key: 'initState', value: function initState(props) { var _this5 = this; var state = { fields: props.fields.reduce(function (fields, field) { var currentFieldState = _this5.state && _this5.state.fields && _this5.state.fields[field.name]; return Object.assign(fields, _defineProperty({}, field.name, { value: currentFieldState !== undefined && !currentFieldState.pristine ? currentFieldState.value : field.value, pristine: currentFieldState !== undefined ? currentFieldState.value === field.value : true, validating: currentFieldState !== undefined ? currentFieldState.validating : false, valid: currentFieldState !== undefined ? currentFieldState.valid : true, error: currentFieldState && currentFieldState.error || undefined })); }, {}) }; state.form = { pristine: Object.keys(state.fields).reduce(function (p, c) { return p && state.fields[c].pristine; }, true), validating: Object.keys(state.fields).reduce(function (p, c) { return p || state.fields[c].validating; }, false), valid: Object.keys(state.fields).reduce(function (p, c) { return p && state.fields[c].valid; }, true) }; return state; } /** * Create an object with a property for each field that has async validators, which is later used * to store Rx.Observable's for any currently running async validators * * @param props * @returns {*} */ }, { key: 'createAsyncValidators', value: function createAsyncValidators(props) { var _this6 = this; return props.fields.filter(function (field) { return Array.isArray(field.asyncValidators) && field.asyncValidators.length; }).reduce(function (p, currentField) { p[currentField.name] = _this6.asyncValidators && _this6.asyncValidators[currentField.name] || undefined; return p; }, {}); } /** * Cancel the currently running async validators for the specified field name, if any. * * @param fieldName */ }, { key: 'cancelAsyncValidators', value: function cancelAsyncValidators(fieldName) { if (this.asyncValidators[fieldName]) { this.asyncValidators[fieldName].dispose(); this.asyncValidators[fieldName] = undefined; } } /** * Utility method to mutate the provided state object in place * * @param state A state object * @param fieldName A valid field name * @param fieldState Mutations to apply to the specified field name * @returns {*} A reference to the mutated state object for chaining */ }, { key: 'updateFieldState', value: function updateFieldState(state, fieldName, fieldState) { var fieldProp = this.getFieldProp(fieldName); state.fields[fieldName] = { pristine: fieldState.pristine !== undefined ? !!fieldState.pristine : state.fields[fieldName].value === fieldProp.value, validating: fieldState.validating !== undefined ? !!fieldState.validating : state.fields[fieldName].validating, valid: fieldState.valid !== undefined ? !!fieldState.valid : state.fields[fieldName].valid, error: fieldState.error, value: fieldState.value !== undefined ? fieldState.value : state.fields[fieldName].value }; // Form state is a composite of field states var fieldNames = Object.keys(state.fields); state.form = { pristine: fieldNames.reduce(function (p, current) { return p && state.fields[current].pristine; }, true), validating: fieldNames.reduce(function (p, current) { return p || state.fields[current].validating; }, false), valid: fieldNames.reduce(function (p, current) { return p && state.fields[current].valid; }, true) }; return state; } /** * Field value change event * * This is called whenever the value of the specified field has changed. This will be the onChange event handler, unless * the changeEvent prop for this field is set to 'onBlur'. * * The change event is processed as follows: * * - If the value hasn't actually changed, processing stops * - The field status is set to [not pristine] * - Any currently running async validators are cancelled * * - All synchronous validators are called in the order specified * - If a validator fails: * - The field status is set to invalid * - The field error message is set to the error message for the validator that failed * - Processing stops * * - If all synchronous validators pass: * - The field status is set to [valid] * - If there are NO async validators for the field: * - The onUpdateField callback is called, and processing is finished * * - If there ARE async validators for the field: * - All async validators are started immediately * - The field status is set to [valid, validating] * - The validators keep running asynchronously, but the handleFieldChange function terminates * * - The async validators keep running in the background until ONE of them fail, or ALL of them succeed: * - The first async validator to fail causes all processing to stop: * - The field status is set to [invalid, not validating] * - The field error message is set to the value that the validator rejected with * - If all async validators complete successfully: * - The field status is set to [valid, not validating] * - The onUpdateField callback is called * * @param fieldName The name of the field that changed. * @param event An event object. Only `event.target.value` is used. */ }, { key: 'handleFieldChange', value: function handleFieldChange(fieldName, event) { var _this7 = this; var newValue = event.target.value; var field = this.getFieldProp(fieldName); // If the field has changeEvent=onBlur the change handler is triggered whenever the field loses focus. // So if the value didn't actually change, abort the change handler here. if (field.props && field.props.changeEvent === 'onBlur' && newValue === field.value) { return; } // Using custom clone function to maximize speed, albeit more error prone var stateClone = this.getStateClone(); // Update value, and set pristine to false this.setState(this.updateFieldState(stateClone, fieldName, { pristine: false, value: newValue }), function () { if (!isObject(newValue) && newValue === (!!field.value ? field.value : '')) { _this7.props.onUpdateField(fieldName, newValue); return; } // Cancel async validators in progress (if any) if (_this7.asyncValidators[fieldName]) { _this7.cancelAsyncValidators(fieldName); _this7.setState(_this7.updateFieldState(stateClone, fieldName, { validating: false })); } // Run synchronous validators var validatorResult = _this7.validateField(stateClone, fieldName, newValue); // Async validators - only run if sync validators pass if (validatorResult === true) { _this7.runAsyncValidators(field, stateClone, fieldName, newValue); } else { // Sync validators failed set field status to false _this7.setState(_this7.updateFieldState(stateClone, fieldName, { valid: false, error: validatorResult }), function () { // Also emit when the validator result is false _this7.props.onUpdateFormStatus(_this7.state.form); _this7.props.onUpdateField(fieldName, newValue); }); } }); } }, { key: 'runAsyncValidators', value: function runAsyncValidators(field, stateClone, fieldName, newValue) { var _this8 = this; if ((field.asyncValidators || []).length > 0) { // Set field and form state to 'validating' this.setState(this.updateFieldState(stateClone, fieldName, { validating: true }), function () { _this8.props.onUpdateFormStatus(_this8.state.form); _this8.props.onUpdateField(fieldName, newValue); // TODO: Subscription to validation results could be done once in `componentDidMount` and be // disposed in the `componentWillUnmount` method. This way we don't have to create the // subscription every time the field is changed. _this8.asyncValidators[fieldName] = _this8.asyncValidationRunner.listenToValidatorsFor(fieldName).subscribe(function (status) { _this8.setState(_this8.updateFieldState(_this8.getStateClone(), status.fieldName, { validating: false, valid: status.isValid, error: status.message }), function () { _this8.cancelAsyncValidators(status.fieldName); _this8.props.onUpdateFormStatus(_this8.state.form); }); }); _this8.asyncValidationRunner.run(fieldName, field.asyncValidators, newValue); }); } else { this.setState(this.updateFieldState(stateClone, fieldName, { valid: true }), function () { _this8.props.onUpdateFormStatus(_this8.state.form); _this8.props.onUpdateField(fieldName, newValue); }); } } /** * Run all synchronous validators (if any) for the field and value, and update the state clone depending on the * outcome * * @param stateClone A clone of the current state * @param fieldName The name of the field to validate * @param newValue The value to validate * @returns {true|String} The error message from the first validator that fails, or true if they all pass */ }, { key: 'validateField', value: function validateField(stateClone, fieldName, newValue) { var field = this.getFieldProp(fieldName); var validatorResult = (field.validators || []).reduce(function (pass, currentValidator) { return pass === true ? currentValidator.validator(newValue) === true || currentValidator.message : pass; }, true); this.updateFieldState(stateClone, fieldName, { valid: validatorResult === true, error: validatorResult === true ? undefined : validatorResult }); return validatorResult; } /** * Retreive the field that has the specified field name * * @param fieldName * @returns {} */ }, { key: 'getFieldProp', value: function getFieldProp(fieldName) { return this.props.fields.filter(function (f) { return f.name === fieldName; })[0]; } }]); return FormBuilder; }(React.Component); /** * Component prop types * @type {{fields: (Object|isRequired), validatingLabelText: *, validatingProgressStyle: *, onUpdateField: (Function|isRequired)}} */ FormBuilder.propTypes = { fields: React.PropTypes.arrayOf(React.PropTypes.shape({ name: React.PropTypes.string.isRequired, value: React.PropTypes.any, component: React.PropTypes.func.isRequired, props: React.PropTypes.shape({ changeEvent: React.PropTypes.oneOf(['onChange', 'onBlur']) }), validators: React.PropTypes.arrayOf(React.PropTypes.shape({ validator: React.PropTypes.func.isRequired, message: React.PropTypes.string.isRequired })), asyncValidators: React.PropTypes.arrayOf(React.PropTypes.func.isRequired), validatingLabelText: React.PropTypes.string })).isRequired, validatingLabelText: React.PropTypes.string, validatingProgressStyle: React.PropTypes.object, onUpdateField: React.PropTypes.func.isRequired, onUpdateFormStatus: React.PropTypes.func, style: React.PropTypes.object, fieldWrapStyle: React.PropTypes.object }; /** * Default values for optional props * @type {{validatingLabelText: string, validatingProgressStyle: {position: string, right: number, top: number}}} */ FormBuilder.defaultProps = { validatingLabelText: 'Validating...', validatingProgressStyle: { position: 'absolute', right: -12, top: 16 }, onUpdateFormStatus: noop }; export default FormBuilder;