Skip to content

Commit f27229f

Browse files
zombieJcrazyair
andauthored
feat: support useWatch (#413)
* feat: init * feat: watch * feat: watch * feat: watch * feat: watch * feat: watch * feat: watch * feat: watch * fix: resetFields * feat: watch * feat: watch * feat: watch * feat: watch * feat: add test * feat: add test * feat: add test * feat: add test * feat: add test * feat: add test * feat: add test * feat: test * feat: test * feat: test * feat: add list * feat: add list * feat: add test * feat: add demo * feat: add demo * feat: api * feat: api * feat: review * feat: watchId * feat: watch * feat: review * feat: review * feat: all values * feat: all values * feat: all values * feat: all values * feat: test * feat: watch * feat: watch * feat: watch * feat: watch * feat: watch * feat: watch * feat: file name * feat: watch * feat: watch * feat: watch * feat: watch * feat: watch * feat: watch * feat: watch * feat: watch * feat: watch * feat: watch * feat: review * feat: review * feat: review * feat: review * feat: review * feat: review * feat: review * feat: add demo * feat: values * feat: remove getRegisterFieldsValue * feat: watch * feat: remove setWatchCallbacks * feat: submit demo * feat: remove ref id * feat: ts * feat: rename * feat: init map * refactor: Internal namePath logic adjust * chore: misc update * fix: not crash if form is not exist * fix: form warning check * chore: adjust init logic * chore: clean up Co-authored-by: crazyair <[email protected]>
1 parent b8a15f5 commit f27229f

File tree

10 files changed

+487
-3
lines changed

10 files changed

+487
-3
lines changed

docs/demo/useWatch-list.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## useWatch-list
2+
3+
<code src="../examples/useWatch-list.tsx" />

docs/demo/useWatch.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## useWatch
2+
3+
<code src="../examples/useWatch.tsx" />

docs/examples/useWatch-list.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React from 'react';
2+
import Form, { Field } from 'rc-field-form';
3+
import Input from './components/Input';
4+
5+
const { List, useForm } = Form;
6+
7+
const Demo = () => {
8+
const [form] = useForm();
9+
const users = Form.useWatch(['users'], form) || [];
10+
11+
console.log('values', users);
12+
13+
return (
14+
<div>
15+
<Form form={form} style={{ border: '1px solid red', padding: 15 }}>
16+
list length:{users.length}
17+
<br />
18+
Users: {JSON.stringify(users, null, 2)}
19+
<Field name="main">
20+
<Input />
21+
</Field>
22+
<List name="users" initialValue={['bamboo', 'light']}>
23+
{(fields, { add, remove }) => {
24+
return (
25+
<div>
26+
{fields.map((field, index) => (
27+
<Field key={field.key} {...field} rules={[{ required: true }]}>
28+
{control => (
29+
<div style={{ display: 'flex', alignItems: 'center' }}>
30+
{index + 1}
31+
<Input {...control} />
32+
<a onClick={() => remove(index)}>Remove</a>
33+
</div>
34+
)}
35+
</Field>
36+
))}
37+
<button type="button" onClick={() => add()}>
38+
+ New User
39+
</button>
40+
</div>
41+
);
42+
}}
43+
</List>
44+
</Form>
45+
</div>
46+
);
47+
};
48+
49+
export default Demo;

docs/examples/useWatch.tsx

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import React, { useState } from 'react';
2+
import Form, { Field } from 'rc-field-form';
3+
import Input from './components/Input';
4+
5+
let x = 0;
6+
7+
const Demo = React.memo(() => {
8+
const values = Form.useWatch(['demo']);
9+
console.log('demo watch', values);
10+
return (
11+
<Field name="demo">
12+
<Input />
13+
</Field>
14+
);
15+
});
16+
const Demo2 = React.memo(() => {
17+
const values = Form.useWatch(['demo2']);
18+
console.log('demo2 watch', values);
19+
return (
20+
<Field name="demo2">
21+
<Input />
22+
</Field>
23+
);
24+
});
25+
26+
export default () => {
27+
const [form] = Form.useForm();
28+
const [visible, setVisible] = useState(true);
29+
const [visible2, setVisible2] = useState(true);
30+
const [visible3, setVisible3] = useState(true);
31+
const values = Form.useWatch([], form);
32+
console.log('main watch', values);
33+
return (
34+
<>
35+
<Form
36+
form={form}
37+
initialValues={{ id: 1, age: '10', name: 'default' }}
38+
onFinish={v => console.log('submit values', v)}
39+
>
40+
no render
41+
<Field name="main">
42+
<Input />
43+
</Field>
44+
name
45+
{visible && (
46+
<Field name="name">
47+
<Input />
48+
</Field>
49+
)}
50+
age
51+
<Field name="age">
52+
<Input />
53+
</Field>
54+
initialValue
55+
{visible3 && (
56+
<Field name="initialValue" initialValue="initialValue">
57+
<Input />
58+
</Field>
59+
)}
60+
name、age 改变 render
61+
<Field dependencies={['field_1']}>
62+
{() => {
63+
x += 1;
64+
return ` ${x}`;
65+
}}
66+
</Field>
67+
<br />
68+
demo1
69+
<Demo />
70+
demo2
71+
{visible2 && <Demo2 />}
72+
<button type="submit">submit</button>
73+
</Form>
74+
<button
75+
onClick={() => {
76+
console.log('values', form.getFieldsValue());
77+
console.log('values all', form.getFieldsValue(true));
78+
}}
79+
>
80+
getFieldsValue
81+
</button>
82+
<button
83+
onClick={() => {
84+
form.setFields([
85+
{ name: 'name', value: 'name' },
86+
{ name: 'age', value: 'age' },
87+
]);
88+
}}
89+
>
90+
setFields
91+
</button>
92+
<button onClick={() => form.resetFields()}>resetFields</button>
93+
<button onClick={() => form.setFieldsValue({ name: `${form.getFieldValue('name') || ''}1` })}>
94+
setFieldsValue
95+
</button>
96+
<button onClick={() => setVisible(c => !c)}>isShow name</button>
97+
<button onClick={() => setVisible3(c => !c)}>isShow initialValue</button>
98+
<button onClick={() => setVisible2(c => !c)}>isShow demo2</button>
99+
</>
100+
);
101+
};

src/FieldContext.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const Context = React.createContext<InternalFormInstance>({
3636
setInitialValues: warningFunc,
3737
destroyForm: warningFunc,
3838
setCallbacks: warningFunc,
39+
registerWatch: warningFunc,
3940
getFields: warningFunc,
4041
setValidateMessages: warningFunc,
4142
setPreserve: warningFunc,

src/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import FieldForm, { FormProps } from './Form';
77
import { FormProvider } from './FormContext';
88
import FieldContext from './FieldContext';
99
import ListContext from './ListContext';
10+
import useWatch from './useWatch';
1011

1112
const InternalForm = React.forwardRef<FormInstance, FormProps>(FieldForm) as <Values = any>(
1213
props: FormProps<Values> & { ref?: React.Ref<FormInstance<Values>> },
@@ -18,6 +19,7 @@ interface RefFormType extends InternalFormType {
1819
Field: typeof Field;
1920
List: typeof List;
2021
useForm: typeof useForm;
22+
useWatch: typeof useWatch;
2123
}
2224

2325
const RefForm: RefFormType = InternalForm as RefFormType;
@@ -26,6 +28,7 @@ RefForm.FormProvider = FormProvider;
2628
RefForm.Field = Field;
2729
RefForm.List = List;
2830
RefForm.useForm = useForm;
31+
RefForm.useWatch = useWatch;
2932

3033
export { FormInstance, Field, List, useForm, FormProvider, FormProps, FieldContext, ListContext };
3134

src/interface.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ export interface Callbacks<Values = any> {
193193
onFinishFailed?: (errorInfo: ValidateErrorEntity<Values>) => void;
194194
}
195195

196+
export type WatchCallBack = (values: Store, namePathList: InternalNamePath[]) => void;
197+
196198
export interface InternalHooks {
197199
dispatch: (action: ReducerAction) => void;
198200
initEntityValue: (entity: FieldEntity) => void;
@@ -201,6 +203,7 @@ export interface InternalHooks {
201203
setInitialValues: (values: Store, init: boolean) => void;
202204
destroyForm: () => void;
203205
setCallbacks: (callbacks: Callbacks) => void;
206+
registerWatch: (callback: WatchCallBack) => () => void;
204207
getFields: (namePathList?: InternalNamePath[]) => FieldData[];
205208
setValidateMessages: (validateMessages: ValidateMessages) => void;
206209
setPreserve: (preserve?: boolean) => void;
@@ -255,6 +258,9 @@ export type InternalFormInstance = Omit<FormInstance, 'validateFields'> & {
255258
* We pass the `HOOK_MARK` as key to avoid user call the function.
256259
*/
257260
getInternalHooks: (secret: string) => InternalHooks | null;
261+
262+
/** @private Internal usage. Do not use it in your production */
263+
_init?: boolean;
258264
};
259265

260266
// eslint-disable-next-line @typescript-eslint/no-explicit-any

src/useForm.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type {
2121
InternalFieldData,
2222
ValuedNotifyInfo,
2323
RuleError,
24+
WatchCallBack,
2425
} from './interface';
2526
import { HOOK_MARK } from './FieldContext';
2627
import { allPromiseFinish } from './utils/asyncUtil';
@@ -93,6 +94,7 @@ export class FormStore {
9394
setFieldsValue: this.setFieldsValue,
9495
validateFields: this.validateFields,
9596
submit: this.submit,
97+
_init: true,
9698

9799
getInternalHooks: this.getInternalHooks,
98100
});
@@ -114,6 +116,7 @@ export class FormStore {
114116
getFields: this.getFields,
115117
setPreserve: this.setPreserve,
116118
getInitialValue: this.getInitialValue,
119+
registerWatch: this.registerWatch,
117120
};
118121
}
119122

@@ -141,6 +144,7 @@ export class FormStore {
141144

142145
// We will take consider prev form unmount fields.
143146
// When the field is not `preserve`, we need fill this with initialValues instead of store.
147+
// eslint-disable-next-line array-callback-return
144148
this.prevWithoutPreserves?.map(({ key: namePath }) => {
145149
nextStore = setValue(nextStore, namePath, getValue(initialValues, namePath));
146150
});
@@ -180,6 +184,28 @@ export class FormStore {
180184
this.preserve = preserve;
181185
};
182186

187+
// ============================= Watch ============================
188+
private watchList: WatchCallBack[] = [];
189+
190+
private registerWatch: InternalHooks['registerWatch'] = callback => {
191+
this.watchList.push(callback);
192+
193+
return () => {
194+
this.watchList = this.watchList.filter(fn => fn !== callback);
195+
};
196+
};
197+
198+
private notifyWatch = (namePath: InternalNamePath[] = []) => {
199+
// No need to cost perf when nothing need to watch
200+
if (this.watchList.length) {
201+
const values = this.getFieldsValue();
202+
203+
this.watchList.forEach(callback => {
204+
callback(values, namePath);
205+
});
206+
}
207+
};
208+
183209
// ========================== Dev Warning =========================
184210
private timeoutId: any = null;
185211

@@ -498,6 +524,7 @@ export class FormStore {
498524
this.updateStore(setValues({}, this.initialValues));
499525
this.resetWithFieldInitialValue();
500526
this.notifyObservers(prevStore, null, { type: 'reset' });
527+
this.notifyWatch();
501528
return;
502529
}
503530

@@ -509,16 +536,20 @@ export class FormStore {
509536
});
510537
this.resetWithFieldInitialValue({ namePathList });
511538
this.notifyObservers(prevStore, namePathList, { type: 'reset' });
539+
this.notifyWatch(namePathList);
512540
};
513541

514542
private setFields = (fields: FieldData[]) => {
515543
this.warningUnhooked();
516544

517545
const prevStore = this.store;
518546

547+
const namePathList: InternalNamePath[] = [];
548+
519549
fields.forEach((fieldData: FieldData) => {
520550
const { name, errors, ...data } = fieldData;
521551
const namePath = getNamePath(name);
552+
namePathList.push(namePath);
522553

523554
// Value
524555
if ('value' in data) {
@@ -530,6 +561,8 @@ export class FormStore {
530561
data: fieldData,
531562
});
532563
});
564+
565+
this.notifyWatch(namePathList);
533566
};
534567

535568
private getFields = (): InternalFieldData[] => {
@@ -573,6 +606,8 @@ export class FormStore {
573606

574607
private registerField = (entity: FieldEntity) => {
575608
this.fieldEntities.push(entity);
609+
const namePath = entity.getNamePath();
610+
this.notifyWatch([namePath]);
576611

577612
// Set initial values
578613
if (entity.props.initialValue !== undefined) {
@@ -591,8 +626,6 @@ export class FormStore {
591626
const mergedPreserve = preserve !== undefined ? preserve : this.preserve;
592627

593628
if (mergedPreserve === false && (!isListField || subNamePath.length > 1)) {
594-
const namePath = entity.getNamePath();
595-
596629
const defaultValue = isListField ? undefined : this.getInitialValue(namePath);
597630

598631
if (
@@ -614,6 +647,8 @@ export class FormStore {
614647
this.triggerDependenciesUpdate(prevStore, namePath);
615648
}
616649
}
650+
651+
this.notifyWatch([namePath]);
617652
};
618653
};
619654

@@ -679,6 +714,7 @@ export class FormStore {
679714
type: 'valueUpdate',
680715
source: 'internal',
681716
});
717+
this.notifyWatch([namePath]);
682718

683719
// Dependencies update
684720
const childrenFields = this.triggerDependenciesUpdate(prevStore, namePath);
@@ -701,13 +737,15 @@ export class FormStore {
701737
const prevStore = this.store;
702738

703739
if (store) {
704-
this.updateStore(setValues(this.store, store));
740+
const nextStore = setValues(this.store, store);
741+
this.updateStore(nextStore);
705742
}
706743

707744
this.notifyObservers(prevStore, null, {
708745
type: 'valueUpdate',
709746
source: 'external',
710747
});
748+
this.notifyWatch();
711749
};
712750

713751
private getDependencyChildrenFields = (rootNamePath: InternalNamePath): InternalNamePath[] => {

0 commit comments

Comments
 (0)