UNPKG

@shopify/react-form-state

Version:
354 lines (353 loc) 14.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var tslib_1 = require("tslib"); /* eslint-disable no-case-declarations */ var React = tslib_1.__importStar(require("react")); var utilities_1 = require("./utilities"); var components_1 = require("./components"); var FormState = /** @class */ (function (_super) { tslib_1.__extends(FormState, _super); function FormState() { var _this = _super !== null && _super.apply(this, arguments) || this; _this.state = createFormState(_this.props.initialValues, _this.props.externalErrors); _this.mounted = false; _this.fieldsWithHandlers = new WeakMap(); _this.reset = function () { return new Promise(function (resolve) { _this.setState(function (_state, props) { return createFormState(props.initialValues, props.externalErrors); }, function () { return resolve(); }); }); }; _this.submit = function (event) { return tslib_1.__awaiter(_this, void 0, void 0, function () { var _a, onSubmit, validateOnSubmit, formData, errors; return tslib_1.__generator(this, function (_b) { switch (_b.label) { case 0: _a = this.props, onSubmit = _a.onSubmit, validateOnSubmit = _a.validateOnSubmit; formData = this.formData; if (!this.mounted) { return [2 /*return*/]; } if (event && event.preventDefault && !event.defaultPrevented) { event.preventDefault(); } if (onSubmit == null) { return [2 /*return*/]; } this.setState({ submitting: true }); if (!validateOnSubmit) return [3 /*break*/, 2]; return [4 /*yield*/, this.validateForm()]; case 1: _b.sent(); if (this.hasClientErrors) { this.setState({ submitting: false }); return [2 /*return*/]; } _b.label = 2; case 2: return [4 /*yield*/, onSubmit(formData)]; case 3: errors = (_b.sent()) || []; if (!this.mounted) { return [2 /*return*/]; } if (errors.length > 0) { this.updateRemoteErrors(errors); this.setState({ submitting: false }); } else { this.setState({ submitting: false, errors: errors }); } return [2 /*return*/]; } }); }); }; _this.fieldWithHandlers = function (field, fieldPath) { if (_this.fieldsWithHandlers.has(field)) { // eslint-disable-next-line typescript/no-non-null-assertion return _this.fieldsWithHandlers.get(field); } var result = tslib_1.__assign({}, field, { name: String(fieldPath), onChange: _this.updateField.bind(_this, fieldPath), onBlur: _this.blurField.bind(_this, fieldPath) }); _this.fieldsWithHandlers.set(field, result); return result; }; return _this; } FormState.getDerivedStateFromProps = function (newProps, oldState) { var initialValues = newProps.initialValues, onInitialValuesChange = newProps.onInitialValuesChange, _a = newProps.externalErrors, externalErrors = _a === void 0 ? [] : _a; var externalErrorsChanged = !utilities_1.isEqual(externalErrors, oldState.externalErrors); var updatedExternalErrors = externalErrorsChanged ? { externalErrors: externalErrors, fields: fieldsWithErrors(oldState.fields, tslib_1.__spread(externalErrors, oldState.errors)), } : null; switch (onInitialValuesChange) { case 'ignore': return updatedExternalErrors; case 'reset-where-changed': return reconcileFormState(initialValues, oldState, externalErrors); case 'reset-all': default: var oldInitialValues = initialValuesFromFields(oldState.fields); var valuesMatch = utilities_1.isEqual(oldInitialValues, initialValues); if (valuesMatch) { return updatedExternalErrors; } return createFormState(initialValues, externalErrors); } }; FormState.prototype.componentDidMount = function () { this.mounted = true; }; FormState.prototype.componentWillUnmount = function () { this.mounted = false; }; FormState.prototype.render = function () { var children = this.props.children; var submitting = this.state.submitting; var _a = this, submit = _a.submit, reset = _a.reset, formData = _a.formData; return children(tslib_1.__assign({}, formData, { submit: submit, reset: reset, submitting: submitting })); }; FormState.prototype.validateForm = function () { var _this = this; return new Promise(function (resolve) { _this.setState(runAllValidators, function () { return resolve(); }); }); }; Object.defineProperty(FormState.prototype, "formData", { get: function () { var errors = this.state.errors; var _a = this.props.externalErrors, externalErrors = _a === void 0 ? [] : _a; var _b = this, fields = _b.fields, dirty = _b.dirty, valid = _b.valid; return { dirty: dirty, valid: valid, errors: tslib_1.__spread(errors, externalErrors), fields: fields, }; }, enumerable: true, configurable: true }); Object.defineProperty(FormState.prototype, "dirty", { get: function () { return this.state.dirtyFields.length > 0; }, enumerable: true, configurable: true }); Object.defineProperty(FormState.prototype, "valid", { get: function () { var _a = this.state, errors = _a.errors, externalErrors = _a.externalErrors; return (!this.hasClientErrors && errors.length === 0 && externalErrors.length === 0); }, enumerable: true, configurable: true }); Object.defineProperty(FormState.prototype, "hasClientErrors", { get: function () { var fields = this.state.fields; return Object.keys(fields).some(function (fieldPath) { var field = fields[fieldPath]; return field.error != null; }); }, enumerable: true, configurable: true }); Object.defineProperty(FormState.prototype, "fields", { get: function () { var fields = this.state.fields; var fieldDescriptors = utilities_1.mapObject(fields, this.fieldWithHandlers); return fieldDescriptors; }, enumerable: true, configurable: true }); FormState.prototype.updateField = function (fieldPath, value) { var _this = this; this.setState(function (_a) { var fields = _a.fields, dirtyFields = _a.dirtyFields; var _b; var field = fields[fieldPath]; var newValue = typeof value === 'function' ? value(field.value) : value; var dirty = !utilities_1.isEqual(newValue, field.initialValue); var updatedField = _this.getUpdatedField({ fieldPath: fieldPath, field: field, value: newValue, dirty: dirty, }); return { dirtyFields: _this.getUpdatedDirtyFields({ fieldPath: fieldPath, dirty: dirty, dirtyFields: dirtyFields, }), fields: updatedField === field ? fields : tslib_1.__assign({}, fields, (_b = {}, _b[fieldPath] = updatedField, _b)), }; }); }; FormState.prototype.getUpdatedDirtyFields = function (_a) { var fieldPath = _a.fieldPath, dirty = _a.dirty, dirtyFields = _a.dirtyFields; var dirtyFieldsSet = new Set(dirtyFields); if (dirty) { dirtyFieldsSet.add(fieldPath); } else { dirtyFieldsSet.delete(fieldPath); } var newDirtyFields = Array.from(dirtyFieldsSet); return dirtyFields.length === newDirtyFields.length ? dirtyFields : newDirtyFields; }; FormState.prototype.getUpdatedField = function (_a) { var fieldPath = _a.fieldPath, field = _a.field, value = _a.value, dirty = _a.dirty; // We only want to update errors as the user types if they already have an error. // https://polaris.shopify.com/patterns/error-messages#section-form-validation var skipValidation = field.error == null; var error = skipValidation ? field.error : this.validateFieldValue(fieldPath, { value: value, dirty: dirty }); if (value === field.value && error === field.error) { return field; } return tslib_1.__assign({}, field, { value: value, dirty: dirty, error: error }); }; FormState.prototype.blurField = function (fieldPath) { var fields = this.state.fields; var field = fields[fieldPath]; var error = this.validateFieldValue(fieldPath, field); if (error == null) { return; } this.setState(function (state) { var _a; return ({ fields: tslib_1.__assign({}, state.fields, (_a = {}, _a[fieldPath] = tslib_1.__assign({}, state.fields[fieldPath], { error: error }), _a)), }); }); }; FormState.prototype.validateFieldValue = function (fieldPath, _a) { var value = _a.value, dirty = _a.dirty; if (!dirty) { return; } var _b = this.props.validators, validators = _b === void 0 ? {} : _b; var fields = this.state.fields; // eslint-disable-next-line consistent-return return runValidator(validators[fieldPath], value, fields); }; FormState.prototype.updateRemoteErrors = function (errors) { this.setState(function (_a) { var fields = _a.fields, externalErrors = _a.externalErrors; return ({ errors: errors, fields: fieldsWithErrors(fields, tslib_1.__spread(errors, externalErrors)), }); }); }; FormState.List = components_1.List; FormState.Nested = components_1.Nested; return FormState; }(React.PureComponent)); exports.default = FormState; function fieldsWithErrors(fields, errors) { var errorDictionary = errors.reduce(function (accumulator, _a) { var field = _a.field, message = _a.message; if (field == null) { return accumulator; } return utilities_1.set(accumulator, field, message); }, {}); return utilities_1.mapObject(fields, function (field, path) { if (!errorDictionary[path]) { return field; } return tslib_1.__assign({}, field, { error: errorDictionary[path] }); }); } function reconcileFormState(values, oldState, externalErrors) { if (externalErrors === void 0) { externalErrors = []; } var oldFields = oldState.fields; var dirtyFields = new Set(oldState.dirtyFields); var fields = utilities_1.mapObject(values, function (value, key) { var oldField = oldFields[key]; if (utilities_1.isEqual(value, oldField.initialValue)) { return oldField; } dirtyFields.delete(key); return { value: value, initialValue: value, dirty: false, }; }); return tslib_1.__assign({}, oldState, { dirtyFields: Array.from(dirtyFields), fields: fieldsWithErrors(fields, externalErrors) }); } function createFormState(values, externalErrors) { if (externalErrors === void 0) { externalErrors = []; } var fields = utilities_1.mapObject(values, function (value) { return { value: value, initialValue: value, dirty: false, }; }); return { dirtyFields: [], errors: [], submitting: false, externalErrors: externalErrors, fields: fieldsWithErrors(fields, externalErrors), }; } function initialValuesFromFields(fields) { return utilities_1.mapObject(fields, function (_a) { var initialValue = _a.initialValue; return initialValue; }); } function runValidator(validate, value, fields) { if (validate === void 0) { validate = function () { }; } if (typeof validate === 'function') { // eslint-disable-next-line consistent-return return validate(value, fields); } if (!Array.isArray(validate)) { // eslint-disable-next-line consistent-return return; } var errors = validate .map(function (validator) { return validator(value, fields); }) .filter(function (input) { return input != null; }); if (errors.length === 0) { // eslint-disable-next-line consistent-return return; } // eslint-disable-next-line consistent-return return errors; } function runAllValidators(state, props) { var fields = state.fields; var validators = props.validators; if (!validators) { return null; } var updatedFields = utilities_1.mapObject(fields, function (field, path) { return tslib_1.__assign({}, field, { error: runValidator(validators[path], field.value, fields) }); }); return tslib_1.__assign({}, state, { fields: updatedFields }); }