@shopify/react-form-state
Version:
Manage react forms tersely and type-safe with no magic.
354 lines (353 loc) • 14.4 kB
JavaScript
"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 });
}