UNPKG

@react-rx/form

Version:

rx form for react, use typescript for development

654 lines (571 loc) 19.8 kB
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 };