diff --git a/docs/index.md b/docs/index.md index 88b1701c6aa..9e2db9bc1a8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -122,14 +122,10 @@ export default () => (
- - +
) diff --git a/docs/index.zh-CN.md b/docs/index.zh-CN.md index 96fb6dc5af3..7d6a80e8edb 100644 --- a/docs/index.zh-CN.md +++ b/docs/index.zh-CN.md @@ -120,16 +120,12 @@ import './site/styles.less' export default () => (
- - +
) diff --git a/packages/antd/src/array-base/index.tsx b/packages/antd/src/array-base/index.tsx index 6979cceb07e..fffa40d0c5e 100644 --- a/packages/antd/src/array-base/index.tsx +++ b/packages/antd/src/array-base/index.tsx @@ -15,7 +15,8 @@ import { useFieldSchema, Schema, JSXComponent, - ExpressionScope, + RecordScope, + RecordsScope, } from '@formily/react' import { isValid, clone } from '@formily/shared' import { SortableHandle } from 'react-sortable-hoc' @@ -109,18 +110,25 @@ export const ArrayBase: ComposedArrayBase = (props) => { const field = useField() const schema = useFieldSchema() return ( - - {props.children} - + field.value}> + + {props.children} + + ) } ArrayBase.Item = ({ children, ...props }) => { return ( - + props.index} + getRecord={() => + typeof props.record === 'function' ? props.record() : props.record + } + > {children} - + ) } diff --git a/packages/antd/src/array-cards/index.tsx b/packages/antd/src/array-cards/index.tsx index 4a8a93959b4..7f543949dde 100644 --- a/packages/antd/src/array-cards/index.tsx +++ b/packages/antd/src/array-cards/index.tsx @@ -98,7 +98,11 @@ export const ArrayCards: ComposedArrayCards = observer((props) => { /> ) return ( - + dataSource[index]} + > {}} diff --git a/packages/antd/src/array-collapse/index.tsx b/packages/antd/src/array-collapse/index.tsx index 5c77274bfcc..f3895e5d83d 100644 --- a/packages/antd/src/array-collapse/index.tsx +++ b/packages/antd/src/array-collapse/index.tsx @@ -140,7 +140,7 @@ export const ArrayCollapse: ComposedArrayCollapse = observer( address: `${path}.**`, }) return ( - + dataSource[index]}> { ? schema.items[index] || schema.items[0] : schema.items return ( - + dataSource[index]} + >
diff --git a/packages/antd/src/array-table/index.tsx b/packages/antd/src/array-table/index.tsx index e116f092516..a44486254cf 100644 --- a/packages/antd/src/array-table/index.tsx +++ b/packages/antd/src/array-table/index.tsx @@ -135,7 +135,7 @@ const useArrayTableColumns = ( render: (value: any, record: any) => { const index = dataSource.indexOf(record) const children = ( - + dataSource[index]}> ) diff --git a/packages/antd/src/form/index.tsx b/packages/antd/src/form/index.tsx index 6bcf6a6cd50..8e7b4429657 100644 --- a/packages/antd/src/form/index.tsx +++ b/packages/antd/src/form/index.tsx @@ -1,10 +1,15 @@ import React from 'react' -import { Form as FormType, ObjectField, IFormFeedback } from '@formily/core' +import { + Form as FormType, + ObjectField, + IFormFeedback, + isForm, +} from '@formily/core' import { useParentForm, FormProvider, - ExpressionScope, JSXComponent, + RecordScope, } from '@formily/react' import { FormLayout, IFormLayoutProps } from '../form-layout' import { PreviewText } from '../preview-text' @@ -26,7 +31,7 @@ export const Form: React.FC> = ({ }) => { const top = useParentForm() const renderContent = (form: FormType | ObjectField) => ( - + (isForm(form) ? form.values : form.value)}> {React.createElement( @@ -42,7 +47,7 @@ export const Form: React.FC> = ({ )} - + ) if (form) return {renderContent(form)} diff --git a/packages/next/src/array-base/index.tsx b/packages/next/src/array-base/index.tsx index 2a7c714acab..dd614141b6f 100644 --- a/packages/next/src/array-base/index.tsx +++ b/packages/next/src/array-base/index.tsx @@ -8,7 +8,8 @@ import { useFieldSchema, Schema, JSXComponent, - ExpressionScope, + RecordScope, + RecordsScope, } from '@formily/react' import { SortableHandle } from 'react-sortable-hoc' import { @@ -105,18 +106,25 @@ export const ArrayBase: ComposedArrayBase = (props) => { const field = useField() const schema = useFieldSchema() return ( - - {props.children} - + field.value}> + + {props.children} + + ) } ArrayBase.Item = ({ children, ...props }) => { return ( - + props.index} + getRecord={() => + typeof props.record === 'function' ? props.record() : props.record + } + > {children} - + ) } diff --git a/packages/next/src/array-cards/index.tsx b/packages/next/src/array-cards/index.tsx index 3354d7924f9..7a9dc3a0071 100644 --- a/packages/next/src/array-cards/index.tsx +++ b/packages/next/src/array-cards/index.tsx @@ -151,7 +151,11 @@ export const ArrayCards: ComposedArrayCards = observer((props) => { /> ) return ( - + dataSource[index]} + > - + dataSource[index]} + > {content} diff --git a/packages/next/src/array-items/index.tsx b/packages/next/src/array-items/index.tsx index 731e8e8eb44..b730927b740 100644 --- a/packages/next/src/array-items/index.tsx +++ b/packages/next/src/array-items/index.tsx @@ -96,7 +96,11 @@ export const ArrayItems: ComposedArrayItems = observer((props) => { ? schema.items[index] || schema.items[0] : schema.items return ( - + dataSource[index]} + >
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 }