diff --git a/packages/next/src/array-table/index.tsx b/packages/next/src/array-table/index.tsx
index 1422d42f246..b52bb2a66aa 100644
--- a/packages/next/src/array-table/index.tsx
+++ b/packages/next/src/array-table/index.tsx
@@ -133,7 +133,11 @@ const useArrayTableColumns = (
cell: (value: any, _: number, record: any) => {
const index = dataSource.indexOf(record)
const children = (
-
+ dataSource[index]}
+ >
)
diff --git a/packages/next/src/form/index.tsx b/packages/next/src/form/index.tsx
index a8be73504fa..aa04fe2ab38 100644
--- a/packages/next/src/form/index.tsx
+++ b/packages/next/src/form/index.tsx
@@ -1,7 +1,7 @@
import React, { useMemo } from 'react'
import {
FormProvider,
- ExpressionScope,
+ RecordScope,
JSXComponent,
useParentForm,
} from '@formily/react'
@@ -13,6 +13,7 @@ import {
Form as FormType,
ObjectField,
IFormFeedback,
+ isForm,
} from '@formily/core'
import { PreviewText } from '../preview-text'
export interface FormProps extends IFormLayoutProps {
@@ -40,7 +41,7 @@ export const Form: React.FC> = ({
}, [lang])
const renderContent = (form: FormType | ObjectField) => (
-
+ (isForm(form) ? form.values : form.value)}>
{React.createElement(
@@ -56,7 +57,7 @@ export const Form: React.FC> = ({
)}
-
+
)
if (form)
diff --git a/packages/react/docs/api/components/ExpressionScope.md b/packages/react/docs/api/components/ExpressionScope.md
index e69de29bb2d..874c0b10087 100644
--- a/packages/react/docs/api/components/ExpressionScope.md
+++ b/packages/react/docs/api/components/ExpressionScope.md
@@ -0,0 +1,62 @@
+---
+order: 8
+---
+
+# ExpressionScope
+
+## Description
+
+Used to pass local scopes to json-schema expressions inside custom components
+
+## Signature
+
+```ts
+interface IExpressionScopeProps {
+ value?: any
+}
+type ExpressionScope = React.FC>
+```
+
+## Example
+
+```tsx
+import React from 'react'
+import { createForm } from '@formily/core'
+import {
+ FormProvider,
+ createSchemaField,
+ ExpressionScope,
+} from '@formily/react'
+import { Input } from 'antd'
+
+const form = createForm()
+
+const Container = (props) => {
+ return (
+
+ {props.children}
+
+ )
+}
+
+const SchemaField = createSchemaField({
+ components: {
+ Container,
+ Input,
+ },
+})
+
+export default () => (
+
+
+
+
+
+
+
+)
+```
diff --git a/packages/react/docs/api/components/RecordScope.md b/packages/react/docs/api/components/RecordScope.md
new file mode 100644
index 00000000000..9ba06952628
--- /dev/null
+++ b/packages/react/docs/api/components/RecordScope.md
@@ -0,0 +1,110 @@
+---
+order: 9
+---
+
+# RecordScope
+
+## Description
+
+Standard scoped injection component for injecting the following built-in variables:
+
+- `$record` current record data
+- `$record.$lookup` The parent record of the current record, you can always look up
+- `$record.$index` the index of the current record
+- `$index` The current record index, equivalent to `$record.$index`, considering that if the record data is not an object, it needs to be read independently
+- `$lookup` The parent record of the current record, equivalent to `$record.$lookup`, considering that if the record data is not an object, it needs to be read independently
+
+## Signature
+
+```ts
+interface IRecordScopeProps {
+ getRecord(): any
+ getIndex?(): number
+}
+
+type RecordScope = React.FC>
+```
+
+## Usage
+
+Any auto-increment list extension component should use RecordScope internally to pass record scope variables. Components that have implemented this convention include:
+All components of the ArrayX family in @formily/antd and @formily/next
+
+## Custom component extension use case
+
+```tsx
+import React from 'react'
+import { createForm } from '@formily/core'
+import { FormProvider, createSchemaField, RecordScope } from '@formily/react'
+import { Input } from 'antd'
+
+const form = createForm()
+
+const MyCustomComponent = (props) => {
+ return (
+ props.record} getIndex={() => props.index}>
+ {props.children}
+
+ )
+}
+
+const SchemaField = createSchemaField({
+ components: {
+ Input,
+ MyCustomComponent,
+ },
+})
+
+export default () => (
+
+
+
+)
+```
diff --git a/packages/react/docs/api/components/RecordScope.zh-CN.md b/packages/react/docs/api/components/RecordScope.zh-CN.md
new file mode 100644
index 00000000000..35268f77f7a
--- /dev/null
+++ b/packages/react/docs/api/components/RecordScope.zh-CN.md
@@ -0,0 +1,110 @@
+---
+order: 9
+---
+
+# RecordScope
+
+## 描述
+
+标准作用域注入组件,用于注入以下内置变量:
+
+- `$record` 当前记录数据
+- `$record.$lookup` 当前记录的父级记录,可以一直往上查找
+- `$record.$index` 当前记录的索引
+- `$index` 当前记录索引,等同于`$record.$index`,考虑到记录数据如果不是对象,则需要独立读取
+- `$lookup` 当前记录的父级记录,等同于`$record.$lookup`,考虑到记录数据如果不是对象,则需要独立读取
+
+## 签名
+
+```ts
+interface IRecordScopeProps {
+ getRecord(): any
+ getIndex?(): number
+}
+
+type RecordScope = React.FC>
+```
+
+## 使用约定
+
+任何自增列表扩展组件,内部都应该使用 RecordScope,用于传递记录作用域变量,目前已实现该约定的组件包括:
+@formily/antd 和 @formily/next 中的 ArrayX 系列的所有组件
+
+## 自定义组件扩展用例
+
+```tsx
+import React from 'react'
+import { createForm } from '@formily/core'
+import { FormProvider, createSchemaField, RecordScope } from '@formily/react'
+import { Input } from 'antd'
+
+const form = createForm()
+
+const MyCustomComponent = (props) => {
+ return (
+ props.record} getIndex={() => props.index}>
+ {props.children}
+
+ )
+}
+
+const SchemaField = createSchemaField({
+ components: {
+ Input,
+ MyCustomComponent,
+ },
+})
+
+export default () => (
+
+
+
+)
+```
diff --git a/packages/react/docs/api/components/RecordsScope.md b/packages/react/docs/api/components/RecordsScope.md
new file mode 100644
index 00000000000..248b962f7e6
--- /dev/null
+++ b/packages/react/docs/api/components/RecordsScope.md
@@ -0,0 +1,87 @@
+---
+order: 10
+---
+
+# RecordsScope
+
+## Description
+
+Standard scoped injection component for injecting the following built-in variables:
+
+- `$records` current record list data
+
+## Signature
+
+```ts
+interface IRecordsScopeProps {
+ getRecords(): any[]
+}
+
+type RecordsScope = React.FC>
+```
+
+## Usage
+
+Any auto-incrementing list extension component should use RecordsScope internally to pass record scope variables. Components that have implemented this convention include:
+All components of the ArrayX family in @formily/antd and @formily/next
+
+## Custom component extension use case
+
+```tsx
+import React from 'react'
+import { createForm } from '@formily/core'
+import { FormProvider, createSchemaField, RecordsScope } from '@formily/react'
+import { Input } from 'antd'
+
+const form = createForm()
+
+const MyCustomComponent = (props) => {
+ return (
+ props.records}>
+ {props.children}
+
+ )
+}
+
+const SchemaField = createSchemaField({
+ components: {
+ Input,
+ MyCustomComponent,
+ },
+})
+
+export default () => (
+
+
+
+)
+```
diff --git a/packages/react/docs/api/components/RecordsScope.zh-CN.md b/packages/react/docs/api/components/RecordsScope.zh-CN.md
new file mode 100644
index 00000000000..cd66a5012ae
--- /dev/null
+++ b/packages/react/docs/api/components/RecordsScope.zh-CN.md
@@ -0,0 +1,87 @@
+---
+order: 10
+---
+
+# RecordsScope
+
+## 描述
+
+标准作用域注入组件,用于注入以下内置变量:
+
+- `$records` 当前记录列表数据
+
+## 签名
+
+```ts
+interface IRecordsScopeProps {
+ getRecords(): any[]
+}
+
+type RecordsScope = React.FC>
+```
+
+## 使用约定
+
+任何自增列表扩展组件,内部都应该使用 RecordsScope,用于传递记录作用域变量,目前已实现该约定的组件包括:
+@formily/antd 和 @formily/next 中的 ArrayX 系列的所有组件
+
+## 自定义组件扩展用例
+
+```tsx
+import React from 'react'
+import { createForm } from '@formily/core'
+import { FormProvider, createSchemaField, RecordsScope } from '@formily/react'
+import { Input } from 'antd'
+
+const form = createForm()
+
+const MyCustomComponent = (props) => {
+ return (
+ props.records}>
+ {props.children}
+
+ )
+}
+
+const SchemaField = createSchemaField({
+ components: {
+ Input,
+ MyCustomComponent,
+ },
+})
+
+export default () => (
+
+
+
+)
+```
diff --git a/packages/react/docs/api/hooks/useExpressionScope.md b/packages/react/docs/api/hooks/useExpressionScope.md
new file mode 100644
index 00000000000..78e6aad3981
--- /dev/null
+++ b/packages/react/docs/api/hooks/useExpressionScope.md
@@ -0,0 +1,65 @@
+# useExpressionScope
+
+## Description
+
+The expression scope is mainly read in the custom component. The sources of the expression scope are:
+
+- createSchemaField top-level delivery
+- SchemaField component attribute delivery
+- ExpressionScope/RecordScope/RecordsScope are issued inside custom components
+
+## Signature
+
+```ts
+interface useExpressionScope {
+ (): any
+}
+```
+
+## Example
+
+```tsx
+import React from 'react'
+import { createForm } from '@formily/core'
+import {
+ FormProvider,
+ createSchemaField,
+ useExpressionScope,
+ RecordScope,
+} from '@formily/react'
+
+const form = createForm()
+
+const Custom = () => {
+ const scope = useExpressionScope()
+ return (
+
+ {JSON.stringify(scope, null, 2)}
+
+ )
+}
+
+const SchemaField = createSchemaField({
+ components: {
+ Custom,
+ },
+ scope: {
+ topScope: {
+ aa: 123,
+ },
+ },
+})
+
+export default () => (
+
+ ({ name: 'Record Name', code: 'Record Code' })}
+ getIndex={() => 2}
+ >
+
+
+
+
+
+)
+```
diff --git a/packages/react/docs/api/hooks/useExpressionScope.zh-CN.md b/packages/react/docs/api/hooks/useExpressionScope.zh-CN.md
new file mode 100644
index 00000000000..d3dd55a4fca
--- /dev/null
+++ b/packages/react/docs/api/hooks/useExpressionScope.zh-CN.md
@@ -0,0 +1,65 @@
+# useExpressionScope
+
+## 描述
+
+主要在自定义组件中读取表达式作用域,表达式作用域的来源主要有:
+
+- createSchemaField 顶层下发
+- SchemaField 组件属性下发
+- ExpressionScope/RecordScope/RecordsScope 自定义组件内部下发
+
+## 签名
+
+```ts
+interface useExpressionScope {
+ (): any
+}
+```
+
+## 用例
+
+```tsx
+import React from 'react'
+import { createForm } from '@formily/core'
+import {
+ FormProvider,
+ createSchemaField,
+ useExpressionScope,
+ RecordScope,
+} from '@formily/react'
+
+const form = createForm()
+
+const Custom = () => {
+ const scope = useExpressionScope()
+ return (
+
+ {JSON.stringify(scope, null, 2)}
+
+ )
+}
+
+const SchemaField = createSchemaField({
+ components: {
+ Custom,
+ },
+ scope: {
+ topScope: {
+ aa: 123,
+ },
+ },
+})
+
+export default () => (
+
+ ({ name: 'Record Name', code: 'Record Code' })}
+ getIndex={() => 2}
+ >
+
+
+
+
+
+)
+```
diff --git a/packages/react/src/__tests__/expression.spec.tsx b/packages/react/src/__tests__/expression.spec.tsx
index b6f78d88afd..e910be14b72 100644
--- a/packages/react/src/__tests__/expression.spec.tsx
+++ b/packages/react/src/__tests__/expression.spec.tsx
@@ -70,6 +70,6 @@ test('x-compile-omitted', async () => {
)
await waitFor(() => {
- expect(queryByTestId('input').textContent).toBe('{{fake}}123321extra')
+ expect(queryByTestId('input')?.textContent).toBe('{{fake}}123321extra')
})
})
diff --git a/packages/react/src/__tests__/field.spec.tsx b/packages/react/src/__tests__/field.spec.tsx
index 6948387a0c6..4dd88e364b1 100644
--- a/packages/react/src/__tests__/field.spec.tsx
+++ b/packages/react/src/__tests__/field.spec.tsx
@@ -208,14 +208,14 @@ test('useFormEffects', async () => {
)
- expect(queryByTestId('custom-value').textContent).toEqual('')
+ expect(queryByTestId('custom-value')?.textContent).toEqual('')
form.query('aa').take((aa) => {
if (isField(aa)) {
aa.setValue('123')
}
})
await waitFor(() => {
- expect(queryByTestId('custom-value').textContent).toEqual('123')
+ expect(queryByTestId('custom-value')?.textContent).toEqual('123')
})
rerender(
diff --git a/packages/react/src/__tests__/schema.markup.spec.tsx b/packages/react/src/__tests__/schema.markup.spec.tsx
index a14865864e2..ad042ead1e1 100644
--- a/packages/react/src/__tests__/schema.markup.spec.tsx
+++ b/packages/react/src/__tests__/schema.markup.spec.tsx
@@ -6,6 +6,8 @@ import {
useFieldSchema,
useField,
RecursionField,
+ RecordScope,
+ RecordsScope,
} from '../index'
import { render, fireEvent, waitFor, act } from '@testing-library/react'
@@ -865,7 +867,7 @@ test('void field children', async () => {
)
await waitFor(() => {
- expect(queryByTestId('btn').textContent).toBe('placeholder')
+ expect(queryByTestId('btn')?.textContent).toBe('placeholder')
})
})
@@ -969,3 +971,86 @@ test('multi x-reactions isolate effect', async () => {
expect(otherEffect).toBeCalledTimes(1)
})
})
+
+test('nested record scope', async () => {
+ const form = createForm()
+ const SchemaField = createSchemaField({
+ components: {
+ Text: (props) => {props.text}
,
+ },
+ })
+
+ const { queryByTestId } = render(
+
+ ({ bb: '321' })} getIndex={() => 1}>
+ ({ aa: '123' })} getIndex={() => 2}>
+
+
+
+
+
+
+ )
+ await waitFor(() => {
+ expect(queryByTestId('text')?.textContent).toBe('12332121')
+ })
+})
+
+test('literal record scope', async () => {
+ const form = createForm()
+ const SchemaField = createSchemaField({
+ components: {
+ Text: (props) => {props.text}
,
+ },
+ })
+
+ const { queryByTestId } = render(
+
+ '123'} getIndex={() => 2}>
+
+
+
+
+
+ )
+ await waitFor(() => {
+ expect(queryByTestId('text')?.textContent).toBe('1232')
+ })
+})
+
+test('records scope', async () => {
+ const form = createForm()
+ const SchemaField = createSchemaField({
+ components: {
+ Text: (props) => {props.text}
,
+ },
+ })
+
+ const { queryByTestId } = render(
+
+ [1, 2, 3]}>
+
+
+
+
+
+ )
+ await waitFor(() => {
+ expect(queryByTestId('text')?.textContent).toBe('3')
+ })
+})
diff --git a/packages/react/src/components/RecordScope.tsx b/packages/react/src/components/RecordScope.tsx
new file mode 100644
index 00000000000..928eb6958eb
--- /dev/null
+++ b/packages/react/src/components/RecordScope.tsx
@@ -0,0 +1,37 @@
+import React from 'react'
+import { lazyMerge } from '@formily/shared'
+import { ExpressionScope } from './ExpressionScope'
+import { ReactFC, IRecordScopeProps } from '../types'
+import { useExpressionScope } from '../hooks'
+
+export const RecordScope: ReactFC = (props) => {
+ const scope = useExpressionScope()
+ return (
+
+ {props.children}
+
+ )
+}
diff --git a/packages/react/src/components/RecordsScope.tsx b/packages/react/src/components/RecordsScope.tsx
new file mode 100644
index 00000000000..1e8417da998
--- /dev/null
+++ b/packages/react/src/components/RecordsScope.tsx
@@ -0,0 +1,17 @@
+import React from 'react'
+import { ExpressionScope } from './ExpressionScope'
+import { ReactFC, IRecordsScopeProps } from '../types'
+
+export const RecordsScope: ReactFC = (props) => {
+ return (
+
+ {props.children}
+
+ )
+}
diff --git a/packages/react/src/components/RecursionField.tsx b/packages/react/src/components/RecursionField.tsx
index a94f24c4657..6d2bff19da8 100644
--- a/packages/react/src/components/RecursionField.tsx
+++ b/packages/react/src/components/RecursionField.tsx
@@ -1,17 +1,17 @@
-import React, { Fragment, useContext, useMemo } from 'react'
+import React, { Fragment, useMemo } from 'react'
import { isFn, isValid } from '@formily/shared'
import { GeneralField } from '@formily/core'
import { Schema } from '@formily/json-schema'
-import { SchemaContext, SchemaExpressionScopeContext } from '../shared'
+import { SchemaContext } from '../shared'
import { IRecursionFieldProps, ReactFC } from '../types'
-import { useField } from '../hooks'
+import { useField, useExpressionScope } from '../hooks'
import { ObjectField } from './ObjectField'
import { ArrayField } from './ArrayField'
import { Field } from './Field'
import { VoidField } from './VoidField'
const useFieldProps = (schema: Schema) => {
- const scope = useContext(SchemaExpressionScopeContext)
+ const scope = useExpressionScope()
return schema.toFieldProps({
scope,
}) as any
diff --git a/packages/react/src/components/SchemaField.tsx b/packages/react/src/components/SchemaField.tsx
index 48334607c14..7c4614713fe 100644
--- a/packages/react/src/components/SchemaField.tsx
+++ b/packages/react/src/components/SchemaField.tsx
@@ -4,7 +4,6 @@ import { RecursionField } from './RecursionField'
import { render } from '../shared/render'
import {
SchemaMarkupContext,
- SchemaExpressionScopeContext,
SchemaOptionsContext,
SchemaComponentsContext,
} from '../shared'
@@ -18,6 +17,7 @@ import {
ISchemaTypeFieldProps,
} from '../types'
import { lazyMerge } from '@formily/shared'
+import { ExpressionScope } from './ExpressionScope'
const env = {
nonameId: 0,
}
@@ -58,12 +58,10 @@ export function createSchemaField(
-
+
{renderMarkup()}
{renderChildren()}
-
+
)
diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts
index c17533ccd06..3edb5560322 100644
--- a/packages/react/src/components/index.ts
+++ b/packages/react/src/components/index.ts
@@ -5,5 +5,7 @@ export * from './ObjectField'
export * from './VoidField'
export * from './RecursionField'
export * from './ExpressionScope'
+export * from './RecordsScope'
+export * from './RecordScope'
export * from './SchemaField'
export * from './Field'
diff --git a/packages/react/src/hooks/index.ts b/packages/react/src/hooks/index.ts
index 89bbdf8187c..ec868cd9bdc 100644
--- a/packages/react/src/hooks/index.ts
+++ b/packages/react/src/hooks/index.ts
@@ -3,3 +3,4 @@ export * from './useField'
export * from './useParentForm'
export * from './useFieldSchema'
export * from './useFormEffects'
+export * from './useExpressionScope'
diff --git a/packages/react/src/hooks/useExpressionScope.ts b/packages/react/src/hooks/useExpressionScope.ts
new file mode 100644
index 00000000000..9e095821f63
--- /dev/null
+++ b/packages/react/src/hooks/useExpressionScope.ts
@@ -0,0 +1,4 @@
+import { useContext } from 'react'
+import { SchemaExpressionScopeContext } from '../shared/context'
+
+export const useExpressionScope = () => useContext(SchemaExpressionScopeContext)
diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts
index 4b537f2be4d..248c3e9b22c 100644
--- a/packages/react/src/types.ts
+++ b/packages/react/src/types.ts
@@ -187,6 +187,15 @@ export interface IExpressionScopeProps {
value?: any
}
+export interface IRecordScopeProps {
+ getIndex?(): number
+ getRecord(): any
+}
+
+export interface IRecordsScopeProps {
+ getRecords(): any[]
+}
+
export type ReactChild = React.ReactElement | string | number
export { ReactFC }