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
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019-present react-component
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+286
View File
@@ -0,0 +1,286 @@
# rc-field-form
React Performance First Form Component.
[![NPM version][npm-image]][npm-url] [![dumi](https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square)](https://github.com/umijs/dumi) [![build status][github-actions-image]][github-actions-url] [![Codecov][codecov-image]][codecov-url] [![npm download][download-image]][download-url]
[npm-image]: http://img.shields.io/npm/v/rc-field-form.svg?style=flat-square
[npm-url]: http://npmjs.org/package/rc-field-form
[github-actions-image]: https://github.com/react-component/field-form/actions/workflows/main.yml/badge.svg
[github-actions-url]: https://github.com/react-component/field-form/actions/workflows/main.yml
[codecov-image]: https://img.shields.io/codecov/c/github/react-component/field-form/master.svg?style=flat-square
[codecov-url]: https://codecov.io/gh/react-component/field-form/branch/master
[download-image]: https://img.shields.io/npm/dm/rc-field-form.svg?style=flat-square
[download-url]: https://npmjs.org/package/rc-field-form
## Development
```bash
npm install
npm start
open http://localhost:8000
```
## Feature
- Support react.js and even react-native
- Validate fields with [@rc-component/async-validator](https://github.com/react-component/async-validator/)
## Install
[![rc-field-form](https://nodei.co/npm/rc-field-form.png)](https://npmjs.org/package/rc-field-form)
## Usage
```js | pure
import Form, { Field } from 'rc-field-form';
const Input = ({ value = '', ...props }) => <input value={value} {...props} />;
const Demo = () => {
return (
<Form
onFinish={values => {
console.log('Finish:', values);
}}
>
<Field name="username">
<Input placeholder="Username" />
</Field>
<Field name="password">
<Input placeholder="Password" />
</Field>
<button>Submit</button>
</Form>
);
};
export default Demo;
```
## 🔥 API
We use typescript to create the Type definition. You can view directly in IDE. But you can still check the type definition [here](https://github.com/react-component/field-form/blob/master/src/interface.ts).
### Form
| Prop | Description | Type | Default |
| ---------------- | -------------------------------------------------- | -------------------------------------------- | ---------------- |
| component | Customize Form render component | string \| Component \| false | form |
| fields | Control Form fields status. Only use when in Redux | [FieldData](#fielddata)[] | - |
| form | Set form instance created by `useForm` | [FormInstance](#useform) | `Form.useForm()` |
| initialValues | Initial value of Form | Object | - |
| name | Config name with [FormProvider](#formprovider) | string | - |
| preserve | Preserve value when field removed | boolean | false |
| validateMessages | Set validate message template | [ValidateMessages](#validatemessages) | - |
| onFieldsChange | Trigger when any value of Field changed | (changedFields, allFields) => void | - |
| onFinish | Trigger when form submit and success | (values) => void | - |
| onFinishFailed | Trigger when form submit and failed | ({ values, errorFields, outOfDate }) => void | - |
| onValuesChange | Trigger when any value of Field changed | (changedValues, values) => void | - |
### Field
| Prop | Description | Type | Default |
| ----------------- | ----------------------------------------------------------------------------- | ---------------------------------------------- | -------- |
| dependencies | Will re-render if dependencies changed | [NamePath](#namepath)[] | - |
| getValueFromEvent | Specify how to get value from event | (..args: any[]) => any | - |
| getValueProps | Customize additional props with value. This prop will disable `valuePropName` | (value) => any | - |
| initialValue | Field initial value | any | - |
| name | Field name path | [NamePath](#namepath) | - |
| normalize | Normalize value before update | (value, prevValue, prevValues) => any | - |
| preserve | Preserve value when field removed | boolean | false |
| rules | Validate rules | [Rule](#rule)[] | - |
| shouldUpdate | Check if Field should update | boolean \| (prevValues, nextValues) => boolean | - |
| trigger | Collect value update by event trigger | string | onChange |
| validateTrigger | Config trigger point with rule validate | string \| string[] | onChange |
| valuePropName | Config value mapping prop with element | string | value |
### List
| Prop | Description | Type | Default |
| -------- | ------------------------------- | ------------------------------------------------------------------------------------------------------- | ------- |
| name | List field name path | [NamePath](#namepath)[] | - |
| children | Render props for listing fields | (fields: { name: [NamePath](#namepath) }[], operations: [ListOperations](#listoperations)) => ReactNode | - |
### useForm
Form component default create an form instance by `Form.useForm`. But you can create it and pass to Form also. This allow you to call some function on the form instance.
```jsx | pure
const Demo = () => {
const [form] = Form.useForm();
return <Form form={form} />;
};
```
For class component user, you can use `ref` to get form instance:
```jsx | pure
class Demo extends React.Component {
setRef = form => {
// Form instance here
};
render() {
return <Form ref={this.setRef} />;
}
}
```
| Prop | Description | Type |
| ----------------- | ------------------------------------------ | -------------------------------------------------------------------------- |
| getFieldValue | Get field value by name path | (name: [NamePath](#namepath)) => any |
| getFieldsValue | Get list of field values by name path list | (nameList?: ([NamePath](#namepath)[]) => any) \| true |
| getFieldError | Get field errors by name path | (name: [NamePath](#namepath)) => string[] |
| getFieldsError | Get list of field errors by name path list | (nameList?: [NamePath](#namepath)[]) => FieldError[] |
| isFieldsTouched | Check if list of fields are touched | (nameList?: [NamePath](#namepath)[], allTouched?: boolean) => boolean |
| isFieldTouched | Check if a field is touched | (name: [NamePath](#namepath)) => boolean |
| isFieldValidating | Check if a field is validating | (name: [NamePath](#namepath)) => boolean |
| resetFields | Reset fields status | (fields?: [NamePath](#namepath)[]) => void |
| setFields | Set fields status | (fields: FieldData[]) => void |
| setFieldsValue | Set fields value | (values) => void |
| submit | Trigger form submit | () => void |
| validateFields | Trigger fields to validate | (nameList?: [NamePath](#namepath)[], options?: ValidateOptions) => Promise |
### FormProvider
| Prop | Description | Type | Default |
| ---------------- | ----------------------------------------- | ---------------------------------------- | ------- |
| validateMessages | Config global `validateMessages` template | [ValidateMessages](#validatemessages) | - |
| onFormChange | Trigger by named form fields change | (name, { changedFields, forms }) => void | - |
| onFormFinish | Trigger by named form fields finish | (name, { values, forms }) => void | - |
## 📋 Interface
### NamePath
| Type |
| ---------------------------------------- |
| string \| number \| (string \| number)[] |
### FieldData
| Prop | Type |
| ---------- | ---------------------------------------- |
| touched | boolean |
| validating | boolean |
| errors | string[] |
| name | string \| number \| (string \| number)[] |
| value | any |
### Rule
| Prop | Type |
| --------------- | ----------------------------------------------------------------------------------------------- |
| enum | any[] |
| len | number |
| max | number |
| message | string |
| min | number |
| pattern | RegExp |
| required | boolean |
| transform | (value) => any |
| type | string |
| validator | ([rule](#rule), value, callback: (error?: string) => void, [form](#useform)) => Promise \| void |
| whitespace | boolean |
| validateTrigger | string \| string[] |
#### validator
To keep sync with `rc-form` legacy usage of `validator`, we still provides `callback` to trigger validate finished. But in `rc-field-form`, we strongly recommend to return a Promise instead.
### ListOperations
| Prop | Type |
| ------ | ------------------------ |
| add | (initValue: any) => void |
| remove | (index: number) => void |
### ValidateMessages
Validate Messages provides a list of error template. You can ref [here](https://github.com/react-component/field-form/blob/master/src/utils/messages.ts) for fully default templates.
| Prop | Description |
| ------- | ------------------- |
| enum | Rule `enum` prop |
| len | Rule `len` prop |
| max | Rule `max` prop |
| min | Rule `min` prop |
| name | Field name |
| pattern | Rule `pattern` prop |
| type | Rule `type` prop |
## Different with `rc-form`
`rc-field-form` is try to keep sync with `rc-form` in api level, but there still have something to change:
### 1. Field will not keep snyc with `initialValues` when un-touched
In `rc-form`, field value will get from `initialValues` if user not operate on it.
It's a bug but user use as a feature which makes fixing will be a breaking change and we have to keep it.
In Field Form, this bug will not exist anymore. If you want to change a field value, use `setFieldsValue` instead.
### 2. Remove Field will not clean up related value
We do lots of logic to clean up the value when Field removed before. But with user feedback, remove exist value increase the additional work to keep value back with conditional field.
### 3. Nest name use array instead of string
In `rc-form`, we support like `user.name` to be a name and convert value to `{ user: { name: 'Bamboo' } }`. This makes '.' always be the route of variable, this makes developer have to do additional work if name is real contains a point like `app.config.start` to be `app_config_start` and parse back to point when submit.
Field Form will only trade `['user', 'name']` to be `{ user: { name: 'Bamboo' } }`, and `user.name` to be `{ ['user.name']: 'Bamboo' }`.
### 4. Remove `validateFieldsAndScroll`
Since `findDomNode` is marked as warning in [StrictMode](https://reactjs.org/docs/strict-mode.html#warning-about-deprecated-finddomnode-usage). It seems over control of Form component.
We decide to remove `validateFieldsAndScroll` method and you should handle it with you own logic:
```jsx | pure
<Form>
<Field name="username">
<input ref={this.inputRef} />
</Field>
</Form>
```
### 5. `getFieldsError` always return array
`rc-form` returns `null` when no error happen. This makes user have to do some additional code like:
```js | pure
(form.getFieldsError('fieldName') || []).forEach(() => {
// Do something...
});
```
Now `getFieldsError` will return `[]` if no errors.
### 6. Remove `callback` with `validateFields`
Since ES8 is support `async/await`, that's no reason not to use it. Now you can easily handle your validate logic:
```js | pure
async function() {
try {
const values = await form.validateFields();
console.log(values);
} catch (errorList) {
errorList.forEach(({ name, errors }) => {
// Do something...
});
}
}
```
**Notice: Now if your validator return an `Error(message)`, not need to get error by `e => e.message`. FieldForm will handle this.**
### 7. `preserve` is default to false
In `rc-form` you should use `preserve` to keep a value cause Form will auto remove a value from Field removed. Field Form will always keep the value in the Form whatever Field removed. But you can still use `preserve=false` to disable value keeping since `1.5.0`.
### 8. `setFields` not trigger `onFieldsChange` and `setFieldsValue` not trigger `onValuesChange`
In `rc-form`, we hope to help user auto trigger change event by setting to make redux dispatch easier, but it's not good design since it makes code logic couping.
Additionally, user control update trigger `onFieldsChange` & `onValuesChange` event has potential dead loop risk.
+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;
}
+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;
+614
View File
@@ -0,0 +1,614 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _toArray = _interopRequireDefault(require("@rc-component/util/lib/Children/toArray"));
var _isEqual = _interopRequireDefault(require("@rc-component/util/lib/isEqual"));
var _warning = _interopRequireDefault(require("@rc-component/util/lib/warning"));
var React = _interopRequireWildcard(require("react"));
var _FieldContext = _interopRequireWildcard(require("./FieldContext"));
var _ListContext = _interopRequireDefault(require("./ListContext"));
var _typeUtil = require("./utils/typeUtil");
var _validateUtil = require("./utils/validateUtil");
var _valueUtil = require("./utils/valueUtil");
var _delayUtil = _interopRequireDefault(require("./utils/delayUtil"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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); }
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.default;
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(_FieldContext.HOOK_MARK);
initEntityValue(this);
}
}
componentDidMount() {
const {
shouldUpdate,
fieldContext
} = this.props;
this.mounted = true;
// Register on init
if (fieldContext) {
const {
getInternalHooks
} = fieldContext;
const {
registerField
} = getInternalHooks(_FieldContext.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, (0, _valueUtil.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 (!(0, _isEqual.default)(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 && (0, _valueUtil.containsNamePath)(namePathList, namePath);
// `setFieldsValue` is a quick access to update related status
if (info.type === 'valueUpdate' && info.source === 'external' && !(0, _isEqual.default)(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 && (0, _valueUtil.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(_valueUtil.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 => (0, _valueUtil.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 (0, _delayUtil.default)();
}
// Start validate
let filteredRules = this.getRules();
if (triggerName) {
filteredRules = filteredRules.filter(rule => rule).filter(rule => {
const {
validateTrigger
} = rule;
if (!validateTrigger) {
return true;
}
const triggerList = (0, _typeUtil.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 = (0, _validateUtil.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(_FieldContext.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 = (0, _toArray.default)(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 (0, _valueUtil.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(_FieldContext.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 => {
(0, _warning.default)(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 = (0, _valueUtil.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 = (0, _typeUtil.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 {
(0, _warning.default)(!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.default);
const listContext = React.useContext(_ListContext.default);
const namePath = name !== undefined ? (0, _valueUtil.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) {
(0, _warning.default)(false, '`preserve` should not apply on Form.List fields.');
}
return /*#__PURE__*/React.createElement(Field, _extends({
key: key,
name: namePath,
isListField: isMergedListField
}, restProps, {
fieldContext: fieldContext
}));
}
var _default = exports.default = WrapperField;
@@ -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;
+52
View File
@@ -0,0 +1,52 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = exports.HOOK_MARK = void 0;
var _warning = _interopRequireDefault(require("@rc-component/util/lib/warning"));
var React = _interopRequireWildcard(require("react"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const HOOK_MARK = exports.HOOK_MARK = 'RC_FORM_INTERNAL_HOOKS';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const warningFunc = () => {
(0, _warning.default)(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
};
}
});
var _default = exports.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;
+147
View File
@@ -0,0 +1,147 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var React = _interopRequireWildcard(require("react"));
var _useForm = _interopRequireDefault(require("./hooks/useForm"));
var _FieldContext = _interopRequireWildcard(require("./FieldContext"));
var _FormContext = _interopRequireDefault(require("./FormContext"));
var _valueUtil = require("./utils/valueUtil");
var _ListContext = _interopRequireDefault(require("./ListContext"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
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); }
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.default);
// We customize handle event since Context will makes all the consumer re-render:
// https://reactjs.org/docs/context.html#contextprovider
const [formInstance] = (0, _useForm.default)(form);
const {
useSubscribe,
setInitialValues,
setCallbacks,
setValidateMessages,
setPreserve,
destroyForm
} = formInstance.getInternalHooks(_FieldContext.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 (!(0, _valueUtil.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.default.Provider, {
value: null
}, /*#__PURE__*/React.createElement(_FieldContext.default.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);
};
var _default = exports.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;
+73
View File
@@ -0,0 +1,73 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = exports.FormProvider = void 0;
var React = _interopRequireWildcard(require("react"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
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);
};
exports.FormProvider = FormProvider;
var _default = exports.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;
+153
View File
@@ -0,0 +1,153 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var React = _interopRequireWildcard(require("react"));
var _warning = _interopRequireDefault(require("@rc-component/util/lib/warning"));
var _FieldContext = _interopRequireDefault(require("./FieldContext"));
var _Field = _interopRequireDefault(require("./Field"));
var _valueUtil = require("./utils/valueUtil");
var _ListContext = _interopRequireDefault(require("./ListContext"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function List({
name,
initialValue,
children,
rules,
validateTrigger,
isListField
}) {
const context = React.useContext(_FieldContext.default);
const wrapperListContext = React.useContext(_ListContext.default);
const keyRef = React.useRef({
keys: [],
id: 0
});
const keyManager = keyRef.current;
const prefixName = React.useMemo(() => {
const parentPrefixName = (0, _valueUtil.getNamePath)(context.prefixName) || [];
return [...parentPrefixName, ...(0, _valueUtil.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') {
(0, _warning.default)(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.default.Provider, {
value: listContext
}, /*#__PURE__*/React.createElement(_FieldContext.default.Provider, {
value: fieldContext
}, /*#__PURE__*/React.createElement(_Field.default, {
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)) {
(0, _warning.default)(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 = (0, _valueUtil.move)(keyManager.keys, from, to);
// Trigger store change
onChange((0, _valueUtil.move)(newValue, from, to));
}
};
let listValue = value || [];
if (!Array.isArray(listValue)) {
listValue = [];
if (process.env.NODE_ENV !== 'production') {
(0, _warning.default)(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);
})));
}
var _default = exports.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;
+11
View File
@@ -0,0 +1,11 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var React = _interopRequireWildcard(require("react"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
const ListContext = /*#__PURE__*/React.createContext(null);
var _default = exports.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;
+930
View File
@@ -0,0 +1,930 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = exports.FormStore = void 0;
var _set = require("@rc-component/util/lib/utils/set");
var _warning = _interopRequireDefault(require("@rc-component/util/lib/warning"));
var React = _interopRequireWildcard(require("react"));
var _FieldContext = require("../FieldContext");
var _asyncUtil = require("../utils/asyncUtil");
var _messages = require("../utils/messages");
var _NameMap = _interopRequireDefault(require("../utils/NameMap"));
var _valueUtil = require("../utils/valueUtil");
var _useNotifyWatch = _interopRequireDefault(require("./useNotifyWatch"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
class FormStore {
formHooked = false;
forceRootUpdate;
subscribable = true;
store = {};
fieldEntities = [];
initialValues = {};
callbacks = {};
validateMessages = null;
preserve = null;
lastValidatePromise = null;
watcherCenter = new _useNotifyWatch.default(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 === _FieldContext.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
};
}
(0, _warning.default)(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 = (0, _set.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 = (0, _valueUtil.setValue)(nextStore, namePath, (0, _valueUtil.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.default();
this.getFieldEntities(true).forEach(entity => {
if (!this.isMergedPreserve(entity.isPreserve())) {
prevWithoutPreserves.set(entity.getNamePath(), true);
}
});
this.prevWithoutPreserves = prevWithoutPreserves;
}
};
getInitialValue = namePath => {
const initValue = (0, _valueUtil.getValue)(this.initialValues, namePath);
// Not cloneDeep when without `namePath`
return namePath.length ? (0, _set.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) {
(0, _warning.default)(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.default();
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 = (0, _valueUtil.getNamePath)(name);
return cache.get(namePath) || {
INVALIDATE_NAME_PATH: (0, _valueUtil.getNamePath)(name)
};
});
}
return nameList.flatMap(name => {
const namePath = (0, _valueUtil.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 = (0, _valueUtil.cloneByNamePathList)(this.store, filteredNameList.map(_valueUtil.getNamePath));
// We need fill the list as [] if Form.List is empty
listNamePaths.forEach(namePath => {
if (!(0, _valueUtil.getValue)(mergedValues, namePath)) {
mergedValues = (0, _valueUtil.setValue)(mergedValues, namePath, []);
}
});
return mergedValues;
};
getFieldValue = name => {
this.warningUnhooked();
const namePath = (0, _valueUtil.getNamePath)(name);
return (0, _valueUtil.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: (0, _valueUtil.getNamePath)(nameList[index]),
errors: [],
warnings: []
};
});
};
getFieldError = name => {
this.warningUnhooked();
const namePath = (0, _valueUtil.getNamePath)(name);
const fieldError = this.getFieldsError([namePath])[0];
return fieldError.errors;
};
getFieldWarning = name => {
this.warningUnhooked();
const namePath = (0, _valueUtil.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(_valueUtil.getNamePath);
isAllFieldsTouched = false;
} else {
namePathList = null;
isAllFieldsTouched = arg0;
}
} else {
namePathList = arg0.map(_valueUtil.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.default();
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(_valueUtil.getNamePath);
return fieldEntities.some(testField => {
const fieldNamePath = testField.getNamePath();
return (0, _valueUtil.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.default();
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
(0, _warning.default)(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
(0, _warning.default)(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((0, _valueUtil.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((0, _set.merge)(this.initialValues));
this.resetWithFieldInitialValue();
this.notifyObservers(prevStore, null, {
type: 'reset'
});
this.notifyWatch();
return;
}
// Reset by `nameList`
const namePathList = nameList.map(_valueUtil.getNamePath);
namePathList.forEach(namePath => {
const initialValue = this.getInitialValue(namePath);
this.updateStore((0, _valueUtil.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 = (0, _valueUtil.getNamePath)(name);
namePathList.push(namePath);
// Value
if ('value' in data) {
this.updateStore((0, _valueUtil.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 = (0, _valueUtil.getValue)(this.store, namePath);
if (prevValue === undefined) {
this.updateStore((0, _valueUtil.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
!(0, _valueUtil.matchNamePath)(field.getNamePath(), namePath))) {
const prevStore = this.store;
this.updateStore((0, _valueUtil.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 = (0, _valueUtil.getNamePath)(name);
const prevStore = this.store;
this.updateStore((0, _valueUtil.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 = (0, _valueUtil.cloneByNamePathList)(this.store, [namePath]);
const allValues = this.getFieldsValue();
const mergedAllValues = (0, _valueUtil.setValue)(allValues, namePath, (0, _valueUtil.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 = (0, _set.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.default();
/**
* 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 = (0, _valueUtil.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.default();
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
}) => (0, _valueUtil.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(_valueUtil.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 => (0, _valueUtil.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 || (0, _valueUtil.containsNamePath)(namePathList, fieldNamePath, recursive)) {
const promise = field.validateRules({
validateMessages: {
..._messages.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 = (0, _asyncUtil.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);
}
});
};
}
exports.FormStore = FormStore;
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];
}
var _default = exports.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,56 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.macroTask = exports.default = void 0;
var _valueUtil = require("../utils/valueUtil");
/**
* Call action with delay in macro task.
*/
const macroTask = fn => {
const channel = new MessageChannel();
channel.port1.onmessage = fn;
channel.port2.postMessage(null);
};
exports.macroTask = macroTask;
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 => !(0, _valueUtil.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 = [];
}
});
}
}
exports.default = WatcherCenter;
@@ -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;
+95
View File
@@ -0,0 +1,95 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
exports.stringify = stringify;
var _warning = _interopRequireDefault(require("@rc-component/util/lib/warning"));
var _react = require("react");
var _FieldContext = _interopRequireWildcard(require("../FieldContext"));
var _typeUtil = require("../utils/typeUtil");
var _valueUtil = require("../utils/valueUtil");
var _util = require("@rc-component/util");
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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 = (0, _typeUtil.isFormInstance)(_form) ? {
form: _form
} : _form;
const form = options.form;
const [value, setValue] = (0, _react.useState)(() => typeof dependencies === 'function' ? dependencies({}) : undefined);
const valueStr = (0, _react.useMemo)(() => stringify(value), [value]);
const valueStrRef = (0, _react.useRef)(valueStr);
valueStrRef.current = valueStr;
const fieldContext = (0, _react.useContext)(_FieldContext.default);
const formInstance = form || fieldContext;
const isValidForm = formInstance && formInstance._init;
// Warning if not exist form instance
if (process.env.NODE_ENV !== 'production') {
(0, _warning.default)(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(_FieldContext.HOOK_MARK);
// ============================= Update =============================
const triggerUpdate = (0, _util.useEvent)((values, allValues) => {
const watchValue = options.preserve ? allValues ?? getFieldsValue(true) : values ?? getFieldsValue();
const nextValue = typeof dependencies === 'function' ? dependencies(watchValue) : (0, _valueUtil.getValue)(watchValue, (0, _valueUtil.getNamePath)(dependencies));
if (stringify(value) !== stringify(nextValue)) {
setValue(nextValue);
}
});
// ============================= Effect =============================
const flattenDeps = typeof dependencies === 'function' ? dependencies : JSON.stringify(dependencies);
// Deps changed
(0, _react.useEffect)(() => {
// Skip if not exist form instance
if (!isValidForm) {
return;
}
triggerUpdate();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isValidForm, flattenDeps]);
// Value changed
(0, _react.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;
}
var _default = exports.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;
+68
View File
@@ -0,0 +1,68 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "Field", {
enumerable: true,
get: function () {
return _Field.default;
}
});
Object.defineProperty(exports, "FieldContext", {
enumerable: true,
get: function () {
return _FieldContext.default;
}
});
Object.defineProperty(exports, "FormProvider", {
enumerable: true,
get: function () {
return _FormContext.FormProvider;
}
});
Object.defineProperty(exports, "List", {
enumerable: true,
get: function () {
return _List.default;
}
});
Object.defineProperty(exports, "ListContext", {
enumerable: true,
get: function () {
return _ListContext.default;
}
});
exports.default = void 0;
Object.defineProperty(exports, "useForm", {
enumerable: true,
get: function () {
return _useForm.default;
}
});
Object.defineProperty(exports, "useWatch", {
enumerable: true,
get: function () {
return _useWatch.default;
}
});
var React = _interopRequireWildcard(require("react"));
var _Field = _interopRequireDefault(require("./Field"));
var _List = _interopRequireDefault(require("./List"));
var _useForm = _interopRequireDefault(require("./hooks/useForm"));
var _Form = _interopRequireDefault(require("./Form"));
var _FormContext = require("./FormContext");
var _FieldContext = _interopRequireDefault(require("./FieldContext"));
var _ListContext = _interopRequireDefault(require("./ListContext"));
var _useWatch = _interopRequireDefault(require("./hooks/useWatch"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
const InternalForm = /*#__PURE__*/React.forwardRef(_Form.default);
const RefForm = InternalForm;
RefForm.FormProvider = _FormContext.FormProvider;
RefForm.Field = _Field.default;
RefForm.List = _List.default;
RefForm.useForm = _useForm.default;
RefForm.useWatch = _useWatch.default;
var _default = exports.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 {};
+5
View File
@@ -0,0 +1,5 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
+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 {};
+5
View File
@@ -0,0 +1,5 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
+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;
+82
View File
@@ -0,0 +1,82 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
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;
}
}
var _default = exports.default = NameMap;
@@ -0,0 +1,2 @@
import type { FieldError } from '../interface';
export declare function allPromiseFinish(promiseList: Promise<FieldError>[]): Promise<FieldError[]>;
+32
View File
@@ -0,0 +1,32 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.allPromiseFinish = allPromiseFinish;
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>;
+18
View File
@@ -0,0 +1,18 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = delayFrame;
var _useNotifyWatch = require("../hooks/useNotifyWatch");
var _raf = _interopRequireDefault(require("@rc-component/util/lib/raf"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
async function delayFrame() {
return new Promise(resolve => {
(0, _useNotifyWatch.macroTask)(() => {
(0, _raf.default)(() => {
resolve();
});
});
});
}
@@ -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;
};
};
+55
View File
@@ -0,0 +1,55 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.defaultValidateMessages = void 0;
const typeTemplate = "'${name}' is not a valid ${type}";
const defaultValidateMessages = exports.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;
+16
View File
@@ -0,0 +1,16 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.isFormInstance = isFormInstance;
exports.toArray = toArray;
function toArray(value) {
if (value === undefined || value === null) {
return [];
}
return Array.isArray(value) ? value : [value];
}
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,235 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.validateRules = validateRules;
var _asyncValidator = _interopRequireDefault(require("@rc-component/async-validator"));
var React = _interopRequireWildcard(require("react"));
var _warning = _interopRequireDefault(require("@rc-component/util/lib/warning"));
var _messages = require("./messages");
var _set = require("@rc-component/util/lib/utils/set");
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// Remove incorrect original ts define
const AsyncValidator = _asyncValidator.default;
/**
* 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 = (0, _set.merge)(_messages.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.
*/
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(() => {
(0, _warning.default)(!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
*/
(0, _warning.default)(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[];
+139
View File
@@ -0,0 +1,139 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.cloneByNamePathList = cloneByNamePathList;
exports.containsNamePath = containsNamePath;
exports.defaultGetValueFromEvent = defaultGetValueFromEvent;
exports.getNamePath = getNamePath;
Object.defineProperty(exports, "getValue", {
enumerable: true,
get: function () {
return _get.default;
}
});
exports.isSimilar = isSimilar;
exports.matchNamePath = matchNamePath;
exports.move = move;
Object.defineProperty(exports, "setValue", {
enumerable: true,
get: function () {
return _set.default;
}
});
var _get = _interopRequireDefault(require("@rc-component/util/lib/utils/get"));
var _set = _interopRequireDefault(require("@rc-component/util/lib/utils/set"));
var _typeUtil = require("./typeUtil");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* 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]
*/
function getNamePath(path) {
return (0, _typeUtil.toArray)(path);
}
/**
* Create a new store object that contains only the values referenced by
* the provided list of name paths.
*/
function cloneByNamePathList(store, namePathList) {
let newStore = {};
namePathList.forEach(namePath => {
const value = (0, _get.default)(store, namePath);
newStore = (0, _set.default)(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]`
*/
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]`
*/
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
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;
});
}
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)
*/
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;
}
+84
View File
@@ -0,0 +1,84 @@
{
"name": "@rc-component/form",
"version": "1.8.1",
"description": "React Form Component",
"typings": "es/index.d.ts",
"engines": {
"node": ">=8.x"
},
"keywords": [
"react",
"react-component",
"react-form",
"form"
],
"homepage": "https://github.com/react-component/field-form",
"author": "smith3816@gmail.com",
"repository": {
"type": "git",
"url": "https://github.com/react-component/field-form.git"
},
"bugs": {
"url": "https://github.com/react-component/field-form/issues"
},
"files": [
"lib",
"es",
"dist",
"assets/*.css"
],
"license": "MIT",
"main": "./lib/index",
"module": "./es/index",
"scripts": {
"start": "dumi dev",
"docs:build": "dumi build",
"docs:deploy": "gh-pages -d docs-dist",
"compile": "father build",
"deploy": "npm run docs:build && npm run docs:deploy",
"prettier": "prettier --write \"**/*.{js,jsx,tsx,ts,less,md,json}\"",
"test": "rc-test",
"test:coverage": "umi-test --coverage",
"prepublishOnly": "npm run compile && rc-np",
"lint": "eslint src/ --ext .tsx,.ts",
"lint:tsc": "tsc -p tsconfig.json --noEmit",
"now-build": "npm run docs:build"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
},
"dependencies": {
"@rc-component/async-validator": "^5.1.0",
"@rc-component/util": "^1.6.2",
"clsx": "^2.1.1"
},
"devDependencies": {
"@rc-component/father-plugin": "^2.0.1",
"@rc-component/np": "^1.0.2",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^16.0.0",
"@types/jest": "^29.2.5",
"@types/lodash": "^4.14.135",
"@types/node": "^22.0.2",
"@types/react": "^19.0.6",
"@types/react-dom": "^19.0.1",
"@umijs/fabric": "^4.0.1",
"dumi": "^2.0.0",
"eslint": "^8.54.0",
"eslint-plugin-jest": "^27.6.0",
"eslint-plugin-unicorn": "^56.0.1",
"father": "^4.0.0",
"gh-pages": "^6.1.0",
"jest": "^29.0.0",
"prettier": "^3.1.0",
"rc-test": "^7.1.3",
"react": "^18.0.0",
"react-dnd": "^8.0.3",
"react-dnd-html5-backend": "^8.0.3",
"react-dom": "^18.0.0",
"react-redux": "^9.0.4",
"redux": "^5.0.0",
"typescript": "^5.1.6"
}
}