Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"proseWrap": "never"
}
122 changes: 59 additions & 63 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@

React Performance First Form Component.

[![NPM version][npm-image]][npm-url]
[![build status][circleci-image]][circleci-url]
[![Test coverage][coveralls-image]][coveralls-url]
[![node version][node-image]][node-url]
[![npm download][download-image]][download-url]
[![NPM version][npm-image]][npm-url] [![build status][circleci-image]][circleci-url] [![Test coverage][coveralls-image]][coveralls-url] [![node version][node-image]][node-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
Expand Down Expand Up @@ -61,43 +57,40 @@ 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).
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 |
| ---------------- | -------------------------------------------------- | ------------------------------------- | ---------------- |
| 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 | - |
| validateMessages | Set validate message template | [ValidateMessages](#validatemessages) | - |
| onFieldsChange | Trigger when any value of Field changed | (changedFields, allFields): void | - |
| onValuesChange | Trigger when any value of Field changed | (changedValues, values): void | - |
| Prop | Description | Type | Default |
| --- | --- | --- | --- |
| 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 | - |
| validateMessages | Set validate message template | [ValidateMessages](#validatemessages) | - |
| onFieldsChange | Trigger when any value of Field changed | (changedFields, allFields): void | - |
| onValuesChange | Trigger when any value of Field changed | (changedValues, values): void | - |

## Field

| Prop | Description | Type | Default |
| --------------- | --------------------------------------- | --------------------------------- | -------- |
| name | Field name path | [NamePath](#namepath)[] | - |
| rules | Validate rules | [Rule](#rule)[] | - |
| shouldUpdate | Check if Field should update | (prevValues, nextValues): boolean | - |
| trigger | Collect value update by event trigger | string | onChange |
| validateTrigger | Config trigger point with rule validate | string \| string[] | onChange |
| Prop | Description | Type | Default |
| --- | --- | --- | --- |
| name | Field name path | [NamePath](#namepath)[] | - |
| rules | Validate rules | [Rule](#rule)[] | - |
| shouldUpdate | Check if Field should update | (prevValues, nextValues): boolean | - |
| trigger | Collect value update by event trigger | string | onChange |
| validateTrigger | Config trigger point with rule validate | string \| string[] | onChange |

## 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 | - |
| 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.
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
const Demo = () => {
Expand All @@ -120,26 +113,26 @@ class Demo extends React.Component {
}
```

| 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 |
| 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)[]) => 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 |
| validateFields | Trigger fields to validate | (nameList?: [NamePath](#namepath)[], options?: ValidateOptions) => Promise |
| 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 |
| 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)[]) => 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 |
| 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 | - |
| Prop | Description | Type | Default |
| --- | --- | --- | --- |
| validateMessages | Config global `validateMessages` template | [ValidateMessages](#validatemessages) | - |
| onFormChange | Trigger by named form fields change | (name, { changedFields, forms }) => void | - |

## Interface

Expand All @@ -161,20 +154,24 @@ class Demo extends React.Component {

### 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)) => void |
| whitespace | boolean |
| validateTrigger | string \| string[] |
| 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

Expand All @@ -185,8 +182,7 @@ class Demo extends React.Component {

### 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.
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 |
| ------- | ------------------- |
Expand Down
7 changes: 3 additions & 4 deletions examples/StateForm-validate-perf.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,11 @@ export default class Demo extends React.Component {
rules={[
{ required: true },
{
validator(_, value, callback, { getFieldValue }) {
async validator(_, value, __, { getFieldValue }) {
if (getFieldValue('password') !== value) {
callback('password2 is not same as password');
return;
return Promise.reject('password2 is not same as password');
}
callback();
return Promise.resolve();
},
},
]}
Expand Down
4 changes: 2 additions & 2 deletions examples/components/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ const Input = (props: any) => {
return <input {...props} />;
};

const CustomizeInput = (props: any) => (
const CustomizeInput = ({ value = '', ...props }: any) => (
<div style={{ padding: 10 }}>
<Input style={{ outline: 'none' }} {...props} />
<Input style={{ outline: 'none' }} value={value} {...props} />
</div>
);

Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
"build": "father doc build --storybook",
"compile": "rc-tools run compile --babel-runtime",
"gh-pages": "rc-tools run gh-pages",
"start1": "rc-tools run storybook",
"pub": "rc-tools run pub --babel-runtime",
"lint": "eslint src/**/*",
"test": "father test",
Expand All @@ -46,7 +45,7 @@
"enzyme": "^3.1.0",
"enzyme-adapter-react-16": "^1.0.2",
"enzyme-to-json": "^3.1.4",
"father": "^2.6.6",
"father": "^2.7.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-redux": "^4.4.10",
Expand Down
2 changes: 1 addition & 1 deletion src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export interface Rule {
value: any,
callback: (error?: string) => void,
context: FormInstance, // TODO: Maybe not good place to export this?
) => void;
) => Promise<void> | void;
whitespace?: boolean;

/** Customize rule level `validateTrigger`. Must be subset of Field `validateTrigger` */
Expand Down
3 changes: 2 additions & 1 deletion src/useForm.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import warning from 'warning';
import {
Callbacks,
FieldData,
Expand Down Expand Up @@ -89,7 +90,7 @@ export class FormStore {
};
}

console.error('`getInternalHooks` is internal usage. Should not call directly.');
warning(false, '`getInternalHooks` is internal usage. Should not call directly.');
return null;
};

Expand Down
58 changes: 48 additions & 10 deletions src/utils/validateUtil.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AsyncValidator from 'async-validator';
import warning from 'warning';
import {
FieldError,
InternalNamePath,
Expand Down Expand Up @@ -61,7 +62,7 @@ function convertMessages(messages: ValidateMessages, name: string, rule: Rule) {
return fillTemplate(setValues({}, defaultValidateMessages, messages));
}

function validateRule(
async function validateRule(
name: string,
value: any,
rule: Rule,
Expand All @@ -74,14 +75,15 @@ function validateRule(
const messages = convertMessages(options.validateMessages, name, rule);
validator.messages(messages);

return Promise.resolve(validator.validate({ [name]: value }, { ...options }))
.then(() => [])
.catch(errObj => {
if (errObj.errors) {
return errObj.errors.map(e => e.message);
}
return messages.default();
});
try {
await Promise.resolve(validator.validate({ [name]: value }, { ...options }));
return [];
} catch (errObj) {
if (errObj.errors) {
return errObj.errors.map(e => e.message);
}
return messages.default();
}
}

/**
Expand All @@ -105,7 +107,43 @@ export function validateRules(
return {
...currentRule,
validator(rule: any, val: any, callback: any) {
currentRule.validator(rule, val, callback, context);
let hasPromise = false;

// Wrap callback only accept when promise not provided
const wrappedCallback = (...args: string[]) => {
// 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 = currentRule.validator(rule, val, wrappedCallback, context);
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 as Promise<void>)
.then(() => {
callback();
})
.catch(err => {
callback(err);
});
}
},
};
});
Expand Down
31 changes: 31 additions & 0 deletions tests/common/InfoField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React, { ReactElement } from 'react';
import { Field } from '../../src';
import { FieldProps } from '../../src/Field';

interface InfoFieldProps extends FieldProps {
children: ReactElement;
}

/**
* Return a wrapped Field with meta info
*/
const InfoField: React.FC<InfoFieldProps> = ({ children, ...props }) => (
<Field {...props}>
{(control, { errors }) => (
<div>
{children ? (
React.cloneElement(children, control)
) : (
<input {...control} value={control.value || ''} />
)}
<ul className="errors">
{errors.map(error => (
<li key={error}>{error}</li>
))}
</ul>
</div>
)}
</Field>
);

export default InfoField;
19 changes: 19 additions & 0 deletions tests/common/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import timeout from './timeout';

export async function changeValue(wrapper, value) {
wrapper.find('input').simulate('change', { target: { value } });
await timeout();
wrapper.update();
}

export function matchError(wrapper, error) {
if (error) {
expect(wrapper.find('.errors li').length).toBeTruthy();
} else {
expect(wrapper.find('.errors li').length).toBeFalsy();
}

if (error && typeof error !== 'boolean') {
expect(wrapper.find('.errors li').text()).toBe(error);
}
}
5 changes: 5 additions & 0 deletions tests/common/timeout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default (timeout: number = 0) => {
return new Promise(resolve => {
setTimeout(resolve, timeout);
});
};
Loading