This commit is contained in:
2026-05-25 17:08:18 +08:00
parent df501f6151
commit f1259b71b5
9178 changed files with 1626125 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
import * as React from 'react';
import type { EventArgs, FormInstance, InternalFormInstance, InternalNamePath, Meta, NamePath, Rule, Store, StoreValue } from './interface';
export type ShouldUpdate<Values = any> = boolean | ((prevValues: Values, nextValues: Values, info: {
source?: string;
}) => boolean);
interface ChildProps {
[name: string]: any;
}
export type MetaEvent = Meta & {
destroy?: boolean;
};
export interface InternalFieldProps<Values = any> {
children?: React.ReactElement | ((control: ChildProps, meta: Meta, form: FormInstance<Values>) => React.ReactNode);
/**
* Set up `dependencies` field.
* When dependencies field update and current field is touched,
* will trigger validate rules and render.
*/
dependencies?: NamePath[];
getValueFromEvent?: (...args: EventArgs) => StoreValue;
name?: InternalNamePath;
normalize?: (value: StoreValue, prevValue: StoreValue, allValues: Store) => StoreValue;
rules?: Rule[];
shouldUpdate?: ShouldUpdate<Values>;
trigger?: string;
validateTrigger?: string | string[] | false;
/**
* Trigger will after configured milliseconds.
*/
validateDebounce?: number;
validateFirst?: boolean | 'parallel';
valuePropName?: string;
getValueProps?: (value: StoreValue) => Record<string, unknown>;
messageVariables?: Record<string, string>;
initialValue?: any;
onReset?: () => void;
onMetaChange?: (meta: MetaEvent) => void;
preserve?: boolean;
/** @private Passed by Form.List props. Do not use since it will break by path check. */
isListField?: boolean;
/** @private Passed by Form.List props. Do not use since it will break by path check. */
isList?: boolean;
/** @private Pass context as prop instead of context api
* since class component can not get context in constructor */
fieldContext?: InternalFormInstance;
}
export interface FieldProps<Values = any> extends Omit<InternalFieldProps<Values>, 'name' | 'fieldContext'> {
name?: NamePath<Values>;
}
export interface FieldState {
resetCount: number;
}
declare function WrapperField<Values = any>({ name, ...restProps }: FieldProps<Values>): React.JSX.Element;
export default WrapperField;
+605
View File
@@ -0,0 +1,605 @@
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import toChildrenArray from "@rc-component/util/es/Children/toArray";
import isEqual from "@rc-component/util/es/isEqual";
import warning from "@rc-component/util/es/warning";
import * as React from 'react';
import FieldContext, { HOOK_MARK } from "./FieldContext";
import ListContext from "./ListContext";
import { toArray } from "./utils/typeUtil";
import { validateRules } from "./utils/validateUtil";
import { containsNamePath, defaultGetValueFromEvent, getNamePath, getValue } from "./utils/valueUtil";
import delayFrame from "./utils/delayUtil";
const EMPTY_ERRORS = [];
const EMPTY_WARNINGS = [];
function requireUpdate(shouldUpdate, prev, next, prevValue, nextValue, info) {
if (typeof shouldUpdate === 'function') {
return shouldUpdate(prev, next, 'source' in info ? {
source: info.source
} : {});
}
return prevValue !== nextValue;
}
// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
// We use Class instead of Hooks here since it will cost much code by using Hooks.
class Field extends React.Component {
static contextType = FieldContext;
state = {
resetCount: 0
};
cancelRegisterFunc = null;
mounted = false;
/**
* Follow state should not management in State since it will async update by React.
* This makes first render of form can not get correct state value.
*/
touched = false;
/**
* Mark when touched & validated. Currently only used for `dependencies`.
* Note that we do not think field with `initialValue` is dirty
* but this will be by `isFieldDirty` func.
*/
dirty = false;
validatePromise;
prevValidating;
errors = EMPTY_ERRORS;
warnings = EMPTY_WARNINGS;
// ============================== Subscriptions ==============================
constructor(props) {
super(props);
// Register on init
if (props.fieldContext) {
const {
getInternalHooks
} = props.fieldContext;
const {
initEntityValue
} = getInternalHooks(HOOK_MARK);
initEntityValue(this);
}
}
componentDidMount() {
const {
shouldUpdate,
fieldContext
} = this.props;
this.mounted = true;
// Register on init
if (fieldContext) {
const {
getInternalHooks
} = fieldContext;
const {
registerField
} = getInternalHooks(HOOK_MARK);
this.cancelRegisterFunc = registerField(this);
}
// One more render for component in case fields not ready
if (shouldUpdate === true) {
this.reRender();
}
}
componentWillUnmount() {
this.cancelRegister();
this.triggerMetaEvent(true);
this.mounted = false;
}
cancelRegister = () => {
const {
preserve,
isListField,
name
} = this.props;
if (this.cancelRegisterFunc) {
this.cancelRegisterFunc(isListField, preserve, getNamePath(name));
}
this.cancelRegisterFunc = null;
};
// ================================== Utils ==================================
getNamePath = () => {
const {
name,
fieldContext
} = this.props;
const {
prefixName = []
} = fieldContext;
return name !== undefined ? [...prefixName, ...name] : [];
};
getRules = () => {
const {
rules = [],
fieldContext
} = this.props;
return rules.map(rule => {
if (typeof rule === 'function') {
return rule(fieldContext);
}
return rule;
});
};
reRender() {
if (!this.mounted) return;
this.forceUpdate();
}
refresh = () => {
if (!this.mounted) return;
/**
* Clean up current node.
*/
this.setState(({
resetCount
}) => ({
resetCount: resetCount + 1
}));
};
// Event should only trigger when meta changed
metaCache = null;
triggerMetaEvent = destroy => {
const {
onMetaChange
} = this.props;
if (onMetaChange) {
const meta = {
...this.getMeta(),
destroy
};
if (!isEqual(this.metaCache, meta)) {
onMetaChange(meta);
}
this.metaCache = meta;
} else {
this.metaCache = null;
}
};
// ========================= Field Entity Interfaces =========================
// Trigger by store update. Check if need update the component
onStoreChange = (prevStore, namePathList, info) => {
const {
shouldUpdate,
dependencies = [],
onReset
} = this.props;
const {
store
} = info;
const namePath = this.getNamePath();
const prevValue = this.getValue(prevStore);
const curValue = this.getValue(store);
const namePathMatch = namePathList && containsNamePath(namePathList, namePath);
// `setFieldsValue` is a quick access to update related status
if (info.type === 'valueUpdate' && info.source === 'external' && !isEqual(prevValue, curValue)) {
this.touched = true;
this.dirty = true;
this.validatePromise = null;
this.errors = EMPTY_ERRORS;
this.warnings = EMPTY_WARNINGS;
this.triggerMetaEvent();
}
switch (info.type) {
case 'reset':
if (!namePathList || namePathMatch) {
// Clean up state
this.touched = false;
this.dirty = false;
this.validatePromise = undefined;
this.errors = EMPTY_ERRORS;
this.warnings = EMPTY_WARNINGS;
this.triggerMetaEvent();
onReset?.();
this.refresh();
return;
}
break;
/**
* In case field with `preserve = false` nest deps like:
* - A = 1 => show B
* - B = 1 => show C
* - Reset A, need clean B, C
*/
case 'remove':
{
if (shouldUpdate && requireUpdate(shouldUpdate, prevStore, store, prevValue, curValue, info)) {
this.reRender();
return;
}
break;
}
case 'setField':
{
const {
data
} = info;
if (namePathMatch) {
if ('touched' in data) {
this.touched = data.touched;
}
if ('validating' in data && !('originRCField' in data)) {
this.validatePromise = data.validating ? Promise.resolve([]) : null;
}
if ('errors' in data) {
this.errors = data.errors || EMPTY_ERRORS;
}
if ('warnings' in data) {
this.warnings = data.warnings || EMPTY_WARNINGS;
}
this.dirty = true;
this.triggerMetaEvent();
this.reRender();
return;
} else if ('value' in data && containsNamePath(namePathList, namePath, true)) {
// Contains path with value should also check
this.reRender();
return;
}
// Handle update by `setField` with `shouldUpdate`
if (shouldUpdate && !namePath.length && requireUpdate(shouldUpdate, prevStore, store, prevValue, curValue, info)) {
this.reRender();
return;
}
break;
}
case 'dependenciesUpdate':
{
/**
* Trigger when marked `dependencies` updated. Related fields will all update
*/
const dependencyList = dependencies.map(getNamePath);
// No need for `namePathMath` check and `shouldUpdate` check, since `valueUpdate` will be
// emitted earlier and they will work there
// If set it may cause unnecessary twice rerendering
if (dependencyList.some(dependency => containsNamePath(info.relatedFields, dependency))) {
this.reRender();
return;
}
break;
}
default:
// 1. If `namePath` exists in `namePathList`, means it's related value and should update
// For example <List name="list"><Field name={['list', 0]}></List>
// If `namePathList` is [['list']] (List value update), Field should be updated
// If `namePathList` is [['list', 0]] (Field value update), List shouldn't be updated
// 2.
// 2.1 If `dependencies` is set, `name` is not set and `shouldUpdate` is not set,
// don't use `shouldUpdate`. `dependencies` is view as a shortcut if `shouldUpdate`
// is not provided
// 2.2 If `shouldUpdate` provided, use customize logic to update the field
// else to check if value changed
if (namePathMatch || (!dependencies.length || namePath.length || shouldUpdate) && requireUpdate(shouldUpdate, prevStore, store, prevValue, curValue, info)) {
this.reRender();
return;
}
break;
}
if (shouldUpdate === true) {
this.reRender();
}
};
validateRules = options => {
// We should fixed namePath & value to avoid developer change then by form function
const namePath = this.getNamePath();
const currentValue = this.getValue();
const {
triggerName,
validateOnly = false,
delayFrame: showDelayFrame
} = options || {};
// Force change to async to avoid rule OOD under renderProps field
const rootPromise = Promise.resolve().then(async () => {
if (!this.mounted) {
return [];
}
const {
validateFirst = false,
messageVariables,
validateDebounce
} = this.props;
// Should wait for the frame render,
// since developer may `useWatch` value in the rules.
if (showDelayFrame) {
await delayFrame();
}
// Start validate
let filteredRules = this.getRules();
if (triggerName) {
filteredRules = filteredRules.filter(rule => rule).filter(rule => {
const {
validateTrigger
} = rule;
if (!validateTrigger) {
return true;
}
const triggerList = toArray(validateTrigger);
return triggerList.includes(triggerName);
});
}
// Wait for debounce. Skip if no `triggerName` since its from `validateFields / submit`
if (validateDebounce && triggerName) {
await new Promise(resolve => {
setTimeout(resolve, validateDebounce);
});
// Skip since out of date
if (this.validatePromise !== rootPromise) {
return [];
}
}
const promise = validateRules(namePath, currentValue, filteredRules, options, validateFirst, messageVariables);
promise.catch(e => e).then((ruleErrors = EMPTY_ERRORS) => {
if (this.validatePromise === rootPromise) {
this.validatePromise = null;
// Get errors & warnings
const nextErrors = [];
const nextWarnings = [];
ruleErrors.forEach?.(({
rule: {
warningOnly
},
errors = EMPTY_ERRORS
}) => {
if (warningOnly) {
nextWarnings.push(...errors);
} else {
nextErrors.push(...errors);
}
});
this.errors = nextErrors;
this.warnings = nextWarnings;
this.triggerMetaEvent();
this.reRender();
}
});
return promise;
});
if (validateOnly) {
return rootPromise;
}
this.validatePromise = rootPromise;
this.dirty = true;
this.errors = EMPTY_ERRORS;
this.warnings = EMPTY_WARNINGS;
this.triggerMetaEvent();
// Force trigger re-render since we need sync renderProps with new meta
this.reRender();
return rootPromise;
};
isFieldValidating = () => !!this.validatePromise;
isFieldTouched = () => this.touched;
isFieldDirty = () => {
// Touched or validate or has initialValue
if (this.dirty || this.props.initialValue !== undefined) {
return true;
}
// Form set initialValue
const {
fieldContext
} = this.props;
const {
getInitialValue
} = fieldContext.getInternalHooks(HOOK_MARK);
if (getInitialValue(this.getNamePath()) !== undefined) {
return true;
}
return false;
};
getErrors = () => this.errors;
getWarnings = () => this.warnings;
isListField = () => this.props.isListField;
isList = () => this.props.isList;
isPreserve = () => this.props.preserve;
// ============================= Child Component =============================
getMeta = () => {
// Make error & validating in cache to save perf
this.prevValidating = this.isFieldValidating();
const meta = {
touched: this.isFieldTouched(),
validating: this.prevValidating,
errors: this.errors,
warnings: this.warnings,
name: this.getNamePath(),
validated: this.validatePromise === null
};
return meta;
};
// Only return validate child node. If invalidate, will do nothing about field.
getOnlyChild = children => {
// Support render props
if (typeof children === 'function') {
const meta = this.getMeta();
return {
...this.getOnlyChild(children(this.getControlled(), meta, this.props.fieldContext)),
isFunction: true
};
}
// Filed element only
const childList = toChildrenArray(children);
if (childList.length !== 1 || ! /*#__PURE__*/React.isValidElement(childList[0])) {
return {
child: childList,
isFunction: false
};
}
return {
child: childList[0],
isFunction: false
};
};
// ============================== Field Control ==============================
getValue = store => {
const {
getFieldsValue
} = this.props.fieldContext;
const namePath = this.getNamePath();
return getValue(store || getFieldsValue(true), namePath);
};
getControlled = (childProps = {}) => {
const {
name,
trigger = 'onChange',
validateTrigger,
getValueFromEvent,
normalize,
valuePropName = 'value',
getValueProps,
fieldContext
} = this.props;
const mergedValidateTrigger = validateTrigger !== undefined ? validateTrigger : fieldContext.validateTrigger;
const namePath = this.getNamePath();
const {
getInternalHooks,
getFieldsValue
} = fieldContext;
const {
dispatch
} = getInternalHooks(HOOK_MARK);
const value = this.getValue();
const mergedGetValueProps = getValueProps || (val => ({
[valuePropName]: val
}));
const originTriggerFunc = childProps[trigger];
const valueProps = name !== undefined ? mergedGetValueProps(value) : {};
// warning when prop value is function
if (process.env.NODE_ENV !== 'production' && valueProps) {
Object.keys(valueProps).forEach(key => {
warning(typeof valueProps[key] !== 'function', `It's not recommended to generate dynamic function prop by \`getValueProps\`. Please pass it to child component directly (prop: ${key})`);
});
}
const control = {
...childProps,
...valueProps
};
// Add trigger
control[trigger] = (...args) => {
// Mark as touched
this.touched = true;
this.dirty = true;
this.triggerMetaEvent();
let newValue;
if (getValueFromEvent) {
newValue = getValueFromEvent(...args);
} else {
newValue = defaultGetValueFromEvent(valuePropName, ...args);
}
if (normalize) {
newValue = normalize(newValue, value, getFieldsValue(true));
}
if (newValue !== value) {
dispatch({
type: 'updateValue',
namePath,
value: newValue
});
}
if (originTriggerFunc) {
originTriggerFunc(...args);
}
};
// Add validateTrigger
const validateTriggerList = toArray(mergedValidateTrigger || []);
validateTriggerList.forEach(triggerName => {
// Wrap additional function of component, so that we can get latest value from store
const originTrigger = control[triggerName];
control[triggerName] = (...args) => {
if (originTrigger) {
originTrigger(...args);
}
// Always use latest rules
const {
rules
} = this.props;
if (rules && rules.length) {
// We dispatch validate to root,
// since it will update related data with other field with same name
dispatch({
type: 'validateField',
namePath,
triggerName
});
}
};
});
return control;
};
render() {
const {
resetCount
} = this.state;
const {
children
} = this.props;
const {
child,
isFunction
} = this.getOnlyChild(children);
// Not need to `cloneElement` since user can handle this in render function self
let returnChildNode;
if (isFunction) {
returnChildNode = child;
} else if ( /*#__PURE__*/React.isValidElement(child)) {
returnChildNode = /*#__PURE__*/React.cloneElement(child, this.getControlled(child.props));
} else {
warning(!child, '`children` of Field is not validate ReactElement.');
returnChildNode = child;
}
return /*#__PURE__*/React.createElement(React.Fragment, {
key: resetCount
}, returnChildNode);
}
}
function WrapperField({
name,
...restProps
}) {
const fieldContext = React.useContext(FieldContext);
const listContext = React.useContext(ListContext);
const namePath = name !== undefined ? getNamePath(name) : undefined;
const isMergedListField = restProps.isListField ?? !!listContext;
let key = 'keep';
if (!isMergedListField) {
key = `_${(namePath || []).join('_')}`;
}
// Warning if it's a directly list field.
// We can still support multiple level field preserve.
if (process.env.NODE_ENV !== 'production' && restProps.preserve === false && isMergedListField && namePath.length <= 1) {
warning(false, '`preserve` should not apply on Form.List fields.');
}
return /*#__PURE__*/React.createElement(Field, _extends({
key: key,
name: namePath,
isListField: isMergedListField
}, restProps, {
fieldContext: fieldContext
}));
}
export default WrapperField;
+5
View File
@@ -0,0 +1,5 @@
import * as React from 'react';
import type { InternalFormInstance } from './interface';
export declare const HOOK_MARK = "RC_FORM_INTERNAL_HOOKS";
declare const Context: React.Context<InternalFormInstance>;
export default Context;
+43
View File
@@ -0,0 +1,43 @@
import warning from "@rc-component/util/es/warning";
import * as React from 'react';
export const HOOK_MARK = 'RC_FORM_INTERNAL_HOOKS';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const warningFunc = () => {
warning(false, 'Can not find FormContext. Please make sure you wrap Field under Form.');
};
const Context = /*#__PURE__*/React.createContext({
getFieldValue: warningFunc,
getFieldsValue: warningFunc,
getFieldError: warningFunc,
getFieldWarning: warningFunc,
getFieldsError: warningFunc,
isFieldsTouched: warningFunc,
isFieldTouched: warningFunc,
isFieldValidating: warningFunc,
isFieldsValidating: warningFunc,
resetFields: warningFunc,
setFields: warningFunc,
setFieldValue: warningFunc,
setFieldsValue: warningFunc,
validateFields: warningFunc,
submit: warningFunc,
getInternalHooks: () => {
warningFunc();
return {
dispatch: warningFunc,
initEntityValue: warningFunc,
registerField: warningFunc,
useSubscribe: warningFunc,
setInitialValues: warningFunc,
destroyForm: warningFunc,
setCallbacks: warningFunc,
registerWatch: warningFunc,
getFields: warningFunc,
setValidateMessages: warningFunc,
setPreserve: warningFunc,
getInitialValue: warningFunc
};
}
});
export default Context;
+22
View File
@@ -0,0 +1,22 @@
import * as React from 'react';
import type { Store, FormInstance, FieldData, ValidateMessages, Callbacks, FormRef } from './interface';
type BaseFormProps = Omit<React.FormHTMLAttributes<HTMLFormElement>, 'onSubmit' | 'children'>;
type RenderProps = (values: Store, form: FormInstance) => React.ReactNode;
export interface FormProps<Values = any> extends BaseFormProps {
initialValues?: Store;
form?: FormInstance<Values>;
children?: RenderProps | React.ReactNode;
component?: false | string | React.FC<any> | React.ComponentClass<any>;
fields?: FieldData[];
name?: string;
validateMessages?: ValidateMessages;
onValuesChange?: Callbacks<Values>['onValuesChange'];
onFieldsChange?: Callbacks<Values>['onFieldsChange'];
onFinish?: Callbacks<Values>['onFinish'];
onFinishFailed?: Callbacks<Values>['onFinishFailed'];
validateTrigger?: string | string[] | false;
preserve?: boolean;
clearOnDestroy?: boolean;
}
declare const Form: React.ForwardRefRenderFunction<FormRef, FormProps>;
export default Form;
+138
View File
@@ -0,0 +1,138 @@
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import * as React from 'react';
import useForm from "./hooks/useForm";
import FieldContext, { HOOK_MARK } from "./FieldContext";
import FormContext from "./FormContext";
import { isSimilar } from "./utils/valueUtil";
import ListContext from "./ListContext";
const Form = ({
name,
initialValues,
fields,
form,
preserve,
children,
component: Component = 'form',
validateMessages,
validateTrigger = 'onChange',
onValuesChange,
onFieldsChange,
onFinish,
onFinishFailed,
clearOnDestroy,
...restProps
}, ref) => {
const nativeElementRef = React.useRef(null);
const formContext = React.useContext(FormContext);
// We customize handle event since Context will makes all the consumer re-render:
// https://reactjs.org/docs/context.html#contextprovider
const [formInstance] = useForm(form);
const {
useSubscribe,
setInitialValues,
setCallbacks,
setValidateMessages,
setPreserve,
destroyForm
} = formInstance.getInternalHooks(HOOK_MARK);
// Pass ref with form instance
React.useImperativeHandle(ref, () => ({
...formInstance,
nativeElement: nativeElementRef.current
}));
// Register form into Context
React.useEffect(() => {
formContext.registerForm(name, formInstance);
return () => {
formContext.unregisterForm(name);
};
}, [formContext, formInstance, name]);
// Pass props to store
setValidateMessages({
...formContext.validateMessages,
...validateMessages
});
setCallbacks({
onValuesChange,
onFieldsChange: (changedFields, ...rest) => {
formContext.triggerFormChange(name, changedFields);
if (onFieldsChange) {
onFieldsChange(changedFields, ...rest);
}
},
onFinish: values => {
formContext.triggerFormFinish(name, values);
if (onFinish) {
onFinish(values);
}
},
onFinishFailed
});
setPreserve(preserve);
// Set initial value, init store value when first mount
const mountRef = React.useRef(null);
setInitialValues(initialValues, !mountRef.current);
if (!mountRef.current) {
mountRef.current = true;
}
// ========================== Unmount ===========================
React.useEffect(() => () => destroyForm(clearOnDestroy),
// eslint-disable-next-line react-hooks/exhaustive-deps
[]);
// Prepare children by `children` type
let childrenNode;
const childrenRenderProps = typeof children === 'function';
if (childrenRenderProps) {
const values = formInstance.getFieldsValue(true);
childrenNode = children(values, formInstance);
} else {
childrenNode = children;
}
// Not use subscribe when using render props
useSubscribe(!childrenRenderProps);
// Listen if fields provided. We use ref to save prev data here to avoid additional render
const prevFieldsRef = React.useRef(null);
React.useEffect(() => {
if (!isSimilar(prevFieldsRef.current || [], fields || [])) {
formInstance.setFields(fields || []);
}
prevFieldsRef.current = fields;
}, [fields, formInstance]);
// =========================== Render ===========================
const formContextValue = React.useMemo(() => ({
...formInstance,
validateTrigger
}), [formInstance, validateTrigger]);
const wrapperNode = /*#__PURE__*/React.createElement(ListContext.Provider, {
value: null
}, /*#__PURE__*/React.createElement(FieldContext.Provider, {
value: formContextValue
}, childrenNode));
if (Component === false) {
return wrapperNode;
}
return /*#__PURE__*/React.createElement(Component, _extends({}, restProps, {
ref: nativeElementRef,
onSubmit: event => {
event.preventDefault();
event.stopPropagation();
formInstance.submit();
},
onReset: event => {
event.preventDefault();
formInstance.resetFields();
restProps.onReset?.(event);
}
}), wrapperNode);
};
export default Form;
+27
View File
@@ -0,0 +1,27 @@
import * as React from 'react';
import type { ValidateMessages, FormInstance, FieldData, Store } from './interface';
export type Forms = Record<string, FormInstance>;
export interface FormChangeInfo {
changedFields: FieldData[];
forms: Forms;
}
export interface FormFinishInfo {
values: Store;
forms: Forms;
}
export interface FormProviderProps {
validateMessages?: ValidateMessages;
onFormChange?: (name: string, info: FormChangeInfo) => void;
onFormFinish?: (name: string, info: FormFinishInfo) => void;
children?: React.ReactNode;
}
export interface FormContextProps extends FormProviderProps {
triggerFormChange: (name: string, changedFields: FieldData[]) => void;
triggerFormFinish: (name: string, values: Store) => void;
registerForm: (name: string, form: FormInstance) => void;
unregisterForm: (name: string) => void;
}
declare const FormContext: React.Context<FormContextProps>;
declare const FormProvider: React.FunctionComponent<FormProviderProps>;
export { FormProvider };
export default FormContext;
+65
View File
@@ -0,0 +1,65 @@
import * as React from 'react';
const FormContext = /*#__PURE__*/React.createContext({
triggerFormChange: () => {},
triggerFormFinish: () => {},
registerForm: () => {},
unregisterForm: () => {}
});
const FormProvider = ({
validateMessages,
onFormChange,
onFormFinish,
children
}) => {
const formContext = React.useContext(FormContext);
const formsRef = React.useRef({});
return /*#__PURE__*/React.createElement(FormContext.Provider, {
value: {
...formContext,
validateMessages: {
...formContext.validateMessages,
...validateMessages
},
// =========================================================
// = Global Form Control =
// =========================================================
triggerFormChange: (name, changedFields) => {
if (onFormChange) {
onFormChange(name, {
changedFields,
forms: formsRef.current
});
}
formContext.triggerFormChange(name, changedFields);
},
triggerFormFinish: (name, values) => {
if (onFormFinish) {
onFormFinish(name, {
values,
forms: formsRef.current
});
}
formContext.triggerFormFinish(name, values);
},
registerForm: (name, form) => {
if (name) {
formsRef.current = {
...formsRef.current,
[name]: form
};
}
formContext.registerForm(name, form);
},
unregisterForm: name => {
const newForms = {
...formsRef.current
};
delete newForms[name];
formsRef.current = newForms;
formContext.unregisterForm(name);
}
}
}, children);
};
export { FormProvider };
export default FormContext;
+23
View File
@@ -0,0 +1,23 @@
import * as React from 'react';
import type { NamePath, StoreValue, ValidatorRule, Meta } from './interface';
export interface ListField {
name: number;
key: number;
isListField: boolean;
}
export interface ListOperations {
add: (defaultValue?: StoreValue, index?: number) => void;
remove: (index: number | number[]) => void;
move: (from: number, to: number) => void;
}
export interface ListProps<Values = any> {
name: NamePath<Values>;
rules?: ValidatorRule[];
validateTrigger?: string | string[] | false;
initialValue?: any[];
children?: (fields: ListField[], operations: ListOperations, meta: Meta) => React.ReactNode;
/** @private Passed by Form.List props. Do not use since it will break by path check. */
isListField?: boolean;
}
declare function List<Values = any>({ name, initialValue, children, rules, validateTrigger, isListField, }: ListProps<Values>): React.JSX.Element;
export default List;
+144
View File
@@ -0,0 +1,144 @@
import * as React from 'react';
import warning from "@rc-component/util/es/warning";
import FieldContext from "./FieldContext";
import Field from "./Field";
import { move, getNamePath } from "./utils/valueUtil";
import ListContext from "./ListContext";
function List({
name,
initialValue,
children,
rules,
validateTrigger,
isListField
}) {
const context = React.useContext(FieldContext);
const wrapperListContext = React.useContext(ListContext);
const keyRef = React.useRef({
keys: [],
id: 0
});
const keyManager = keyRef.current;
const prefixName = React.useMemo(() => {
const parentPrefixName = getNamePath(context.prefixName) || [];
return [...parentPrefixName, ...getNamePath(name)];
}, [context.prefixName, name]);
const fieldContext = React.useMemo(() => ({
...context,
prefixName
}), [context, prefixName]);
// List context
const listContext = React.useMemo(() => ({
getKey: namePath => {
const len = prefixName.length;
const pathName = namePath[len];
return [keyManager.keys[pathName], namePath.slice(len + 1)];
}
}), [keyManager, prefixName]);
// User should not pass `children` as other type.
if (typeof children !== 'function') {
warning(false, 'Form.List only accepts function as children.');
return null;
}
const shouldUpdate = (prevValue, nextValue, {
source
}) => {
if (source === 'internal') {
return false;
}
return prevValue !== nextValue;
};
return /*#__PURE__*/React.createElement(ListContext.Provider, {
value: listContext
}, /*#__PURE__*/React.createElement(FieldContext.Provider, {
value: fieldContext
}, /*#__PURE__*/React.createElement(Field, {
name: [],
shouldUpdate: shouldUpdate,
rules: rules,
validateTrigger: validateTrigger,
initialValue: initialValue,
isList: true,
isListField: isListField ?? !!wrapperListContext
}, ({
value = [],
onChange
}, meta) => {
const {
getFieldValue
} = context;
const getNewValue = () => {
const values = getFieldValue(prefixName || []);
return values || [];
};
/**
* Always get latest value in case user update fields by `form` api.
*/
const operations = {
add: (defaultValue, index) => {
// Mapping keys
const newValue = getNewValue();
if (index >= 0 && index <= newValue.length) {
keyManager.keys = [...keyManager.keys.slice(0, index), keyManager.id, ...keyManager.keys.slice(index)];
onChange([...newValue.slice(0, index), defaultValue, ...newValue.slice(index)]);
} else {
if (process.env.NODE_ENV !== 'production' && (index < 0 || index > newValue.length)) {
warning(false, 'The second parameter of the add function should be a valid positive number.');
}
keyManager.keys = [...keyManager.keys, keyManager.id];
onChange([...newValue, defaultValue]);
}
keyManager.id += 1;
},
remove: index => {
const newValue = getNewValue();
const indexSet = new Set(Array.isArray(index) ? index : [index]);
if (indexSet.size <= 0) {
return;
}
keyManager.keys = keyManager.keys.filter((_, keysIndex) => !indexSet.has(keysIndex));
// Trigger store change
onChange(newValue.filter((_, valueIndex) => !indexSet.has(valueIndex)));
},
move(from, to) {
if (from === to) {
return;
}
const newValue = getNewValue();
// Do not handle out of range
if (from < 0 || from >= newValue.length || to < 0 || to >= newValue.length) {
return;
}
keyManager.keys = move(keyManager.keys, from, to);
// Trigger store change
onChange(move(newValue, from, to));
}
};
let listValue = value || [];
if (!Array.isArray(listValue)) {
listValue = [];
if (process.env.NODE_ENV !== 'production') {
warning(false, `Current value of '${prefixName.join(' > ')}' is not an array type.`);
}
}
return children(listValue.map((__, index) => {
let key = keyManager.keys[index];
if (key === undefined) {
keyManager.keys[index] = keyManager.id;
key = keyManager.keys[index];
keyManager.id += 1;
}
return {
name: index,
key,
isListField: true
};
}), operations, meta);
})));
}
export default List;
+7
View File
@@ -0,0 +1,7 @@
import * as React from 'react';
import type { InternalNamePath } from './interface';
export interface ListContextProps {
getKey: (namePath: InternalNamePath) => [InternalNamePath[number], InternalNamePath];
}
declare const ListContext: React.Context<ListContextProps>;
export default ListContext;
+3
View File
@@ -0,0 +1,3 @@
import * as React from 'react';
const ListContext = /*#__PURE__*/React.createContext(null);
export default ListContext;
+104
View File
@@ -0,0 +1,104 @@
import type { FormInstance, InternalFormInstance, InternalNamePath, StoreValue } from '../interface';
interface UpdateAction {
type: 'updateValue';
namePath: InternalNamePath;
value: StoreValue;
}
interface ValidateAction {
type: 'validateField';
namePath: InternalNamePath;
triggerName: string;
}
export type ReducerAction = UpdateAction | ValidateAction;
export declare class FormStore {
private formHooked;
private forceRootUpdate;
private subscribable;
private store;
private fieldEntities;
private initialValues;
private callbacks;
private validateMessages;
private preserve?;
private lastValidatePromise;
private watcherCenter;
constructor(forceRootUpdate: () => void);
getForm: () => InternalFormInstance;
private getInternalHooks;
private useSubscribe;
/**
* Record prev Form unmount fieldEntities which config preserve false.
* This need to be refill with initialValues instead of store value.
*/
private prevWithoutPreserves;
/**
* First time `setInitialValues` should update store with initial value
*/
private setInitialValues;
private destroyForm;
private getInitialValue;
private setCallbacks;
private setValidateMessages;
private setPreserve;
private registerWatch;
private notifyWatch;
private timeoutId;
private warningUnhooked;
private updateStore;
/**
* Get registered field entities.
* @param pure Only return field which has a `name`. Default: false
*/
private getFieldEntities;
/**
* Get a map of registered field entities with their name path as the key.
* @param pure Only include fields which have a `name`. Default: false
* @returns A NameMap containing field entities indexed by their name paths
*/
private getFieldsMap;
/**
* Get field entities based on a list of name paths.
* @param nameList - Array of name paths to search for. If not provided, returns all field entities with names.
* @param includesSubNamePath - Whether to include fields that have the given name path as a prefix.
*/
private getFieldEntitiesForNamePathList;
private getFieldsValue;
private getFieldValue;
private getFieldsError;
private getFieldError;
private getFieldWarning;
private isFieldsTouched;
private isFieldTouched;
private isFieldsValidating;
private isFieldValidating;
/**
* Reset Field with field `initialValue` prop.
* Can pass `entities` or `namePathList` or just nothing.
*/
private resetWithFieldInitialValue;
private resetFields;
private setFields;
private getFields;
/**
* This only trigger when a field is on constructor to avoid we get initialValue too late
*/
private initEntityValue;
private isMergedPreserve;
private registerField;
private dispatch;
private notifyObservers;
/**
* Notify dependencies children with parent update
* We need delay to trigger validate in case Field is under render props
*/
private triggerDependenciesUpdate;
private updateValue;
private setFieldsValue;
private setFieldValue;
private getDependencyChildrenFields;
private triggerOnFieldsChange;
private validateFields;
private submit;
}
declare function useForm<Values = any>(form?: FormInstance<Values>): [FormInstance<Values>];
export default useForm;
+920
View File
@@ -0,0 +1,920 @@
import { merge } from "@rc-component/util/es/utils/set";
import warning from "@rc-component/util/es/warning";
import * as React from 'react';
import { HOOK_MARK } from "../FieldContext";
import { allPromiseFinish } from "../utils/asyncUtil";
import { defaultValidateMessages } from "../utils/messages";
import NameMap from "../utils/NameMap";
import { cloneByNamePathList, containsNamePath, getNamePath, getValue, matchNamePath, setValue } from "../utils/valueUtil";
import WatcherCenter from "./useNotifyWatch";
export class FormStore {
formHooked = false;
forceRootUpdate;
subscribable = true;
store = {};
fieldEntities = [];
initialValues = {};
callbacks = {};
validateMessages = null;
preserve = null;
lastValidatePromise = null;
watcherCenter = new WatcherCenter(this);
constructor(forceRootUpdate) {
this.forceRootUpdate = forceRootUpdate;
}
getForm = () => ({
getFieldValue: this.getFieldValue,
getFieldsValue: this.getFieldsValue,
getFieldError: this.getFieldError,
getFieldWarning: this.getFieldWarning,
getFieldsError: this.getFieldsError,
isFieldsTouched: this.isFieldsTouched,
isFieldTouched: this.isFieldTouched,
isFieldValidating: this.isFieldValidating,
isFieldsValidating: this.isFieldsValidating,
resetFields: this.resetFields,
setFields: this.setFields,
setFieldValue: this.setFieldValue,
setFieldsValue: this.setFieldsValue,
validateFields: this.validateFields,
submit: this.submit,
_init: true,
getInternalHooks: this.getInternalHooks
});
// ======================== Internal Hooks ========================
getInternalHooks = key => {
if (key === HOOK_MARK) {
this.formHooked = true;
return {
dispatch: this.dispatch,
initEntityValue: this.initEntityValue,
registerField: this.registerField,
useSubscribe: this.useSubscribe,
setInitialValues: this.setInitialValues,
destroyForm: this.destroyForm,
setCallbacks: this.setCallbacks,
setValidateMessages: this.setValidateMessages,
getFields: this.getFields,
setPreserve: this.setPreserve,
getInitialValue: this.getInitialValue,
registerWatch: this.registerWatch
};
}
warning(false, '`getInternalHooks` is internal usage. Should not call directly.');
return null;
};
useSubscribe = subscribable => {
this.subscribable = subscribable;
};
/**
* Record prev Form unmount fieldEntities which config preserve false.
* This need to be refill with initialValues instead of store value.
*/
prevWithoutPreserves = null;
/**
* First time `setInitialValues` should update store with initial value
*/
setInitialValues = (initialValues, init) => {
this.initialValues = initialValues || {};
if (init) {
let nextStore = merge(initialValues, this.store);
// We will take consider prev form unmount fields.
// When the field is not `preserve`, we need fill this with initialValues instead of store.
// eslint-disable-next-line array-callback-return
this.prevWithoutPreserves?.map(({
key: namePath
}) => {
nextStore = setValue(nextStore, namePath, getValue(initialValues, namePath));
});
this.prevWithoutPreserves = null;
this.updateStore(nextStore);
}
};
destroyForm = clearOnDestroy => {
if (clearOnDestroy) {
// destroy form reset store
this.updateStore({});
} else {
// Fill preserve fields
const prevWithoutPreserves = new NameMap();
this.getFieldEntities(true).forEach(entity => {
if (!this.isMergedPreserve(entity.isPreserve())) {
prevWithoutPreserves.set(entity.getNamePath(), true);
}
});
this.prevWithoutPreserves = prevWithoutPreserves;
}
};
getInitialValue = namePath => {
const initValue = getValue(this.initialValues, namePath);
// Not cloneDeep when without `namePath`
return namePath.length ? merge(initValue) : initValue;
};
setCallbacks = callbacks => {
this.callbacks = callbacks;
};
setValidateMessages = validateMessages => {
this.validateMessages = validateMessages;
};
setPreserve = preserve => {
this.preserve = preserve;
};
// ============================= Watch ============================
registerWatch = callback => {
return this.watcherCenter.register(callback);
};
notifyWatch = (namePath = []) => {
this.watcherCenter.notify(namePath);
};
// ========================== Dev Warning =========================
timeoutId = null;
warningUnhooked = () => {
if (process.env.NODE_ENV !== 'production' && !this.timeoutId && typeof window !== 'undefined') {
this.timeoutId = setTimeout(() => {
this.timeoutId = null;
if (!this.formHooked) {
warning(false, 'Instance created by `useForm` is not connected to any Form element. Forget to pass `form` prop?');
}
});
}
};
// ============================ Store =============================
updateStore = nextStore => {
this.store = nextStore;
};
// ============================ Fields ============================
/**
* Get registered field entities.
* @param pure Only return field which has a `name`. Default: false
*/
getFieldEntities = (pure = false) => {
if (!pure) {
return this.fieldEntities;
}
return this.fieldEntities.filter(field => field.getNamePath().length);
};
/**
* Get a map of registered field entities with their name path as the key.
* @param pure Only include fields which have a `name`. Default: false
* @returns A NameMap containing field entities indexed by their name paths
*/
getFieldsMap = (pure = false) => {
const cache = new NameMap();
this.getFieldEntities(pure).forEach(field => {
const namePath = field.getNamePath();
cache.set(namePath, field);
});
return cache;
};
/**
* Get field entities based on a list of name paths.
* @param nameList - Array of name paths to search for. If not provided, returns all field entities with names.
* @param includesSubNamePath - Whether to include fields that have the given name path as a prefix.
*/
getFieldEntitiesForNamePathList = (nameList, includesSubNamePath = false) => {
if (!nameList) {
return this.getFieldEntities(true);
}
const cache = this.getFieldsMap(true);
if (!includesSubNamePath) {
return nameList.map(name => {
const namePath = getNamePath(name);
return cache.get(namePath) || {
INVALIDATE_NAME_PATH: getNamePath(name)
};
});
}
return nameList.flatMap(name => {
const namePath = getNamePath(name);
const fields = cache.getAsPrefix(namePath);
if (fields.length) {
return fields;
}
return [{
INVALIDATE_NAME_PATH: namePath
}];
});
};
getFieldsValue = (nameList, filterFunc) => {
this.warningUnhooked();
// Fill args
let mergedNameList;
let mergedFilterFunc;
if (nameList === true || Array.isArray(nameList)) {
mergedNameList = nameList;
mergedFilterFunc = filterFunc;
} else if (nameList && typeof nameList === 'object') {
mergedFilterFunc = nameList.filter;
}
if (mergedNameList === true && !mergedFilterFunc) {
return this.store;
}
const fieldEntities = this.getFieldEntitiesForNamePathList(Array.isArray(mergedNameList) ? mergedNameList : null, true);
const filteredNameList = [];
const listNamePaths = [];
fieldEntities.forEach(entity => {
const namePath = entity.INVALIDATE_NAME_PATH || entity.getNamePath();
// Ignore when it's a list item and not specific the namePath,
// since parent field is already take in count
if (entity.isList?.()) {
listNamePaths.push(namePath);
return;
}
if (!mergedFilterFunc) {
filteredNameList.push(namePath);
} else {
const meta = 'getMeta' in entity ? entity.getMeta() : null;
if (mergedFilterFunc(meta)) {
filteredNameList.push(namePath);
}
}
});
let mergedValues = cloneByNamePathList(this.store, filteredNameList.map(getNamePath));
// We need fill the list as [] if Form.List is empty
listNamePaths.forEach(namePath => {
if (!getValue(mergedValues, namePath)) {
mergedValues = setValue(mergedValues, namePath, []);
}
});
return mergedValues;
};
getFieldValue = name => {
this.warningUnhooked();
const namePath = getNamePath(name);
return getValue(this.store, namePath);
};
getFieldsError = nameList => {
this.warningUnhooked();
const fieldEntities = this.getFieldEntitiesForNamePathList(nameList);
return fieldEntities.map((entity, index) => {
if (entity && !entity.INVALIDATE_NAME_PATH) {
return {
name: entity.getNamePath(),
errors: entity.getErrors(),
warnings: entity.getWarnings()
};
}
return {
name: getNamePath(nameList[index]),
errors: [],
warnings: []
};
});
};
getFieldError = name => {
this.warningUnhooked();
const namePath = getNamePath(name);
const fieldError = this.getFieldsError([namePath])[0];
return fieldError.errors;
};
getFieldWarning = name => {
this.warningUnhooked();
const namePath = getNamePath(name);
const fieldError = this.getFieldsError([namePath])[0];
return fieldError.warnings;
};
isFieldsTouched = (...args) => {
this.warningUnhooked();
const [arg0, arg1] = args;
let namePathList;
let isAllFieldsTouched = false;
if (args.length === 0) {
namePathList = null;
} else if (args.length === 1) {
if (Array.isArray(arg0)) {
namePathList = arg0.map(getNamePath);
isAllFieldsTouched = false;
} else {
namePathList = null;
isAllFieldsTouched = arg0;
}
} else {
namePathList = arg0.map(getNamePath);
isAllFieldsTouched = arg1;
}
const fieldEntities = this.getFieldEntities(true);
const isFieldTouched = field => field.isFieldTouched();
// ===== Will get fully compare when not config namePathList =====
if (!namePathList) {
return isAllFieldsTouched ? fieldEntities.every(entity => isFieldTouched(entity) || entity.isList()) : fieldEntities.some(isFieldTouched);
}
// Generate a nest tree for validate
const map = new NameMap();
namePathList.forEach(shortNamePath => {
map.set(shortNamePath, []);
});
fieldEntities.forEach(field => {
const fieldNamePath = field.getNamePath();
// Find matched entity and put into list
namePathList.forEach(shortNamePath => {
if (shortNamePath.every((nameUnit, i) => fieldNamePath[i] === nameUnit)) {
map.update(shortNamePath, list => [...list, field]);
}
});
});
// Check if NameMap value is touched
const isNamePathListTouched = entities => entities.some(isFieldTouched);
const namePathListEntities = map.map(({
value
}) => value);
return isAllFieldsTouched ? namePathListEntities.every(isNamePathListTouched) : namePathListEntities.some(isNamePathListTouched);
};
isFieldTouched = name => {
this.warningUnhooked();
return this.isFieldsTouched([name]);
};
isFieldsValidating = nameList => {
this.warningUnhooked();
const fieldEntities = this.getFieldEntities();
if (!nameList) {
return fieldEntities.some(testField => testField.isFieldValidating());
}
const namePathList = nameList.map(getNamePath);
return fieldEntities.some(testField => {
const fieldNamePath = testField.getNamePath();
return containsNamePath(namePathList, fieldNamePath) && testField.isFieldValidating();
});
};
isFieldValidating = name => {
this.warningUnhooked();
return this.isFieldsValidating([name]);
};
/**
* Reset Field with field `initialValue` prop.
* Can pass `entities` or `namePathList` or just nothing.
*/
resetWithFieldInitialValue = (info = {}) => {
// Create cache
const cache = new NameMap();
const fieldEntities = this.getFieldEntities(true);
fieldEntities.forEach(field => {
const {
initialValue
} = field.props;
const namePath = field.getNamePath();
// Record only if has `initialValue`
if (initialValue !== undefined) {
const records = cache.get(namePath) || new Set();
records.add({
entity: field,
value: initialValue
});
cache.set(namePath, records);
}
});
// Reset
const resetWithFields = entities => {
entities.forEach(field => {
const {
initialValue
} = field.props;
if (initialValue !== undefined) {
const namePath = field.getNamePath();
const formInitialValue = this.getInitialValue(namePath);
if (formInitialValue !== undefined) {
// Warning if conflict with form initialValues and do not modify value
warning(false, `Form already set 'initialValues' with path '${namePath.join('.')}'. Field can not overwrite it.`);
} else {
const records = cache.get(namePath);
if (records && records.size > 1) {
// Warning if multiple field set `initialValue`and do not modify value
warning(false, `Multiple Field with path '${namePath.join('.')}' set 'initialValue'. Can not decide which one to pick.`);
} else if (records) {
const originValue = this.getFieldValue(namePath);
const isListField = field.isListField();
// Set `initialValue`
if (!isListField && (!info.skipExist || originValue === undefined)) {
this.updateStore(setValue(this.store, namePath, [...records][0].value));
}
}
}
}
});
};
let requiredFieldEntities;
if (info.entities) {
requiredFieldEntities = info.entities;
} else if (info.namePathList) {
requiredFieldEntities = [];
info.namePathList.forEach(namePath => {
const records = cache.get(namePath);
if (records) {
requiredFieldEntities.push(...[...records].map(r => r.entity));
}
});
} else {
requiredFieldEntities = fieldEntities;
}
resetWithFields(requiredFieldEntities);
};
resetFields = nameList => {
this.warningUnhooked();
const prevStore = this.store;
if (!nameList) {
this.updateStore(merge(this.initialValues));
this.resetWithFieldInitialValue();
this.notifyObservers(prevStore, null, {
type: 'reset'
});
this.notifyWatch();
return;
}
// Reset by `nameList`
const namePathList = nameList.map(getNamePath);
namePathList.forEach(namePath => {
const initialValue = this.getInitialValue(namePath);
this.updateStore(setValue(this.store, namePath, initialValue));
});
this.resetWithFieldInitialValue({
namePathList
});
this.notifyObservers(prevStore, namePathList, {
type: 'reset'
});
this.notifyWatch(namePathList);
};
setFields = fields => {
this.warningUnhooked();
const prevStore = this.store;
const namePathList = [];
fields.forEach(fieldData => {
const {
name,
...data
} = fieldData;
const namePath = getNamePath(name);
namePathList.push(namePath);
// Value
if ('value' in data) {
this.updateStore(setValue(this.store, namePath, data.value));
}
this.notifyObservers(prevStore, [namePath], {
type: 'setField',
data: fieldData
});
});
this.notifyWatch(namePathList);
};
getFields = () => {
const entities = this.getFieldEntities(true);
const fields = entities.map(field => {
const namePath = field.getNamePath();
const meta = field.getMeta();
const fieldData = {
...meta,
name: namePath,
value: this.getFieldValue(namePath)
};
Object.defineProperty(fieldData, 'originRCField', {
value: true
});
return fieldData;
});
return fields;
};
// =========================== Observer ===========================
/**
* This only trigger when a field is on constructor to avoid we get initialValue too late
*/
initEntityValue = entity => {
const {
initialValue
} = entity.props;
if (initialValue !== undefined) {
const namePath = entity.getNamePath();
const prevValue = getValue(this.store, namePath);
if (prevValue === undefined) {
this.updateStore(setValue(this.store, namePath, initialValue));
}
}
};
isMergedPreserve = fieldPreserve => {
const mergedPreserve = fieldPreserve !== undefined ? fieldPreserve : this.preserve;
return mergedPreserve ?? true;
};
registerField = entity => {
this.fieldEntities.push(entity);
const namePath = entity.getNamePath();
this.notifyWatch([namePath]);
// Set initial values
if (entity.props.initialValue !== undefined) {
const prevStore = this.store;
this.resetWithFieldInitialValue({
entities: [entity],
skipExist: true
});
this.notifyObservers(prevStore, [entity.getNamePath()], {
type: 'valueUpdate',
source: 'internal'
});
}
// un-register field callback
return (isListField, preserve, subNamePath = []) => {
this.fieldEntities = this.fieldEntities.filter(item => item !== entity);
// Clean up store value if not preserve
if (!this.isMergedPreserve(preserve) && (!isListField || subNamePath.length > 1)) {
const defaultValue = isListField ? undefined : this.getInitialValue(namePath);
if (namePath.length && this.getFieldValue(namePath) !== defaultValue && this.fieldEntities.every(field =>
// Only reset when no namePath exist
!matchNamePath(field.getNamePath(), namePath))) {
const prevStore = this.store;
this.updateStore(setValue(prevStore, namePath, defaultValue, true));
// Notify that field is unmount
this.notifyObservers(prevStore, [namePath], {
type: 'remove'
});
// Dependencies update
this.triggerDependenciesUpdate(prevStore, namePath);
}
}
this.notifyWatch([namePath]);
};
};
dispatch = action => {
switch (action.type) {
case 'updateValue':
{
const {
namePath,
value
} = action;
this.updateValue(namePath, value);
break;
}
case 'validateField':
{
const {
namePath,
triggerName
} = action;
this.validateFields([namePath], {
triggerName
});
break;
}
default:
// Currently we don't have other action. Do nothing.
}
};
notifyObservers = (prevStore, namePathList, info) => {
if (this.subscribable) {
const mergedInfo = {
...info,
store: this.getFieldsValue(true)
};
this.getFieldEntities().forEach(({
onStoreChange
}) => {
onStoreChange(prevStore, namePathList, mergedInfo);
});
} else {
this.forceRootUpdate();
}
};
/**
* Notify dependencies children with parent update
* We need delay to trigger validate in case Field is under render props
*/
triggerDependenciesUpdate = (prevStore, namePath) => {
const childrenFields = this.getDependencyChildrenFields(namePath);
if (childrenFields.length) {
this.validateFields(childrenFields, {
// Delay to avoid `useWatch` dynamic adjust rules that deps not get latest one
delayFrame: true
});
}
this.notifyObservers(prevStore, childrenFields, {
type: 'dependenciesUpdate',
relatedFields: [namePath, ...childrenFields]
});
return childrenFields;
};
updateValue = (name, value) => {
const namePath = getNamePath(name);
const prevStore = this.store;
this.updateStore(setValue(this.store, namePath, value));
this.notifyObservers(prevStore, [namePath], {
type: 'valueUpdate',
source: 'internal'
});
this.notifyWatch([namePath]);
// Dependencies update
const childrenFields = this.triggerDependenciesUpdate(prevStore, namePath);
// trigger callback function
const {
onValuesChange
} = this.callbacks;
if (onValuesChange) {
const changedValues = cloneByNamePathList(this.store, [namePath]);
const allValues = this.getFieldsValue();
const mergedAllValues = setValue(allValues, namePath, getValue(changedValues, namePath));
onValuesChange(changedValues, mergedAllValues);
}
this.triggerOnFieldsChange([namePath, ...childrenFields]);
};
// Let all child Field get update.
setFieldsValue = store => {
this.warningUnhooked();
const prevStore = this.store;
if (store) {
const nextStore = merge(this.store, store);
this.updateStore(nextStore);
}
this.notifyObservers(prevStore, null, {
type: 'valueUpdate',
source: 'external'
});
this.notifyWatch();
};
setFieldValue = (name, value) => {
this.setFields([{
name,
value,
errors: [],
warnings: [],
touched: true
}]);
};
getDependencyChildrenFields = rootNamePath => {
const children = new Set();
const childrenFields = [];
const dependencies2fields = new NameMap();
/**
* Generate maps
* Can use cache to save perf if user report performance issue with this
*/
this.getFieldEntities().forEach(field => {
const {
dependencies
} = field.props;
(dependencies || []).forEach(dependency => {
const dependencyNamePath = getNamePath(dependency);
dependencies2fields.update(dependencyNamePath, (fields = new Set()) => {
fields.add(field);
return fields;
});
});
});
const fillChildren = namePath => {
const fields = dependencies2fields.get(namePath) || new Set();
fields.forEach(field => {
if (!children.has(field)) {
children.add(field);
const fieldNamePath = field.getNamePath();
if (field.isFieldDirty() && fieldNamePath.length) {
childrenFields.push(fieldNamePath);
fillChildren(fieldNamePath);
}
}
});
};
fillChildren(rootNamePath);
return childrenFields;
};
triggerOnFieldsChange = (namePathList, filedErrors) => {
const {
onFieldsChange
} = this.callbacks;
if (onFieldsChange) {
const fields = this.getFields();
/**
* Fill errors since `fields` may be replaced by controlled fields
*/
if (filedErrors) {
const cache = new NameMap();
filedErrors.forEach(({
name,
errors
}) => {
cache.set(name, errors);
});
fields.forEach(field => {
// eslint-disable-next-line no-param-reassign
field.errors = cache.get(field.name) || field.errors;
});
}
const changedFields = fields.filter(({
name: fieldName
}) => containsNamePath(namePathList, fieldName));
if (changedFields.length) {
onFieldsChange(changedFields, fields);
}
}
};
// =========================== Validate ===========================
validateFields = (arg1, arg2) => {
this.warningUnhooked();
let nameList;
let options;
if (Array.isArray(arg1) || typeof arg1 === 'string' || typeof arg2 === 'string') {
nameList = arg1;
options = arg2;
} else {
options = arg1;
}
const provideNameList = !!nameList;
const namePathList = provideNameList ? nameList.map(getNamePath) : [];
// Same namePathList, but does not include Form.List name
const finalValueNamePathList = [...namePathList];
// Collect result in promise list
const promiseList = [];
// We temp save the path which need trigger for `onFieldsChange`
const TMP_SPLIT = String(Date.now());
const validateNamePathList = new Set();
const {
recursive,
dirty
} = options || {};
this.getFieldEntities(true).forEach(field => {
const fieldNamePath = field.getNamePath();
// Add field if not provide `nameList`
if (!provideNameList) {
if (
// If is field, pass directly
!field.isList() ||
// If is list, do not add if already exist sub field in the namePathList
!namePathList.some(name => matchNamePath(name, fieldNamePath, true))) {
finalValueNamePathList.push(fieldNamePath);
}
namePathList.push(fieldNamePath);
}
// Skip if without rule
if (!field.props.rules || !field.props.rules.length) {
return;
}
// Skip if only validate dirty field
if (dirty && !field.isFieldDirty()) {
return;
}
validateNamePathList.add(fieldNamePath.join(TMP_SPLIT));
// Add field validate rule in to promise list
if (!provideNameList || containsNamePath(namePathList, fieldNamePath, recursive)) {
const promise = field.validateRules({
validateMessages: {
...defaultValidateMessages,
...this.validateMessages
},
...options
});
// Wrap promise with field
promiseList.push(promise.then(() => ({
name: fieldNamePath,
errors: [],
warnings: []
})).catch(ruleErrors => {
const mergedErrors = [];
const mergedWarnings = [];
ruleErrors.forEach?.(({
rule: {
warningOnly
},
errors
}) => {
if (warningOnly) {
mergedWarnings.push(...errors);
} else {
mergedErrors.push(...errors);
}
});
if (mergedErrors.length) {
return Promise.reject({
name: fieldNamePath,
errors: mergedErrors,
warnings: mergedWarnings
});
}
return {
name: fieldNamePath,
errors: mergedErrors,
warnings: mergedWarnings
};
}));
}
});
const summaryPromise = allPromiseFinish(promiseList);
this.lastValidatePromise = summaryPromise;
// Notify fields with rule that validate has finished and need update
summaryPromise.catch(results => results).then(results => {
const resultNamePathList = results.map(({
name
}) => name);
this.notifyObservers(this.store, resultNamePathList, {
type: 'validateFinish'
});
this.triggerOnFieldsChange(resultNamePathList, results);
});
const returnPromise = summaryPromise.then(() => {
if (this.lastValidatePromise === summaryPromise) {
return Promise.resolve(this.getFieldsValue(finalValueNamePathList));
}
return Promise.reject([]);
}).catch(results => {
const errorList = results.filter(result => result && result.errors.length);
const errorMessage = errorList[0]?.errors?.[0];
return Promise.reject({
message: errorMessage,
values: this.getFieldsValue(namePathList),
errorFields: errorList,
outOfDate: this.lastValidatePromise !== summaryPromise
});
});
// Do not throw in console
returnPromise.catch(e => e);
// `validating` changed. Trigger `onFieldsChange`
const triggerNamePathList = namePathList.filter(namePath => validateNamePathList.has(namePath.join(TMP_SPLIT)));
this.triggerOnFieldsChange(triggerNamePathList);
return returnPromise;
};
// ============================ Submit ============================
submit = () => {
this.warningUnhooked();
this.validateFields().then(values => {
const {
onFinish
} = this.callbacks;
if (onFinish) {
try {
onFinish(values);
} catch (err) {
// Should print error if user `onFinish` callback failed
console.error(err);
}
}
}).catch(e => {
const {
onFinishFailed
} = this.callbacks;
if (onFinishFailed) {
onFinishFailed(e);
}
});
};
}
function useForm(form) {
const formRef = React.useRef(null);
const [, forceUpdate] = React.useState({});
// Create singleton FormStore
if (!formRef.current) {
if (form) {
formRef.current = form;
} else {
// Create a new FormStore if not provided
const forceReRender = () => {
forceUpdate({});
};
const formStore = new FormStore(forceReRender);
formRef.current = formStore.getForm();
}
}
return [formRef.current];
}
export default useForm;
@@ -0,0 +1,16 @@
import type { InternalNamePath, WatchCallBack } from '../interface';
import type { FormStore } from './useForm';
/**
* Call action with delay in macro task.
*/
export declare const macroTask: (fn: VoidFunction) => void;
export default class WatcherCenter {
namePathList: InternalNamePath[];
taskId: number;
watcherList: Set<WatchCallBack>;
form: FormStore;
constructor(form: FormStore);
register(callback: WatchCallBack): VoidFunction;
notify(namePath: InternalNamePath[]): void;
private doBatch;
}
@@ -0,0 +1,48 @@
import { matchNamePath } from "../utils/valueUtil";
/**
* Call action with delay in macro task.
*/
export const macroTask = fn => {
const channel = new MessageChannel();
channel.port1.onmessage = fn;
channel.port2.postMessage(null);
};
export default class WatcherCenter {
namePathList = [];
taskId = 0;
watcherList = new Set();
form;
constructor(form) {
this.form = form;
}
register(callback) {
this.watcherList.add(callback);
return () => {
this.watcherList.delete(callback);
};
}
notify(namePath) {
// Insert with deduplication
namePath.forEach(path => {
if (this.namePathList.every(exist => !matchNamePath(exist, path))) {
this.namePathList.push(path);
}
});
this.doBatch();
}
doBatch() {
this.taskId += 1;
const currentId = this.taskId;
macroTask(() => {
if (currentId === this.taskId && this.watcherList.size) {
const formInst = this.form.getForm();
const values = formInst.getFieldsValue();
const allValues = formInst.getFieldsValue(true);
this.watcherList.forEach(callback => {
callback(values, allValues, this.namePathList);
});
this.namePathList = [];
}
});
}
}
+14
View File
@@ -0,0 +1,14 @@
import type { FormInstance, NamePath, Store, WatchOptions } from '../interface';
type ReturnPromise<T> = T extends Promise<infer ValueType> ? ValueType : never;
type GetGeneric<TForm extends FormInstance> = ReturnPromise<ReturnType<TForm['validateFields']>>;
export declare function stringify(value: any): string | number;
declare function useWatch<TDependencies1 extends keyof GetGeneric<TForm>, TForm extends FormInstance, TDependencies2 extends keyof GetGeneric<TForm>[TDependencies1], TDependencies3 extends keyof GetGeneric<TForm>[TDependencies1][TDependencies2], TDependencies4 extends keyof GetGeneric<TForm>[TDependencies1][TDependencies2][TDependencies3]>(dependencies: [TDependencies1, TDependencies2, TDependencies3, TDependencies4], form?: TForm | WatchOptions<TForm>): GetGeneric<TForm>[TDependencies1][TDependencies2][TDependencies3][TDependencies4];
declare function useWatch<TDependencies1 extends keyof GetGeneric<TForm>, TForm extends FormInstance, TDependencies2 extends keyof GetGeneric<TForm>[TDependencies1], TDependencies3 extends keyof GetGeneric<TForm>[TDependencies1][TDependencies2]>(dependencies: [TDependencies1, TDependencies2, TDependencies3], form?: TForm | WatchOptions<TForm>): GetGeneric<TForm>[TDependencies1][TDependencies2][TDependencies3];
declare function useWatch<TDependencies1 extends keyof GetGeneric<TForm>, TForm extends FormInstance, TDependencies2 extends keyof GetGeneric<TForm>[TDependencies1]>(dependencies: [TDependencies1, TDependencies2], form?: TForm | WatchOptions<TForm>): GetGeneric<TForm>[TDependencies1][TDependencies2];
declare function useWatch<TDependencies extends keyof GetGeneric<TForm>, TForm extends FormInstance>(dependencies: TDependencies | [TDependencies], form?: TForm | WatchOptions<TForm>): GetGeneric<TForm>[TDependencies];
declare function useWatch<TForm extends FormInstance>(dependencies: [], form?: TForm | WatchOptions<TForm>): GetGeneric<TForm>;
declare function useWatch<TForm extends FormInstance, TSelected = unknown>(selector: (values: GetGeneric<TForm>) => TSelected, form?: TForm | WatchOptions<TForm>): TSelected;
declare function useWatch<ValueType = Store, TSelected = unknown>(selector: (values: ValueType) => TSelected, form?: FormInstance | WatchOptions<FormInstance>): TSelected;
declare function useWatch<TForm extends FormInstance>(dependencies: NamePath, form?: TForm | WatchOptions<TForm>): any;
declare function useWatch<ValueType = Store>(dependencies: NamePath, form?: FormInstance | WatchOptions<FormInstance>): ValueType;
export default useWatch;
+85
View File
@@ -0,0 +1,85 @@
import warning from "@rc-component/util/es/warning";
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
import FieldContext, { HOOK_MARK } from "../FieldContext";
import { isFormInstance } from "../utils/typeUtil";
import { getNamePath, getValue } from "../utils/valueUtil";
import { useEvent } from '@rc-component/util';
export function stringify(value) {
try {
return JSON.stringify(value);
} catch {
return Math.random();
}
}
// ------- selector type -------
// ------- selector type end -------
function useWatch(...args) {
const [dependencies, _form = {}] = args;
const options = isFormInstance(_form) ? {
form: _form
} : _form;
const form = options.form;
const [value, setValue] = useState(() => typeof dependencies === 'function' ? dependencies({}) : undefined);
const valueStr = useMemo(() => stringify(value), [value]);
const valueStrRef = useRef(valueStr);
valueStrRef.current = valueStr;
const fieldContext = useContext(FieldContext);
const formInstance = form || fieldContext;
const isValidForm = formInstance && formInstance._init;
// Warning if not exist form instance
if (process.env.NODE_ENV !== 'production') {
warning(args.length === 2 ? form ? isValidForm : true : isValidForm, 'useWatch requires a form instance since it can not auto detect from context.');
}
// ============================== Form ==============================
const {
getFieldsValue,
getInternalHooks
} = formInstance;
const {
registerWatch
} = getInternalHooks(HOOK_MARK);
// ============================= Update =============================
const triggerUpdate = useEvent((values, allValues) => {
const watchValue = options.preserve ? allValues ?? getFieldsValue(true) : values ?? getFieldsValue();
const nextValue = typeof dependencies === 'function' ? dependencies(watchValue) : getValue(watchValue, getNamePath(dependencies));
if (stringify(value) !== stringify(nextValue)) {
setValue(nextValue);
}
});
// ============================= Effect =============================
const flattenDeps = typeof dependencies === 'function' ? dependencies : JSON.stringify(dependencies);
// Deps changed
useEffect(() => {
// Skip if not exist form instance
if (!isValidForm) {
return;
}
triggerUpdate();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isValidForm, flattenDeps]);
// Value changed
useEffect(() => {
// Skip if not exist form instance
if (!isValidForm) {
return;
}
const cancelRegister = registerWatch((values, allValues) => {
triggerUpdate(values, allValues);
});
return cancelRegister;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isValidForm]);
return value;
}
export default useWatch;
+25
View File
@@ -0,0 +1,25 @@
import * as React from 'react';
import type { FormRef, FormInstance } from './interface';
import Field from './Field';
import List from './List';
import useForm from './hooks/useForm';
import type { FormProps } from './Form';
import { FormProvider } from './FormContext';
import FieldContext from './FieldContext';
import ListContext from './ListContext';
import useWatch from './hooks/useWatch';
declare const InternalForm: <Values = any>(props: FormProps<Values> & {
ref?: React.Ref<FormRef<Values>>;
}) => React.ReactElement;
type InternalFormType = typeof InternalForm;
interface RefFormType extends InternalFormType {
FormProvider: typeof FormProvider;
Field: typeof Field;
List: typeof List;
useForm: typeof useForm;
useWatch: typeof useWatch;
}
declare const RefForm: RefFormType;
export { Field, List, useForm, FormProvider, FieldContext, ListContext, useWatch };
export type { FormProps, FormInstance, FormRef };
export default RefForm;
+18
View File
@@ -0,0 +1,18 @@
import * as React from 'react';
import Field from "./Field";
import List from "./List";
import useForm from "./hooks/useForm";
import FieldForm from "./Form";
import { FormProvider } from "./FormContext";
import FieldContext from "./FieldContext";
import ListContext from "./ListContext";
import useWatch from "./hooks/useWatch";
const InternalForm = /*#__PURE__*/React.forwardRef(FieldForm);
const RefForm = InternalForm;
RefForm.FormProvider = FormProvider;
RefForm.Field = Field;
RefForm.List = List;
RefForm.useForm = useForm;
RefForm.useWatch = useWatch;
export { Field, List, useForm, FormProvider, FieldContext, ListContext, useWatch };
export default RefForm;
+276
View File
@@ -0,0 +1,276 @@
import type { ReactElement } from 'react';
import type { DeepNamePath } from './namePathType';
import type { ReducerAction } from './hooks/useForm';
export type InternalNamePath = (string | number)[];
export type NamePath<T = any> = DeepNamePath<T>;
export type StoreValue = any;
export type Store = Record<string, StoreValue>;
export interface Meta {
touched: boolean;
validating: boolean;
errors: string[];
warnings: string[];
name: InternalNamePath;
validated: boolean;
}
export interface InternalFieldData extends Meta {
value: StoreValue;
}
/**
* Used by `setFields` config
*/
export interface FieldData<Values = any> extends Partial<Omit<InternalFieldData, 'name'>> {
name: NamePath<Values>;
}
export type RuleType = 'string' | 'number' | 'boolean' | 'method' | 'regexp' | 'integer' | 'float' | 'object' | 'enum' | 'date' | 'url' | 'hex' | 'email' | 'tel';
type Validator = (rule: RuleObject, value: StoreValue, callback: (error?: string) => void) => Promise<void | any> | void;
export type RuleRender = (form: FormInstance) => RuleObject;
export interface ValidatorRule {
warningOnly?: boolean;
message?: string | ReactElement;
validator: Validator;
}
interface BaseRule {
warningOnly?: boolean;
enum?: StoreValue[];
len?: number;
max?: number;
message?: string | ReactElement;
min?: number;
pattern?: RegExp;
required?: boolean;
transform?: (value: StoreValue) => StoreValue;
type?: RuleType;
whitespace?: boolean;
/** Customize rule level `validateTrigger`. Must be subset of Field `validateTrigger` */
validateTrigger?: string | string[];
}
type AggregationRule = BaseRule & Partial<ValidatorRule>;
interface ArrayRule extends Omit<AggregationRule, 'type'> {
type: 'array';
defaultField?: RuleObject;
}
export type RuleObject = AggregationRule | ArrayRule;
export type Rule = RuleObject | RuleRender;
export interface ValidateErrorEntity<Values = any> {
message: string;
values: Values;
errorFields: {
name: InternalNamePath;
errors: string[];
}[];
outOfDate: boolean;
}
export interface FieldEntity {
onStoreChange: (store: Store, namePathList: InternalNamePath[] | null, info: ValuedNotifyInfo) => void;
isFieldTouched: () => boolean;
isFieldDirty: () => boolean;
isFieldValidating: () => boolean;
isListField: () => boolean;
isList: () => boolean;
isPreserve: () => boolean;
validateRules: (options?: InternalValidateOptions) => Promise<RuleError[]>;
getMeta: () => Meta;
getNamePath: () => InternalNamePath;
getErrors: () => string[];
getWarnings: () => string[];
props: {
name?: NamePath;
rules?: Rule[];
dependencies?: NamePath[];
initialValue?: any;
};
/**
* Mask as invalidate.
* This will filled when Field is removed but not updated in render yet.
*/
INVALIDATE_NAME_PATH?: InternalNamePath;
}
export interface FieldError {
name: InternalNamePath;
errors: string[];
warnings: string[];
}
export interface RuleError {
errors: string[];
rule: RuleObject;
}
export interface ValidateOptions {
/**
* Validate only and not trigger UI and Field status update
*/
validateOnly?: boolean;
/**
* Recursive validate. It will validate all the name path that contains the provided one.
* e.g. [['a']] will validate ['a'] , ['a', 'b'] and ['a', 1].
*/
recursive?: boolean;
/** Validate when a field is dirty (validated or touched) */
dirty?: boolean;
}
export type ValidateFields<Values = any> = {
(opt?: ValidateOptions): Promise<Values>;
(nameList?: NamePath[], opt?: ValidateOptions): Promise<Values>;
};
export interface InternalValidateOptions extends ValidateOptions {
triggerName?: string;
validateMessages?: ValidateMessages;
delayFrame?: boolean;
}
export type InternalValidateFields<Values = any> = {
(options?: InternalValidateOptions): Promise<Values>;
(nameList?: NamePath[], options?: InternalValidateOptions): Promise<Values>;
};
interface ValueUpdateInfo {
type: 'valueUpdate';
source: 'internal' | 'external';
}
interface ValidateFinishInfo {
type: 'validateFinish';
}
interface ResetInfo {
type: 'reset';
}
interface RemoveInfo {
type: 'remove';
}
interface SetFieldInfo {
type: 'setField';
data: FieldData;
}
interface DependenciesUpdateInfo {
type: 'dependenciesUpdate';
/**
* Contains all the related `InternalNamePath[]`.
* a <- b <- c : change `a`
* relatedFields=[a, b, c]
*/
relatedFields: InternalNamePath[];
}
export type NotifyInfo = ValueUpdateInfo | ValidateFinishInfo | ResetInfo | RemoveInfo | SetFieldInfo | DependenciesUpdateInfo;
export type ValuedNotifyInfo = NotifyInfo & {
store: Store;
};
export interface Callbacks<Values = any> {
onValuesChange?: (changedValues: Partial<Values>, values: Values) => void;
onFieldsChange?: (changedFields: FieldData[], allFields: FieldData[]) => void;
onFinish?: (values: Values) => void;
onFinishFailed?: (errorInfo: ValidateErrorEntity<Values>) => void;
}
export type WatchCallBack = (values: Store, allValues: Store, namePathList: InternalNamePath[]) => void;
export interface WatchOptions<Form extends FormInstance = FormInstance> {
form?: Form;
preserve?: boolean;
}
export interface InternalHooks {
dispatch: (action: ReducerAction) => void;
initEntityValue: (entity: FieldEntity) => void;
registerField: (entity: FieldEntity) => () => void;
useSubscribe: (subscribable: boolean) => void;
setInitialValues: (values: Store, init: boolean) => void;
destroyForm: (clearOnDestroy?: boolean) => void;
setCallbacks: (callbacks: Callbacks) => void;
registerWatch: (callback: WatchCallBack) => () => void;
getFields: (namePathList?: InternalNamePath[]) => FieldData[];
setValidateMessages: (validateMessages: ValidateMessages) => void;
setPreserve: (preserve?: boolean) => void;
getInitialValue: (namePath: InternalNamePath) => StoreValue;
}
/** Only return partial when type is not any */
type RecursivePartial<T> = T extends (infer U)[] ? RecursivePartial<U>[] : T extends object ? {
[P in keyof T]?: RecursivePartial<T[P]>;
} : T;
export type FilterFunc = (meta: Meta | null) => boolean;
export type GetFieldsValueConfig = {
/**
* @deprecated `strict` is deprecated and not working anymore
*/
strict?: boolean;
filter?: FilterFunc;
};
export interface FormInstance<Values = any> {
getFieldValue: (name: NamePath<Values>) => StoreValue;
getFieldsValue: (() => Values) & ((nameList: NamePath<Values>[] | true, filterFunc?: FilterFunc) => any) & ((config: GetFieldsValueConfig) => any);
getFieldError: (name: NamePath<Values>) => string[];
getFieldsError: (nameList?: NamePath<Values>[]) => FieldError[];
getFieldWarning: (name: NamePath<Values>) => string[];
isFieldsTouched: ((nameList?: NamePath<Values>[], allFieldsTouched?: boolean) => boolean) & ((allFieldsTouched?: boolean) => boolean);
isFieldTouched: (name: NamePath<Values>) => boolean;
isFieldValidating: (name: NamePath<Values>) => boolean;
isFieldsValidating: (nameList?: NamePath<Values>[]) => boolean;
resetFields: (fields?: NamePath<Values>[]) => void;
setFields: (fields: FieldData<Values>[]) => void;
setFieldValue: (name: NamePath<Values>, value: any) => void;
setFieldsValue: (values: RecursivePartial<Values>) => void;
validateFields: ValidateFields<Values>;
submit: () => void;
}
export type FormRef<Values = any> = FormInstance<Values> & {
nativeElement?: HTMLElement;
};
export type InternalFormInstance = Omit<FormInstance, 'validateFields'> & {
validateFields: InternalValidateFields;
/**
* Passed by field context props
*/
prefixName?: InternalNamePath;
validateTrigger?: string | string[] | false;
/**
* Form component should register some content into store.
* We pass the `HOOK_MARK` as key to avoid user call the function.
*/
getInternalHooks: (secret: string) => InternalHooks | null;
/** @private Internal usage. Do not use it in your production */
_init?: boolean;
};
export type EventArgs = any[];
type ValidateMessage = string | (() => string);
export interface ValidateMessages {
default?: ValidateMessage;
required?: ValidateMessage;
enum?: ValidateMessage;
whitespace?: ValidateMessage;
date?: {
format?: ValidateMessage;
parse?: ValidateMessage;
invalid?: ValidateMessage;
};
types?: {
string?: ValidateMessage;
method?: ValidateMessage;
array?: ValidateMessage;
object?: ValidateMessage;
number?: ValidateMessage;
date?: ValidateMessage;
boolean?: ValidateMessage;
integer?: ValidateMessage;
float?: ValidateMessage;
regexp?: ValidateMessage;
email?: ValidateMessage;
tel?: ValidateMessage;
url?: ValidateMessage;
hex?: ValidateMessage;
};
string?: {
len?: ValidateMessage;
min?: ValidateMessage;
max?: ValidateMessage;
range?: ValidateMessage;
};
number?: {
len?: ValidateMessage;
min?: ValidateMessage;
max?: ValidateMessage;
range?: ValidateMessage;
};
array?: {
len?: ValidateMessage;
min?: ValidateMessage;
max?: ValidateMessage;
range?: ValidateMessage;
};
pattern?: {
mismatch?: ValidateMessage;
};
}
export {};
+1
View File
@@ -0,0 +1 @@
export {};
+13
View File
@@ -0,0 +1,13 @@
type BaseNamePath = string | number | boolean | (string | number | boolean)[];
/**
* Store: The store type from `FormInstance<Store>`
* ParentNamePath: Auto generate by nest logic. Do not fill manually.
*/
export type DeepNamePath<Store = any, ParentNamePath extends any[] = []> = ParentNamePath['length'] extends 5 ? never : true extends (Store extends BaseNamePath ? true : false) ? ParentNamePath['length'] extends 0 ? Store | BaseNamePath : Store extends any[] ? [...ParentNamePath, number] : never : Store extends any[] ? // Connect path. e.g. { a: { b: string }[] }
[
...ParentNamePath,
number
] | DeepNamePath<Store[number], [...ParentNamePath, number]> : keyof Store extends never ? Store : {
[FieldKey in keyof Store]: Store[FieldKey] extends Function ? never : (ParentNamePath['length'] extends 0 ? FieldKey : never) | [...ParentNamePath, FieldKey] | DeepNamePath<Required<Store>[FieldKey], [...ParentNamePath, FieldKey]>;
}[keyof Store];
export {};
+1
View File
@@ -0,0 +1 @@
export {};
+19
View File
@@ -0,0 +1,19 @@
import type { InternalNamePath } from '../interface';
interface KV<T> {
key: InternalNamePath;
value: T;
}
/**
* NameMap like a `Map` but accepts `string[]` as key.
*/
declare class NameMap<T> {
private kvs;
set(key: InternalNamePath, value: T): void;
get(key: InternalNamePath): T;
getAsPrefix(key: InternalNamePath): T[];
update(key: InternalNamePath, updater: (origin: T) => T | null): void;
delete(key: InternalNamePath): void;
map<U>(callback: (kv: KV<T>) => U): U[];
toJSON(): Record<string, T>;
}
export default NameMap;
+76
View File
@@ -0,0 +1,76 @@
const SPLIT = '__@field_split__';
/**
* Convert name path into string to fast the fetch speed of Map.
*/
function normalize(namePath) {
return namePath.map(cell => `${typeof cell}:${cell}`)
// Magic split
.join(SPLIT);
}
/**
* NameMap like a `Map` but accepts `string[]` as key.
*/
class NameMap {
kvs = new Map();
set(key, value) {
this.kvs.set(normalize(key), value);
}
get(key) {
return this.kvs.get(normalize(key));
}
getAsPrefix(key) {
const normalizedKey = normalize(key);
const normalizedPrefix = normalizedKey + SPLIT;
const results = [];
const current = this.kvs.get(normalizedKey);
if (current !== undefined) {
results.push(current);
}
this.kvs.forEach((value, itemNormalizedKey) => {
if (itemNormalizedKey.startsWith(normalizedPrefix)) {
results.push(value);
}
});
return results;
}
update(key, updater) {
const origin = this.get(key);
const next = updater(origin);
if (!next) {
this.delete(key);
} else {
this.set(key, next);
}
}
delete(key) {
this.kvs.delete(normalize(key));
}
// Since we only use this in test, let simply realize this
map(callback) {
return [...this.kvs.entries()].map(([key, value]) => {
const cells = key.split(SPLIT);
return callback({
key: cells.map(cell => {
const [, type, unit] = cell.match(/^([^:]*):(.*)$/);
return type === 'number' ? Number(unit) : unit;
}),
value
});
});
}
toJSON() {
const json = {};
this.map(({
key,
value
}) => {
json[key.join('.')] = value;
return null;
});
return json;
}
}
export default NameMap;
@@ -0,0 +1,2 @@
import type { FieldError } from '../interface';
export declare function allPromiseFinish(promiseList: Promise<FieldError>[]): Promise<FieldError[]>;
+26
View File
@@ -0,0 +1,26 @@
export function allPromiseFinish(promiseList) {
let hasError = false;
let count = promiseList.length;
const results = [];
if (!promiseList.length) {
return Promise.resolve([]);
}
return new Promise((resolve, reject) => {
promiseList.forEach((promise, index) => {
promise.catch(e => {
hasError = true;
return e;
}).then(result => {
count -= 1;
results[index] = result;
if (count > 0) {
return;
}
if (hasError) {
reject(results);
}
resolve(results);
});
});
});
}
@@ -0,0 +1 @@
export default function delayFrame(): Promise<void>;
+11
View File
@@ -0,0 +1,11 @@
import { macroTask } from "../hooks/useNotifyWatch";
import raf from "@rc-component/util/es/raf";
export default async function delayFrame() {
return new Promise(resolve => {
macroTask(() => {
raf(() => {
resolve();
});
});
});
}
+48
View File
@@ -0,0 +1,48 @@
export declare const defaultValidateMessages: {
default: string;
required: string;
enum: string;
whitespace: string;
date: {
format: string;
parse: string;
invalid: string;
};
types: {
string: string;
method: string;
array: string;
object: string;
number: string;
date: string;
boolean: string;
integer: string;
float: string;
regexp: string;
email: string;
tel: string;
url: string;
hex: string;
};
string: {
len: string;
min: string;
max: string;
range: string;
};
number: {
len: string;
min: string;
max: string;
range: string;
};
array: {
len: string;
min: string;
max: string;
range: string;
};
pattern: {
mismatch: string;
};
};
+49
View File
@@ -0,0 +1,49 @@
const typeTemplate = "'${name}' is not a valid ${type}";
export const defaultValidateMessages = {
default: "Validation error on field '${name}'",
required: "'${name}' is required",
enum: "'${name}' must be one of [${enum}]",
whitespace: "'${name}' cannot be empty",
date: {
format: "'${name}' is invalid for format date",
parse: "'${name}' could not be parsed as date",
invalid: "'${name}' is invalid date"
},
types: {
string: typeTemplate,
method: typeTemplate,
array: typeTemplate,
object: typeTemplate,
number: typeTemplate,
date: typeTemplate,
boolean: typeTemplate,
integer: typeTemplate,
float: typeTemplate,
regexp: typeTemplate,
email: typeTemplate,
tel: typeTemplate,
url: typeTemplate,
hex: typeTemplate
},
string: {
len: "'${name}' must be exactly ${len} characters",
min: "'${name}' must be at least ${min} characters",
max: "'${name}' cannot be longer than ${max} characters",
range: "'${name}' must be between ${min} and ${max} characters"
},
number: {
len: "'${name}' must equal ${len}",
min: "'${name}' cannot be less than ${min}",
max: "'${name}' cannot be greater than ${max}",
range: "'${name}' must be between ${min} and ${max}"
},
array: {
len: "'${name}' must be exactly ${len} in length",
min: "'${name}' cannot be less than ${min} in length",
max: "'${name}' cannot be greater than ${max} in length",
range: "'${name}' must be between ${min} and ${max} in length"
},
pattern: {
mismatch: "'${name}' does not match pattern ${pattern}"
}
};
@@ -0,0 +1,3 @@
import type { FormInstance } from '../interface';
export declare function toArray<T>(value?: T | T[] | null): T[];
export declare function isFormInstance<T>(form: T | FormInstance): form is FormInstance;
+9
View File
@@ -0,0 +1,9 @@
export function toArray(value) {
if (value === undefined || value === null) {
return [];
}
return Array.isArray(value) ? value : [value];
}
export function isFormInstance(form) {
return form && !!form._init;
}
@@ -0,0 +1,6 @@
import type { InternalNamePath, InternalValidateOptions, RuleObject, StoreValue, RuleError } from '../interface';
/**
* We use `async-validator` to validate the value.
* But only check one value in a time to avoid namePath validate issue.
*/
export declare function validateRules(namePath: InternalNamePath, value: StoreValue, rules: RuleObject[], options: InternalValidateOptions, validateFirst: boolean | 'parallel', messageVariables?: Record<string, string>): Promise<RuleError[]>;
@@ -0,0 +1,227 @@
import RawAsyncValidator from '@rc-component/async-validator';
import * as React from 'react';
import warning from "@rc-component/util/es/warning";
import { defaultValidateMessages } from "./messages";
import { merge } from "@rc-component/util/es/utils/set";
// Remove incorrect original ts define
const AsyncValidator = RawAsyncValidator;
/**
* Replace with template.
* `I'm ${name}` + { name: 'bamboo' } = I'm bamboo
*/
function replaceMessage(template, kv) {
return template.replace(/\\?\$\{\w+\}/g, str => {
if (str.startsWith('\\')) {
return str.slice(1);
}
const key = str.slice(2, -1);
return kv[key];
});
}
const CODE_LOGIC_ERROR = 'CODE_LOGIC_ERROR';
async function validateRule(name, value, rule, options, messageVariables) {
const cloneRule = {
...rule
};
// Bug of `async-validator`
// https://github.com/react-component/field-form/issues/316
// https://github.com/react-component/field-form/issues/313
delete cloneRule.ruleIndex;
// https://github.com/ant-design/ant-design/issues/40497#issuecomment-1422282378
AsyncValidator.warning = () => void 0;
if (cloneRule.validator) {
const originValidator = cloneRule.validator;
cloneRule.validator = (...args) => {
try {
return originValidator(...args);
} catch (error) {
console.error(error);
return Promise.reject(CODE_LOGIC_ERROR);
}
};
}
// We should special handle array validate
let subRuleField = null;
if (cloneRule && cloneRule.type === 'array' && cloneRule.defaultField) {
subRuleField = cloneRule.defaultField;
delete cloneRule.defaultField;
}
const validator = new AsyncValidator({
[name]: [cloneRule]
});
const messages = merge(defaultValidateMessages, options.validateMessages);
validator.messages(messages);
let result = [];
try {
await Promise.resolve(validator.validate({
[name]: value
}, {
...options
}));
} catch (errObj) {
if (errObj.errors) {
result = errObj.errors.map(({
message
}, index) => {
const mergedMessage = message === CODE_LOGIC_ERROR ? messages.default : message;
return /*#__PURE__*/React.isValidElement(mergedMessage) ?
/*#__PURE__*/
// Wrap ReactNode with `key`
React.cloneElement(mergedMessage, {
key: `error_${index}`
}) : mergedMessage;
});
}
}
if (!result.length && subRuleField && Array.isArray(value) && value.length > 0) {
const subResults = await Promise.all(value.map((subValue, i) => validateRule(`${name}.${i}`, subValue, subRuleField, options, messageVariables)));
return subResults.reduce((prev, errors) => [...prev, ...errors], []);
}
// Replace message with variables
const kv = {
...rule,
name,
enum: (rule.enum || []).join(', '),
...messageVariables
};
const fillVariableResult = result.map(error => {
if (typeof error === 'string') {
return replaceMessage(error, kv);
}
return error;
});
return fillVariableResult;
}
/**
* We use `async-validator` to validate the value.
* But only check one value in a time to avoid namePath validate issue.
*/
export function validateRules(namePath, value, rules, options, validateFirst, messageVariables) {
const name = namePath.join('.');
// Fill rule with context
const filledRules = rules.map((currentRule, ruleIndex) => {
const originValidatorFunc = currentRule.validator;
const cloneRule = {
...currentRule,
ruleIndex
};
// Replace validator if needed
if (originValidatorFunc) {
cloneRule.validator = (rule, val, callback) => {
let hasPromise = false;
// Wrap callback only accept when promise not provided
const wrappedCallback = (...args) => {
// Wait a tick to make sure return type is a promise
Promise.resolve().then(() => {
warning(!hasPromise, 'Your validator function has already return a promise. `callback` will be ignored.');
if (!hasPromise) {
callback(...args);
}
});
};
// Get promise
const promise = originValidatorFunc(rule, val, wrappedCallback);
hasPromise = promise && typeof promise.then === 'function' && typeof promise.catch === 'function';
/**
* 1. Use promise as the first priority.
* 2. If promise not exist, use callback with warning instead
*/
warning(hasPromise, '`callback` is deprecated. Please return a promise instead.');
if (hasPromise) {
promise.then(() => {
callback();
}).catch(err => {
callback(err || ' ');
});
}
};
}
return cloneRule;
}).sort(({
warningOnly: w1,
ruleIndex: i1
}, {
warningOnly: w2,
ruleIndex: i2
}) => {
if (!!w1 === !!w2) {
// Let keep origin order
return i1 - i2;
}
if (w1) {
return 1;
}
return -1;
});
// Do validate rules
let summaryPromise;
if (validateFirst === true) {
// >>>>> Validate by serialization
summaryPromise = new Promise(async (resolve, reject) => {
/* eslint-disable no-await-in-loop */
for (let i = 0; i < filledRules.length; i += 1) {
const rule = filledRules[i];
const errors = await validateRule(name, value, rule, options, messageVariables);
if (errors.length) {
reject([{
errors,
rule
}]);
return;
}
}
/* eslint-enable */
resolve([]);
});
} else {
// >>>>> Validate by parallel
const rulePromises = filledRules.map(rule => validateRule(name, value, rule, options, messageVariables).then(errors => ({
errors,
rule
})));
summaryPromise = (validateFirst ? finishOnFirstFailed(rulePromises) : finishOnAllFailed(rulePromises)).then(errors => {
// Always change to rejection for Field to catch
return Promise.reject(errors);
});
}
// Internal catch error to avoid console error log.
summaryPromise.catch(e => e);
return summaryPromise;
}
async function finishOnAllFailed(rulePromises) {
return Promise.all(rulePromises).then(errorsList => {
const errors = [].concat(...errorsList);
return errors;
});
}
async function finishOnFirstFailed(rulePromises) {
let count = 0;
return new Promise(resolve => {
rulePromises.forEach(promise => {
promise.then(ruleError => {
if (ruleError.errors.length) {
resolve([ruleError]);
}
count += 1;
if (count === rulePromises.length) {
resolve([]);
}
});
});
});
}
@@ -0,0 +1,45 @@
import getValue from '@rc-component/util/lib/utils/get';
import setValue from '@rc-component/util/lib/utils/set';
import type { InternalNamePath, NamePath, Store, EventArgs } from '../interface';
export { getValue, setValue };
/**
* Convert name to internal supported format.
* This function should keep since we still thinking if need support like `a.b.c` format.
* 'a' => ['a']
* 123 => [123]
* ['a', 123] => ['a', 123]
*/
export declare function getNamePath(path: NamePath | null): InternalNamePath;
/**
* Create a new store object that contains only the values referenced by
* the provided list of name paths.
*/
export declare function cloneByNamePathList(store: Store, namePathList: InternalNamePath[]): Store;
/**
* Check if `namePathList` includes `namePath`.
* @param namePathList A list of `InternalNamePath[]`
* @param namePath Compare `InternalNamePath`
* @param partialMatch True will make `[a, b]` match `[a, b, c]`
*/
export declare function containsNamePath(namePathList: InternalNamePath[], namePath: InternalNamePath, partialMatch?: boolean): boolean;
/**
* Check if `namePath` is super set or equal of `subNamePath`.
* @param namePath A list of `InternalNamePath[]`
* @param subNamePath Compare `InternalNamePath`
* @param partialMatch Default false. True will make `[a, b]` match `[a, b, c]`
*/
export declare function matchNamePath(namePath: InternalNamePath, subNamePath: InternalNamePath | null, partialMatch?: boolean): boolean;
type SimilarObject = string | number | object;
export declare function isSimilar(source: SimilarObject, target: SimilarObject): boolean;
export declare function defaultGetValueFromEvent(valuePropName: string, ...args: EventArgs): any;
/**
* Moves an array item from one position in an array to another.
*
* Note: This is a pure function so a new array will be returned, instead
* of altering the array argument.
*
* @param array Array in which to move an item. (required)
* @param moveIndex The index of the item to move. (required)
* @param toIndex The index to move item at moveIndex to. (required)
*/
export declare function move<T>(array: T[], moveIndex: number, toIndex: number): T[];
+116
View File
@@ -0,0 +1,116 @@
import getValue from "@rc-component/util/es/utils/get";
import setValue from "@rc-component/util/es/utils/set";
import { toArray } from "./typeUtil";
export { getValue, setValue };
/**
* Convert name to internal supported format.
* This function should keep since we still thinking if need support like `a.b.c` format.
* 'a' => ['a']
* 123 => [123]
* ['a', 123] => ['a', 123]
*/
export function getNamePath(path) {
return toArray(path);
}
/**
* Create a new store object that contains only the values referenced by
* the provided list of name paths.
*/
export function cloneByNamePathList(store, namePathList) {
let newStore = {};
namePathList.forEach(namePath => {
const value = getValue(store, namePath);
newStore = setValue(newStore, namePath, value);
});
return newStore;
}
/**
* Check if `namePathList` includes `namePath`.
* @param namePathList A list of `InternalNamePath[]`
* @param namePath Compare `InternalNamePath`
* @param partialMatch True will make `[a, b]` match `[a, b, c]`
*/
export function containsNamePath(namePathList, namePath, partialMatch = false) {
return namePathList && namePathList.some(path => matchNamePath(namePath, path, partialMatch));
}
/**
* Check if `namePath` is super set or equal of `subNamePath`.
* @param namePath A list of `InternalNamePath[]`
* @param subNamePath Compare `InternalNamePath`
* @param partialMatch Default false. True will make `[a, b]` match `[a, b, c]`
*/
export function matchNamePath(namePath, subNamePath, partialMatch = false) {
if (!namePath || !subNamePath) {
return false;
}
if (!partialMatch && namePath.length !== subNamePath.length) {
return false;
}
return subNamePath.every((nameUnit, i) => namePath[i] === nameUnit);
}
// Like `shallowEqual`, but we not check the data which may cause re-render
export function isSimilar(source, target) {
if (source === target) {
return true;
}
if (!source && target || source && !target) {
return false;
}
if (!source || !target || typeof source !== 'object' || typeof target !== 'object') {
return false;
}
const sourceKeys = Object.keys(source);
const targetKeys = Object.keys(target);
const keys = new Set([...sourceKeys, ...targetKeys]);
return [...keys].every(key => {
const sourceValue = source[key];
const targetValue = target[key];
if (typeof sourceValue === 'function' && typeof targetValue === 'function') {
return true;
}
return sourceValue === targetValue;
});
}
export function defaultGetValueFromEvent(valuePropName, ...args) {
const event = args[0];
if (event && event.target && typeof event.target === 'object' && valuePropName in event.target) {
return event.target[valuePropName];
}
return event;
}
/**
* Moves an array item from one position in an array to another.
*
* Note: This is a pure function so a new array will be returned, instead
* of altering the array argument.
*
* @param array Array in which to move an item. (required)
* @param moveIndex The index of the item to move. (required)
* @param toIndex The index to move item at moveIndex to. (required)
*/
export function move(array, moveIndex, toIndex) {
const {
length
} = array;
if (moveIndex < 0 || moveIndex >= length || toIndex < 0 || toIndex >= length) {
return array;
}
const item = array[moveIndex];
const diff = moveIndex - toIndex;
if (diff > 0) {
// move left
return [...array.slice(0, toIndex), item, ...array.slice(toIndex, moveIndex), ...array.slice(moveIndex + 1, length)];
}
if (diff < 0) {
// move right
return [...array.slice(0, moveIndex), ...array.slice(moveIndex + 1, toIndex + 1), item, ...array.slice(toIndex + 1, length)];
}
return array;
}