diff --git a/packages/client/src/async-data-provider/demos/demo1.tsx b/packages/client/src/async-data-provider/demos/demo1.tsx new file mode 100644 index 000000000..f8c2dc224 --- /dev/null +++ b/packages/client/src/async-data-provider/demos/demo1.tsx @@ -0,0 +1,22 @@ +import React, { createContext, useContext } from 'react'; +import MockAdapter from 'axios-mock-adapter'; +import { APIClient, APIClientProvider, compose, AsyncDataProvider, useAsyncData } from '@nocobase/client'; + +const apiClient = new APIClient(); + +const mock = new MockAdapter(apiClient.axios); + +mock.onGet('/users/1').reply(200, { + data: { id: 1, name: 'John Smith' }, +}); + +const providers = [ + [APIClientProvider, { apiClient }], + [AsyncDataProvider, { request: { url: '/users/1' } }], +]; + +export default compose(...providers)(() => { + const { data } = useAsyncData(); + console.log(data); + return
{data?.data?.name}
; +}); diff --git a/packages/client/src/async-data-provider/demos/demo2.tsx b/packages/client/src/async-data-provider/demos/demo2.tsx new file mode 100644 index 000000000..1e687b572 --- /dev/null +++ b/packages/client/src/async-data-provider/demos/demo2.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Table } from 'antd'; +import MockAdapter from 'axios-mock-adapter'; +import { APIClientProvider, SchemaComponentProvider, SchemaComponent, useRequest, APIClient } from '@nocobase/client'; +import { observer } from '@formily/react'; + +const apiClient = new APIClient(); + +const mock = new MockAdapter(apiClient.axios); + +mock.onGet('/users').reply(200, { + data: [ + { id: 1, name: 'John Smith' }, + { id: 2, name: 'Mike' }, + ], +}); + +const TableView = (props) => { + const { request, ...others } = props; + const callback = () => props.dataSource || []; + const { data, loading } = useRequest(props.request || callback); + return ; +}; + +export default () => { + return ( + + + + + + ); +}; diff --git a/packages/client/src/async-data-provider/demos/demo3.tsx b/packages/client/src/async-data-provider/demos/demo3.tsx new file mode 100644 index 000000000..61c3ae551 --- /dev/null +++ b/packages/client/src/async-data-provider/demos/demo3.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { Table } from 'antd'; +import MockAdapter from 'axios-mock-adapter'; +import { + APIClientProvider, + SchemaComponentProvider, + SchemaComponent, + useRequest, + APIClient, + AsyncDataProvider, + useAsyncData, +} from '@nocobase/client'; + +const apiClient = new APIClient(); + +const mock = new MockAdapter(apiClient.axios); + +mock.onGet('/users').reply(200, { + data: [ + { id: 1, name: 'John Smith' }, + { id: 2, name: 'Mike' }, + ], +}); + +const TableView = (props) => { + const { data, loading } = useAsyncData(); + return
; +}; + +const AsyncDataTableView = (props) => { + const { request, ...others } = props; + return ( + + + + ); +}; + +export default () => { + return ( + + + + + + ); +}; diff --git a/packages/client/src/async-data-provider/index.md b/packages/client/src/async-data-provider/index.md new file mode 100644 index 000000000..5bb92c274 --- /dev/null +++ b/packages/client/src/async-data-provider/index.md @@ -0,0 +1,155 @@ +--- +nav: + path: /client +group: + path: /client +--- + +# AsyncDataProvider + +利用 Formily 提供的 [Effect Hooks](https://core.formilyjs.org/zh-CN/api/entry/form-effect-hooks) 或 [x-reactions](https://react.formilyjs.org/zh-CN/api/shared/schema#schemareactions) 协议能一定程度的解决某些场景的异步数据源问题,但是如果只用来处理异步请求,并不会很好用,而且局限于 Formily 体系内。为了更灵活的处理异步请求的各种需求,NocoBase 提供了非常好用的可配置化的 [useRequest()](api-client#userequest),用于配置各种异步请求。与此同时,又提供了 `` 组件,可以将请求结果共享给子组件,方便处理其他更复杂的需求。以平时最常见,但却可能非常复杂的表格为例。表格的使用场景有: + +- TableView 只作为表格视图 +- RowSelection 动态数据的勾选项 +- TableField 表格字段 + +各自的 schema 可以这么表示: + +```js +// 表格视图,type 为 void,无 default 默认值,无 field.value, +// dataSource 直接写在组件的 props 里 +{ + type: 'void', + 'x-component': 'TableView', + 'x-component-props': { + columns: [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + }, + ], + dataSource: [ + { id: 1, name: 'Name1' }, + { id: 2, name: 'Name2' }, + ], + }, +} + +// 单选(表格视图),类似于 Radio.Group 字段,可选项由表格视图提供 +{ + type: 'number', + 'x-component': 'RowSelection', + 'x-component-props': { + rowSelection: { + type: 'radio', + }, + columns: [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + }, + ], + dataSource: [ + { id: 1, name: 'Name1' }, + { id: 2, name: 'Name2' }, + ], + }, + default: 1, +} + +// 多选(表格视图),类似于 Checkbox.Group 字段,可选项由表格视图提供 +{ + type: 'array', + 'x-component': 'RowSelection', + 'x-component-props': { + rowSelection: { + type: 'checkbox', + }, + columns: [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + }, + ], + dataSource: [ + { id: 1, name: 'Name1' }, + { id: 2, name: 'Name2' }, + ], + }, + default: [1], +} + +// 表格字段,数据由表单提供,schema 层面可以配置默认值 +// 与表格视图的方式不同,dataSource 和 columns 不适合直接配置在 x-component-props 里,怎么处理先忽略。 +{ + type: 'array', + 'x-component': 'TableField', + 'x-component-props': {}, + default: [ + { id: 1, name: 'Name1' }, + { id: 2, name: 'Name2' }, + ], +} +``` + +大多数实际应用中,表格视图的 dataSource 是异步获取的,可能也需要过滤和分页,这样一来直接在 x-component-props 里写 dataSource 并不合适。为了解决这个问题,可以给组件扩展一个 request 属性,用于请求远程数据,例子如下: + + + +上述例子的 `` 组件就是一个 React Component,可以正常使用,而不局限于 SchemaComponent 场景。更进一步,为了处理过滤表单、分页等问题,建议将 request 通过 `` 共享给 `` 组件。 + +```tsx | pure +const result = useRequest(props.request); + + + +``` + +或者直接将 request 交给 AsyncDataProvider + +```tsx | pure + + + +``` + +在 `` 的子组件里就可以通过 `useAsyncData()` 来获取 result,可用于处理过滤、分页等。 + +```ts +const { data, loading, params, run, refresh, parent } = useAsyncData(); +``` + +转化为 Schema 时,可以将 AsyncDataProvider 放在 `x-decorator` 里,也可以将重新组合的 AsyncDataProvider + TableView,放在 `x-component` 里。 + + + +将 AsyncDataProvider 独立出来,放到 x-decorator 里的另一个好处,例子如下: + +```tsx | pure + + {/* 从 CollectionProvider 里获取资源请求参数,并将请求结果同时共享给 DesignableBar 和 TableView */} + + + + + + {/* 从 CollectionFieldProvider 里获取关系资源请求参数,并将请求结果同时共享给子组件 */} + + {/* 组件 */} + + + + + + +``` + +上面例子嵌套了两层 AsyncDataProvider,在子 AsyncDataProvider 里,也可以获取到更上一级的请求结果,如: + +```ts +const { parent } = useAsyncData(); +parent.refresh(); +``` diff --git a/packages/client/src/async-data-provider/index.tsx b/packages/client/src/async-data-provider/index.tsx new file mode 100644 index 000000000..fe77e9c55 --- /dev/null +++ b/packages/client/src/async-data-provider/index.tsx @@ -0,0 +1,27 @@ +import React, { createContext, useContext } from 'react'; +import { Result } from 'ahooks/lib/useRequest/src/types'; +import { useRequest } from '../api-client'; + +export const AsyncDataContext = createContext>(null); + +export interface AsyncDataProviderProps { + value?: any; + resource?: any; + request?: any; + action?: string; + defaultParams?: any; +} + +export const AsyncDataProvider: React.FC = (props) => { + const { value, request, resource, action, defaultParams, children } = props; + if (value) { + return {children}; + } + const callback = (params?: any) => resource[action]({ ...defaultParams, ...params }); + const result = useRequest(request || callback); + return {children}; +}; + +export const useAsyncData = () => { + return useContext(AsyncDataContext); +}; diff --git a/packages/client/src/index.tsx b/packages/client/src/index.tsx index 83e3dcf72..dc3453820 100644 --- a/packages/client/src/index.tsx +++ b/packages/client/src/index.tsx @@ -22,3 +22,4 @@ export * from './dnd-context'; export * from './grid'; export * from './application'; export * from './record-provider'; +export * from './async-data-provider';