diff --git a/src/packages/empty/empty.taro.tsx b/src/packages/empty/empty.taro.tsx
index f37cdc1075..3d23f51170 100644
--- a/src/packages/empty/empty.taro.tsx
+++ b/src/packages/empty/empty.taro.tsx
@@ -69,7 +69,7 @@ export const Empty: FunctionComponent<
useEffect(() => {
setImgStyle(() => {
- if (!imageSize) {
+ if (typeof imageSize !== 'number' && typeof imageSize !== 'string') {
return {}
}
if (typeof imageSize === 'number') {
diff --git a/src/packages/empty/empty.tsx b/src/packages/empty/empty.tsx
index 6117a84344..662062f812 100644
--- a/src/packages/empty/empty.tsx
+++ b/src/packages/empty/empty.tsx
@@ -71,7 +71,7 @@ export const Empty: FunctionComponent<
useEffect(() => {
setImgStyle(() => {
- if (!imageSize) {
+ if (typeof imageSize !== 'number' && typeof imageSize !== 'string') {
return {}
}
if (typeof imageSize === 'number') {
diff --git a/src/packages/form/__tests__/form.spec.tsx b/src/packages/form/__tests__/form.spec.tsx
index 8a07b93e01..d5d3950501 100644
--- a/src/packages/form/__tests__/form.spec.tsx
+++ b/src/packages/form/__tests__/form.spec.tsx
@@ -2,6 +2,7 @@ import * as React from 'react'
import { useEffect } from 'react'
import { fireEvent, render, waitFor } from '@testing-library/react'
import '@testing-library/jest-dom'
+import { Button, Radio, Space } from '@nutui/nutui-react'
import Form, { FormInstance } from '@/packages/form'
import Input from '@/packages/input'
@@ -312,3 +313,73 @@ test('no-style and render function', async () => {
expect(relatedInput).toBeTruthy()
})
})
+
+test('useWatch', async () => {
+ const Demo = () => {
+ const [form] = Form.useForm()
+ const account = Form.useWatch('account', form)
+ const loginMethod = Form.useWatch('loginMethod', form)
+ return (
+
+
+
+ 手机号
+
+ 邮箱
+
+
+
+
+
+ <>
+ {loginMethod === 'mobile' && (
+
+
+
+ )}
+ {loginMethod === 'email' && (
+
+
+
+ )}
+ >
+
+ )
+ }
+
+ const { container } = render()
+ const clickTest = container.querySelector('.clickTest')
+ if (clickTest) {
+ fireEvent.click(clickTest)
+ const result = container.querySelector('.result')
+ expect(result).toHaveTextContent('你将使用 邮箱 123 登录')
+ }
+})
diff --git a/src/packages/form/__tests__/merge.spec.ts b/src/packages/form/__tests__/merge.spec.ts
new file mode 100644
index 0000000000..773b44bbac
--- /dev/null
+++ b/src/packages/form/__tests__/merge.spec.ts
@@ -0,0 +1,263 @@
+import { merge, clone, recursive, isPlainObject } from '@/utils/merge'
+
+describe('merge', () => {
+ it('merges two objects', () => {
+ expect(merge({ a: 1 }, { b: 2 })).toStrictEqual({ a: 1, b: 2 })
+ })
+
+ it('merges nested levels', () => {
+ expect(merge({ a: 1 }, { b: { c: { d: 2 } } })).toStrictEqual({
+ a: 1,
+ b: { c: { d: 2 } },
+ })
+ })
+ it('clones the target', () => {
+ let input = {
+ a: 1,
+ b: {
+ c: {
+ d: 2,
+ e: ['x', 'y', { z: { w: ['k'] } }],
+ },
+ },
+ f: null,
+ g: undefined,
+ h: true,
+ }
+
+ const original = {
+ a: 1,
+ b: {
+ c: {
+ d: 2,
+ e: ['x', 'y', { z: { w: ['k'] } }],
+ },
+ },
+ f: null,
+ g: undefined,
+ h: true,
+ }
+
+ let output = merge(true, input)
+
+ input.b.c.d++
+ ;(input.b.c.e[2] as any).z.w = null
+ ;(input as any).h = null
+
+ expect(output).toStrictEqual(original)
+
+ input = original
+
+ output = merge(true, input, { a: 2 })
+
+ expect(output.a).toBe(2)
+ expect(input.a).toBe(1)
+ })
+
+ it('ignores the sources', () => {
+ const values = createNonPlainObjects()
+ const $merge = vi.fn().mockImplementation(merge)
+
+ for (const value of values) expect($merge(value)).toStrictEqual({})
+
+ expect(values.length).toBeGreaterThan(0)
+ expect($merge).toBeCalledTimes(values.length)
+ expect(
+ merge(...values, [0, 1, 2], ...values, { a: 1 }, ...values, {
+ b: 2,
+ })
+ ).toStrictEqual({ a: 1, b: 2 })
+ })
+
+ it('does not merge non plain objects', () => {
+ const values = createNonPlainObjects()
+ expect(values.length).toBeGreaterThan(0)
+ const input: any = {}
+
+ for (const [index, value] of Object.entries(values)) {
+ input[`value${index}`] = value
+ }
+
+ const output = merge({}, input)
+
+ for (const [index] of Object.entries(values)) {
+ const key = `value${index}`
+ const inputValue = input[key]
+ const outputValue = output[key]
+
+ // eslint-disable-next-line no-restricted-globals
+ if (typeof outputValue === 'number' && isNaN(outputValue)) {
+ // eslint-disable-next-line no-restricted-globals
+ expect(isNaN(inputValue), key).toBeTruthy()
+ } else {
+ expect(inputValue === outputValue, key).toBeTruthy()
+ }
+ }
+ })
+
+ it('is safe', () => {
+ expect(
+ merge({}, JSON.parse('{"__proto__": {"evil": true}}'))
+ ).toStrictEqual({})
+ expect(({} as any).evil).toBeUndefined()
+ })
+})
+
+describe('clone', () => {
+ it('clones the input', () => {
+ const object1 = { a: 1, b: { c: 2 } }
+ const object2 = clone(object1)
+
+ expect(object1).toStrictEqual(object2)
+ expect(object1 === object2).toBeFalsy()
+ expect(object1.b === object2.b).toBeFalsy()
+ })
+
+ it('clones each item of the array', () => {
+ const object1 = [{ a: 1, b: { c: 2 } }]
+ const object2 = clone(object1)
+
+ expect(object1).toStrictEqual(object2)
+ expect(object1 === object2).toBeFalsy()
+ expect(object1[0] === object2[0]).toBeFalsy()
+ expect(object1[0].b === object2[0].b).toBeFalsy()
+ })
+
+ it('returns the same input', () => {
+ const values = createNonPlainObjects()
+ const $clone = vi.fn().mockImplementation(clone)
+ for (const value of values) {
+ const cloned = $clone(value)
+ // eslint-disable-next-line no-restricted-globals
+ if (typeof cloned === 'number' && isNaN(cloned)) {
+ // eslint-disable-next-line no-restricted-globals
+ expect(isNaN(value)).toBeTruthy()
+ } else if (Array.isArray(cloned)) {
+ expect(Array.isArray(value)).toBeTruthy()
+ } else {
+ expect(cloned === value).toBeTruthy()
+ }
+ }
+ expect(values.length).toBeGreaterThan(0)
+ expect($clone).toBeCalledTimes(values.length)
+ })
+})
+
+describe('recursive', () => {
+ it('merges recursively', () => {
+ expect(recursive({ a: { b: 1 } }, { a: { c: 1 } })).toStrictEqual({
+ a: { b: 1, c: 1 },
+ })
+
+ expect(recursive({ a: { b: 1, c: 1 } }, { a: { b: 2 } })).toStrictEqual({
+ a: { b: 2, c: 1 },
+ })
+
+ expect(
+ recursive({ a: { b: [1, 2, 3], c: 1 } }, { a: { b: ['a'] } })
+ ).toStrictEqual({ a: { b: ['a'], c: 1 } })
+
+ expect(
+ recursive({ a: { b: { b: 2 }, c: 1 } }, { a: { b: 2 } })
+ ).toStrictEqual({
+ a: { b: 2, c: 1 },
+ })
+ })
+
+ it('clones recursively', () => {
+ const test1 = { a: { b: 1 } }
+
+ expect(recursive(true, test1, { a: { c: 1 } })).toStrictEqual({
+ a: { b: 1, c: 1 },
+ })
+
+ expect(test1).toStrictEqual({ a: { b: 1 } })
+
+ const test2 = { a: { b: 1, c: 1 } }
+
+ expect(recursive(true, test2, { a: { b: 2 } })).toStrictEqual({
+ a: { b: 2, c: 1 },
+ })
+
+ expect(test2).toStrictEqual({ a: { b: 1, c: 1 } })
+
+ const test3 = { a: { b: [1, 2, 3], c: 1 } }
+
+ expect(recursive(true, test3, { a: { b: ['a'] } })).toStrictEqual({
+ a: { b: ['a'], c: 1 },
+ })
+
+ expect(test3).toStrictEqual({ a: { b: [1, 2, 3], c: 1 } })
+
+ const test4 = { a: { b: { b: 2 }, c: 1 } }
+
+ expect(recursive(true, test4, { a: { b: 2 } })).toStrictEqual({
+ a: { b: 2, c: 1 },
+ })
+
+ expect(test4).toStrictEqual({ a: { b: { b: 2 }, c: 1 } })
+ })
+
+ it('does not merge non plain objects', () => {
+ const object = recursive({ map: { length: 1 } }, { map: new Map() })
+ expect(object.map).toBeInstanceOf(Map)
+ })
+
+ it('is safe', () => {
+ const payload = '{"__proto__": {"a": true}}'
+ expect(recursive({}, JSON.parse(payload))).toStrictEqual({})
+ expect(({} as any).a).toBeUndefined()
+ expect(recursive({ deep: {} }, JSON.parse(payload))).toStrictEqual({
+ deep: {},
+ })
+ expect(({} as any).b).toBeUndefined()
+ })
+})
+
+describe('isPlainObject', () => {
+ it('returns true', () => {
+ expect(isPlainObject({})).toBeTruthy()
+ expect(isPlainObject({ v: 1 })).toBeTruthy()
+ expect(isPlainObject(Object.create(null))).toBeTruthy()
+ expect(isPlainObject({})).toBeTruthy()
+ })
+ it('returns false', () => {
+ const values = createNonPlainObjects()
+ const $isPlainObject = vi.fn().mockImplementation(isPlainObject)
+ for (const value of values) expect($isPlainObject(value)).toBeFalsy()
+ expect(values.length).toBeGreaterThan(0)
+ expect($isPlainObject).toBeCalledTimes(values.length)
+ })
+})
+
+function createNonPlainObjects(): any[] {
+ class SubObject extends Object {}
+
+ return [
+ null,
+ undefined,
+ 1,
+ '',
+ 'str',
+ [],
+ [1],
+ () => {},
+ function () {},
+ true,
+ false,
+ NaN,
+ Infinity,
+ class {},
+ new (class {})(),
+ new Map(),
+ new Set(),
+ new Date(),
+ [],
+ new Date(),
+ /./,
+ /./,
+ SubObject,
+ new SubObject(),
+ Symbol(''),
+ ]
+}
diff --git a/src/packages/form/demo.taro.tsx b/src/packages/form/demo.taro.tsx
index 3881c4d845..0076b72f6a 100644
--- a/src/packages/form/demo.taro.tsx
+++ b/src/packages/form/demo.taro.tsx
@@ -9,6 +9,7 @@ import Demo4 from './demos/taro/demo4'
import Demo5 from './demos/taro/demo5'
import Demo6 from './demos/taro/demo6'
import Demo7 from './demos/taro/demo7'
+import Demo8 from './demos/taro/demo8'
const FormDemo = () => {
const [translated] = useTranslate({
@@ -20,6 +21,7 @@ const FormDemo = () => {
title4: 'Form.useForm 对表单数据域进行交互。',
title5: '表单类型',
validateTrigger: '校验触发时机',
+ useWatch: 'useWatch',
},
'en-US': {
basic: 'Basic Usage',
@@ -29,6 +31,7 @@ const FormDemo = () => {
title4: 'Interact with form data fields via Form.useForm',
title5: 'Form Type',
validateTrigger: 'Validate Trigger',
+ useWatch: 'useWatch',
},
})
@@ -50,6 +53,8 @@ const FormDemo = () => {
{translated.title5}
+ {translated.useWatch}
+
>
)
diff --git a/src/packages/form/demo.tsx b/src/packages/form/demo.tsx
index effa42d77a..dfbcccd246 100644
--- a/src/packages/form/demo.tsx
+++ b/src/packages/form/demo.tsx
@@ -7,6 +7,7 @@ import Demo4 from './demos/h5/demo4'
import Demo5 from './demos/h5/demo5'
import Demo6 from './demos/h5/demo6'
import Demo7 from './demos/h5/demo7'
+import Demo8 from './demos/h5/demo8'
const FormDemo = () => {
const [translated] = useTranslate({
@@ -18,6 +19,7 @@ const FormDemo = () => {
title4: 'Form.useForm 对表单数据域进行交互。',
title5: '表单类型',
validateTrigger: '校验触发时机',
+ useWatch: 'useWatch',
},
'en-US': {
basic: 'Basic Usage',
@@ -27,6 +29,7 @@ const FormDemo = () => {
title4: 'Interact with form data fields via Form.useForm',
title5: 'Form Type',
validateTrigger: 'Validate Trigger',
+ useWatch: 'useWatch',
},
})
@@ -47,6 +50,8 @@ const FormDemo = () => {
{translated.title5}
+ {translated.useWatch}
+
>
)
diff --git a/src/packages/form/demos/h5/demo8.tsx b/src/packages/form/demos/h5/demo8.tsx
new file mode 100644
index 0000000000..4cec12da48
--- /dev/null
+++ b/src/packages/form/demos/h5/demo8.tsx
@@ -0,0 +1,64 @@
+import React from 'react'
+import { Button, Form, Input, Radio, Space } from '@nutui/nutui-react'
+
+type FieldType = { account?: string; loginMethod?: 'mobile' | 'email' }
+
+const Demo8 = () => {
+ const [form] = Form.useForm()
+ const account = Form.useWatch('account', form)
+ const loginMethod = Form.useWatch('loginMethod', form)
+
+ return (
+
+
+
+ 手机号
+ 电子邮箱
+
+
+
+
+ <>
+ {loginMethod === 'mobile' && (
+
+
+
+ )}
+ {loginMethod === 'email' && (
+
+
+
+ )}
+ >
+
+ )
+}
+
+export default Demo8
diff --git a/src/packages/form/demos/taro/demo8.tsx b/src/packages/form/demos/taro/demo8.tsx
new file mode 100644
index 0000000000..ca743e2e13
--- /dev/null
+++ b/src/packages/form/demos/taro/demo8.tsx
@@ -0,0 +1,64 @@
+import React from 'react'
+import { Button, Form, Input, Radio, Space } from '@nutui/nutui-react-taro'
+
+type FieldType = { account?: string; loginMethod?: 'mobile' | 'email' }
+
+const Demo8 = () => {
+ const [form] = Form.useForm()
+ const account = Form.useWatch('account', form)
+ const loginMethod = Form.useWatch('loginMethod', form)
+
+ return (
+
+
+
+ 手机号
+ 电子邮箱
+
+
+
+
+ <>
+ {loginMethod === 'mobile' && (
+
+
+
+ )}
+ {loginMethod === 'email' && (
+
+
+
+ )}
+ >
+
+ )
+}
+
+export default Demo8
diff --git a/src/packages/form/doc.en-US.md b/src/packages/form/doc.en-US.md
index ed414f760b..fa01e2fac5 100644
--- a/src/packages/form/doc.en-US.md
+++ b/src/packages/form/doc.en-US.md
@@ -58,6 +58,14 @@ import { Form } from '@nutui/nutui-react'
:::
+### useWatch
+
+:::demo
+
+
+
+:::
+
### Form Type
:::demo
@@ -121,7 +129,7 @@ The rule validation process is based on [async-validator](https://github.com/yim
### FormInstance
-Form.useForm() creates a Form instance, which is used to manage all data states.
+`Form.useForm()` creates a Form instance, which is used to manage all data states.
| Property | Description | Type |
| --- | --- | --- |
@@ -132,6 +140,8 @@ Form.useForm() creates a Form instance, which is used to manage all data states.
| resetFields | Reset form prompt state | `() => void` |
| submit | method to submit a form for validation | `Promise` |
+`Form.useWatch()`, this method will watch specified inputs and return their values. It is useful to render input value and for determining what to render by condition.
+
## Theming
### CSS Variables
diff --git a/src/packages/form/doc.md b/src/packages/form/doc.md
index 2b176efff3..d9619d0048 100644
--- a/src/packages/form/doc.md
+++ b/src/packages/form/doc.md
@@ -58,6 +58,14 @@ import { Form } from '@nutui/nutui-react'
:::
+### useWatch
+
+:::demo
+
+
+
+:::
+
### 表单类型
:::demo
@@ -120,7 +128,7 @@ import { Form } from '@nutui/nutui-react'
### FormInstance
-Form.useForm()创建 Form 实例,用于管理所有数据状态。
+`Form.useForm()`创建 Form 实例,用于管理所有数据状态。
| 属性 | 说明 | 类型 |
| --- | --- | --- |
@@ -131,6 +139,8 @@ Form.useForm()创建 Form 实例,用于管理所有数据状态。
| resetFields | 重置表单提示状态 | `() => void` |
| submit | 提交表单进行校验的方法 | `Promise` |
+`Form.useWatch()`此方法将监视指定的输入并返回其值。它对于呈现输入值和确定根据条件呈现的内容很有用。
+
## 主题定制
### 样式变量
diff --git a/src/packages/form/doc.taro.md b/src/packages/form/doc.taro.md
index a6b01d1b5b..748db03665 100644
--- a/src/packages/form/doc.taro.md
+++ b/src/packages/form/doc.taro.md
@@ -58,6 +58,14 @@ import { Form } from '@nutui/nutui-react-taro'
:::
+### useWatch
+
+:::demo
+
+
+
+:::
+
### 表单类型
:::demo
@@ -120,7 +128,7 @@ import { Form } from '@nutui/nutui-react-taro'
### FormInstance
-Form.useForm()创建 Form 实例,用于管理所有数据状态。
+`Form.useForm()`创建 Form 实例,用于管理所有数据状态。
| 属性 | 说明 | 类型 |
| --- | --- | --- |
@@ -131,6 +139,8 @@ Form.useForm()创建 Form 实例,用于管理所有数据状态。
| resetFields | 重置表单提示状态 | `() => void` |
| submit | 提交表单进行校验的方法 | `Promise` |
+`Form.useWatch()`此方法将监视指定的输入并返回其值。它对于呈现输入值和确定根据条件呈现的内容很有用。
+
## 主题定制
### 样式变量
diff --git a/src/packages/form/doc.zh-TW.md b/src/packages/form/doc.zh-TW.md
index 8abf1361c3..8725a0a372 100644
--- a/src/packages/form/doc.zh-TW.md
+++ b/src/packages/form/doc.zh-TW.md
@@ -58,6 +58,14 @@ import { Form } from '@nutui/nutui-react'
:::
+### useWatch
+
+:::demo
+
+
+
+:::
+
### 表單類型
:::demo
@@ -120,7 +128,7 @@ import { Form } from '@nutui/nutui-react'
### FormInstance
-Form.useForm()創建 Form 實例,用於管理所有數據狀態。
+`Form.useForm()`創建 Form 實例,用於管理所有數據狀態。
| 屬性 | 說明 | 類型 |
| --- | --- | --- |
@@ -131,6 +139,8 @@ Form.useForm()創建 Form 實例,用於管理所有數據狀態。
| resetFields | 重置錶單提示狀態 | `() => void` |
| submit | 提交錶單進行校驗的方法 | `Promise` |
+`Form.useWatch()`此方法將監視指定的輸入並傳回其值。它對於呈現輸入值和確定根據條件呈現的內容很有用。
+
## 主題定制
### 樣式變量
diff --git a/src/packages/form/index.taro.ts b/src/packages/form/index.taro.ts
index 093d5218c8..fdfefdae35 100644
--- a/src/packages/form/index.taro.ts
+++ b/src/packages/form/index.taro.ts
@@ -2,7 +2,7 @@ import React from 'react'
import { Form, FormProps } from './form.taro'
import { FormItem } from '../formitem/formitem.taro'
import { FormInstance } from './types'
-import { useForm } from '@/packages/form/useform.taro'
+import { useForm, useWatch } from '@/packages/form/useform.taro'
export type {
FormItemRuleWithoutValidator,
@@ -17,11 +17,13 @@ type CompoundedComponent = React.ForwardRefExoticComponent<
> & {
Item: typeof FormItem
useForm: typeof useForm
+ useWatch: typeof useWatch
}
const InnerForm = Form as CompoundedComponent
InnerForm.Item = FormItem
InnerForm.useForm = useForm
+InnerForm.useWatch = useWatch
export default InnerForm
diff --git a/src/packages/form/index.ts b/src/packages/form/index.ts
index 6563f8a986..748c374c25 100644
--- a/src/packages/form/index.ts
+++ b/src/packages/form/index.ts
@@ -2,7 +2,7 @@ import React from 'react'
import { Form, FormProps } from './form'
import { FormItem } from '../formitem/formitem'
import { FormInstance } from './types'
-import { useForm } from '@/packages/form/useform'
+import { useForm, useWatch } from '@/packages/form/useform'
export type {
FormItemRuleWithoutValidator,
@@ -17,11 +17,13 @@ type CompoundedComponent = React.ForwardRefExoticComponent<
> & {
Item: typeof FormItem
useForm: typeof useForm
+ useWatch: typeof useWatch
}
const InnerForm = Form as CompoundedComponent
InnerForm.Item = FormItem
InnerForm.useForm = useForm
+InnerForm.useWatch = useWatch
export default InnerForm
diff --git a/src/packages/form/useform.taro.ts b/src/packages/form/useform.taro.ts
index 8559e5c5d7..4ca2f92679 100644
--- a/src/packages/form/useform.taro.ts
+++ b/src/packages/form/useform.taro.ts
@@ -1,6 +1,6 @@
-import { useRef } from 'react'
+import { useEffect, useRef, useState } from 'react'
import Schema from 'async-validator'
-import { merge } from '@/utils/merge'
+import { merge, recursive } from '@/utils/merge'
import {
Callbacks,
FormFieldEntity,
@@ -11,6 +11,7 @@ import {
export const SECRET = 'NUT_FORM_INTERNAL'
type UpdateItem = { entity: FormFieldEntity; condition: any }
+type WatchCallback = (value: Store, namePath: NamePath[]) => void
/**
* 用于存储表单的数据
@@ -45,11 +46,9 @@ class FormStore {
*/
registerField = (field: any) => {
this.fieldEntities.push(field)
+
return () => {
this.fieldEntities = this.fieldEntities.filter((item) => item !== field)
- if (this.store) {
- delete this.store[field.props.name]
- }
}
}
@@ -90,6 +89,7 @@ class FormStore {
if (init) {
const nextStore = merge(initialValues, this.store)
this.updateStore(nextStore)
+ this.notifyWatch()
}
}
@@ -98,7 +98,7 @@ class FormStore {
* @param newStore { [name]: newValue }
*/
setFieldsValue = (newStore: any) => {
- const nextStore = merge(this.store, newStore)
+ const nextStore = recursive(true, this.store, newStore)
this.updateStore(nextStore)
this.fieldEntities.forEach((entity: FormFieldEntity) => {
const { name } = entity.props
@@ -117,6 +117,7 @@ class FormStore {
item.entity.onStoreChange('update')
}
})
+ this.notifyWatch()
}
setFieldValue = (name: NamePath, value: T) => {
@@ -124,6 +125,7 @@ class FormStore {
[name]: value,
}
this.setFieldsValue(store)
+ this.notifyWatch([name])
}
setCallback = (callback: Callbacks) => {
@@ -234,6 +236,7 @@ class FormStore {
store: this.store,
fieldEntities: this.fieldEntities,
registerUpdate: this.registerUpdate,
+ registerWatch: this.registerWatch,
}
}
}
@@ -251,6 +254,30 @@ class FormStore {
getInternal: this.getInternal,
}
}
+
+ private watchList: WatchCallback[] = []
+
+ private registerWatch = (callback: WatchCallback) => {
+ this.watchList.push(callback)
+
+ return () => {
+ this.watchList = this.watchList.filter((fn) => fn !== callback)
+ }
+ }
+
+ private notifyWatch = (namePath: NamePath[] = []) => {
+ if (this.watchList.length) {
+ let allValues
+ if (!namePath || namePath.length === 0) {
+ allValues = this.getFieldsValue(true)
+ } else {
+ allValues = this.getFieldsValue(namePath)
+ }
+ this.watchList.forEach((callback) => {
+ callback(allValues, namePath)
+ })
+ }
+ }
}
export const useForm = (form?: FormInstance): [FormInstance] => {
@@ -265,3 +292,22 @@ export const useForm = (form?: FormInstance): [FormInstance] => {
}
return [formRef.current as FormInstance]
}
+
+export const useWatch = (path: NamePath, form: FormInstance) => {
+ const formInstance = form.getInternal(SECRET)
+ const [value, setValue] = useState()
+ useEffect(() => {
+ const unsubscribe = formInstance.registerWatch(
+ (data: any, namePath: NamePath) => {
+ const value = data[path]
+ setValue(value)
+ }
+ )
+ const initialValue = form.getFieldsValue(true)
+ if (value !== initialValue[path]) {
+ setValue(initialValue[path])
+ }
+ return () => unsubscribe()
+ }, [form])
+ return value
+}
diff --git a/src/packages/form/useform.ts b/src/packages/form/useform.ts
index 8559e5c5d7..4ca2f92679 100644
--- a/src/packages/form/useform.ts
+++ b/src/packages/form/useform.ts
@@ -1,6 +1,6 @@
-import { useRef } from 'react'
+import { useEffect, useRef, useState } from 'react'
import Schema from 'async-validator'
-import { merge } from '@/utils/merge'
+import { merge, recursive } from '@/utils/merge'
import {
Callbacks,
FormFieldEntity,
@@ -11,6 +11,7 @@ import {
export const SECRET = 'NUT_FORM_INTERNAL'
type UpdateItem = { entity: FormFieldEntity; condition: any }
+type WatchCallback = (value: Store, namePath: NamePath[]) => void
/**
* 用于存储表单的数据
@@ -45,11 +46,9 @@ class FormStore {
*/
registerField = (field: any) => {
this.fieldEntities.push(field)
+
return () => {
this.fieldEntities = this.fieldEntities.filter((item) => item !== field)
- if (this.store) {
- delete this.store[field.props.name]
- }
}
}
@@ -90,6 +89,7 @@ class FormStore {
if (init) {
const nextStore = merge(initialValues, this.store)
this.updateStore(nextStore)
+ this.notifyWatch()
}
}
@@ -98,7 +98,7 @@ class FormStore {
* @param newStore { [name]: newValue }
*/
setFieldsValue = (newStore: any) => {
- const nextStore = merge(this.store, newStore)
+ const nextStore = recursive(true, this.store, newStore)
this.updateStore(nextStore)
this.fieldEntities.forEach((entity: FormFieldEntity) => {
const { name } = entity.props
@@ -117,6 +117,7 @@ class FormStore {
item.entity.onStoreChange('update')
}
})
+ this.notifyWatch()
}
setFieldValue = (name: NamePath, value: T) => {
@@ -124,6 +125,7 @@ class FormStore {
[name]: value,
}
this.setFieldsValue(store)
+ this.notifyWatch([name])
}
setCallback = (callback: Callbacks) => {
@@ -234,6 +236,7 @@ class FormStore {
store: this.store,
fieldEntities: this.fieldEntities,
registerUpdate: this.registerUpdate,
+ registerWatch: this.registerWatch,
}
}
}
@@ -251,6 +254,30 @@ class FormStore {
getInternal: this.getInternal,
}
}
+
+ private watchList: WatchCallback[] = []
+
+ private registerWatch = (callback: WatchCallback) => {
+ this.watchList.push(callback)
+
+ return () => {
+ this.watchList = this.watchList.filter((fn) => fn !== callback)
+ }
+ }
+
+ private notifyWatch = (namePath: NamePath[] = []) => {
+ if (this.watchList.length) {
+ let allValues
+ if (!namePath || namePath.length === 0) {
+ allValues = this.getFieldsValue(true)
+ } else {
+ allValues = this.getFieldsValue(namePath)
+ }
+ this.watchList.forEach((callback) => {
+ callback(allValues, namePath)
+ })
+ }
+ }
}
export const useForm = (form?: FormInstance): [FormInstance] => {
@@ -265,3 +292,22 @@ export const useForm = (form?: FormInstance): [FormInstance] => {
}
return [formRef.current as FormInstance]
}
+
+export const useWatch = (path: NamePath, form: FormInstance) => {
+ const formInstance = form.getInternal(SECRET)
+ const [value, setValue] = useState()
+ useEffect(() => {
+ const unsubscribe = formInstance.registerWatch(
+ (data: any, namePath: NamePath) => {
+ const value = data[path]
+ setValue(value)
+ }
+ )
+ const initialValue = form.getFieldsValue(true)
+ if (value !== initialValue[path]) {
+ setValue(initialValue[path])
+ }
+ return () => unsubscribe()
+ }, [form])
+ return value
+}
diff --git a/src/packages/popup/doc.taro.md b/src/packages/popup/doc.taro.md
index 8e9880be00..5badf6c610 100644
--- a/src/packages/popup/doc.taro.md
+++ b/src/packages/popup/doc.taro.md
@@ -135,3 +135,7 @@ import { Popup } from '@nutui/nutui-react-taro'
| \--nutui-popup-title-height | 标题栏的高度 | `50px` |
| \--nutui-popup-title-border-bottom | 标题栏底部边框 | `0` |
| \--nutui-popup-animation-duration | 弹框动画的延时 | `0.3s` |
+
+## FAQ
+
+在 iOS 下,有时候 `lockScroll` 可能不生效,此刻在打开 `popup` 时,可将宿主页面的样式增加 `overflow: hidden;`,关闭弹层时,再重置样式。
diff --git a/src/utils/merge.ts b/src/utils/merge.ts
index 0d6cb8ced6..fe2668ab92 100644
--- a/src/utils/merge.ts
+++ b/src/utils/merge.ts
@@ -1,29 +1,89 @@
-export function merge(...objects: any[]) {
- const result: any = Array.isArray(objects[0]) ? [] : {}
-
- function mergeHelper(obj: any, path: string[] = []) {
- for (const [key, value] of Object.entries(obj)) {
- const newPath = [...path, key]
-
- if (Array.isArray(value)) {
- // Arrays are always overridden
- result[key] = [...value]
- } else if (typeof value === 'object' && value !== null) {
- // Check for circular references
+export default main
+
+export function main(clone: boolean, ...items: any[]): any
+export function main(...items: any[]): any
+export function main(...items: any[]) {
+ return merge(...items)
+}
+
+main.clone = clone
+main.isPlainObject = isPlainObject
+main.recursive = recursive
+
+export function merge(clone: boolean, ...items: any[]): any
+export function merge(...items: any[]): any
+export function merge(...items: any[]) {
+ return _merge(items[0] === true, false, items)
+}
+
+export function recursive(clone: boolean, ...items: any[]): any
+export function recursive(...items: any[]): any
+export function recursive(...items: any[]) {
+ return _merge(items[0] === true, true, items)
+}
+
+export function clone(input: T): T {
+ if (Array.isArray(input)) {
+ const output = []
+
+ for (let index = 0; index < input.length; ++index)
+ output.push(clone(input[index]))
+
+ return output as any
+ }
+ if (isPlainObject(input)) {
+ const output: any = {}
+
+ // eslint-disable-next-line guard-for-in
+ for (const index in input) output[index] = clone((input as any)[index])
+
+ return output as any
+ }
+ return input
+}
+
+export function isPlainObject(input: unknown): input is NonNullable {
+ if (input === null || typeof input !== 'object') return false
+ if (Object.getPrototypeOf(input) === null) return true
+ let ref = input
+ while (Object.getPrototypeOf(ref) !== null) ref = Object.getPrototypeOf(ref)
+ return Object.getPrototypeOf(input) === ref
+}
+
+function _recursiveMerge(base: any, extend: any) {
+ if (!isPlainObject(base) || !isPlainObject(extend)) return extend
+ for (const key in extend) {
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype')
+ // eslint-disable-next-line no-continue
+ continue
+ base[key] =
+ isPlainObject(base[key]) && isPlainObject(extend[key])
+ ? _recursiveMerge(base[key], extend[key])
+ : extend[key]
+ }
+
+ return base
+}
+
+function _merge(isClone: boolean, isRecursive: boolean, items: any[]) {
+ let result
+
+ if (isClone || !isPlainObject((result = items.shift()))) result = {}
+
+ for (let index = 0; index < items.length; ++index) {
+ const item = items[index]
+
+ // eslint-disable-next-line no-continue
+ if (!isPlainObject(item)) continue
+
+ for (const key in item) {
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype')
// eslint-disable-next-line no-continue
- if (path.some((p: any) => p === value)) continue
-
- // Recursively merge objects
- if (!result[key]) result[key] = {}
- mergeHelper(value, newPath)
- } else {
- // Set primitive values
- result[key] = value
- }
+ continue
+ const value = isClone ? clone(item[key]) : item[key]
+ result[key] = isRecursive ? _recursiveMerge(result[key], value) : value
}
}
- objects.filter((obj) => !!obj).forEach((obj) => mergeHelper(obj))
-
return result
}