@react-rx/form
Version:
rx form for react, use typescript for development
654 lines (571 loc) • 19.8 kB
JavaScript
import { __assign, __rest } from 'tslib';
import { isObject, isUndefined, isArray, reduce, isEqual, cloneDeep, omitBy, isBoolean, isFunction, isNumber, isEmpty, get, times, size, filter as filter$1, set, map as map$1, mapValues, keys, omit } from 'lodash';
import React, { createContext, useRef, useLayoutEffect, useContext, useState, useMemo } from 'react';
import { map, distinctUntilChanged, tap, filter } from 'rxjs/operators';
import isPropValid from '@emotion/is-prop-valid';
import { Subject, BehaviorSubject } from 'rxjs';
var isFieldDirty = function (value, defaultValue) {
return !isEqual(value, defaultValue);
};
var validateField = function (value, validate) {
if (isUndefined(validate)) {
return;
}
if (isArray(validate)) {
return combineValidators(validate)(value);
}
if (typeof validate === "function") {
return validate(value);
}
return;
};
var combineValidators = function (validators) {
return function (value) {
return reduce(validators, function (error, validator) {
return error || validateField(value, validator);
}, undefined);
};
};
var pickValue = function (evtOrValue) {
var isEvent = isObject(evtOrValue) && evtOrValue.target;
return isEvent ? evtOrValue.target.value : evtOrValue;
};
var FormContext = /*#__PURE__*/createContext({
subscribe: function (_) {},
dispatch: function (_) {},
subscribeFormAction: function (_) {},
updateFormValues: function (_) {},
getFormValues: function () {
return {};
},
getFormState: function () {
return {};
},
setErrors: function (_) {}
});
var Provider = FormContext.Provider,
Consumer = FormContext.Consumer;
var FieldActionTypes;
(function (FieldActionTypes) {
FieldActionTypes["register"] = "@@rx-form/field/REGISTER_FIELD";
FieldActionTypes["change"] = "@@rx-form/field/CHANGE";
FieldActionTypes["focus"] = "@@rx-form/field/FOCUS";
FieldActionTypes["blur"] = "@@rx-form/field/BLUR";
FieldActionTypes["destroy"] = "@@rx-form/field/DESTROY_FIELD";
})(FieldActionTypes || (FieldActionTypes = {}));
var FormActionTypes;
(function (FormActionTypes) {
FormActionTypes["startSubmit"] = "@@rx-form/form/START_SUBMIT";
FormActionTypes["endSubmit"] = "@@rx-form/form/END_SUBMIT";
})(FormActionTypes || (FormActionTypes = {}));
var log = function (_a) {
var action = _a.action,
prevState = _a.prevState,
nextState = _a.nextState;
if (process.env.NODE_ENV === "development") {
console.groupCollapsed(action.type + " " + new Date()); // use cloneDeep here in case state is a reference type
console.log("prevState", /*#__PURE__*/cloneDeep(prevState));
console.log("action", /*#__PURE__*/cloneDeep(action));
console.log("nextState", /*#__PURE__*/cloneDeep(nextState));
console.groupEnd();
}
};
var dropEmpty = function (values) {
return omitBy(values, isEmptyValue);
};
var isEmptyValue = function (value) {
if (isBoolean(value) || isFunction(value) || isNumber(value)) {
return false;
}
if (isArray(value) && value.length === 0) {
return true;
}
return isEmpty(value);
};
var pickInputPropsFromFieldProps = function (_a) {
var meta = _a.meta,
others = /*#__PURE__*/__rest(_a, ["meta"]);
return __assign( /*#__PURE__*/__assign({}, others), {
error: meta ? meta.error : undefined
});
};
var pickDOMAttrs = function (props) {
var o = {};
for (var k in props) {
if (props.hasOwnProperty(k) && isPropValid(k)) {
o[k] = props[k];
}
}
return o;
};
var useValueRef = function (value) {
var ref = /*#__PURE__*/useRef(null);
ref.current = value;
return ref;
};
var getFieldValue = function (_a) {
var defaultValue = _a.defaultValue,
formValues = _a.formValues,
prefixedName = _a.prefixedName;
var initialValue = /*#__PURE__*/get(formValues, prefixedName);
if (!isUndefined(initialValue)) {
return initialValue;
}
return defaultValue;
};
function Field(props) {
var _a = /*#__PURE__*/useContext(FormContext),
dispatch = _a.dispatch,
subscribe = _a.subscribe,
subscribeFormAction = _a.subscribeFormAction,
getFormValues = _a.getFormValues,
fieldPrefix = _a.fieldPrefix;
var prefixedName = "" + (fieldPrefix || "") + props.name;
var defaultFieldValue = /*#__PURE__*/getFieldValue({
defaultValue: props.defaultValue,
formValues: /*#__PURE__*/getFormValues(),
prefixedName: prefixedName
});
var _b = /*#__PURE__*/useState(defaultFieldValue),
fieldValue = _b[0],
setFieldValue = _b[1];
var _c = /*#__PURE__*/useState({}),
fieldMeta = _c[0],
setFieldMeta = _c[1];
var fieldValueRef = /*#__PURE__*/useValueRef(fieldValue);
var fieldMetaRef = /*#__PURE__*/useValueRef(fieldMeta);
var propsRef = /*#__PURE__*/useValueRef(props);
var _d = /*#__PURE__*/useMemo(function () {
var parseValue = function (value) {
var _a = propsRef.current || {},
parse = _a.parse,
normalize = _a.normalize;
if (parse && typeof parse === "function") {
value = /*#__PURE__*/parse(value);
}
if (normalize && typeof normalize === "function") {
value = /*#__PURE__*/normalize(value);
}
return value;
};
var triggerChange = function (evtOrValue, otherMeta) {
var validate = (propsRef.current || {}).validate;
var value = /*#__PURE__*/parseValue( /*#__PURE__*/pickValue(evtOrValue));
var dirty = /*#__PURE__*/isFieldDirty(value, defaultFieldValue); // use `defaultValue` here previously
var meta = /*#__PURE__*/__assign( /*#__PURE__*/__assign({}, otherMeta), {
error: /*#__PURE__*/validateField(value, validate),
dirty: dirty
});
dispatch({
name: prefixedName,
type: FieldActionTypes.change,
meta: meta,
payload: value
});
};
return {
registerField: function (_a) {
var value = _a.value,
meta = _a.meta;
dispatch({
name: prefixedName,
type: FieldActionTypes.register,
meta: meta,
payload: /*#__PURE__*/parseValue(value)
});
},
onFocus: function () {
dispatch({
name: prefixedName,
type: FieldActionTypes.focus,
meta: {
visited: true
}
});
},
triggerChange: triggerChange,
onChange: function (evtOrValue) {
return triggerChange(evtOrValue);
},
onBlur: function (evtOrValue) {
var value = /*#__PURE__*/pickValue(evtOrValue);
dispatch({
name: prefixedName,
type: FieldActionTypes.blur,
meta: {
visited: true,
touched: true
},
payload: /*#__PURE__*/parseValue(value)
});
},
formatValue: function (value) {
var _a = propsRef.current || {},
format = _a.format,
normalize = _a.normalize;
if (format && typeof format === "function") {
value = /*#__PURE__*/format(value);
}
if (normalize && typeof normalize === "function") {
value = /*#__PURE__*/normalize(value);
}
return value;
}
};
}, []),
registerField = _d.registerField,
triggerChange = _d.triggerChange,
onFocus = _d.onFocus,
onChange = _d.onChange,
onBlur = _d.onBlur,
formatValue = _d.formatValue;
useLayoutEffect(function () {
var formStateSubscription = null;
var formActionSubscription = null;
var onFormStateChange = function () {
var formStateObserver$ = new Subject();
formStateObserver$.pipe( /*#__PURE__*/map(function (_a) {
var fields = _a.fields,
values = _a.values;
return {
meta: fields[prefixedName],
value: /*#__PURE__*/get(values, prefixedName)
};
}), /*#__PURE__*/distinctUntilChanged(function (next, prev) {
return next.meta === prev.meta && prev.value === next.value;
}), /*#__PURE__*/tap(function (_a) {
var meta = _a.meta,
value = _a.value;
if (meta || value) {
setFieldValue(value);
setFieldMeta(meta);
}
})).subscribe();
formStateSubscription = /*#__PURE__*/subscribe(formStateObserver$);
};
var onFormActionChange = function () {
var formActionObserver$ = new Subject();
formActionObserver$.pipe( /*#__PURE__*/filter(function (_a) {
var type = _a.type;
return type === FormActionTypes.startSubmit;
}), /*#__PURE__*/tap(function () {
return triggerChange(fieldValueRef.current, /*#__PURE__*/__assign( /*#__PURE__*/__assign({}, fieldMetaRef.current), {
visited: true,
touched: true
}));
})).subscribe();
formActionSubscription = /*#__PURE__*/subscribeFormAction(formActionObserver$);
}; // should register observers before register field, otherwise the last field will lost field state
// this cause the last field render even if I only changed the first field
onFormStateChange();
onFormActionChange();
registerField({
value: fieldValueRef.current,
meta: fieldMetaRef.current
});
return function () {
dispatch({
name: prefixedName,
type: FieldActionTypes.destroy,
meta: {
destroyValueOnUnmount: (propsRef.current || {}).destroyValueOnUnmount
}
});
if (formStateSubscription) {
formStateSubscription.unsubscribe();
}
if (formActionSubscription) {
formActionSubscription.unsubscribe();
}
};
}, []);
return props.children({
value: /*#__PURE__*/formatValue(fieldValueRef.current),
meta: fieldMeta,
name: prefixedName,
onChange: onChange,
onFocus: onFocus,
onBlur: onBlur
});
}
function FieldArrayCore(props) {
var _a = /*#__PURE__*/useContext(FormContext),
getFormValues = _a.getFormValues,
updateFormValues = _a.updateFormValues;
var getFieldsByIdx = function () {
return map$1( /*#__PURE__*/get( /*#__PURE__*/getFormValues(), props.name), function (_, idx) {
return "[" + idx + "]";
});
};
var defaultFields = /*#__PURE__*/getFieldsByIdx();
var _b = /*#__PURE__*/useState(defaultFields),
fields = _b[0],
setFields = _b[1];
var _c = /*#__PURE__*/useMemo(function () {
return {
remove: function (idx) {
var formValues = /*#__PURE__*/getFormValues();
var newFieldArrayValues = /*#__PURE__*/filter$1( /*#__PURE__*/get(formValues, props.name), function (_, n) {
return n !== idx;
});
var nextFormValues = /*#__PURE__*/set(formValues, props.name, newFieldArrayValues);
updateFormValues(nextFormValues);
setFields(getFieldsByIdx);
},
add: function () {
var formValues = /*#__PURE__*/getFormValues(); // undefined 只是为了增加一个节点,让 Field 自己去渲染,然后 Field 渲染时才会把 key 注册给 form,得到新的包含 field 的 form value
var fieldValues = /*#__PURE__*/get(formValues, props.name, []).concat(undefined);
var nextFormValues = /*#__PURE__*/set(formValues, props.name, fieldValues);
updateFormValues(nextFormValues);
setFields(getFieldsByIdx);
},
each: function (mapper) {
var fieldValues = /*#__PURE__*/get( /*#__PURE__*/getFormValues(), props.name);
return map$1(fieldValues, function (_, idx) {
var name = "[" + idx + "]";
return mapper(name, idx);
});
}
};
}, []),
add = _c.add,
each = _c.each,
remove = _c.remove;
useLayoutEffect(function () {
var fieldArrayValues = /*#__PURE__*/get( /*#__PURE__*/getFormValues(), props.name);
if (props.initLength) {
times(props.initLength - size(fieldArrayValues), add);
}
return function () {};
}, []);
return props.children({
fields: fields,
add: add,
each: each,
remove: function (idx) {
return remove(idx);
}
});
}
var FieldArray = function (props) {
var formContext = /*#__PURE__*/useContext(FormContext);
var name = "" + (formContext.fieldPrefix || "") + props.name;
return React.createElement(Provider, {
value: /*#__PURE__*/__assign( /*#__PURE__*/__assign({}, formContext), {
fieldPrefix: name
})
}, /*#__PURE__*/React.createElement(FieldArrayCore, /*#__PURE__*/__assign({}, props, {
name: name
})));
};
var FormSection = function (_a) {
var name = _a.name,
children = _a.children;
var formContext = /*#__PURE__*/useContext(FormContext);
return React.createElement(Provider, {
value: /*#__PURE__*/__assign( /*#__PURE__*/__assign({}, formContext), {
fieldPrefix: "" + (formContext.fieldPrefix || "") + name + "."
})
}, children);
};
var formUpdateField = function (state, action) {
var _a;
var fields = state.fields,
values = state.values;
var payload = action.payload,
_b = action.meta,
meta = _b === void 0 ? {} : _b,
name = action.name;
return {
fields: /*#__PURE__*/__assign( /*#__PURE__*/__assign({}, fields), (_a = {}, _a[name] = /*#__PURE__*/__assign( /*#__PURE__*/__assign({}, fields[name]), meta), _a)),
values: /*#__PURE__*/set( /*#__PURE__*/__assign({}, values), name, payload) // Notice: _.set will mutate object,
};
};
var formFocusField = function (state, action) {
var _a;
var fields = state.fields,
values = state.values;
var name = action.name,
meta = action.meta;
return {
values: values,
fields: /*#__PURE__*/__assign( /*#__PURE__*/__assign({}, fields), (_a = {}, _a[name] = /*#__PURE__*/__assign({}, meta), _a))
};
};
var formRemoveField = function (state, action) {
if (action.meta && action.meta.destroyValueOnUnmount) {
return {
fields: /*#__PURE__*/omit(state.fields, action.name),
values: /*#__PURE__*/omit(state.values, action.name)
};
} // keep values when field destroy for cross page form
// eg: in PageA, switch to PageB, should keep PageA fields values.
return __assign( /*#__PURE__*/__assign({}, state), {
fields: /*#__PURE__*/omit(state.fields, action.name)
});
};
var formUpdateValues = function (formState) {
return function (formValues) {
return {
fields: formState.fields,
values: formValues
};
};
};
var formSetErrors = function (errors, fields) {
if (isEmpty(errors)) {
return mapValues(fields, function (field) {
return __assign( /*#__PURE__*/__assign({}, field), {
error: undefined
});
});
}
var temp = {};
keys(errors).forEach(function (name) {
if (fields[name]) {
temp[name] = /*#__PURE__*/__assign( /*#__PURE__*/__assign({}, fields[name]), {
error: errors[name]
});
}
});
return __assign( /*#__PURE__*/__assign({}, fields), temp);
};
var isFormValid = function (fields) {
return reduce(fields, function (result, item) {
return result && item && !item.error;
}, true);
};
function RxForm(props) {
var _a = /*#__PURE__*/useMemo(function () {
var formStateSubject$ = new BehaviorSubject({
fields: {},
values: cloneDeep(props.initialValues) || {}
});
var formActionSubject$ = new Subject();
var dispatch = function (action) {
var prevState = /*#__PURE__*/(process.env.NODE_ENV === "development" ? cloneDeep : function (v) {
return v;
})( /*#__PURE__*/formStateSubject$.getValue());
switch (action.type) {
case FieldActionTypes.register:
case FieldActionTypes.blur:
case FieldActionTypes.change:
{
var nextFormState = /*#__PURE__*/formUpdateField( /*#__PURE__*/formStateSubject$.getValue(), action);
formStateSubject$.next(nextFormState);
break;
}
case FieldActionTypes.focus:
{
var nextFormState = /*#__PURE__*/formFocusField( /*#__PURE__*/formStateSubject$.getValue(), action);
formStateSubject$.next(nextFormState);
break;
}
case FieldActionTypes.destroy:
{
var nextFormState = /*#__PURE__*/formRemoveField( /*#__PURE__*/formStateSubject$.getValue(), action);
formStateSubject$.next(nextFormState);
break;
}
case FormActionTypes.startSubmit:
{
notifyFormActionChange(action);
break;
}
}
var nextState = /*#__PURE__*/(process.env.NODE_ENV === "development" ? cloneDeep : function (v) {
return v;
})( /*#__PURE__*/formStateSubject$.getValue());
log({
action: action,
prevState: prevState,
nextState: nextState
});
};
var notifyFormActionChange = function (action) {
formActionSubject$.next(action);
};
var setErrors = function (errors) {
var fromState = /*#__PURE__*/formStateSubject$.getValue();
var nextFormState = {
values: fromState.values,
fields: /*#__PURE__*/formSetErrors(errors, fromState.fields)
};
formStateSubject$.next(nextFormState);
};
var getFormValues = function () {
return formStateSubject$.getValue().values;
};
var getFormState = function () {
return formStateSubject$.getValue();
};
return {
subscribe: function (observer) {
return formStateSubject$.subscribe(observer);
},
dispatch: dispatch,
subscribeFormAction: function (observer) {
return formActionSubject$.subscribe(observer);
},
updateFormValues: function (formValues) {
formUpdateValues( /*#__PURE__*/formStateSubject$.getValue())(formValues);
},
getFormValues: getFormValues,
getFormState: getFormState,
setErrors: setErrors,
handleSubmit: function (onSubmit) {
return function (evt) {
evt.preventDefault();
dispatch({
type: FormActionTypes.startSubmit
});
if (!isFormValid(formStateSubject$.getValue().fields)) {
return;
}
onSubmit( /*#__PURE__*/getFormValues(), setErrors);
dispatch({
type: FormActionTypes.endSubmit
});
};
}
};
}, []),
handleSubmit = _a.handleSubmit,
formCtxValue = /*#__PURE__*/__rest(_a, ["handleSubmit"]);
return React.createElement(Provider, {
value: formCtxValue
}, /*#__PURE__*/props.children({
handleSubmit: handleSubmit
}));
}
function FormValues(props) {
var _a = /*#__PURE__*/useContext(FormContext),
updateFormValues = _a.updateFormValues,
getFormValues = _a.getFormValues,
subscribe = _a.subscribe;
var defaultFormValues = /*#__PURE__*/getFormValues();
var _b = /*#__PURE__*/useState(defaultFormValues),
formValues = _b[0],
setFormValues = _b[1];
useLayoutEffect(function () {
var subscription = null;
var formStateObserver$ = new Subject();
formStateObserver$.pipe( /*#__PURE__*/map(function (formState) {
return __assign({}, formState.values);
}), /*#__PURE__*/distinctUntilChanged(isEqual), /*#__PURE__*/tap(function (values) {
return setFormValues(values);
})).subscribe();
subscription = /*#__PURE__*/subscribe(formStateObserver$);
return function () {
if (subscription) {
subscription.unsubscribe();
subscription = null;
}
};
}, []);
return props.children({
formValues: formValues,
updateFormValues: updateFormValues
});
}
export { Field, FieldActionTypes, FieldArray, FormActionTypes, FormSection, FormValues, RxForm, Consumer as WithForm, dropEmpty, isEmptyValue, log, pickDOMAttrs, pickInputPropsFromFieldProps, useValueRef };