formular
Version:
Build forms in React. Easy-Peasy!
866 lines (842 loc) • 32.4 kB
JavaScript
import equal from 'fast-deep-equal';
import { useCallback, useState, useEffect, useMemo } from 'react';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function __awaiter(thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
function __generator(thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
}
var Event = /** @class */ (function () {
function Event(name) {
this.name = name;
this.handlers = [];
}
/**
* Add handler to current Event
*/
Event.prototype.addHandler = function (handler) {
this.handlers.push(handler);
};
/**
* Remove handler from current Event
*/
Event.prototype.removeHandler = function (handler) {
this.handlers = this.handlers.filter(function (h) { return h !== handler; });
};
/**
* Call all handlers in all priorities of current Event
*/
Event.prototype.call = function () {
var eventArgs = [];
for (var _i = 0; _i < arguments.length; _i++) {
eventArgs[_i] = arguments[_i];
}
this.handlers.forEach(function (handler) {
try {
handler.apply(void 0, eventArgs);
}
catch (err) {
console.error(err);
}
});
};
return Event;
}());
var EventAggregator = /** @class */ (function () {
function EventAggregator() {
this.events = {};
}
/**
* Get Event by name
*/
EventAggregator.prototype.getEvent = function (name) {
var event = this.events[name];
if (!event) {
event = new Event(name);
this.events[name] = event;
}
return event;
};
EventAggregator.prototype.subscribe = function (name, handler) {
var _this = this;
var event = this.getEvent(name);
event.addHandler(handler);
return function () {
_this.unsubscribe(name, handler);
};
};
EventAggregator.prototype.unsubscribe = function (name, handler) {
var event = this.getEvent(name);
event.removeHandler(handler);
};
EventAggregator.prototype.dispatch = function (name) {
var eventArgs = [];
for (var _i = 1; _i < arguments.length; _i++) {
eventArgs[_i - 1] = arguments[_i];
}
var event = this.getEvent(name);
if (event) {
event.call.apply(event, eventArgs);
}
};
/**
* Subscribe to Event and unsubscribe after call
*/
EventAggregator.prototype.once = function (name, handler) {
var event = this.getEvent(name);
var handlerWrapper = function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
var result = handler.apply(void 0, args);
if (result) {
event.removeHandler(handlerWrapper);
}
};
event.addHandler(handlerWrapper);
return { event: event, handlerWrapper: handlerWrapper };
};
return EventAggregator;
}());
/*
https://github.com/alkemics/CancelablePromise
*/
var handleCallback = function (resolve, reject, callback, result) {
try {
resolve(callback(result));
}
catch (e) {
reject(e);
}
};
var CancelablePromise = /** @class */ (function () {
function CancelablePromise(executor) {
// @ts-ignore
this._promise = new Promise(executor);
this._canceled = false;
}
CancelablePromise.all = function (iterable) {
return new CancelablePromise(function (y, n) {
Promise.all(iterable).then(y, n);
});
};
CancelablePromise.race = function (iterable) {
return new CancelablePromise(function (y, n) {
Promise.race(iterable).then(y, n);
});
};
CancelablePromise.reject = function (value) {
return new CancelablePromise(function (y, n) {
Promise.reject(value).then(y, n);
});
};
CancelablePromise.resolve = function (value) {
return new CancelablePromise(function (y, n) {
Promise.resolve(value).then(y, n);
});
};
CancelablePromise.prototype.then = function (success, error) {
var _this = this;
var promise = new CancelablePromise(function (resolve, reject) {
_this._promise.then(function (result) {
if (_this._canceled) {
promise.cancel();
}
if (success && !_this._canceled) {
handleCallback(resolve, reject, success, result);
}
else {
resolve(result);
}
}, function (result) {
if (_this._canceled) {
promise.cancel();
}
if (error && !_this._canceled) {
handleCallback(resolve, reject, error, result);
}
else {
reject(result);
}
});
});
return promise;
};
CancelablePromise.prototype.catch = function (error) {
return this.then(undefined, error);
};
CancelablePromise.prototype.cancel = function (errorCallback) {
this._canceled = true;
if (errorCallback) {
// @ts-ignore
this._promise.catch(errorCallback);
}
return this;
};
return CancelablePromise;
}());
var asyncSome = function (arr, calle) { return __awaiter(void 0, void 0, void 0, function () {
var item, restItems, isMatch;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!arr.length) return [3 /*break*/, 2];
item = arr[0], restItems = arr.slice(1);
return [4 /*yield*/, calle(item)];
case 1:
isMatch = _a.sent();
if (isMatch) {
return [2 /*return*/, true];
}
return [2 /*return*/, asyncSome(restItems, calle)];
case 2: return [2 /*return*/, false];
}
});
}); };
function debounce(func, wait, immediate) {
var timeout;
return function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
var context = this;
var later = function () {
timeout = undefined;
if (!immediate) {
func.apply(context, args);
}
};
var callNow = immediate && timeout === undefined;
if (timeout !== undefined) {
clearTimeout(timeout);
}
timeout = setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
}
};
}
var eventNames = {
stateChange: 'state change',
valueChange: 'value change',
change: 'change',
unset: 'unset',
focus: 'focus',
blur: 'blur',
startValidate: 'start validate',
validate: 'validate',
};
var Field = /** @class */ (function () {
function Field(opts, form) {
var _this = this;
this.handleFocus = function (event) {
_this._events.dispatch(eventNames.focus, event);
};
this.handleBlur = function (event) {
_this._events.dispatch(eventNames.blur, event);
};
this.validate = function () {
if (!_this.validators || !_this.validators.length) {
// existing error state should be cleared bcs it could be set from server validation via field.setError(err)
_this.setState({
error: null,
isValid: true,
isValidating: false,
isValidated: true,
});
return CancelablePromise.resolve();
}
var error;
_this._events.dispatch(eventNames.startValidate);
var setError = function (error) {
_this.setState({
error: error,
isValid: !error,
isValidating: false,
isValidated: true,
});
_this._events.dispatch(eventNames.validate, _this.state.error);
};
_this.setState({
isValidating: true,
});
if (_this._cancelablePromise) {
_this._cancelablePromise.cancel();
}
_this._cancelablePromise = new CancelablePromise(function (resolve) { return __awaiter(_this, void 0, void 0, function () {
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, asyncSome(this.validators, function (validator) { return __awaiter(_this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, validator(this.state.value, this.form && this.form.fields)]; // error here is String error message
case 1:
error = _a.sent(); // error here is String error message
return [2 /*return*/, Boolean(error)];
}
});
}); })];
case 1:
_a.sent();
resolve();
return [2 /*return*/];
}
});
}); });
return _this._cancelablePromise.then(function () {
setError(error);
return error;
});
};
this.form = form;
this.opts = opts || {};
this.name = this.opts.name;
this.node = this.opts.node;
this.readOnly = this.opts.readOnly || false;
this.validators = this.opts.validate || [];
this.debounceValidate = this.opts.validationDelay ? debounce(this.validate, this.opts.validationDelay) : this.validate;
this.state = {
value: Field.modifyValue(this.opts.value),
error: null,
isChanged: false,
isValidating: false,
isValidated: false,
isValid: true,
};
this.props = {
ref: function (node) { return _this.node = node; },
onChange: function (event) { return _this.set(event.currentTarget.value); },
onFocus: this.handleFocus,
onBlur: this.handleBlur,
};
this._events = new EventAggregator();
this._initialValue = this.state.value;
this._cancelablePromise = null;
// TODO how to detect this?
// this.hasAsyncValidators = this.validators.some((validator) => (
// validator.constructor.name === 'AsyncFunction'
// ))
}
Field.modifyValue = function (value) {
return value === undefined || value === null ? '' : value;
};
// Common methods
Field.prototype.setState = function (values) {
var newState = __assign(__assign({}, this.state), values);
var isEqual = equal(this.state, newState);
if (!isEqual) {
this.state = newState;
this._events.dispatch(eventNames.stateChange, this.state);
}
};
Field.prototype.setRef = function (node) {
this.node = node;
if (this.node) {
this.node.addEventListener(eventNames.focus, this.handleFocus);
this.node.addEventListener(eventNames.blur, this.handleBlur);
}
};
Field.prototype.unsetRef = function () {
if (this.node) {
this.node.removeEventListener(eventNames.focus, this.handleFocus);
this.node.removeEventListener(eventNames.blur, this.handleBlur);
}
this.node = undefined;
};
Field.prototype.set = function (value, opts) {
var silent = (opts || {}).silent;
var modifiedValue = Field.modifyValue(value);
if (modifiedValue !== this.state.value && !this.readOnly) {
this.setState({
value: modifiedValue,
isChanged: true,
});
if (!silent) {
this._events.dispatch(eventNames.valueChange, this.state.value);
this._events.dispatch(eventNames.change, this.state.value);
}
}
};
Field.prototype.unset = function () {
this.setState({
value: this._initialValue,
error: null,
isChanged: false,
isValidating: false,
isValidated: false,
isValid: true,
});
this._events.dispatch(eventNames.unset);
};
Field.prototype.setError = function (error) {
if (error === this.state.error) {
return;
}
this.setState({
error: error,
isChanged: true,
isValid: false,
isValidating: false,
isValidated: true,
});
this._events.dispatch(eventNames.change, this.state.value);
};
Field.prototype.on = function (eventName, handler) {
this._events.subscribe(eventName, handler);
};
Field.prototype.off = function (eventName, handler) {
this._events.unsubscribe(eventName, handler);
};
return Field;
}());
var eventNames$1 = {
stateChange: 'state change',
attachFields: 'attach fields',
detachFields: 'detach fields',
forceUpdate: 'force update',
change: 'change',
focus: 'focus',
blur: 'blur',
submit: 'submit',
};
var defaultOptions = {
initialValues: {},
};
var Form = /** @class */ (function () {
function Form(opts) {
this.name = opts.name;
this.opts = __assign(__assign({}, defaultOptions), opts);
// @ts-ignore
this.fields = {};
this.state = {
isValid: true,
isChanged: false,
isValidating: false,
isSubmitting: false,
isSubmitted: false,
};
this._events = new EventAggregator();
this._attachFields(this.opts.fields);
}
Form.prototype._attachFields = function (fieldOpts) {
var _this = this;
var fieldNames = Object.keys(fieldOpts);
fieldNames.forEach(function (fieldName) {
var initialValue = _this.opts.initialValues && _this.opts.initialValues[fieldName];
var opts = fieldOpts[fieldName];
opts = Array.isArray(opts) ? { validate: opts } : opts;
opts.name = fieldName;
if (typeof initialValue !== 'undefined') {
opts.value = initialValue;
}
var field = new Field(opts, _this);
_this.fields[fieldName] = field;
var eventKeys = ['change', 'focus', 'blur'];
eventKeys.forEach(function (key) {
field.on(eventNames[key], function () {
_this._events.dispatch(eventNames$1[key], field);
});
});
});
};
Form.prototype.attachFields = function (fieldOpts) {
this._attachFields(fieldOpts);
this._events.dispatch(eventNames$1.attachFields);
this.forceUpdate();
};
Form.prototype.detachFields = function (fieldNames) {
var _this = this;
fieldNames.forEach(function (fieldName) {
delete _this.fields[fieldName];
});
this._events.dispatch(eventNames$1.detachFields);
this.forceUpdate();
};
Form.prototype.forceUpdate = function () {
this._events.dispatch(eventNames$1.forceUpdate);
};
Form.prototype.setState = function (values) {
this.state = __assign(__assign({}, this.state), values);
this._events.dispatch(eventNames$1.stateChange, this.state);
};
Form.prototype.setValues = function (values) {
var _this = this;
var fieldNames = Object.keys(values);
// TODO should we mark form as changed and validate it?
fieldNames.forEach(function (fieldName) {
var field = _this.fields[fieldName];
if (field) {
field.set(values[fieldName]);
}
});
};
Form.prototype.getValues = function () {
var _this = this;
var fieldNames = Object.keys(this.fields);
var values = {};
fieldNames.forEach(function (fieldName) {
values[fieldName] = _this.fields[fieldName].state.value;
});
return values;
};
Form.prototype.unsetValues = function () {
var _this = this;
this.setState({
isChanged: false,
isValid: true,
});
Object.keys(this.fields).forEach(function (fieldName) {
_this.fields[fieldName].unset();
});
};
Form.prototype.setErrors = function (errors) {
var _this = this;
var fieldNames = Object.keys(errors);
fieldNames.forEach(function (fieldName) {
var field = _this.fields[fieldName];
if (field) {
field.setError(errors[fieldName]);
}
});
};
Form.prototype.getErrors = function () {
var _this = this;
var fieldNames = Object.keys(this.fields);
var errors = {};
fieldNames.forEach(function (fieldName) {
var error = _this.fields[fieldName].state.error;
if (error) {
errors[fieldName] = error;
}
});
return Object.keys(errors).length ? errors : null;
};
Form.prototype.validate = function () {
return __awaiter(this, void 0, void 0, function () {
var promises, errors, isValid;
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
promises = Object.keys(this.fields).map(function (fieldName) { return _this.fields[fieldName].validate(); });
return [4 /*yield*/, Promise.all(promises)];
case 1:
errors = _a.sent();
isValid = errors.every(function (error) { return !error; });
this.setState({ isValid: isValid });
return [2 /*return*/, isValid];
}
});
});
};
Form.prototype.submit = function () {
return __awaiter(this, void 0, void 0, function () {
var values, errors;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
values = this.getValues();
// TODO don't validate if all fields are changed and valid
return [4 /*yield*/, this.validate()];
case 1:
// TODO don't validate if all fields are changed and valid
_a.sent();
errors = this.getErrors();
this._events.dispatch(eventNames$1.submit, errors, values);
return [2 /*return*/, {
values: values,
errors: errors,
}];
}
});
});
};
Form.prototype.on = function (eventName, handler) {
this._events.subscribe(eventName, handler);
};
Form.prototype.off = function (eventName, handler) {
this._events.unsubscribe(eventName, handler);
};
return Form;
}());
var eventNames$2 = {
replace: 'replace',
setValues: 'set values',
unsetValues: 'unset values',
attachForms: 'attach forms',
detachForms: 'detach forms',
forceUpdate: 'force update',
submit: 'submit',
};
var FormGroup = /** @class */ (function () {
function FormGroup(forms) {
var _this = this;
this._handleFormEvent = function (eventName) { return debounce(function () {
_this._events.dispatch(eventName);
}, 100); };
this._events = new EventAggregator();
// @ts-ignore
this.forms = forms || {};
this._subscribe();
}
FormGroup.prototype._subscribe = function () {
var _this = this;
var forms = Object.values(this.forms);
forms.forEach(function (form) {
var eventNames = Object.keys(eventNames$1);
eventNames.forEach(function (eventName) {
form.on(eventName, _this._handleFormEvent(eventName));
});
});
};
FormGroup.prototype._unsubscribe = function () {
var _this = this;
var forms = Object.values(this.forms);
forms.forEach(function (form) {
var eventNames = Object.keys(eventNames$1);
eventNames.forEach(function (eventName) {
form.off(eventName, _this._handleFormEvent(eventName));
});
});
};
FormGroup.prototype.attachForms = function (forms) {
var _this = this;
var formNames = Object.keys(forms);
formNames.forEach(function (formName) {
if (formName in _this.forms) {
console.error("Form with name \"" + formName + "\" already exists in FormGroup");
}
else {
_this.forms[formName] = forms[formName];
}
});
this._events.dispatch(eventNames$2.attachForms);
this.forceUpdate();
};
FormGroup.prototype.detachForms = function (formNames) {
var _this = this;
formNames.forEach(function (fieldName) {
delete _this.forms[fieldName];
});
this._events.dispatch(eventNames$2.detachForms);
this.forceUpdate();
};
FormGroup.prototype.replace = function (newForms) {
this._unsubscribe();
this.forms = newForms;
this._subscribe();
this._events.dispatch(eventNames$2.replace);
this.forceUpdate();
};
FormGroup.prototype.forceUpdate = function () {
this._events.dispatch(eventNames$2.forceUpdate);
};
FormGroup.prototype.validate = function () {
return __awaiter(this, void 0, void 0, function () {
var forms, statuses, isValid;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
forms = Object.values(this.forms);
return [4 /*yield*/, Promise.all(forms.map(function (form) { return form.validate(); }))];
case 1:
statuses = _a.sent();
isValid = statuses.every(function (isValid) { return isValid; });
return [2 /*return*/, isValid];
}
});
});
};
FormGroup.prototype.setValues = function (values) {
var _this = this;
var formNames = Object.keys(this.forms);
formNames.forEach(function (formName) {
var form = _this.forms[formName];
var formValues = values[formName];
if (formValues) {
form.setValues(formValues);
}
});
this._events.dispatch(eventNames$2.setValues);
};
FormGroup.prototype.getValues = function () {
var _this = this;
var formNames = Object.keys(this.forms);
var values = {};
formNames.forEach(function (formName) {
var form = _this.forms[formName];
values[formName] = form.getValues();
});
return values;
};
FormGroup.prototype.unsetValues = function () {
var _this = this;
var formNames = Object.keys(this.forms);
formNames.forEach(function (formName) {
var form = _this.forms[formName];
form.unsetValues();
});
this._events.dispatch(eventNames$2.unsetValues);
};
// TODO looks like getValues() if we need rewrite it? Write getKeyValues(key)
FormGroup.prototype.getErrors = function () {
var _this = this;
var formNames = Object.keys(this.forms);
var errors = {};
formNames.forEach(function (formName) {
var form = _this.forms[formName];
var formErrors = form.getErrors();
if (formErrors) {
errors[formName] = formErrors;
}
});
return Object.keys(errors).length ? errors : null;
};
FormGroup.prototype.submit = function () {
return __awaiter(this, void 0, void 0, function () {
var values, errors;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
values = this.getValues();
return [4 /*yield*/, this.validate()];
case 1:
_a.sent();
errors = this.getErrors();
this._events.dispatch(eventNames$2.submit, errors, values);
return [2 /*return*/, {
values: values,
errors: errors,
}];
}
});
});
};
FormGroup.prototype.on = function (eventName, handler) {
this._events.subscribe(eventName, handler);
};
FormGroup.prototype.off = function (eventName, handler) {
this._events.unsubscribe(eventName, handler);
};
return FormGroup;
}());
var useForceUpdate = function () {
var _a = useState(0), _ = _a[0], setState = _a[1];
return useCallback(function () { return setState(function (v) { return v + 1; }); }, []);
};
var useFieldState = function (field) {
var forceUpdate = useForceUpdate();
useEffect(function () {
field.on('state change', forceUpdate);
return function () {
field.off('state change', forceUpdate);
};
}, [field]);
return field.state;
};
var FieldState = function (_a) {
var children = _a.children, field = _a.field;
var fieldState = useFieldState(field);
return children(fieldState);
};
var useForm = function (opts, deps) {
var _a = useState(0), _ = _a[0], update = _a[1];
var form = useMemo(function () { return new Form(opts); }, deps || []);
useEffect(function () {
var handleUpdate = function () {
update(function (v) { return ++v; });
};
form.on('force update', handleUpdate);
return function () {
form.off('force update', handleUpdate);
};
}, []);
return form;
};
var useField = function (opts, deps) {
return useMemo(function () { return new Field(opts); }, deps || []);
};
var useFormGroup = function (forms, deps) {
var _a = useState(0), _ = _a[0], update = _a[1];
var formGroup = useMemo(function () { return new FormGroup(forms); }, deps || []);
useEffect(function () {
var handleUpdate = function () {
update(function (v) { return ++v; });
};
formGroup.on('force update', handleUpdate);
return function () {
formGroup.off('force update', handleUpdate);
};
}, []);
return formGroup;
};
var useFormState = function (form) {
var forceUpdate = useForceUpdate();
useEffect(function () {
form.on('change', forceUpdate);
form.on('state change', forceUpdate);
return function () {
form.off('change', forceUpdate);
form.off('state change', forceUpdate);
};
}, [form]);
var state = form.state;
var values = form.getValues();
var errors = form.getErrors();
return __assign(__assign({}, state), { values: values, errors: errors });
};
export { Field, FieldState, Form, FormGroup, useField, useFieldState, useForm, useFormGroup, useFormState };