1
This commit is contained in:
+21
@@ -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
@@ -0,0 +1,286 @@
|
||||
# rc-field-form
|
||||
|
||||
React Performance First Form Component.
|
||||
|
||||
[![NPM version][npm-image]][npm-url] [](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
|
||||
|
||||
[](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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
|
||||
import * as React from 'react';
|
||||
const ListContext = /*#__PURE__*/React.createContext(null);
|
||||
export default ListContext;
|
||||
+104
@@ -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
@@ -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;
|
||||
+16
@@ -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;
|
||||
}
|
||||
+48
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
export {};
|
||||
+13
@@ -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
@@ -0,0 +1 @@
|
||||
export {};
|
||||
+19
@@ -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
@@ -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;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import type { FieldError } from '../interface';
|
||||
export declare function allPromiseFinish(promiseList: Promise<FieldError>[]): Promise<FieldError[]>;
|
||||
+26
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export default function delayFrame(): Promise<void>;
|
||||
+11
@@ -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
@@ -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
@@ -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}"
|
||||
}
|
||||
};
|
||||
+3
@@ -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
@@ -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;
|
||||
}
|
||||
+6
@@ -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[]>;
|
||||
+227
@@ -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([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+45
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
+5
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
+16
@@ -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;
|
||||
}
|
||||
+56
@@ -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;
|
||||
+14
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
+13
@@ -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
@@ -0,0 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
+19
@@ -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
@@ -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;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import type { FieldError } from '../interface';
|
||||
export declare function allPromiseFinish(promiseList: Promise<FieldError>[]): Promise<FieldError[]>;
|
||||
+32
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export default function delayFrame(): Promise<void>;
|
||||
+18
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+48
@@ -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
@@ -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}"
|
||||
}
|
||||
};
|
||||
+3
@@ -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
@@ -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;
|
||||
}
|
||||
+6
@@ -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[]>;
|
||||
+235
@@ -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([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+45
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user