UNPKG

react-dynamic-forms

Version:

Dynamic forms library for React

599 lines (514 loc) 28.7 kB
"use strict"; 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; }; 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; }; }(); var _react = require("react"); var _react2 = _interopRequireDefault(_react); var _underscore = require("underscore"); var _underscore2 = _interopRequireDefault(_underscore); var _deepcopy = require("deepcopy"); var _deepcopy2 = _interopRequireDefault(_deepcopy); var _flexboxReact = require("flexbox-react"); var _flexboxReact2 = _interopRequireDefault(_flexboxReact); var _propTypes = require("prop-types"); var _propTypes2 = _interopRequireDefault(_propTypes); var _Field = require("./Field"); var _Field2 = _interopRequireDefault(_Field); var _Schema = require("./Schema"); var _Schema2 = _interopRequireDefault(_Schema); var _constants = require("../js/constants"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: 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; } 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; } /** * Copyright (c) 2015 - present, The Regents of the University of California, * through Lawrence Berkeley National Laboratory (subject to receipt * of any required approvals from the U.S. Dept. of Energy). * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ // Pass in the <Schema> element and will return all the <Fields> under it. function getFieldsFromSchema(schema) { if (!_react2.default.isValidElement(schema)) { return {}; } var fields = {}; if (schema.type === _Schema2.default) { _react2.default.Children.forEach(schema.props.children, function (child) { if (child.type === _Field2.default) { fields[child.props.name] = (0, _deepcopy2.default)(child.props); } }); } return fields; } function getRulesFromSchema(schema) { if (!_react2.default.isValidElement(schema)) { return {}; } var rules = {}; if (schema.type === _Schema2.default) { _react2.default.Children.forEach(schema.props.children, function (child) { if (child.type === _Field2.default) { var required = child.props.required || false; var validation = (0, _deepcopy2.default)(child.props.validation); // the conform is a function that can not be copied properly if (child.props.validation && "conform" in child.props.validation) { validation["conform"] = child.props.validation["conform"]; } rules[child.props.name] = { required: required, validation: validation }; } }); } return rules; } var Form = function (_React$Component) { _inherits(Form, _React$Component); function Form(props) { _classCallCheck(this, Form); var _this = _possibleConstructorReturn(this, (Form.__proto__ || Object.getPrototypeOf(Form)).call(this, props)); _this.state = { missingCounts: {}, errorCounts: {}, selection: null }; return _this; } /** * Collect together props for the given fieldName which can * be applied to any of the formGroup wrapped form widgets. These * props contain info extracted from our schema and current * values, namely from: * - formFields * - formRules * - formValues * * In addition, the props contain callbacks for: * - value changed * - missing count changed * - error counts changed * - edit selection */ _createClass(Form, [{ key: "getFieldProps", value: function getFieldProps(_ref, fieldName) { var _this2 = this; var formFields = _ref.formFields, formRules = _ref.formRules, formHiddenList = _ref.formHiddenList; var props = {}; props.labelWidth = this.props.labelWidth || 300; if (_underscore2.default.has(formFields, fieldName)) { props.key = fieldName; props.name = fieldName; props.label = formFields[fieldName].label; props.placeholder = formFields[fieldName].placeholder; props.help = formFields[fieldName].help; props.hidden = false; props.disabled = false; props.edit = false; props.showRequired = true; if (this.props.edit === _constants.FormEditStates.SELECTED) { props.edit = this.state.selection === fieldName; props.showRequired = props.edit; props.allowEdit = true; } else if (this.props.edit === _constants.FormEditStates.ALWAYS) { props.edit = true; } else if (this.props.edit === _constants.FormEditStates.NEVER) { props.showRequired = false; } if (this.props.edit === _constants.FormEditStates.TABLE) { props.layout = _constants.FormGroupLayout.INLINE; } else { props.layout = this.props.groupLayout; } if (formFields[fieldName].disabled) { props.disabled = true; } if (_underscore2.default.contains(formHiddenList, fieldName)) { props.disabled = true; props.hidden = true; } } else { throw new Error("Attr '" + fieldName + "' is not a part of the form schema"); } // If the field is required and validation rules if (_underscore2.default.has(formRules, fieldName)) { props.required = formRules[fieldName].required; props.validation = formRules[fieldName].validation; } // Field value if (this.props.value.has(fieldName)) { props.value = this.props.value.get(fieldName); } // Callbacks props.onSelectItem = function (fieldName) { return _this2.handleSelectItem(fieldName); }; props.onErrorCountChange = function (fieldName, count) { return _this2.handleErrorCountChange(fieldName, count); }; props.onMissingCountChange = function (fieldName, count) { return _this2.handleMissingCountChange(fieldName, count); }; props.onChange = function (fieldName, d) { return _this2.handleChange(fieldName, d); }; props.onBlur = function (fieldName) { return _this2.handleBlur(fieldName); }; return props; } /** * Queue state pushes pending value of state to our parent's callback. The important * thing here is that the action is deferred, meaning it will be called only * after the callstack is unwound. The deferred action also blocks other deferred * actions until it is run. * * When the deferred action takes place, the following happens: * * 1 A user action occurs * 2 queueChange is called one or many times * 3 stack unwinds... * -- * 4 A state structure is constructed out of the pending structures * 5 setState is actually called, which will cause React to re-render * 5a rendering may mount new form elements, which may themselves * result in calls to queueChange() (for example: mounted components * will report their missing/error states via supplied callbacks) * 5b those changes will also be added to the pending structures, but will * not be flushed until the outer queueChange deferred action is complete * 6 callbacks registered with us are called with updated values, missing counts * and error counts * 7 stack unwinds... * -- * 8 stack unwinds again and the deferred action will be called again if another was created * as a side effect of step (5) above */ }, { key: "queueChange", value: function queueChange() { var _this3 = this; if (!this._deferSet) { _underscore2.default.defer(function () { _this3._deferSet = false; // Write in missingCounts and errorCounts into our state var state = {}; if (_this3._pendingMissing) { state.missingCounts = _this3._pendingMissing; } if (_this3._pendingErrors) { state.errorCounts = _this3._pendingErrors; } _this3.setState(state); var missingCount = 0; var errorCount = 0; var schema = _this3.props.schema; var formFields = getFieldsFromSchema(schema); var ignoreList = _this3.getHiddenFields(formFields); // Missing count callback if (_this3._pendingMissing) { var missingFields = []; _underscore2.default.each(_this3._pendingMissing, function (c, fieldName) { if (!_underscore2.default.contains(ignoreList, fieldName)) { missingCount += c; missingFields.push(fieldName); } }); if (_this3.props.onMissingCountChange) { _this3.props.onMissingCountChange(_this3.props.name, missingCount, missingFields); } _this3._pendingMissing = null; } // Error callback if (_this3._pendingErrors) { var errorFields = []; _underscore2.default.each(_this3._pendingErrors, function (c, fieldName) { if (!_underscore2.default.contains(ignoreList, fieldName)) { missingCount += c; errorFields.push(fieldName); } errorCount += c; }); if (_this3.props.onErrorCountChange) { _this3.props.onErrorCountChange(_this3.props.name, errorCount, errorFields); } } // On change callback if (_this3._pendingValues) { if (_this3.props.onChange) { _this3.props.onChange(_this3.props.name, _this3._pendingValues); } _this3._pendingValues = null; } }); this._deferSet = true; } } /** * If the form has a submit input and that fires then this will catch that * and pass it up to the forms onSubmit callback. */ }, { key: "handleSubmit", value: function handleSubmit(e) { e.preventDefault(); } /** * This is the handler for changes to the error state of this form's fields. * * If a field is complex, such as another form or a list view, then errorCount * will be the telly all the errors within that form or list. If it is a simple * field control, such as a textedit then the errorCount will be either 0 or 1. * * The mapping of field names (passed in as the fieldName) and the count is updated * in _pendingErrors until built up state is flushed to the related callback. */ }, { key: "handleErrorCountChange", value: function handleErrorCountChange(fieldName, errorCount) { this._pendingErrors = this._pendingErrors || (0, _deepcopy2.default)(this.state.errorCounts) || {}; this._pendingErrors[fieldName] = errorCount; this.queueChange(); } /** * This is the handler for changes to the missing state of this form controls. * * If a field is complex, such as another form or a list view, then missingCount * will be the telly all the missing values (for required fields) within that * form or list. If it is a simple control such as a textedit then the * missingCount will be either 0 or 1. * * The mapping of field names (passed in as the fieldName) and the missing count is * updated in _pendingMissing until built up state is flushed to the related callback. */ }, { key: "handleMissingCountChange", value: function handleMissingCountChange(fieldName, missingCount) { this._pendingMissing = this._pendingMissing || (0, _deepcopy2.default)(this.state.missingCounts) || {}; this._pendingMissing[fieldName] = missingCount; this.queueChange(); } /** * This is the main handler for value change notifications from * this form's controls. * * As part of this handler we call this.props.onPendingChange() * if it is supplied. This hook enables either the value to be modified * before it is included in the updated state. * * Changes to the formValues are queued in _pendingValues * until built up change is flushed to the onChange callback. */ }, { key: "handleChange", value: function handleChange(fieldName, newValue) { // Hook to allow the component to alter the value before it is set. // However, you should be careful with side effects to state here. var v = newValue; if (this.props.onPendingChange) { v = this.props.onPendingChange(fieldName, newValue) || v; } // If we don't have pending values then we build initialize them // out of the current values, then build on top of that with any // change notifications we get. We deliver those batched together // in queueChange after we've accumulated missing and error counts. this._pendingValues = this._pendingValues || this.props.value; this._pendingValues = this._pendingValues.set(fieldName, v); this.queueChange(); } }, { key: "handleBlur", value: function handleBlur(fieldName) { if (this.state.selection) { this.setState({ selection: null }); } } /** * Handle the selection change. This is when you have an inline form * and the user clicks on the pencil icon to activate editing of * that item. That item is the selection. Only one item can be selected * at once. If the same item is selected again it is deselected. */ }, { key: "handleSelectItem", value: function handleSelectItem(fieldName) { if (this.state.selection !== fieldName) { this.setState({ selection: fieldName }); } else { this.setState({ selection: null }); } } /** * @private * * Returns the current list of hidden form fields using the `visible` prop * That prop is either a tag or list of tags. Those are compared to tags * for each field within the schema to determine a visibility set of fields. * This is called every render. */ }, { key: "getHiddenFields", value: function getHiddenFields(formFields) { var _this4 = this; var result = []; if (this.props.visible) { _underscore2.default.each(formFields, function (field, fieldName) { var makeHidden = void 0; var tags = field.tags || []; if (_underscore2.default.isArray(_this4.props.visible)) { makeHidden = !(_underscore2.default.intersection(tags, _this4.props.visible).length > 0 || _underscore2.default.contains(tags, "all")); } else { makeHidden = !(_underscore2.default.contains(tags, _this4.props.visible) || _underscore2.default.contains(tags, "all")); } if (makeHidden) { result.push(fieldName); } }); } return result; } /** * @private * * Traverses all the children and builds the set of props for each element. * This is what takes the prop `field="field_id"`, looks up "field_id" on the schema * then applies all the needed props from the schema, along with callbacks to * track state. */ }, { key: "renderChildren", value: function renderChildren(formState, childList) { var _this5 = this; var childCount = _react2.default.Children.count(childList); var children = []; _react2.default.Children.forEach(childList, function (child, i) { if (child) { var newChild = void 0; var key = child.key || "key-" + i; var props = { key: key }; if (typeof child.props.children !== "string") { if (_underscore2.default.has(child.props, "field")) { var fieldName = child.props.field; props = _extends({}, props, _this5.getFieldProps(formState, fieldName)); } if (_react2.default.Children.count(child.props.children) > 0) { props = _extends({}, props, { children: _this5.renderChildren(formState, child.props.children) }); } } newChild = _react2.default.cloneElement(child, props); if (childCount > 1) { children.push(newChild); } else { children = newChild; } } else { children = null; } }); return children; } /** * Restrict how often we render the form. It's likely that the container * for the form is keeping track of other state such as missing counts, so * here we make sure something we care about actually changed before doing * the whole form render. */ }, { key: "shouldComponentUpdate", value: function shouldComponentUpdate(nextProps, nextState) { var update = nextProps.value !== this.props.value || nextProps.edit !== this.props.edit || nextProps.schema !== this.props.schema || nextProps.visibility !== this.props.visibility || nextState.selection !== this.state.selection; return update; } /** * Render the form and all its children. */ }, { key: "render", value: function render() { var _this6 = this; var inner = this.props.inner; var schema = this.props.schema; var formFields = getFieldsFromSchema(schema); var formRules = getRulesFromSchema(schema); var formHiddenList = this.getHiddenFields(formFields); var formState = { formFields: formFields, formRules: formRules, formHiddenList: formHiddenList }; /* <form class="form-inline"> <div class="form-group"> <label class="sr-only" for="exampleInputEmail3">Email address</label> <input type="email" class="form-control" id="exampleInputEmail3" placeholder="Email"> </div> <div class="form-group"> <label class="sr-only" for="exampleInputPassword3">Password</label> <input type="password" class="form-control" id="exampleInputPassword3" placeholder="Password"> </div> <div class="checkbox"> <label> <input type="checkbox"> Remember me </label> </div> <button type="submit" class="btn btn-default">Sign in</button> </form> */ var formClass = this.props.formClassName; if (this.props.inline) { formClass += "form-inline"; } if (this.props.edit === _constants.FormEditStates.TABLE) { return _react2.default.createElement( _flexboxReact2.default, { flexDirection: "row", className: this.props.formClassName, key: this.props.formKey }, this.renderChildren(formState, this.props.children) ); } else { if (inner) { return _react2.default.createElement( "form", { className: formClass, style: this.props.formStyle, key: this.props.formKey, onSubmit: function onSubmit(e) { _this6.handleSubmit(e); }, noValidate: true }, this.renderChildren(formState, this.props.children) ); } else { return _react2.default.createElement( "div", { className: this.props.formClassName, style: this.props.formStyle, key: this.props.formKey }, this.renderChildren(formState, this.props.children) ); } } } }]); return Form; }(_react2.default.Component); exports.default = Form; Form.propTypes = { value: _propTypes2.default.object }; Form.defaultProps = { formStyle: {}, formClass: "form-horizontal", formKey: "form", groupLayout: _constants.FormGroupLayout.ROW };