react-form-hooks
Version:
Create forms in react using hooks
499 lines (479 loc) • 19.3 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var react = require('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 __rest(s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
}
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
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) : adopt(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 };
}
}
function __spreadArrays() {
for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;
for (var r = Array(s), k = 0, i = 0; i < il; i++)
for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)
r[k] = a[j];
return r;
}
var handleFormSubmit = function (func) { return function (event) {
if (event) {
event.preventDefault();
}
return func();
}; };
var isObject = function (value) {
return value !== null && typeof value === 'object';
};
var isIndex = function (k) { return /^(\d+)$/.test(k); };
function getProperty(obj, key) {
var parts = key
.replace(/\[(\w+)]/g, '.$1') // convert indexes to properties
.replace(/^\./, '') // strip a leading dot
.split('.');
var curObj = obj;
for (var i = 0; i < parts.length; ++i) {
var k = parts[i];
if (curObj === undefined || curObj === null) {
return undefined;
}
if (typeof curObj === 'string') {
return undefined;
}
if (k in curObj) {
curObj = curObj[k];
}
else {
return undefined;
}
}
return curObj;
}
function setProperty(obj, key, value) {
var path = key
.replace(/\[(\w+)]/g, '.$1') // convert indexes to properties
.replace(/^\./, '') // strip a leading dot
.split('.');
var newObj = __assign({}, obj);
var nested = newObj;
for (var i = 0; nested != null && i < path.length; ++i) {
var key_1 = path[i];
var newValue = value;
if (i !== path.length - 1) {
var objValue = nested[key_1];
if (isObject(objValue)) {
newValue = Array.isArray(objValue) ? __spreadArrays(objValue) : __assign({}, objValue);
}
else {
newValue = isIndex(path[i + 1]) ? [] : {};
}
}
nested[key_1] = newValue;
nested = nested[key_1];
}
return newObj;
}
// Simplified version of createStore from Redux to keep the size small
function createStore(reducer, initialState) {
var state = initialState;
var listeners = [];
function getState() {
return state;
}
function dispatch(action) {
// console.debug(`%cDISPATCH`, 'background:red;color:#fff', action)
state = reducer(state, action);
listeners.forEach(function (listener) { return listener(); });
return action;
}
function subscribe(listener) {
// console.debug('%cSUBSCRIBE', 'color: green')
listeners.push(listener);
return function () {
// console.debug('%cUNSUBSCRIBE', 'color: red')
listeners = listeners.filter(function (l) { return l !== listener; });
};
}
return { subscribe: subscribe, getState: getState, dispatch: dispatch };
}
var ActionTypes;
(function (ActionTypes) {
ActionTypes["INIT_FORM_VALUES"] = "@@form/INIT_FORM_VALUES";
ActionTypes["CHANGE_FIELD_VALUE"] = "@@form/CHANGE_FIELD_VALUE";
ActionTypes["INIT_FIELD"] = "@@form/INIT_FIELD";
ActionTypes["DESTROY_FIELD"] = "@@form/DESTROY_FIELD";
ActionTypes["TOUCH_FIELD"] = "@@form/TOUCH_FIELD";
})(ActionTypes || (ActionTypes = {}));
function fieldReducer(state, action) {
switch (action.type) {
case ActionTypes.INIT_FIELD:
return { touched: false, dirty: false };
case ActionTypes.CHANGE_FIELD_VALUE:
return __assign(__assign({}, state), { dirty: true });
case ActionTypes.TOUCH_FIELD:
return __assign(__assign({}, state), { touched: true });
}
}
function fieldState(state, action) {
var _a;
switch (action.type) {
case ActionTypes.INIT_FIELD:
case ActionTypes.CHANGE_FIELD_VALUE:
case ActionTypes.TOUCH_FIELD:
return __assign(__assign({}, state), (_a = {}, _a[action.field] = fieldReducer(state[action.field], action), _a));
case ActionTypes.DESTROY_FIELD:
var _b = state, _c = action.field, _ = _b[_c], remaining = __rest(_b, [typeof _c === "symbol" ? _c : _c + ""]);
return remaining;
default:
return state;
}
}
function formErrors(state, action) {
var _a;
switch (action.type) {
case ActionTypes.INIT_FIELD:
case ActionTypes.CHANGE_FIELD_VALUE:
var newError = action.error || undefined;
return state[action.field] === newError
? state
: __assign(__assign({}, state), (_a = {}, _a[action.field] = newError, _a));
case ActionTypes.DESTROY_FIELD:
var _b = state, _c = action.field, _ = _b[_c], remaining = __rest(_b, [typeof _c === "symbol" ? _c : _c + ""]);
return remaining;
default:
return state;
}
}
function formValues(state, action) {
switch (action.type) {
case ActionTypes.INIT_FORM_VALUES:
return action.values;
case ActionTypes.CHANGE_FIELD_VALUE:
return setProperty(state, action.field, action.value);
default:
return state;
}
}
function formReducer(state, action) {
return {
formValues: formValues(state.formValues, action),
formErrors: formErrors(state.formErrors, action),
fieldState: fieldState(state.fieldState, action),
};
}
function createForm(_a) {
var _this = this;
var initialValues = (_a === void 0 ? { initialValues: {} } : _a).initialValues;
var store = createStore(formReducer, {
formValues: initialValues,
formErrors: {},
fieldState: {},
});
var fieldRefs = {}; // any = { [ref: symbol]: FieldOptions }
var validateField = function (fieldId, value) {
if (!fieldRefs[fieldId]) {
return undefined;
}
var validate = Object.getOwnPropertySymbols(fieldRefs[fieldId])
.map(function (ref) { return fieldRefs[fieldId][ref]; })
.map(function (x) { return x.validate; })
.find(function (x) { return !!x; }) || (function () { return undefined; });
return validate(value);
};
// State selectors
var getFieldValue = function (fieldId) {
var val = getProperty(store.getState().formValues, fieldId);
return typeof val === 'undefined' ? null : val;
};
var getFieldState = function (fieldId) { return (__assign(__assign({}, store.getState().fieldState[fieldId]), { error: store.getState().formErrors[fieldId], value: getFieldValue(fieldId) })); };
var getFormState = function () {
var state = store.getState();
var anyTouched = Object.values(state.fieldState).some(function (field) { return field.touched; });
return {
values: state.formValues,
errors: state.formErrors,
anyError: Object.values(state.formErrors).some(function (field) { return !!field; }),
anyTouched: anyTouched,
allTouched: anyTouched &&
Object.values(state.fieldState).every(function (field) { return field.touched; }),
anyDirty: Object.values(state.fieldState).some(function (field) { return field.dirty; }),
};
};
// Field handlers
var setFieldOptions = function (fieldId, ref, opts) {
if (opts === void 0) { opts = {}; }
var validate = opts.validate;
if (!fieldRefs[fieldId]) {
fieldRefs[fieldId] = {};
}
fieldRefs[fieldId][ref] = {
validate: validate,
};
};
var unsetFieldOptions = function (fieldId, ref) {
delete fieldRefs[fieldId][ref];
};
var initializeField = function (fieldId) {
var value = getFieldValue(fieldId);
var error = validateField(fieldId, value);
store.dispatch({
type: ActionTypes.INIT_FIELD,
field: fieldId,
error: error,
});
};
var destroyField = function (fieldId) {
store.dispatch({
type: ActionTypes.DESTROY_FIELD,
field: fieldId,
});
};
var changeFieldValue = function (fieldId, value) {
store.dispatch({
type: ActionTypes.CHANGE_FIELD_VALUE,
field: fieldId,
value: value,
error: validateField(fieldId, value),
});
return value;
};
var touchField = function (fieldId) {
store.dispatch({ type: ActionTypes.TOUCH_FIELD, field: fieldId });
};
// Form handlers
var resetFormValues = function (newInitialValues) {
if (newInitialValues) {
initialValues = newInitialValues;
}
store.dispatch({
type: ActionTypes.INIT_FORM_VALUES,
values: initialValues,
});
Object.keys(fieldRefs).forEach(function (fieldId) {
initializeField(fieldId);
});
};
var touchAll = function () {
Object.keys(fieldRefs).forEach(function (fieldId) {
touchField(fieldId);
});
};
var submitHandler = function (fn) {
return handleFormSubmit(function () { return __awaiter(_this, void 0, void 0, function () {
var state;
return __generator(this, function (_a) {
state = getFormState();
if (state.anyError) {
touchAll();
return [2 /*return*/];
}
return [2 /*return*/, fn(state.values)];
});
}); });
};
var id = Math.random()
.toString(36)
.substring(7);
var getId = function () { return id; };
return {
getId: getId,
subscribe: store.subscribe,
formActions: {
resetFormValues: resetFormValues,
submitHandler: submitHandler,
getFormState: getFormState,
},
fieldActions: {
setFieldOptions: setFieldOptions,
unsetFieldOptions: unsetFieldOptions,
initializeField: initializeField,
destroyField: destroyField,
changeFieldValue: changeFieldValue,
touchField: touchField,
getFieldState: getFieldState,
},
};
}
var emptyObj = {};
function useForm(opts) {
if (opts === void 0) { opts = { initialValues: emptyObj }; }
var form = react.useRef(null);
var initialValues = opts.initialValues;
function getForm() {
if (form.current === null) {
form.current = createForm(opts);
}
return form.current;
}
var prevInitialValues = react.useRef(initialValues);
react.useEffect(function () {
var resetFormValues = getForm().formActions.resetFormValues;
if (prevInitialValues.current !== initialValues) {
resetFormValues(initialValues);
}
prevInitialValues.current = initialValues;
}, [initialValues]);
return getForm();
}
var hasOwnProperty = Object.prototype.hasOwnProperty;
function shallowEqual(a, b) {
if (Object.is(a, b)) {
return true;
}
if (typeof a !== 'object' || !a || typeof b !== 'object' || !b) {
return false;
}
var keysA = Object.keys(a);
var keysB = Object.keys(b);
if (keysA.length !== keysB.length) {
return false;
}
// Test for A's keys different from B.
for (var i = 0; i < keysA.length; i++) {
var key = keysA[i];
if (!hasOwnProperty.call(b, keysA[i]) || a[key] !== b[key]) {
return false;
}
}
return true;
}
function useFormState(form, mapState) {
if (mapState === void 0) { mapState = function (s) { return s; }; }
if (!form) {
throw new Error('react-form-hooks requires the form instance ' +
'created using useForm() to be passed to useFormState as 1st argument');
}
var getFormState = form.formActions.getFormState;
var getMappedFormState = react.useCallback(function () { return mapState(getFormState()); }, [
mapState,
getFormState,
]);
var _a = react.useState(getMappedFormState), formState = _a[0], setFormState = _a[1];
var prevFormState = react.useRef(formState);
var updateState = react.useCallback(function () {
var newState = getMappedFormState();
if (!shallowEqual(newState, prevFormState.current)) {
setFormState(newState);
prevFormState.current = newState;
}
}, [getMappedFormState, prevFormState, setFormState]);
react.useEffect(function () {
updateState();
return form.subscribe(updateState);
}, [form, updateState]);
return formState;
}
var defaultMapState = function (s) { return s; };
function useFieldState(form, fieldId, mapState, opts) {
if (mapState === void 0) { mapState = defaultMapState; }
if (opts === void 0) { opts = {}; }
if (!form) {
throw new Error('react-form-hooks requires the form instance ' +
'created using useForm() to be passed to useFieldState as 1st argument');
}
if (!fieldId) {
throw new Error('react-form-hooks requires the id of the field ' +
'to be passed to useFieldState as 2nd argument');
}
var _a = form.fieldActions, setFieldOptions = _a.setFieldOptions, unsetFieldOptions = _a.unsetFieldOptions, initializeField = _a.initializeField, destroyField = _a.destroyField, getFieldState = _a.getFieldState;
var validate = opts.validate;
var getMappedFieldState = react.useCallback(function () { return mapState(getFieldState(fieldId)); }, [mapState, getFieldState, fieldId]);
var ref = react.useRef(null);
var getRef = function () {
if (!ref.current) {
ref.current = Symbol();
}
return ref.current;
};
var _b = react.useState(function () {
setFieldOptions(fieldId, getRef(), opts);
initializeField(fieldId);
return getMappedFieldState();
}), fieldState = _b[0], setFieldState = _b[1];
var prevFieldState = react.useRef(fieldState);
var updateState = react.useCallback(function () {
var newState = getMappedFieldState();
if (!shallowEqual(newState, prevFieldState.current)) {
setFieldState(newState);
prevFieldState.current = newState;
}
}, [getMappedFieldState, prevFieldState, setFieldState]);
react.useEffect(function () {
setFieldOptions(fieldId, getRef(), { validate: validate });
return function () { return unsetFieldOptions(fieldId, getRef()); };
}, [form, fieldId, ref, validate]);
react.useEffect(function () {
initializeField(fieldId);
return function () { return destroyField(fieldId); };
}, [form, fieldId]);
react.useEffect(function () {
updateState();
return form.subscribe(updateState);
}, [form, fieldId, updateState]);
return fieldState;
}
exports.useFieldState = useFieldState;
exports.useForm = useForm;
exports.useFormState = useFormState;
//# sourceMappingURL=index.js.map