diff --git a/.eslintrc b/.eslintrc index 572c8635275..82a84fcae93 100644 --- a/.eslintrc +++ b/.eslintrc @@ -12,5 +12,5 @@ "ecmaFeatures": { "jsx": true } - } + }, } \ No newline at end of file diff --git a/README.md b/README.md index 198796cc813..c4f1ae1b9a7 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,42 @@ Use Fusion Design: npm install --save @alifd/next @uform/next ``` +Use uform react render engine pacakge: + +```bash +npm install --save @uform/react-schema-renderer +``` + +Use uform react package: + +```bash +npm install --save @uform/react +``` + +Use uform core package: + +```bash +npm install --save @uform/core +``` + +## Architecture + +![](https://img.alicdn.com/tfs/TB1i9nmolv0gK0jSZKbXXbK2FXa-1882-1144.png) + + +## WebSite + +https://uformjs.org (0.x) + +https://uform-next.netlify.com (1.x) + ## Documents -https://uformjs.org +- [@uform/antd](./packages/antd/README.md) +- [@uform/next](./packages/next/README.md) +- [@uform/react-schema-renderer](./packages/react-schema-renderer/README.md) +- [@uform/react](./packages/react/README.md) +- [@uform/core](./packages/core/README.md) ## Demo @@ -72,17 +105,6 @@ Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/uform#sponsor)] - - - - - - - - - - - ## LICENSE diff --git a/README.zh-cn.md b/README.zh-cn.md index 28c21dc2eb9..522dc6b1156 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -41,9 +41,42 @@ npm install --save antd @uform/antd npm install --save @alifd/next @uform/next ``` +使用UForm React渲染引擎包: + +```bash +npm install --save @uform/react-schema-renderer +``` + +使用 UForm React包: + +```bash +npm install --save @uform/react +``` + +使用 UForm 核心包: + +```bash +npm install --save @uform/core +``` + +## 官网 + +https://uformjs.org (0.x) + +https://uform-next.netlify.com (1.x) + +## 架构 + +![](https://img.alicdn.com/tfs/TB1i9nmolv0gK0jSZKbXXbK2FXa-1882-1144.png) + ## 文档 -https://uformjs.org +- [@uform/antd](./packages/antd/README.zh-cn.md) +- [@uform/next](./packages/next/README.zh-cn.md) +- [@uform/react-schema-renderer](./packages/react-schema-renderer/README.zh-cn.md) +- [@uform/react](./packages/react/README.zh-cn.md) +- [@uform/core](./packages/core/README.zh-cn.md) + ## 入门案例 @@ -73,19 +106,6 @@ Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/uform#sponsor)] - - - - - - - - - - - - - ## LICENSE diff --git a/docs/Examples/antd/Deconstruction.md b/docs/Examples/antd/Deconstruction.md index 2e99cb3a2b7..1d55549379f 100644 --- a/docs/Examples/antd/Deconstruction.md +++ b/docs/Examples/antd/Deconstruction.md @@ -224,7 +224,7 @@ const App = () => ( { - $('onFieldChange', 'wrapper.relation').subscribe(({ value }) => { + $('onFieldValueChange', 'wrapper.relation').subscribe(({ value }) => { setFieldState( FormPath.match( 'wrapper.[[{aa:{bb:{cc:destructor1,dd:\\[destructor2,destructor3\\],ee}}}]]' diff --git a/docs/Examples/antd/Layout.md b/docs/Examples/antd/Layout.md index a19533af5f6..659b47a9b02 100644 --- a/docs/Examples/antd/Layout.md +++ b/docs/Examples/antd/Layout.md @@ -164,7 +164,7 @@ import Printer from '@uform/printer' import 'antd/dist/antd.css' const App = () => { - const [state, setState] = useState({ editable: true }) + const [state, setState] = useState({ editable: true }) return ( { ​​ - - ​ + ​ + 提交​ - - 重置​ + + 重置 @@ -241,6 +241,7 @@ import { FormPath, FormBlock, FormLayout, + FormTextBox, createFormActions } from '@uform/antd' import { Button } from 'antd' @@ -268,3 +269,94 @@ const App = () => ( ) ReactDOM.render(, document.getElementById('root')) ``` + +## 分步表单 + +```jsx +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + FormEffectHooks, + createFormActions, + FormGridRow, + FormItemGrid, + FormGridCol, + FormPath, + FormLayout, + FormBlock, + FormCard, + FormTextBox, + FormStep +} from '@uform/antd' +import { Button } from 'antd' +import 'antd/dist/antd.css' + +const { onFormInit$ } = FormEffectHooks + +const actions = createFormActions() + +let cache = {} + +export default () => ( + { + console.log('提交') + console.log(values) + }} + actions={actions} + labelCol={{ span: 8 }} + wrapperCol={{ span: 6 }} + validateFirst + effects={({ setFieldState, getFormGraph }) => { + onFormInit$().subscribe(() => { + setFieldState('col1', state => { + state.visible = false + }) + }) + }} + > + + + + + + + + + + + + 提交 + + + + + + +) +``` diff --git a/docs/Examples/antd/List.md b/docs/Examples/antd/List.md index 10ca72e9fde..452896f3f97 100644 --- a/docs/Examples/antd/List.md +++ b/docs/Examples/antd/List.md @@ -3,10 +3,11 @@ > 数组场景,区块型数组,能解决大量字段的聚合输入,但是对于数据的对比化展示,区分 > 度不够明显 -下面对于List场景我们主要封装了 -- Array类型组件 -- Table类型组件 -- Card类型组件 +下面对于 List 场景我们主要封装了 + +- Array 类型组件 +- Table 类型组件 +- Card 类型组件 这些组件你都可以对其做简单的定制来适应你当前的业务需求,比如 @@ -66,8 +67,8 @@ const App = () => { maxItems={3} type="array" x-props={{ - renderAddition:'这是定制的添加文案', - renderRemove:'这是定制的删除文案' + renderAddition: '这是定制的添加文案', + renderRemove: '这是定制的删除文案' }} > @@ -82,11 +83,7 @@ const App = () => { - + @@ -149,11 +146,23 @@ const App = () => ( maxItems={3} type="array" x-component="table" - x-props={{ - renderExtraOperations(){ - return
Hello worldasdasdasdasd
+ x-props={{ + scroll: { x: '200%' }, + renderExtraOperations() { + return ( +
+ Hello worldasdasdasdasd +
+ ) }, - operationsWidth:400 + operationsWidth: 400 }} > @@ -169,7 +178,7 @@ const App = () => ( - +
@@ -207,12 +216,17 @@ import 'antd/dist/antd.css' const App = () => ( - + ( - + - + { } }) }) - $('onFieldChange', 'aa').subscribe(fieldState => { + $('onFieldValueChange', 'aa').subscribe(fieldState => { + console.log(fieldState.value) setFieldState('bb', state => { state.visible = !fieldState.value }) }) - $('onFieldChange', 'cc').subscribe(fieldState => { + $('onFieldValueChange', 'cc').subscribe(fieldState => { setFieldState('dd', state => { state.visible = !fieldState.value }) @@ -88,12 +89,12 @@ const App = () => { } }) }) - $('onFieldChange', 'mm').subscribe(fieldState => { + $('onFieldValueChange', 'mm').subscribe(fieldState => { setFieldState('ff', state => { state.visible = !fieldState.value }) }) - $('onFieldChange', 'gg') + $('onFieldValueChange', 'gg') .pipe( withLatestFrom($('onChangeOption')), map(([fieldState, { payload: option }]) => { @@ -135,7 +136,7 @@ const App = () => { }) }} labelCol={6} - wrapperCol={4} + wrapperCol={12} onSubmit={v => console.log(v)} > @@ -201,6 +202,96 @@ const App = () => { ReactDOM.render(, document.getElementById('root')) ``` + +### 循环联动 + +> 联动关系 +> 总价 = 单价 \* 数量 +> 数量 = 总价 / 单价 +> 单价 = 总价 / 数量 + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormPath, + FormBlock, + FormLayout +} from '@uform/next' +import { filter, withLatestFrom, map, debounceTime } from 'rxjs/operators' +import { Button } from '@alifd/next' +import Printer from '@uform/printer' +import '@alifd/next/dist/next.css' + +const App = () => ( + + { + $('onFieldValueChange', 'total').subscribe(({ value }) => { + if (!value) return + setFieldState('count', state => { + const price = getFieldState('price', state => state.value) + if (!price) return + state.value = value / price + }) + setFieldState('price', state => { + const count = getFieldState('count', state => state.value) + if (!count) return + state.value = value / count + }) + }) + $('onFieldValueChange', 'price').subscribe(({ value }) => { + if (!value) return + setFieldState('total', state => { + const count = getFieldState('count', state => state.value) + if (!count) return + state.value = value * count + }) + setFieldState('count', state => { + const total = getFieldState('total', state => state.value) + if (!total) return + state.value = total / value + }) + }) + $('onFieldValueChange', 'count').subscribe(({ value }) => { + if (!value) return + setFieldState('total', state => { + const price = getFieldState('price', state => state.value) + if (!price) return + state.value = value * price + }) + setFieldState('price', state => { + const total = getFieldState('total', state => state.value) + if (!total) return + state.value = total / value + }) + }) + }} + onChange={v => console.log(v)} + labelCol={6} + wrapperCol={4} + onSubmit={v => console.log(v)} + > + + + + + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + ### 异步数据联动 > 当前例子主要演示了从某个字段的变化,引起某些异步操作,然后再去更新某些字段的状 @@ -266,7 +357,7 @@ const App = () => ( $('onFormInit').subscribe(() => { hide('bb') }) - $('onFieldChange', 'aa').subscribe(fieldState => { + $('onFieldValueChange', 'aa').subscribe(fieldState => { if (!fieldState.value) return show('bb') loading('bb') @@ -276,7 +367,7 @@ const App = () => ( setValue('bb', '1111') }, 1000) }) - $('onFieldChange', 'bb').subscribe(fieldState => { + $('onFieldValueChange', 'bb').subscribe(fieldState => { console.log(fieldState.loading) if (!fieldState.value) return hide('cc') show('cc') @@ -339,7 +430,7 @@ const App = () => ( { - $('onFieldChange', 'bb').subscribe(state => { + $('onFieldValueChange', 'bb').subscribe(state => { if (state.value) { setFieldState('aa', state => { state.value = '123' @@ -451,7 +542,7 @@ const App = () => { $('onFormInit').subscribe(() => { hide(FormPath.match('aa.*.*(cc,gg,dd.*.ee)')) }) - $('onFieldChange', 'aa.*.bb').subscribe(fieldState => { + $('onFieldValueChange', 'aa.*.bb').subscribe(fieldState => { const cc = FormPath.transform( fieldState.name, /\d+/, @@ -469,7 +560,7 @@ const App = () => { setValue(cc, '1111') }, 1000) }) - $('onFieldChange', 'aa.*.dd.*.ee').subscribe(fieldState => { + $('onFieldValueChange', 'aa.*.dd.*.ee').subscribe(fieldState => { const gg = FormPath.transform( fieldState.name, /\d+/, @@ -481,7 +572,7 @@ const App = () => { } }) }) - $('onFieldChange', 'aa.*.dd.*.ff').subscribe(fieldState => { + $('onFieldValueChange', 'aa.*.dd.*.ff').subscribe(fieldState => { const ee = FormPath.transform( fieldState.name, /\d+/, diff --git a/docs/Examples/antd/Sample.md b/docs/Examples/antd/Sample.md index eb0ddeca04c..d3ef12a4f55 100644 --- a/docs/Examples/antd/Sample.md +++ b/docs/Examples/antd/Sample.md @@ -34,7 +34,6 @@ ReactDOM.render( actions={actions} labelCol={7} initialValues={{ - date:'2019-08-01', upload3:[{ downloadURL: "//img.alicdn.com/tfs/TB1n8jfr1uSBuNjy1XcXXcYjFXa-200-200.png", @@ -57,6 +56,7 @@ ReactDOM.render( enum={['1', '2', '3', '4']} title="Radio" name="radio" + default={'2'} /> - + ( labelCol={6} wrapperCol={6} effects={($, { setFieldState, getFieldState }) => { - $('onFieldChange', '*(password,confirm)').subscribe(fieldState => { + $('onFieldValueChange', '*(password,confirm)').subscribe(fieldState => { const selfName = fieldState.name const selfValue = fieldState.value const otherName = selfName == 'password' ? 'confirm' : 'password' diff --git a/docs/Examples/next/Deconstruction.md b/docs/Examples/next/Deconstruction.md index 1fce3ec1bd2..b3a63e65446 100644 --- a/docs/Examples/next/Deconstruction.md +++ b/docs/Examples/next/Deconstruction.md @@ -172,7 +172,7 @@ ReactDOM.render(, document.getElementById('root')) ### 使用 FormPath 路径匹配处理联动 -> 注意使用`[[]]`来包裹带关键字的 name 才能匹配 +> ~~注意使用`[[]]`来包裹带关键字的 name 才能匹配~~ v1.x不再需要包裹 ```jsx import React from 'react' @@ -224,11 +224,9 @@ const App = () => ( { - $('onFieldChange', 'wrapper.relation').subscribe(({ value }) => { + $('onFieldValueChange', 'wrapper.relation').subscribe(({ value }) => { setFieldState( - FormPath.match( - 'wrapper.[[{aa:{bb:{cc:destructor1,dd:\\[destructor2,destructor3\\],ee}}}]]' - ), + 'wrapper.{aa:{bb:{cc:destructor1,dd:[ destructor2, destructor3 ],ee}}}', state => { state.visible = value == 2 } diff --git a/docs/Examples/next/Detail.md b/docs/Examples/next/Detail.md index dbad175b618..b5c27488fef 100644 --- a/docs/Examples/next/Detail.md +++ b/docs/Examples/next/Detail.md @@ -50,7 +50,7 @@ const App = () => { 选项1, value: '1' }, + { label: '选项1', value: '1' }, { label: '选项2', value: '2' }, { label: '选项3', value: '3' }, { label: '选项4', value: '4' } diff --git a/docs/Examples/next/Layout.md b/docs/Examples/next/Layout.md index 630e0e6666c..864d0612f22 100644 --- a/docs/Examples/next/Layout.md +++ b/docs/Examples/next/Layout.md @@ -171,33 +171,55 @@ const App = () => { - - ​ - ​​ - - - - ​ - ​​ - - + + ​ + ​​ + + + + ​ + ​​ + + - - - - + + + + - ​ - + ​提交 @@ -255,3 +277,94 @@ const App = () => ( ) ReactDOM.render(, document.getElementById('root')) ``` + +## 分步表单 + +```jsx +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + FormEffectHooks, + createFormActions, + FormGridRow, + FormItemGrid, + FormGridCol, + FormPath, + FormLayout, + FormBlock, + FormCard, + FormTextBox, + FormStep +} from '@uform/next' +import { Button } from '@alifd/next' +import '@alifd/next/dist/next.css' + +const { onFormInit$ } = FormEffectHooks + +const actions = createFormActions() + +let cache = {} + +export default () => ( + { + console.log('提交') + console.log(values) + }} + actions={actions} + labelCol={{ span: 8 }} + wrapperCol={{ span: 6 }} + validateFirst + effects={({ setFieldState, getFormGraph }) => { + onFormInit$().subscribe(() => { + setFieldState('col1', state => { + state.visible = false + }) + }) + }} + > + + + + + + + + + + + + 提交 + + + + + + +) +``` diff --git a/docs/Examples/next/List.md b/docs/Examples/next/List.md index a1857a9ae6e..4f2f6ee625b 100644 --- a/docs/Examples/next/List.md +++ b/docs/Examples/next/List.md @@ -157,7 +157,12 @@ const App = () => ( - + diff --git a/docs/Examples/next/Pressure.md b/docs/Examples/next/Pressure.md new file mode 100644 index 00000000000..2fd8455c9bd --- /dev/null +++ b/docs/Examples/next/Pressure.md @@ -0,0 +1,41 @@ +# 压测 + +> 1000个表单项,实时卡顿情况监测 + +```jsx +import React from "react"; +import ReactDOM from "react-dom"; +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormButtonGroup, + Submit +} from "@uform/next"; +import "@alifd/next/dist/next.css"; + +const actions = createFormActions(); + +const range = num => { + let result = []; + for (let i = 0; i < num; i++) { + result.push(i); + } + return result; +}; + +const App = () => { + return ( + + {range(1000).map(key => ( + + ))} + + 提交 + + + ); +}; + +ReactDOM.render(, document.getElementById("root")); + +``` \ No newline at end of file diff --git a/docs/Examples/next/Relations.md b/docs/Examples/next/Relations.md index 24d95d4ba22..47561dbdccd 100644 --- a/docs/Examples/next/Relations.md +++ b/docs/Examples/next/Relations.md @@ -41,7 +41,7 @@ import { FormBlock, FormLayout } from '@uform/next' -import { filter, withLatestFrom, map, debounceTime } from 'rxjs/operators' +import { filter, combineLatest, map, debounceTime } from 'rxjs/operators' import { Button } from '@alifd/next' import Printer from '@uform/printer' import '@alifd/next/dist/next.css' @@ -51,7 +51,7 @@ const App = () => { return ( { + effects={($, { setFieldState, getFieldState, getFormGraph }) => { $('onFormInit').subscribe(() => { setFieldState(FormPath.match('*(gg,hh)'), state => { state.props['x-props'] = state.props['x-props'] || {} @@ -63,12 +63,15 @@ const App = () => { } }) }) - $('onFieldChange', 'aa').subscribe(fieldState => { + $('onFieldValueChange', '*(aa,bb)').subscribe(fieldState => { + console.log('aa或者bb发生变化了') + }) + $('onFieldValueChange', 'aa').subscribe(fieldState => { setFieldState('bb', state => { state.visible = !fieldState.value }) }) - $('onFieldChange', 'cc').subscribe(fieldState => { + $('onFieldValueChange', 'cc').subscribe(fieldState => { setFieldState('dd', state => { state.visible = !fieldState.value }) @@ -86,9 +89,9 @@ const App = () => { } }) }) - $('onFieldChange', 'gg') + $('onFieldValueChange', 'gg') .pipe( - withLatestFrom($('onChangeOption')), + combineLatest($('onChangeOption')), map(([fieldState, { payload: option }]) => { return { state: fieldState, @@ -131,13 +134,16 @@ const App = () => { wrapperCol={4} onSubmit={v => console.log(v)} > - + @@ -147,7 +153,10 @@ const App = () => { title="是否隐藏DD" default={true} x-component="radio" - enum={[{ label: '是', value: true }, { label: '否', value: false }]} + enum={[ + { label: '是', value: true }, + { label: '否', value: false } + ]} /> @@ -159,7 +168,7 @@ const App = () => { name="gg" type="string" x-effect={dispatch => ({ - onChange(value,type, option) { + onChange(value, type, option) { dispatch('onChangeOption', option) }, onSearch(value) { @@ -186,6 +195,95 @@ const App = () => { ReactDOM.render(, document.getElementById('root')) ``` +### 循环联动 + +> 联动关系 +> 总价 = 单价 \* 数量 +> 数量 = 总价 / 单价 +> 单价 = 总价 / 数量 + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormPath, + FormBlock, + FormLayout +} from '@uform/next' +import { filter, withLatestFrom, map, debounceTime } from 'rxjs/operators' +import { Button } from '@alifd/next' +import Printer from '@uform/printer' +import '@alifd/next/dist/next.css' + +const App = () => ( + + { + $('onFieldValueChange', 'total').subscribe(({ value }) => { + if (!value) return + setFieldState('count', state => { + const price = getFieldState('price', state => state.value) + if (!price) return + state.value = value / price + }) + setFieldState('price', state => { + const count = getFieldState('count', state => state.value) + if (!count) return + state.value = value / count + }) + }) + $('onFieldValueChange', 'price').subscribe(({ value }) => { + if (!value) return + setFieldState('total', state => { + const count = getFieldState('count', state => state.value) + if (!count) return + state.value = value * count + }) + setFieldState('count', state => { + const total = getFieldState('total', state => state.value) + if (!total) return + state.value = total / value + }) + }) + $('onFieldValueChange', 'count').subscribe(({ value }) => { + if (!value) return + setFieldState('total', state => { + const price = getFieldState('price', state => state.value) + if (!price) return + state.value = value * price + }) + setFieldState('price', state => { + const total = getFieldState('total', state => state.value) + if (!total) return + state.value = total / value + }) + }) + }} + onChange={v => console.log(v)} + labelCol={6} + wrapperCol={4} + onSubmit={v => console.log(v)} + > + + + + + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + ### 异步数据联动 > 当前例子主要演示了从某个字段的变化,引起某些异步操作,然后再去更新某些字段的状 @@ -250,7 +348,7 @@ const App = () => ( $('onFormInit').subscribe(() => { hide('bb') }) - $('onFieldChange', 'aa').subscribe(fieldState => { + $('onFieldValueChange', 'aa').subscribe(fieldState => { if (!fieldState.value) return show('bb') loading('bb') @@ -260,7 +358,7 @@ const App = () => ( setValue('bb', '1111') }, 1000) }) - $('onFieldChange', 'bb').subscribe(fieldState => { + $('onFieldValueChange', 'bb').subscribe(fieldState => { console.log(fieldState.loading) if (!fieldState.value) return hide('cc') show('cc') @@ -322,7 +420,7 @@ const App = () => ( { - $('onFieldChange', 'bb').subscribe(state => { + $('onFieldValueChange', 'bb').subscribe(state => { if (state.value) { setFieldState('aa', state => { state.value = '123' @@ -358,7 +456,7 @@ ReactDOM.render(, document.getElementById('root')) #### Demo 示例 ```jsx -import React,{useState,useEffect} from 'react' +import React, { useState, useEffect } from 'react' import ReactDOM from 'react-dom' import { SchemaForm, @@ -378,9 +476,9 @@ import Printer from '@uform/printer' import '@alifd/next/dist/next.css' const App = () => { - const [values,setValues] = useState({}) - useEffect(()=>{ - setTimeout(()=>{ + const [values, setValues] = useState({}) + useEffect(() => { + setTimeout(() => { setValues({ aa: [ { @@ -393,143 +491,142 @@ const App = () => { } ] }) - },1000) - },[]) + }, 1000) + }, []) return ( - - { - const loading = name => { - setFieldState(name, state => { - state.loading = true - }) - } - const loaded = name => { - setFieldState(name, state => { - state.loading = false - }) - } - const hide = name => { - setFieldState(name, state => { - state.visible = false - }) - } - const show = name => { - setFieldState(name, state => { - state.visible = true + + { + const loading = name => { + setFieldState(name, state => { + state.loading = true + }) + } + const loaded = name => { + setFieldState(name, state => { + state.loading = false + }) + } + const hide = name => { + setFieldState(name, state => { + state.visible = false + }) + } + const show = name => { + setFieldState(name, state => { + state.visible = true + }) + } + const setEnum = (name, value) => { + setFieldState(name, state => { + state.props.enum = value + }) + } + const setValue = (name, value) => { + setFieldState(name, state => { + state.value = value + }) + } + $('onFormInit').subscribe(() => { + hide(FormPath.match('aa.*.*(cc,gg,dd.*.ee)')) }) - } - const setEnum = (name, value) => { - setFieldState(name, state => { - state.props.enum = value + $('onFieldValueChange', 'aa.*.bb').subscribe(fieldState => { + const cc = FormPath.transform( + fieldState.name, + /\d+/, + i => `aa.${i}.cc` + ) + if (!fieldState.value) { + hide(cc) + return + } + show(cc) + loading(cc) + setTimeout(() => { + loaded(cc) + setEnum(cc, ['1111', '2222']) + setValue(cc, '1111') + }, 1000) }) - } - const setValue = (name, value) => { - setFieldState(name, state => { - state.value = value + $('onFieldValueChange', 'aa.*.dd.*.ee').subscribe(fieldState => { + const gg = FormPath.transform( + fieldState.name, + /\d+/, + (i, j) => `aa.${i}.gg` + ) + setFieldState(gg, state => { + if (fieldState.value) { + state.visible = fieldState.value == '是' + } + }) }) - } - $('onFormInit').subscribe(() => { - hide(FormPath.match('aa.*.*(cc,gg,dd.*.ee)')) - }) - $('onFieldChange', 'aa.*.bb').subscribe(fieldState => { - const cc = FormPath.transform( - fieldState.name, - /\d+/, - i => `aa.${i}.cc` - ) - if (!fieldState.value) { - hide(cc) - return - } - show(cc) - loading(cc) - setTimeout(() => { - loaded(cc) - setEnum(cc, ['1111', '2222']) - setValue(cc, '1111') - }, 1000) - }) - $('onFieldChange', 'aa.*.dd.*.ee').subscribe(fieldState => { - const gg = FormPath.transform( - fieldState.name, - /\d+/, - (i, j) => `aa.${i}.gg` - ) - setFieldState(gg, state => { - if (fieldState.value) { + $('onFieldValueChange', 'aa.*.dd.*.ff').subscribe(fieldState => { + const ee = FormPath.transform( + fieldState.name, + /\d+/, + (i, j) => `aa.${i}.dd.${j}.ee` + ) + setFieldState(ee, state => { state.visible = fieldState.value == '是' - } - }) - }) - $('onFieldChange', 'aa.*.dd.*.ff').subscribe(fieldState => { - const ee = FormPath.transform( - fieldState.name, - /\d+/, - (i, j) => `aa.${i}.dd.${j}.ee` - ) - setFieldState(ee, state => { - state.visible = fieldState.value == '是' + }) }) - }) - }} - onSubmit={v => console.log(v)} - initialValues={values} - > - - - - - - - - - - - - - - - - - + }} + onSubmit={v => console.log(v)} + initialValues={values} + > + + + + + + + + + + + + + + + + + + - - + + - - - - - - - - -) + + + + + + +
+ ) } ReactDOM.render(, document.getElementById('root')) ``` - diff --git a/docs/Examples/next/Sample.md b/docs/Examples/next/Sample.md index ce53f8a746b..f620129b629 100644 --- a/docs/Examples/next/Sample.md +++ b/docs/Examples/next/Sample.md @@ -14,6 +14,7 @@ import { FormButtonGroup, Submit, Reset, + filterChanged, createFormActions } from '@uform/next' import { Button } from '@alifd/next' @@ -22,6 +23,11 @@ import Printer from '@uform/printer' const actions = createFormActions() +const sleep = duration => + new Promise(resolve => { + setTimeout(resolve, duration) + }) + ReactDOM.render( { + effects={($, { setFieldState, hasChanged }) => { $('onFormMount').subscribe(() => { setFieldState('radio', state => { state.required = true }) }) + + $('onFormChange').subscribe(async state => { + if (hasChanged(state, 'values.hello')) { + await sleep(1000) + setFieldState('radio', state => { + state.value = '4' + }) + } + }) }} > { + return value > 20 + ? { + type: 'warning', + message: '这是个警告信息' + } + : '' + }} /> @@ -100,7 +123,10 @@ ReactDOM.render( /> @@ -137,6 +163,15 @@ ReactDOM.render( > 改变radio的值 + , diff --git a/docs/Examples/next/Validation.md b/docs/Examples/next/Validation.md index 795fc11d056..37ec76ecc76 100644 --- a/docs/Examples/next/Validation.md +++ b/docs/Examples/next/Validation.md @@ -64,7 +64,7 @@ const App = () => ( title="手机号" required /> - + ( labelCol={6} wrapperCol={6} effects={($, { setFieldState, getFieldState }) => { - $('onFieldChange', '*(password,confirm)').subscribe(fieldState => { + $('onFieldValueChange', '*(password,confirm)').subscribe(fieldState => { const selfName = fieldState.name const selfValue = fieldState.value const otherName = selfName == 'password' ? 'confirm' : 'password' diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000000..de72618f030 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,74 @@ + +## 背景 + +在React中,在受控模式下,表单的整树渲染问题非常明显。特别是对于数据联动的场景,很容易导致页面卡顿,为了解决这个问题,我们将每个表单字段的状态做了分布式管理,从而大大提升了表单操作性能。同时,我们深度整合了JSON Schema协议,可以帮助您快速解决后端驱动表单渲染的问题。 + +## 特性 + +- 🚀 高性能,字段分布式渲染,大大减轻 React 渲染压力 +- 💡 支持 Ant Design/Fusion Next 组件体系 +- 🎨 JSX 标签化写法/JSON Schema 数据驱动方案无缝迁移过渡 +- 🏅 副作用逻辑独立管理,涵盖各种复杂联动校验逻辑 +- 🌯 支持各种表单复杂布局方案 + +## 安装 + +使用 Ant Design: + +```bash +npm install --save antd @uform/antd +``` + +使用 Fusion Design: + +```bash +npm install --save @alifd/next @uform/next +``` + +使用UForm React渲染引擎包: + +```bash +npm install --save @uform/react-schema-renderer +``` + +使用 UForm React包: + +```bash +npm install --save @uform/react +``` + +使用 UForm 核心包: + +```bash +npm install --save @uform/core +``` + +## 社区 + + +| Online Chat Room | 微信 | 钉钉 | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ---- | +| [gitter.im](https://gitter.im/alibaba-uform/community?source=orgpage) | | | + +## 贡献者 + +This project exists thanks to all the people who contribute. + + + +## 点个赞 + +Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/uform#backer)] + + + + +## 捐献我们 + +Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/uform#sponsor)] + + +## LICENSE + +UForm is open source software licensed as +[MIT.](https://github.com/alibaba/uform/blob/master/LICENSE.md) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 7fe1c8f2b7d..3241bbf71da 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -1,39 +1,11 @@ - 开发指南 - - [UForm 是什么?](./Tutorials/UForm是什么.md) - - [UForm 原理](./Tutorials/UForm原理.md) - - [快速入门](./Tutorials/快速入门.md) - - [Form Schema 扩展规范](./Tutorials/FormSchema扩展规范.md) - - [常见问题](./Tutorials/Questions.md) -- API 文档 - - @uform/next or antd - - [](./API/SchemaForm.md) - - [](./API/FormButtonGroup.md) - - [](./API/Submit.md) - - [](./API/Reset.md) - - [](./API/FormLayout.md) - - [](./API/FormCard.md) - - [](./API/FormBlock.md) - - [](./API/FormItemGrid.md) - - [](./API/FormSlot.md) - - [](./API/FormTextBox.md) - - @uform/react - - [](./API/SchemaForm_React.md) - - [](./API/Field_React.md) - - [](./API/FormProvider.md) - - [](./API/FormConsumer.md) - - [registerFormField](./API/registerFormField.md) - - [registerFormFields](./API/registerFormFields.md) - - [registerFormWrapper](./API/registerFormWrapper.md) - - [registerFieldMiddleware](./API/registerFieldMiddleware.md) - - [createVirtualBox](./API/createVirtualBox.md) - - [connect](./API/connect.md) - - [createFormActions](./API/createFormActions.md) - - [createAsyncFormActions](./API/createAsyncFormActions.md) - - @uform/core - - [createForm](./API/createForm.md) - - [setValidationLocale](./API/setValidationLocale.md) - - [setValidationLanguage](./API/setValidationLanguage.md) - - [FormPath](./API/FormPath.md) + - [UForm 是什么?](./README.md) + - API 文档 + - [Antd扩展库](../packages/antd/README.zh-cn.md) + - [Fusion Next扩展库](../packages/next/README.zh-cn.md) + - [Schema渲染库](../packages/react-schema-renderer/README.zh-cn.md) + - [React核心库](../packages/react/README.zh-cn.md) + - [核心库](../packages/core/README.zh-cn.md) - 场景案例 - Fusion Next - [简单场景](./Examples/next/Sample.md) @@ -45,6 +17,7 @@ - [内外通讯联动](./Examples/next/Actions.md) - [国际化](./Examples/next/International.md) - [解构字段数据](./Examples/next/Deconstruction.md) + - [压测](./Examples/next/Pressure.md) - Ant Design - [简单场景](./Examples/antd/Sample.md) - [表单详情](./Examples/antd/Detail.md) diff --git a/package.json b/package.json index 597f0e3cd80..0178487a59e 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,14 @@ "build:docs": "node ./scripts/docs.js build -i docs -o docs", "start": "node ./scripts/docs.js start -i docs", "sort-api": "node ./scripts/sort-api-table.js", - "test": "jest", + "test": "npm run lint && jest", + "test:core": "jest --watch packages/core/src/__tests__/*.spec.ts", + "test:hook": "jest --watch packages/react/src/__tests__/*.spec.tsx", + "test:editor": "jest --watch packages/react-schema-editor/src/__tests__/*.spec.ts", "test:prod": "cross-env TEST_ENV=production npm run build && jest", + "doc:core": "doc-scripts start -i packages/core", + "doc:react": "doc-scripts start -i packages/react", + "doc:editor": "doc-scripts start -i packages/react-schema-editor", "test:watch": "jest --watch", "coverage": "npm run test -- --coverage", "coverage:upload": "rm -rf ./coverage && npm run coverage && node ./scripts/mapCoverage.js && codecov", @@ -34,7 +40,7 @@ "@types/react": "16.8.23" }, "devDependencies": { - "@alifd/next": "^1.14.2", + "@alifd/next": "^1.19.1", "@babel/cli": "^7.2.0", "@babel/core": "^7.3.3", "@babel/plugin-proposal-class-properties": "^7.2.3", @@ -47,7 +53,10 @@ "@babel/preset-react": "^7.0.0", "@babel/register": "^7.0.0", "@babel/runtime-corejs3": "^7.2.0", + "@testing-library/jest-dom": "^4.2.3", "@testing-library/react": "^8.0.0", + "@testing-library/react-hooks": "^3.2.1", + "@types/jest": "^24.0.18", "@types/node": "^12.6.8", "@typescript-eslint/eslint-plugin": "^1.11.0", "@typescript-eslint/parser": "^1.11.0", @@ -78,6 +87,7 @@ "fs-extra": "^7.0.1", "ghooks": "^2.0.4", "glob": "^7.1.3", + "immutable": "^4.0.0-rc.12", "istanbul-api": "^2.1.1", "istanbul-lib-coverage": "^2.0.3", "jest": "^24.1.0", @@ -89,11 +99,15 @@ "lerna": "^3.10.1", "lint-staged": "^8.2.1", "majo": "^0.7.1", + "markdown-toc": "^1.2.0", + "moment": "^2.24.0", "onchange": "^5.2.0", "prettier": "^1.18.2", "pretty-format": "^24.0.0", "react": "^16.8.3", "react-dom": "^16.8.3", + "react-eva": "^1.1.7", + "react-test-renderer": "^16.11.0", "remark-parse": "^6.0.3", "remark-stringify": "^6.0.4", "scheduler": "^0.15.0", @@ -106,6 +120,7 @@ "typescript": "^3.6.2", "unified": "^7.1.0", "user-event": "^1.4.4", + "wait-for-expect": "^3.0.1", "webpack": "^4.35.3" }, "config": { @@ -118,16 +133,17 @@ } }, "lint-staged": { - "packages/**": [ + "packages/*/src/**.@(ts|tsx|js)": [ "npm run lint", "git add" ], - "scripts/**": [ + "scripts/**.@(ts|tsx|js)": [ "npm run lint", "git add" ] }, "dependencies": { + "lodash": "^4.17.15", "opencollective": "^1.0.3", "opencollective-postinstall": "^2.0.2" }, diff --git a/packages/.eslintrc b/packages/.eslintrc index e36576416f9..9e971f60cda 100644 --- a/packages/.eslintrc +++ b/packages/.eslintrc @@ -11,7 +11,12 @@ "jest": true, "commonjs": true }, - "plugins": ["@typescript-eslint", "react", "react-hooks", "prettier"], + "plugins": [ + "@typescript-eslint", + "react", + "react-hooks", + "prettier" + ], "parserOptions": { "project": "./tsconfig.json", "sourceType": "module", @@ -25,10 +30,8 @@ "version": "detect", } }, - "rules": { "prettier/prettier": 2, - // don't force es6 functions to include space before paren "space-before-function-paren": 0, "react/prop-types": 0, @@ -40,13 +43,22 @@ "react/no-did-update-set-state": 0, // maybe we should no-public "@typescript-eslint/explicit-member-accessibility": 0, - "@typescript-eslint/interface-name-prefix": 0, "@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/explicit-function-return-type": 0, "@typescript-eslint/no-parameter-properties": 0, "@typescript-eslint/array-type": 0, "@typescript-eslint/no-object-literal-type-assertion": 0, - "no-console": ["error", { "allow": ["warn", "error", "info"] }] + "@typescript-eslint/no-use-before-define": 0, + "no-console": [ + "error", + { + "allow": [ + "warn", + "error", + "info" + ] + } + ] } -} +} \ No newline at end of file diff --git a/packages/antd/README.md b/packages/antd/README.md index c76e7635a8d..85a992c8da9 100644 --- a/packages/antd/README.md +++ b/packages/antd/README.md @@ -1,2 +1,2956 @@ # @uform/antd -> UForm Ant Design组件插件包 \ No newline at end of file + +### Install + +```bash +npm install --save @uform/antd +``` + +### Table Of Contents + + + +- [Quick-Start](#Quick-Start) +- [Components](#components) + - [``](#SchemaForm) + - [``](#SchemaMarkupField) + - [``](#Submit) + - [``](#Reset) + - [`(deprecated,please use )`](#) +- [Form List](#Array-Components) + - [`array`](#array) + - [`cards`](#cards) + - [`table`](#table) +- [Layout Components](#Layout-Components) + - [``](#FormCard) + - [``](#FormBlock) + - [``](#FormStep) + - [``](#FormLayout) + - [``](#FormItemGrid) + - [``](#FormTextBox) + - [``](#FormButtonGroup) + - [``](#TextButton) + - [``](#CircleButton) +- [Type of SchemaMarkupField](#Type-of-SchemaMarkupField) + - [`string`](#string) + - [`textarea`](#textarea) + - [`password`](#password) + - [`number`](#number) + - [`boolean`](#boolean) + - [`date`](#date) + - [`time`](#time) + - [`range`](#range) + - [`upload`](#upload) + - [`checkbox`](#checkbox) + - [`radio`](#radio) + - [`rating`](#rating) + - [`transfer`](#transfer) +- [API](#API) + - [`createFormActions`](#createFormActions) + - [`createAsyncFormActions`](#createAsyncFormActions) + - [`FormEffectHooks`](#FormEffectHooks) + - [`createEffectHook`](#createEffectHook) + - [`connect`](#connect) + - [`registerFormField`](#registerFormField) +- [Interfaces](#Interfaces) + - [`ButtonProps`](#ButtonProps) + - [`CardProps`](#CardProps) + - [`ICompatItemProps`](#ICompatItemProps) + - [`IFieldState`](#IFieldState) + - [`ISchemaFieldComponentProps`](#ISchemaFieldComponentProps) + - [`ISchemaVirtualFieldComponentProps`](#ISchemaVirtualFieldComponentProps) + - [`ISchemaFieldWrapper`](#ISchemaFieldWrapper) + - [`ISchemaFieldComponent`](#ISchemaFieldComponent) + - [`ISchemaVirtualFieldComponent`](#ISchemaVirtualFieldComponent) + - [`ISchemaFormRegistry`](#ISchemaFormRegistry) + - [`INextSchemaFieldProps`](#INextSchemaFieldProps) + - [`IPreviewTextProps`](#IPreviewTextProps) + - [`IMutators`](#IMutators) + - [`IFieldProps`](#IFieldProps) + - [`IConnectOptions`](#IConnectOptions) + + +### Quick-Start + +--- + +Example:develop with JSX + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import { Button } from 'antd' +import'antd/dist/antd.css' + +const actions = createFormActions() + +const App = () => { + return ( + + + + + + + + + + + + + + + + item.title + }} + title="transfer" + name="transfer" + + /> + + + + + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +Example:develop with JSON Schema + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import { Button } from 'antd' +import'antd/dist/antd.css' + +const actions = createFormActions() + +const App = () => { + const schema = { + type: 'object', + properties: { + radio: { + type: 'radio', + enum: ['1', '2', '3', '4'], + title: 'Radio' + }, + select: { + type: 'string', + enum: ['1', '2', '3', '4'], + title: 'Select', + required: true + }, + checkbox: { + type: 'checkbox', + enum: ['1', '2', '3', '4'], + title: 'Checkbox', + required: true + }, + textarea: { + type: 'string', + 'x-component': 'textarea', + title: 'TextArea' + }, + number: { + type: 'number', + title: 'number' + }, + boolean: { + type: 'boolean', + title: 'boolean' + }, + date: { + type: 'date', + title: 'date' + }, + daterange: { + type: 'daterange', + default: ['2018-12-19', '2018-12-19'], + title: 'daterange' + }, + year: { + type: 'year', + title: 'year' + }, + time: { + type: 'time', + title: 'time' + }, + upload: { + type: 'upload', + 'x-props': { + listType: 'card' + }, + title: 'upload(card)' + }, + upload2: { + type: 'upload', + 'x-props': { + listType: 'dragger' + }, + title: 'uplaod(dragger)' + }, + upload3: { + type: 'upload', + 'x-props': { + listType: 'text' + }, + title: 'upload(text)' + }, + range: { + type: 'range', + 'x-props': { + min: 0, + max: 1024, + marks: [0, 1024] + }, + title: 'range' + }, + transfer: { + type: 'transfer', + enum: [ + { + key: 1, + title: 'opt1' + }, + { + key: 2, + title: 'opt2' + } + ], + 'x-props': { + render: (item) => item.title + }, + title: 'transfer' + }, + rating: { + type: 'rating', + title: 'rating' + }, + layout_btb_group: { + type: 'object', + 'x-component': 'button-group', + 'x-component-props': { + offset:7, + sticky: true, + }, + properties: { + submit_btn: { + type: 'object', + 'x-component': 'submit', + 'x-component-props': { + children: 'Submit', + }, + }, + reset_btn: { + type: 'object', + 'x-component': 'reset', + 'x-component-props': { + children: 'Reset', + }, + }, + } + }, + } + } + return +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### Components + +--- + +#### `` + +Base on `` of @uform/react-schema-renderer. Recommended for production environments. + + +```typescript +interface IAntdSchemaFormProps { + // render by schema + schema?: ISchema; + fields?: ISchemaFormRegistry['fields']; + virtualFields?: ISchemaFormRegistry['virtualFields']; + // pre-registered Form Component + formComponent?: ISchemaFormRegistry['formComponent']; + // pre-registered FormItem Component + formItemComponent?: ISchemaFormRegistry['formItemComponent']; + // layout setting + layout?: FormLayout; + form?: WrappedFormUtils; + // triggered by `htmlType="submit"` or action.submit + onSubmit?: React.FormEventHandler; + style?: React.CSSProperties; + className?: string; + // className of prefix + prefixCls?: string; + hideRequiredMark?: boolean; + // FormItem column settiing + wrapperCol?: ColProps; + // label column settiing + labelCol?: ColProps; + // it there a colon + colon?: boolean; + // alignment of label + labelAlign?: FormLabelAlign; + // is it inline + inline?: boolean + className?: string + style?: React.CSSProperties + // custom placeholder when preivew + previewPlaceholder?: string | ((props: IPreviewTextProps) => string); + // form state value + value?: Value; + // form state defaultValue + defaultValue?: DefaultValue; + // form state initialValues + initialValues?: DefaultValue; + // FormActions instance + actions?: FormActions; + // IFormEffect instance + effects?: IFormEffect; + // form instance + form?: IForm; + // Form change event callback + onChange?: (values: Value) => void; + // triggered by `htmlType="submit"` or actions.submit时 + onSubmit?: (values: Value) => void | Promise; + // triggered by or actions.reset + onReset?: () => void; + // Form verification failure event callback + onValidateFailed?: (valideted: IFormValidateResult) => void; + children?: React.ReactElement | ((form: IForm) => React.ReactElement); + // Whether to use the dirty check, the default will go immer accurate update + useDirty?: boolean; + // Is it editable, overall control in the Form dimension + editable?: boolean | ((name: string) => boolean); + // Whether to go pessimistic check, stop the subsequent check when the first check fails + validateFirst?: boolean; +} +``` + +**Usage** + +Example1: Sync value of a and a-mirror + +```jsx +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + registerFormField, + Field, + connect, + createFormActions +} from '@uform/antd' +import'antd/dist/antd.css' + +const actions = createFormActions() + +ReactDOM.render( + { + $('onFieldChange','aa').subscribe((fieldState)=>{ + actions.setFieldState('bb',state=>{ + state.value = fieldState.value + }) + }) + }}> + + + , + document.getElementById('root') +) +``` + + +Example:Layout + +```jsx +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + createFormActions, + FormLayout, + FormButtonGroup, + Submit, + Reset, +} from '@uform/antd' + +const actions = createFormActions() + +ReactDOM.render( +
+
Basic Layout
+ + + + + + + + SubmitReset​ + + +
Inline Layout
+ + + + ​ + + SubmitReset​ + + +
editable = false
+ + + + ​ + + SubmitReset​ + + +
, + document.getElementById('root') +) +``` + +#### `` + +> Core components of @uform/antd, used to describe form fields + +```typescript +interface IMarkupSchemaFieldProps { + name?: string + /** base json schema spec**/ + title?: SchemaMessage + description?: SchemaMessage + default?: any + readOnly?: boolean + writeOnly?: boolean + type?: 'string' | 'object' | 'array' | 'number' | string + enum?: Array + const?: any + multipleOf?: number + maximum?: number + exclusiveMaximum?: number + minimum?: number + exclusiveMinimum?: number + maxLength?: number + minLength?: number + pattern?: string | RegExp + maxItems?: number + minItems?: number + uniqueItems?: boolean + maxProperties?: number + minProperties?: number + required?: string[] | boolean + format?: string + /** nested json schema spec **/ + properties?: { + [key: string]: ISchema + } + items?: ISchema | ISchema[] + additionalItems?: ISchema + patternProperties?: { + [key: string]: ISchema + } + additionalProperties?: ISchema + /** extend json schema specs */ + editable?: boolean + visible?: boolean + display?: boolean + ['x-props']?: { [name: string]: any } + ['x-index']?: number + ['x-rules']?: ValidatePatternRules + ['x-component']?: string + ['x-component-props']?: { [name: string]: any } + ['x-render']?: ( + props: T & { + renderComponent: () => React.ReactElement + } + ) => React.ReactElement + ['x-effect']?: ( + dispatch: (type: string, payload: any) => void, + option?: object + ) => { [key: string]: any } +} +``` + +##### Usage + + +```jsx +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + FormSlot, + Field, + createFormActions, + FormLayout, + FormButtonGroup, + Submit, + Reset, +} from '@uform/antd' + +const actions = createFormActions() + +ReactDOM.render( + +
required
+ + +
description
+ + +
default value
+ + +
readOnly
+ + +
visible = false
+ + +
display = false
+ + +
editable = false
+ +
, + document.getElementById('root') +) +``` + + +#### `` + +> Props of `` + +```typescript +interface ISubmitProps { + /** reset pops **/ + onSubmit?: ISchemaFormProps['onSubmit'] + showLoading?: boolean + /** nextBtnProps **/ + // type of btn + type?: 'primary' | 'secondary' | 'normal' + // size of btn + size?: 'small' | 'medium' | 'large' + // size of Icon + iconSize?: 'xxs' | 'xs' | 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' + // type of button when component = 'button' + htmlType?: 'submit' | 'reset' | 'button' + // typeof btn + component?: 'button' | 'a' + // Set the loading state of the button + loading?: boolean + // Whether it is a ghost button + ghost?: true | false | 'light' | 'dark' + // Whether it is a text button + text?: boolean + // Whether it is a warning button + warning?: boolean + // Whether it is disabled + disabled?: boolean + // Callback for button click + onClick?: (e: {}) => void + // Valid when Button component is set to 'a', which represents the URL of the linked page + href?: string + // Valid when Button component is set to 'a', which represents the way of open the linked document + target?: string +} +``` + +#### `` + +> Props of `` + +```typescript +interface IResetProps { + /** reset pops **/ + forceClear?: boolean + validate?: boolean + /** nextBtnProps **/ + // type of btn + type?: 'primary' | 'secondary' | 'normal' + // size of btn + size?: 'small' | 'medium' | 'large' + // size of Icon + iconSize?: 'xxs' | 'xs' | 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' + // type of button when component = 'button' + htmlType?: 'submit' | 'reset' | 'button' + // typeof btn + component?: 'button' | 'a' + // Set the loading state of the button + loading?: boolean + // Whether it is a ghost button + ghost?: true | false | 'light' | 'dark' + // Whether it is a text button + text?: boolean + // Whether it is a warning button + warning?: boolean + // Whether it is disabled + disabled?: boolean + // Callback for button click + onClick?: (e: {}) => void + // Valid when Button component is set to 'a', which represents the URL of the linked page + href?: string + // Valid when Button component is set to 'a', which represents the way of open the linked document + target?: string +} +``` + +#### `` + +> deprecated,please use [SchemaMarkupField](#SchemaMarkupField) + +### Array Components + +#### array + +```jsx +import React, { useState, useEffect } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + FormItemGrid, + FormButtonGroup, + Submit, + Reset, + FormBlock, + FormLayout +} from '@uform/antd' +import'antd/dist/antd.css' +import Printer from '@uform/printer' + +const App = () => { + const [value, setValues] = useState({}) + useEffect(() => { + setTimeout(() => { + setValues({ + array: [{ array2: [{ aa: '123', bb: '321' }] }] + }) + }, 1000) + }, []) + return ( + + console.log(v)}> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Submit + Reset + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### cards + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + FormItemGrid, + FormButtonGroup, + Submit, + Reset, + FormBlock, + FormLayout +} from '@uform/antd' +import'antd/dist/antd.css' +import Printer from '@uform/printer' + +const App = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### table + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + FormItemGrid, + FormButtonGroup, + Submit, + Reset, + FormBlock, + FormLayout +} from '@uform/antd' +import'antd/dist/antd.css' +import Printer from '@uform/printer' + +const App = () => ( + + + + Hello worldasdasdasdasd + }, + operationsWidth: 300 + }} + > + + + + + + + + + + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +### Layout Components + + +#### `` + +> Props of ``, fully inherited from [CardProps](#CardProps)。 +> The only difference between FormCard [FormBlock](#FormBlock) is a border on the style + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { FormCard, SchemaMarkupField as Field } from '@uform/antd' +import'antd/dist/antd.css' + +const App = () => ( + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of `` , fully inherited from [CardProps](#CardProps) + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { FormBlock, SchemaMarkupField as Field } from '@uform/antd' +import'antd/dist/antd.css' + +const App = () => ( + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of `` + +```typescript +interface IFormStep { + dataSource: StepItemProps[] + /** next step props**/ + // current + current?: number + // direction of step + direction?: 'hoz' | 'ver' + // Content arrangement in horizontal layout + labelPlacement?: 'hoz' | 'ver' + // shape of step + shape?: 'circle' | 'arrow' | 'dot' + readOnly?: boolean + // Whether to activate animation + animation?: boolean + className?: string + // Custom StepItem render + itemRender?: (index: number, status: string) => React.ReactNode +} +``` + +**Usage** + +```jsx +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + FormEffectHooks, + createFormActions, + FormGridRow, + FormItemGrid, + FormGridCol, + FormPath, + FormLayout, + FormBlock, + FormCard, + FormTextBox, + FormStep +} from '@uform/antd' +import { Button } from 'antd' +import'antd/dist/antd.css' + +const { onFormInit$ } = FormEffectHooks + +const actions = createFormActions() + +let cache = {} + +export default () => ( + { + console.log('submit') + console.log(values) + }} + actions={actions} + labelCol={{ span: 8 }} + wrapperCol={{ span: 6 }} + validateFirst + effects={({ setFieldState, getFormGraph }) => { + onFormInit$().subscribe(() => { + setFieldState('col1', state => { + state.visible = false + }) + }) + }} + > + + + + + + + + + + + + Submit + + + + + + +) +``` + +#### `` + +> Props of `` + +```typescript +interface IFormItemTopProps { + inline?: boolean + className?: string + style?: React.CSSProperties + labelCol?: number | { span: number; offset?: number } + wrapperCol?: number | { span: number; offset?: number } +} +``` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormBlock, + FormLayout +} from '@uform/antd' +import { Button } from 'antd' +import Printer from '@uform/printer' +import'antd/dist/antd.css' +const App = () => ( + + + + + + + + + SubmitReset​ + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of `` + +```typescript +interface IFormItemGridProps { + cols?: Array + gutter?: number + /** next Form.Item props**/ + // prefix od FormItem + prefix?: string + + // label od FormItem + label?: React.ReactNode + + // label layout setting, eg, {span: 8, offset: 16} + labelCol?: {} + wrapperCol?: {} + + // Custom prompt information, if not set, it will be automatically generated according to the verification rules. + help?: React.ReactNode + + // Additional prompt information, similar to help, can be used when error messages and prompt copy are required at the same time. Behind the error message. + extra?: React.ReactNode + + // Check status, if not set, it will be generated automatically according to check rules + validateState?: 'error' | 'success' | 'loading' + + // Used in conjunction with the validateState property, whether to display the success / loading validation status icon. Currently only Input supports + hasFeedback?: boolean + + // Custom inline style + + style?: React.CSSProperties + + // node or function(values) + children?: React.ReactNode | (() => void) + + // The size of a single Item is customized, and takes precedence over the size of the Form, and when a component is used with an Item, the component itself does not set the size property. + size?: 'large' | 'small' | 'medium' + + // Position of the label + labelAlign?: 'top' | 'left' | 'inset' + + // alignment of labels + labelTextAlign?: 'left' | 'right' + + className?: string + + // [validation] required + required?: boolean + + // whether required asterisks are displayed + asterisk?: boolean + + // required custom error message + requiredMessage?: string + + // required Custom trigger method + requiredTrigger?: string | Array + + // [validation] min + min?: number + + // [validation] max + max?: number + + // min/max error message + minmaxMessage?: string + + // min/max custom trigger method + minmaxTrigger?: string | Array + + // [validation] min length of string / min length of array + minLength?: number + + // [validation] max length of string / max length of array + maxLength?: number + + // minLength/maxLength custom error message + minmaxLengthMessage?: string + + // minLength/maxLength custom trigger method + minmaxLengthTrigger?: string | Array + + // [validation] length of string / length of array + length?: number + + // length custom error message + lengthMessage?: string + + // length custom trigger method + lengthTrigger?: string | Array + + // Regular pattern + pattern?: any + + // pattern custom error message + patternMessage?: string + + // pattern custom trigger method + patternTrigger?: string | Array + + // [validation] regular pattern + format?: 'number' | 'email' | 'url' | 'tel' + + // format custom error message + formatMessage?: string + + // format custom trigger method + formatTrigger?: string | Array + + // [validation] custom validator + validator?: () => void + + // validator custom trigger method + validatorTrigger?: string | Array + + // Whether to automatically trigger validate when data is modified + autoValidate?: boolean +} +``` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormBlock, + FormLayout +} from '@uform/antd' +import { Button } from 'antd' +import Printer from '@uform/printer' +import'antd/dist/antd.css' + +const App = () => ( + + console.log(v)}> + + + + + + + + + + + + ​SubmitReset​ + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of `` + +```typescript +interface IFormTextBox { + text?: string + gutter?: number + title?: React.ReactText + description?: React.ReactText +} +``` + +**Usage** + +```jsx +import React, { useState } from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormTextBox, + FormCard, + FormLayout +} from '@uform/antd' +import { Button } from 'antd' +import Printer from '@uform/printer' +import'antd/dist/antd.css' + +const App = () => { + return ( + + console.log(v)}> + + + + + + + + + + + + ) +} +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of `` + +```typescript +interface IFormButtonGroupProps { + sticky?: boolean + style?: React.CSSProperties + itemStyle?: React.CSSProperties + className?: string + align?: 'left' | 'right' | 'start' | 'end' | 'top' | 'bottom' | 'center' + triggerDistance?: number + zIndex?: number + span?: ColSpanType + offset?: ColSpanType +} +``` + +**Usage** + +```jsx +import React, { useState } from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormBlock, + FormLayout +} from '@uform/antd' +import { Button } from 'antd' +import Printer from '@uform/printer' +import'antd/dist/antd.css' + +const App = () => { + const [state, setState] = useState({ editable: true }) + return ( + + console.log(v)}> +
normal
+ + ​SubmitReset​ + +
sticky
+ + ​Submit​ + + Reset​ + +
+
+ ) +} +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of ``, fully inherited from [ButtonProps](#ButtonProps) + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { TextButton } from '@uform/antd' +import'antd/dist/antd.css' + +const App = () => ( + + content + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of ``, fully inherited from [ButtonProps](#ButtonProps) + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { CircleButton } from '@uform/antd' +import'antd/dist/antd.css' + +const App = () => ( + + ok + +) +ReactDOM.render(, document.getElementById('root')) +``` + + +### Type of SchemaMarkupField + +#### string + +* Schema Type : `string` +* Schema UI Component: Fusion-Next ``, ``, `` + +```typescript +interface IPasswordProps { + checkStrength: boolean + /** next input props **/ + // value + value?: string | number + + // default value + defaultValue?: string | number + + // callback triggered when value change + onChange?: (value: string, e: React.ChangeEvent) => void + + // callback triggered when keyboard is press + onKeyDown?: (e: React.KeyboardEvent, opts: {}) => void + + // Disabled + disabled?: boolean + + // Max length + maxLength?: number + + // Whether to show the maximum length style + hasLimitHint?: boolean + + // When maxLength is set, whether to truncate beyond the string + cutString?: boolean + + // readOnly + readOnly?: boolean + + // Automatically remove the leading and trailing blank characters when trigger onChange + trim?: boolean + + // placeholder + placeholder?: string + + // callback triggered when focus + onFocus?: () => void + + // callback triggered when blur + onBlur?: () => void + + // Custom string length calculation + getValueLength?: (value: string) => number + + className?: string + style?: React.CSSProperties + htmlType?: string + + // name of field + name?: string + + // state of field + state?: 'error' | 'loading' | 'success' + + // label + label?: React.ReactNode + + // whether to show clear + hasClear?: boolean + + // whether to show border + hasBorder?: boolean + + // size of field + size?: 'small' | 'medium' | 'large' + + // callback triggered when enter is press + onPressEnter?: () => void + + // Watermark (Icon type, shared with hasClear) + hint?: string + + // Append content before text + innerBefore?: React.ReactNode + + // Append content after text + innerAfter?: React.ReactNode + + // Append content before input + addonBefore?: React.ReactNode + + // Append content after input + addonAfter?: React.ReactNode + + // Append text before input + addonTextBefore?: React.ReactNode + + // Append text after input + addonTextAfter?: React.ReactNode + + // (Native supported by input) + autoComplete?: string + + // (Native supported by input) + autoFocus?: boolean +} +``` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### number + +* Schema Type : `number` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### boolean + +* Schema Type : `boolean` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### date + +* Schema Type : `date` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### time + +* Schema Type : `time` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### range + +* Schema Type : `range` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### upload + +* Schema Type : `upload` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### checkbox + +* Schema Type : `checkbox` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### radio + +* Schema Type : `radio` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### rating + +* Schema Type : `rating` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### transfer + +* Schema Type : `transfer` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + item.title + }} + x-component-props={{ + showSearch: true + }} + /> + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### API + +> Fully inherited from @uform/react, The specific API of @uform/antd is listed below. + +--- + +#### `createFormActions` + +> Return [IFormActions](#IFormActions) + +**Signature** + +```typescript +createFormActions(): IFormActions +``` + +**Usage** + +```typescript +import { createFormActions } from '@uform/antd' + +const actions = createFormActions() +console.log(actions.getFieldValue('username')) +``` + +#### `createAsyncFormActions` + +> Return [IFormAsyncActions](#IFormAsyncActions) + +**Signature** + +```typescript +createAsyncFormActions(): IFormAsyncActions +``` + +**Usage** + +```typescript +import { createAsyncFormActions } from '@uform/antd' + +const actions = createAsyncFormActions() +actions.getFieldValue('username').then(val => console.log(val)) +``` + +#### `FormEffectHooks` + +> Return all @uform/core lifeCycles hook which can be subscribe + +**Usage** + +```tsx +import { FormEffectHooks, Form } from '@uform/react' +const { + /** + * Form LifeCycle + **/ + // Form pre-initialization trigger + onFormWillInit$, + // Form initialization trigger + onFormInit$, + // Triggered when the form changes + onFormChange$, + // Triggered when the form event is triggered, used to monitor only manual operations + onFormInputChange$, + // Trigger when the form initial value changes + onFormInitialValueChange$, + // Triggered when the form is reset + onFormReset$, + // Triggered when the form is submitted + onFormSubmit$, + // Triggered when the form submission starts + onFormSubmitStart$, + // Triggered when the form submission ends + onFormSubmitEnd$, + // Triggered when the form is mounted + onFormMount$, + // Triggered when the form is unloaded + onFormUnmount$, + // Triggered when form validation begins + onFormValidateStart$, + // Triggered when the form validation ends + onFormValidateEnd$, + // Trigger when the form initial value changes + onFormValuesChange$, + /** + * FormGraph LifeCycle + **/ + // Triggered when the form observer tree changes + onFormGraphChange$, + /** + * Field LifeCycle + **/ + // Triggered when pre-initialized + onFieldWillInit$, + // Triggered when the field is initialized + onFieldInit$, + // Triggered when the field changes + onFieldChange$, + // Triggered when the field is mounted + onFieldMount$, + // Trigger when the field is unloaded + onFieldUnmount$, + // Triggered when the field event is triggered, used to monitor only manual operations + onFieldInputChange$, + // Triggered when the field value changes + onFieldValueChange$, + // Trigger when the initial value of the field changes + onFieldInitialValueChange$ +} = FormEffectHooks + +const App = () => { + return ( +
{ + onFormInit$().subscribe(() => { + console.log('initialized') + }) + }} + > + ... +
+ ) +} +``` + +#### createEffectHook + +> Custom your own hook by this api + +**Usage** + +```jsx +import SchemaForm, { createEffectHook, createFormActions } from '@uform/antd' + +const actions = createFormActions() +const diyHook1$ = createEffectHook('diy1') +const diyHook2$ = createEffectHook('diy2') + +const App = () => { + return ( + { + diyHook1$().subscribe(payload => { + console.log('diy1 hook triggered', payload) + }) + + diyHook2$().subscribe(payload => { + console.log('diy2 hook triggered', payload) + }) + }} + > + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### connect + +> Wrap field components with value / defaultValue / onChange of field, which make it esaily make custom field + +```typescript +type Connect = >(options?: IConnectOptions) => +(Target: T) => React.PureComponent +``` +**Usage** + +```typescript +import {registerFormField,connect} from '@uform/antd' + +registerFormField( + 'string', + connect()(props => ) +) +``` + +#### registerFormField + +```typescript +type registerFormField( + name : string, // name of field + component : React.ComponentType, // component of field + noMiddleware: boolean // whether use middleware +) +``` + +**Usage** + +```jsx + +import SchemaForm, { SchemaMarkupField as Field, registerFormField, connect, createFormActions } from '@uform/antd' + +registerFormField( + 'custom-string', + connect()(props => ) +) +const actions = createFormActions() + +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### Interfaces + +> The Interfaces is fully inherited from @uform/react. The specific Interfaces of @uform/antd is listed below. +--- + +#### IForm + +> Form instance object API created by using createForm + + + +```typescript +interface IForm { +  /* +   * Form submission, if the callback parameter returns Promise, +   * Then the entire submission process will hold and load is true. +   * Wait for Promise resolve to trigger the form onFormSubmitEnd event while loading is false +   */ +   submit( +      onSubmit?: (values: IFormState['values']) => any | Promise +    ): Promise<{ +       Validated: IFormValidateResult +       Payload: any //onSubmit callback function return value +   }> +    +   /* +    * Clear the error message, you can pass the FormPathPattern to batch or precise control of the field to be cleared. +    * For example, clearErrors("*(aa,bb,cc)") +    */ +   clearErrors: (pattern?: FormPathPattern) => void +    +   /* +    * Get status changes, mainly used to determine which states in the current life cycle have changed in the form lifecycle hook. +    * For example, hasChanged(state,'value.aa') +    */ +   hasChanged(target: IFormState | IFieldState | IVirtualFieldState, path: FormPathPattern): boolean +    +   /* +    * Reset form +    */ +   reset(options?: { +     // Forced to empty +     forceClear?: boolean +     // Forced check +     validate?: boolean +     // Reset range for batch or precise control of the field to be reset +     selector?: FormPathPattern +   }): Promise +    +   /* +    * Validation form +    */ +   validate(path?: FormPathPattern, options?: { +     // Is it pessimistic check, if the current field encounters the first verification error, stop the subsequent verification process +     first?:boolean +   }): Promise +    +   /* +    * Set the form status +    */ +   setFormState( +     // Operation callback +     callback?: (state: IFormState) => any, +     // No trigger the event +     silent?: boolean +   ): void +    +   /* +    * Get form status +    */ +   getFormState( +     //transformer +     callback?: (state: IFormState) => any +   ): any +    +   /* +    * Set the field status +    */ +   setFieldState( +     // Field path +     path: FormPathPattern, +     // Operation callback +     callback?: (state: IFieldState) => void, +     // No trigger the event +     silent?: boolean +   ): void +    +   /* +    * Get the field status +    */ +   getFieldState( +     // Field path +     path: FormPathPattern, +     // Transformer +     callback?: (state: IFieldState) => any +   ): any +    +   /* +    * Registration field +    */ +   registerField(props: { +    // Node path +    path?: FormPathPattern +    // Data path +    name?: string +    // Field value +    value?: any +    // Field multi-value +    values?: any[] +    // Field initial value +    initialValue?: any +    // Field extension properties +    props?: any +    // Field check rule +    rules?: ValidatePatternRules[] +    // Field is required +    required?: boolean +    // Is the field editable? +    editable?: boolean +    // Whether the field is dirty check +    useDirty?: boolean +    // Field state calculation container, mainly used to extend the core linkage rules +    computeState?: (draft: IFieldState, prevState: IFieldState) => void +  }): IField +   +  /* +   * Register virtual fields +   */ +  registerVirtualField(props: { +    // Node path +    path?: FormPathPattern +    // Data path +    name?: string +    // Field extension properties +    props?: any +    // Whether the field is dirty check +    useDirty?: boolean +    // Field state calculation container, mainly used to extend the core linkage rules +    computeState?: (draft: IFieldState, prevState: IFieldState) => void +  }): IVirtualField +   +  /* +   * Create a field data operator, which will explain the returned API in detail later. +   */ +  createMutators(field: IField): IMutators +   +  /* +   * Get the form observer tree +   */ +  getFormGraph(): IFormGraph +   +  /* +   * Set the form observer tree +   */ +  setFormGraph(graph: IFormGraph): void +   +  /* +   * Listen to the form life cycle +   */ +  subscribe(callback?: ({ +    type, +    payload +  }: { +    type: string +    payload: any +  }) => void): number +   +  /* +   * Cancel the listening form life cycle +   */ +  unsubscribe(id: number): void +   +  /* +   * Trigger form custom life cycle +   */ +  notify: (type: string, payload?: T) => void +   +  /* +   * Set the field value +   */ +  setFieldValue(path?: FormPathPattern, value?: any): void +   +  /* +   * Get the field value +   */ +  getFieldValue(path?: FormPathPattern): any +   +  /* +   * Set the initial value of the field +   */ +  setFieldInitialValue(path?: FormPathPattern, value?: any): void +   +  /* +   * Get the initial value of the field +   */ +  getFieldInitialValue(path?: FormPathPattern): any +} +``` + +#### ButtonProps + +```typescript +interface ButtonProps { + href: string; + target?: string; + onClick?: React.MouseEventHandler; + htmlType?: ButtonHTMLType; + onClick?: React.MouseEventHandler; +} +``` + +#### CardProps + +```typescript +interface CardProps extends HTMLAttributesWeak, CommonProps { + prefixCls?: string; + title?: React.ReactNode; + extra?: React.ReactNode; + bordered?: boolean; + headStyle?: React.CSSProperties; + bodyStyle?: React.CSSProperties; + style?: React.CSSProperties; + loading?: boolean; + noHovering?: boolean; + hoverable?: boolean; + children?: React.ReactNode; + id?: string; + className?: string; + size?: CardSize; + type?: CardType; + cover?: React.ReactNode; + actions?: React.ReactNode[]; + tabList?: CardTabListType[]; + tabBarExtraContent?: React.ReactNode | null; + onTabChange?: (key: string) => void; + activeTabKey?: string; + defaultActiveTabKey?: string; +} +``` + +#### ICompatItemProps + +```typescript +interface ICompatItemProps + extends Exclude, + Partial { + labelCol?: number | { span: number; offset?: number } + wrapperCol?: number | { span: number; offset?: number } +} +``` + + +#### IFieldState + +```typescript +interface IFieldState { + /**Read-only attribute**/ + // State name, FieldState + displayName?: string // Data path + name: string // Node path + path: string // Has been initialized + initialized: boolean // Is it in the original state, the state is true only when value===intialValues + pristine: boolean // Is it in a legal state, as long as the error length is greater than 0, the valid is false + valid: boolean // Is it illegal, as long as the error length is greater than 0, the valid is true + invalid: boolean // Is it in check state? + validating: boolean // Is it modified, if the value changes, the property is true, and will be true throughout the life of the field + modified: boolean // Is it touched? + touched: boolean // Is it activated, when the field triggers the onFocus event, it will be triggered to true, when onBlur is triggered, it is false + active: boolean // Have you ever visited, when the field triggers the onBlur event, it will be triggered to true + visited: boolean /** writable property**/ // Is it visible, note: if the state is false, then the value of the field will not be submitted, and the UI will not display + visible: boolean // Whether to show, note: if the state is false, then the value of the field will be submitted, the UI will not display, similar to the form hidden field + display: boolean // Is it editable? + editable: boolean // Is it in the loading state, note: if the field is in asynchronous verification, loading is true + loading: boolean // Field multi-parameter value, such as when the field onChange trigger, the event callback passed multi-parameter data, then the value of all parameters will be stored here + values: any[] // Field error message + errors: string[] // Field alert message + warnings: string[] // Field value, is equal to values[0] + value: any // Initial value + initialValue: any // Check the rules, the specific type description refers to the following documents + rules: ValidatePatternRules[] // Is it required? + required: boolean // Whether to mount + mounted: boolean // Whether to uninstall + unmounted: boolean // field extension properties + props: FieldProps +} +``` + + +#### ISchemaFieldComponentProps + +```typescript +interface ISchemaFieldComponentProps extends IFieldState { + // render by schema + schema: Schema + // mutators of + mutators: IMutators + // form instance + form: IForm + renderField: ( + addtionKey: string | number, + reactKey?: string | number + ) => React.ReactElement +} +``` + +#### ISchemaVirtualFieldComponentProps + +```typescript +interface ISchemaVirtualFieldComponentProps extends IVirtualFieldState { + // render by schema + schema: Schema + // form instance + form: IForm + children: React.ReactElement[] + renderField: ( + addtionKey: string | number, + reactKey?: string | number + ) => React.ReactElement +} +``` + +#### ISchemaFieldWrapper + +```typescript +interface ISchemaFieldWrapper { + (Traget: ISchemaFieldComponent): + | React.FC + | React.ClassicComponent +} +``` + +#### ISchemaFieldComponent + +```typescript +declare type ISchemaFieldComponent = ComponentWithStyleComponent< + ISchemaFieldComponentProps +> & { + __WRAPPERS__?: ISchemaFieldWrapper[] +} +``` + +#### ISchemaVirtualFieldComponent + +```typescript +declare type ISchemaVirtualFieldComponent = ComponentWithStyleComponent< + ISchemaVirtualFieldComponentProps +> & { + __WRAPPERS__?: ISchemaFieldWrapper[] +} +``` + +#### ISchemaFormRegistry + +```typescript +interface ISchemaFormRegistry { + fields: { + [key: string]: ISchemaFieldComponent + } + virtualFields: { + [key: string]: ISchemaVirtualFieldComponent + } + wrappers?: ISchemaFieldWrapper[] + formItemComponent: React.JSXElementConstructor + formComponent: string | React.JSXElementConstructor +} +``` + +#### INextSchemaFieldProps + +```typescript +interface INextSchemaFieldProps { + name?: string; + /** ISchema **/ + title?: SchemaMessage; + description?: SchemaMessage; + default?: any; + readOnly?: boolean; + writeOnly?: boolean; + type?: 'string' | 'object' | 'array' | 'number' | string; + enum?: Array; + const?: any; + multipleOf?: number; + maximum?: number; + exclusiveMaximum?: number; + minimum?: number; + exclusiveMinimum?: number; + maxLength?: number; + minLength?: number; + pattern?: string | RegExp; + maxItems?: number; + minItems?: number; + uniqueItems?: boolean; + maxProperties?: number; + minProperties?: number; + required?: string[] | boolean; + format?: string; + properties?: { + [key: string]: ISchema; + }; + items?: ISchema | ISchema[]; + additionalItems?: ISchema; + patternProperties?: { + [key: string]: ISchema; + }; + additionalProperties?: ISchema; + // Is it in the loading state, + editable?: boolean; + /** writable property**/ // Is it visible, note: if the state is false, then the value of the field will not be submitted, and the UI will not display + visible?: boolean; + // Whether to show, note: if the state is false, then the value of the field will be submitted, the UI will not display, similar to the form hidden field + display?: boolean; + ['x-props']?: { + [name: string]: any; + }; + ['x-index']?: number; + ['x-rules']?: ValidatePatternRules; + ['x-component']?: string; + ['x-component-props']?: { + [name: string]: any; + }; + ['x-render']?: (props: T & { + renderComponent: () => React.ReactElement; + }) => React.ReactElement; + ['x-effect']?: (dispatch: (type: string, payload: any) => void, option?: object) => { + [key: string]: any; + }; + +``` + +#### IPreviewTextProps + +```typescript +interface IPreviewTextProps { + className?: React.ReactText + dataSource?: any[] + value?: any + addonBefore?: React.ReactNode + innerBefore?: React.ReactNode + addonTextBefore?: React.ReactNode + addonTextAfter?: React.ReactNode + innerAfter?: React.ReactNode + addonAfter?: React.ReactNode +} + +``` + +#### IMutators + +> The instance API created by crewikiutators is mainly used to operate field data. + +```typescript +interface IMutators { + // Changing the field value and multi parameter condition will store all parameters in values + change(...values: any[]): any + // Get focus, trigger active state change + focus(): void + // Lose focus, trigger active / visited status change + blur(): void + // Trigger current field verifier + validate(): Promise + // Whether the value of the current field exists in the values property of form + exist(index?: number | string): Boolean + + /**Array operation method**/ + + // Append data + push(value?: any): any[] + // Pop up tail data + pop(): any[] + // Insert data + insert(index: number, value: any): any[] + // Delete data + remove(index: number | string): any + // Head insertion + unshift(value: any): any[] + // Head ejection + shift(): any[] + // Move element + move($from: number, $to: number): any[] + // Move down + moveDown(index: number): any[] + // Move up + moveUp(index: number): any[] +} +``` + +#### IFieldProps + +```typescript +interface IFieldProps { + name : string // Node path + path : Array // Data path + value : V // value + errors : Array // Field error message + editable : boolean | ((name:string) => boolean) // Is it editable? + locale : Locale // i18n locale + loading : boolean // Is it in the loading state, note: if the field is in asynchronous verification, loading is true + schemaPath : Array // schema path + getSchema : (path: string) => ISchema // get schema by path + renderField : (childKey: string, reactKey: string | number) => JSX.Element | string | null + renderComponent : React.FunctionComponent | undefined>, + getOrderProperties : () => Array<{schema: ISchema, key: number, path: string, name: string }>, + mutators : IMutators, + schema : ISchema +} + +``` + +#### IConnectOptions + +```typescript + +interface IConnectOptions { + // name of value property + valueName?: string + // name of event property + eventName?: string + // default props + defaultProps?: Partial + // In some case, the value of our event function is not the first parameter of the event callback, and further customization is required. + getValueFromEvent?: (props: IFieldProps['x-props'], event: Event, ...args: any[]) => any + // props transformer + getProps?: (connectProps: IConnectProps, fieldProps: IFieldProps) => IConnectProps + // component transformer + getComponent?: ( + target: T, + connectProps: IConnectProps, + fieldProps: IFieldProps + ) => T +} + +``` \ No newline at end of file diff --git a/packages/antd/README.zh-cn.md b/packages/antd/README.zh-cn.md new file mode 100644 index 00000000000..46b5dbaefbb --- /dev/null +++ b/packages/antd/README.zh-cn.md @@ -0,0 +1,2934 @@ +# @uform/antd + +### 安装 + +```bash +npm install --save @uform/antd +``` + +### 目录 + + + +- [使用方式](#使用方式) + - [`快速开始`](#快速开始) +- [Components](#components) + - [``](#SchemaForm) + - [``](#SchemaMarkupField) + - [``](#Submit) + - [``](#Reset) + - [`(即将废弃,请使用)`](<#Field(即将废弃,请使用SchemaMarkupField)>) +- [表单List](#Array-Components) + - [`array`](#array) + - [`cards`](#cards) + - [`table`](#table) +- [布局组件](#Layout-Components) + - [``](#FormCard) + - [``](#FormBlock) + - [``](#FormStep) + - [``](#FormLayout) + - [``](#FormItemGrid) + - [``](#FormTextBox) + - [``](#FormButtonGroup) + - [``](#TextButton) + - [``](#CircleButton) +- [字段类型](#Type-of-SchemaMarkupField) + - [`string`](#string) + - [`textarea`](#textarea) + - [`password`](#password) + - [`number`](#number) + - [`boolean`](#boolean) + - [`date`](#date) + - [`time`](#time) + - [`range`](#range) + - [`upload`](#upload) + - [`checkbox`](#checkbox) + - [`radio`](#radio) + - [`rating`](#rating) + - [`transfer`](#transfer) +- [API](#API) + - [`createFormActions`](#createFormActions) + - [`createAsyncFormActions`](#createAsyncFormActions) + - [`FormEffectHooks`](#FormEffectHooks) + - [`createEffectHook`](#createEffectHook) + - [`connect`](#connect) + - [`registerFormField`](#registerFormField) +- [Interfaces](#Interfaces) + - [`ButtonProps`](#ButtonProps) + - [`CardProps`](#CardProps) + - [`ICompatItemProps`](#ICompatItemProps) + - [`IFieldState`](#IFieldState) + - [`ISchemaFieldComponentProps`](#ISchemaFieldComponentProps) + - [`ISchemaVirtualFieldComponentProps`](#ISchemaVirtualFieldComponentProps) + - [`ISchemaFieldWrapper`](#ISchemaFieldWrapper) + - [`ISchemaFieldComponent`](#ISchemaFieldComponent) + - [`ISchemaVirtualFieldComponent`](#ISchemaVirtualFieldComponent) + - [`ISchemaFormRegistry`](#ISchemaFormRegistry) + - [`INextSchemaFieldProps`](#INextSchemaFieldProps) + - [`IPreviewTextProps`](#IPreviewTextProps) + - [`IMutators`](#IMutators) + - [`IFieldProps`](#IFieldProps) + - [`IConnectOptions`](#IConnectOptions) + + +### 使用方式 + +--- + +#### 快速开始 + +例子:使用 JSX 开发 + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import { Button } from 'antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() + +const App = () => { + return ( + + + + + + + + + + + + + + + + item.title + }} + title="穿梭框" + name="transfer" + /> + + + + + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +例子:使用 schema 来开发 + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import { Button } from 'antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() + +const App = () => { + const schema = { + type: 'object', + properties: { + radio: { + type: 'radio', + enum: ['1', '2', '3', '4'], + title: 'Radio' + }, + select: { + type: 'string', + enum: ['1', '2', '3', '4'], + title: 'Select', + required: true + }, + checkbox: { + type: 'checkbox', + enum: ['1', '2', '3', '4'], + title: 'Checkbox', + required: true + }, + textarea: { + type: 'string', + 'x-component': 'textarea', + title: 'TextArea' + }, + number: { + type: 'number', + title: '数字选择' + }, + boolean: { + type: 'boolean', + title: '开关选择' + }, + date: { + type: 'date', + title: '日期选择' + }, + daterange: { + type: 'daterange', + default: ['2018-12-19', '2018-12-19'], + title: '日期范围' + }, + year: { + type: 'year', + title: '年份' + }, + time: { + type: 'time', + title: '时间' + }, + upload: { + type: 'upload', + 'x-props': { + listType: 'card' + }, + title: '卡片上传文件' + }, + upload2: { + type: 'upload', + 'x-props': { + listType: 'dragger' + }, + title: '拖拽上传文件' + }, + upload3: { + type: 'upload', + 'x-props': { + listType: 'text' + }, + title: '普通上传文件' + }, + range: { + type: 'range', + 'x-props': { + min: 0, + max: 1024, + marks: [0, 1024] + }, + title: '范围选择' + }, + transfer: { + type: 'transfer', + enum: [ + { + key: 1, + title: '选项1' + }, + { + key: 2, + title: '选项2' + } + ], + 'x-props': { + render: (item) => item.title + }, + title: '穿梭框' + }, + rating: { + type: 'rating', + title: '等级' + }, + layout_btb_group: { + type: 'object', + 'x-component': 'button-group', + 'x-component-props': { + offset:7, + sticky: true, + }, + properties: { + submit_btn: { + type: 'object', + 'x-component': 'submit', + 'x-component-props': { + children: '提交', + }, + }, + reset_btn: { + type: 'object', + 'x-component': 'reset', + 'x-component-props': { + children: '重置', + }, + }, + } + }, + } + } + return +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### Components + +--- + +#### `` + +基于@uform/react 的核心组件SchemaForm进一步扩展出来的SchemaForm组件,推荐生产环境下使用 + +```typescript +interface IAntdSchemaFormProps { + // 通过schema渲染 + schema?: ISchema; + fields?: ISchemaFormRegistry['fields']; + virtualFields?: ISchemaFormRegistry['virtualFields']; + // 全局注册Form渲染组件 + formComponent?: ISchemaFormRegistry['formComponent']; + // 全局注册FormItem渲染组件 + formItemComponent?: ISchemaFormRegistry['formItemComponent']; + // 布局设置 + layout?: FormLayout; + prefixCls?: string; + // 隐藏required的星标 + hideRequiredMark?: boolean; + colon?: boolean; + // 标签的位置 + labelAlign?: FormLabelAlign; + // 内联表单 + inline?: boolean + // 扩展class + className?: string + style?: React.CSSProperties + // label 布局控制 + labelCol?: number | { span: number; offset?: number } + // FormItem 布局控制 + wrapperCol?: number | { span: number; offset?: number } + // 自定义 placeholder + previewPlaceholder?: string | ((props: IPreviewTextProps) => string); + // 全局value + value?: Value; + // 全局defaultValue + defaultValue?: DefaultValue; + // 全局initialValues + initialValues?: DefaultValue; + // FormActions实例 + actions?: FormActions; + // IFormEffect实例 + effects?: IFormEffect; + // 表单实例 + form?: IForm; + // 表单变化回调 + onChange?: (values: Value) => void; + // form内有 `htmlType="submit"` 或 actions.submit时 触发 + onSubmit?: (values: Value) => void | Promise; + // form内有 或 actions.reset时 触发 + onReset?: () => void; + // 校验失败时触发 + onValidateFailed?: (valideted: IFormValidateResult) => void; + children?: React.ReactElement | ((form: IForm) => React.ReactElement); + //是否使用脏检查,默认会走immer精确更新 + useDirty?: boolean; + // 是否可编辑 + editable?: boolean | ((name: string) => boolean); + // 是否走悲观校验,遇到第一个校验失败就停止后续校验 + validateFirst?: boolean; +} +``` + +**用法** + +例子1: 将a-mirror的值设置为a的值。 + +```jsx +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + connect, + createFormActions +} from '@uform/antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() + +ReactDOM.render( + { + $('onFieldChange','a').subscribe((fieldState)=>{ + actions.setFieldState('a-mirror',state=>{ + state.value = fieldState.value + }) + }) + }}> + + + , + document.getElementById('root') +) +``` + + +例子2: 布局 + +```jsx +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + createFormActions, + FormLayout, + FormButtonGroup, + Submit, + Reset, +} from '@uform/antd' + +const actions = createFormActions() + +ReactDOM.render( +
+
常规布局
+ + + + + + + + 提交重置​ + + +
Inline Layout
+ + + + ​ + + 提交重置​ + + +
editable = false
+ + + + ​ + + 提交重置​ + + +
, + document.getElementById('root') +) +``` + +#### `` + +> @uform/antd 的核心组件,用于描述表单字段 + +```typescript +interface IMarkupSchemaFieldProps { + name?: string + /** base json schema spec**/ + title?: SchemaMessage + description?: SchemaMessage + default?: any + readOnly?: boolean + writeOnly?: boolean + type?: 'string' | 'object' | 'array' | 'number' | string + enum?: Array + const?: any + multipleOf?: number + maximum?: number + exclusiveMaximum?: number + minimum?: number + exclusiveMinimum?: number + maxLength?: number + minLength?: number + pattern?: string | RegExp + maxItems?: number + minItems?: number + uniqueItems?: boolean + maxProperties?: number + minProperties?: number + required?: string[] | boolean + format?: string + /** nested json schema spec **/ + properties?: { + [key: string]: ISchema + } + items?: ISchema | ISchema[] + additionalItems?: ISchema + patternProperties?: { + [key: string]: ISchema + } + additionalProperties?: ISchema + /** extend json schema specs */ + editable?: boolean + visible?: boolean + display?: boolean + ['x-props']?: { [name: string]: any } + ['x-index']?: number + ['x-rules']?: ValidatePatternRules + ['x-component']?: string + ['x-component-props']?: { [name: string]: any } + ['x-render']?: ( + props: T & { + renderComponent: () => React.ReactElement + } + ) => React.ReactElement + ['x-effect']?: ( + dispatch: (type: string, payload: any) => void, + option?: object + ) => { [key: string]: any } +} +``` + +##### 用法 + + +```jsx +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + FormSlot, + Field, + createFormActions, + FormLayout, + FormButtonGroup, + Submit, + Reset, +} from '@uform/antd' + +const actions = createFormActions() + +ReactDOM.render( + +
required
+ + +
description
+ + +
default value
+ + +
readOnly
+ + +
visible = false
+ + +
display = false
+ + +
editable = false
+ +
, + document.getElementById('root') +) +``` + + +#### `` + +> Submit 组件 Props + +```typescript +interface ISubmitProps { + /** reset pops **/ + onSubmit?: ISchemaFormProps['onSubmit'] + showLoading?: boolean + /** nextBtnProps **/ + // 按钮的类型 + type?: 'primary' | 'secondary' | 'normal' + // 按钮的尺寸 + size?: 'small' | 'medium' | 'large' + // 按钮中 Icon 的尺寸,用于替代 Icon 的默认大小 + iconSize?: 'xxs' | 'xs' | 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' + // 当 component = 'button' 时,设置 button 标签的 type 值 + htmlType?: 'submit' | 'reset' | 'button' + // 设置标签类型 + component?: 'button' | 'a' + // 设置按钮的载入状态 + loading?: boolean + // 是否为幽灵按钮 + ghost?: true | false | 'light' | 'dark' + // 是否为文本按钮 + text?: boolean + // 是否为警告按钮 + warning?: boolean + // 是否禁用 + disabled?: boolean + // 点击按钮的回调 + onClick?: (e: {}) => void + // 在Button组件使用component属性值为a时有效,代表链接页面的URL + href?: string + // 在Button组件使用component属性值为a时有效,代表何处打开链接文档 + target?: string +} +``` + +#### `` + +> Reset 组件 Props + +```typescript +interface IResetProps { + /** reset pops **/ + forceClear?: boolean + validate?: boolean + /** nextBtnProps **/ + // 按钮的类型 + type?: 'primary' | 'secondary' | 'normal' + // 按钮的尺寸 + size?: 'small' | 'medium' | 'large' + // 按钮中 Icon 的尺寸,用于替代 Icon 的默认大小 + iconSize?: 'xxs' | 'xs' | 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' + // 当 component = 'button' 时,设置 button 标签的 type 值 + htmlType?: 'submit' | 'reset' | 'button' + // 设置标签类型 + component?: 'button' | 'a' + // 设置按钮的载入状态 + loading?: boolean + // 是否为幽灵按钮 + ghost?: true | false | 'light' | 'dark' + // 是否为文本按钮 + text?: boolean + // 是否为警告按钮 + warning?: boolean + // 是否禁用 + disabled?: boolean + // 点击按钮的回调 + onClick?: (e: {}) => void + // 在Button组件使用component属性值为a时有效,代表链接页面的URL + href?: string + // 在Button组件使用component属性值为a时有效,代表何处打开链接文档 + target?: string +} +``` + +### Array Components + +#### array + +```jsx +import React, { useState, useEffect } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + FormItemGrid, + FormButtonGroup, + Submit, + Reset, + FormBlock, + FormLayout +} from '@uform/antd' +import 'antd/dist/antd.css' +import Printer from '@uform/printer' + +const App = () => { + const [value, setValues] = useState({}) + useEffect(() => { + setTimeout(() => { + setValues({ + array: [{ array2: [{ aa: '123', bb: '321' }] }] + }) + }, 1000) + }, []) + return ( + + console.log(v)}> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 提交 + 重置 + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### cards + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + FormItemGrid, + FormButtonGroup, + Submit, + Reset, + FormBlock, + FormLayout +} from '@uform/antd' +import 'antd/dist/antd.css' +import Printer from '@uform/printer' + +const App = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### table + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + FormItemGrid, + FormButtonGroup, + Submit, + Reset, + FormBlock, + FormLayout +} from '@uform/antd' +import 'antd/dist/antd.css' +import Printer from '@uform/printer' + +const App = () => ( + + + + Hello worldasdasdasdasd + }, + operationsWidth: 300 + }} + > + + + + + + + + + + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +### Layout Components + + +#### `` + +> FormCard 组件 Props, 完全继承自 [CardProps](#CardProps)。 +> FormCard与[FormBlock](#FormBlock) 唯一区别是样式上是否有框 + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { FormCard, SchemaMarkupField as Field } from '@uform/antd' +import 'antd/dist/antd.css' + +const App = () => ( + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> FormBlock 组件 Props, 完全继承自 [CardProps](#CardProps) + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { FormBlock, SchemaMarkupField as Field } from '@uform/antd' +import 'antd/dist/antd.css' + +const App = () => ( + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> FormStep 组件 Props + +```typescript +interface IFormStep { + dataSource: StepItemProps[] + /** next step props**/ + // 当前步骤 + current?: number + // 展示方向 + direction?: 'hoz' | 'ver' + // 横向布局时的内容排列 + labelPlacement?: 'hoz' | 'ver' + // 类型 + shape?: 'circle' | 'arrow' | 'dot' + // 是否只读模式 + readOnly?: boolean + // 是否开启动效 + animation?: boolean + // 自定义样式名 + className?: string + // StepItem 的自定义渲染 + itemRender?: (index: number, status: string) => React.ReactNode +} +``` + +**用法** + +```jsx +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + FormEffectHooks, + createFormActions, + FormGridRow, + FormItemGrid, + FormGridCol, + FormPath, + FormLayout, + FormBlock, + FormCard, + FormTextBox, + FormStep +} from '@uform/antd' +import { Button } from 'antd' +import 'antd/dist/antd.css' + +const { onFormInit$ } = FormEffectHooks + +const actions = createFormActions() + +let cache = {} + +export default () => ( + { + console.log('提交') + console.log(values) + }} + actions={actions} + labelCol={{ span: 8 }} + wrapperCol={{ span: 6 }} + validateFirst + effects={({ setFieldState, getFormGraph }) => { + onFormInit$().subscribe(() => { + setFieldState('col1', state => { + state.visible = false + }) + }) + }} + > + + + + + + + + + + + + 提交 + + + + + + +) +``` + +#### `` + +> FormLayout 组件 Props + +```typescript +interface IFormItemTopProps { + inline?: boolean + className?: string + style?: React.CSSProperties + labelCol?: number | { span: number; offset?: number } + wrapperCol?: number | { span: number; offset?: number } +} +``` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormBlock, + FormLayout +} from '@uform/antd' +import { Button } from 'antd' +import Printer from '@uform/printer' +import 'antd/dist/antd.css' +const App = () => ( + + + + + + + + + 提交重置​ + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> FormItemGrid 组件 Props + +```typescript +interface IFormItemGridProps { + cols?: Array + gutter?: number + /** next Form.Item props**/ + // 样式前缀 + prefix?: string + + // label 标签的文本 + label?: React.ReactNode + + // label 标签布局,通 `` 组件,设置 span offset 值,如 {span: 8, offset: 16},该项仅在垂直表单有效 + labelCol?: {} + + // 需要为输入控件设置布局样式时,使用该属性,用法同 labelCol + wrapperCol?: {} + + // 自定义提示信息,如不设置,则会根据校验规则自动生成. + help?: React.ReactNode + + // 额外的提示信息,和 help 类似,当需要错误信息和提示文案同时出现时,可以使用这个。 位于错误信息后面 + extra?: React.ReactNode + + // 校验状态,如不设置,则会根据校验规则自动生成 + validateState?: 'error' | 'success' | 'loading' + + // 配合 validateState 属性使用,是否展示 success/loading 的校验状态图标, 目前只有Input支持 + hasFeedback?: boolean + + // 自定义内联样式 + style?: React.CSSProperties + + // node 或者 function(values) + children?: React.ReactNode | (() => void) + + // 单个 Item 的 size 自定义,优先级高于 Form 的 size, 并且当组件与 Item 一起使用时,组件自身设置 size 属性无效。 + size?: 'large' | 'small' | 'medium' + + // 标签的位置 + labelAlign?: 'top' | 'left' | 'inset' + + // 标签的左右对齐方式 + labelTextAlign?: 'left' | 'right' + + // 扩展class + className?: string + + // [表单校验] 不能为空 + required?: boolean + + // required 的星号是否显示 + asterisk?: boolean + + // required 自定义错误信息 + requiredMessage?: string + + // required 自定义触发方式 + requiredTrigger?: string | Array + + // [表单校验] 最小值 + min?: number + + // [表单校验] 最大值 + max?: number + + // min/max 自定义错误信息 + minmaxMessage?: string + + // min/max 自定义触发方式 + minmaxTrigger?: string | Array + + // [表单校验] 字符串最小长度 / 数组最小个数 + minLength?: number + + // [表单校验] 字符串最大长度 / 数组最大个数 + maxLength?: number + + // minLength/maxLength 自定义错误信息 + minmaxLengthMessage?: string + + // minLength/maxLength 自定义触发方式 + minmaxLengthTrigger?: string | Array + + // [表单校验] 字符串精确长度 / 数组精确个数 + length?: number + + // length 自定义错误信息 + lengthMessage?: string + + // length 自定义触发方式 + lengthTrigger?: string | Array + + // 正则校验 + pattern?: any + + // pattern 自定义错误信息 + patternMessage?: string + + // pattern 自定义触发方式 + patternTrigger?: string | Array + + // [表单校验] 四种常用的 pattern + format?: 'number' | 'email' | 'url' | 'tel' + + // format 自定义错误信息 + formatMessage?: string + + // format 自定义触发方式 + formatTrigger?: string | Array + + // [表单校验] 自定义校验函数 + validator?: () => void + + // validator 自定义触发方式 + validatorTrigger?: string | Array + + // 是否修改数据时自动触发校验 + autoValidate?: boolean +} +``` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormBlock, + FormLayout +} from '@uform/antd' +import { Button } from 'antd' +import Printer from '@uform/printer' +import 'antd/dist/antd.css' + +const App = () => ( + + console.log(v)}> + + + + + + + + + + + + ​提交重置​ + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> FormTextBox 组件 Props + +```typescript +interface IFormTextBox { + text?: string + gutter?: number + title?: React.ReactText + description?: React.ReactText +} +``` + +**用法** + +```jsx +import React, { useState } from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormTextBox, + FormCard, + FormLayout +} from '@uform/antd' +import { Button } from 'antd' +import Printer from '@uform/printer' +import 'antd/dist/antd.css' + +const App = () => { + return ( + + console.log(v)}> + + + + + + + + + + + + ) +} +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> FormButtonGroup 组件 Props + +```typescript +interface IFormButtonGroupProps { + sticky?: boolean + style?: React.CSSProperties + itemStyle?: React.CSSProperties + className?: string + align?: 'left' | 'right' | 'start' | 'end' | 'top' | 'bottom' | 'center' + triggerDistance?: number + zIndex?: number + span?: ColSpanType + offset?: ColSpanType +} +``` + +**用法** + +```jsx +import React, { useState } from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormBlock, + FormLayout +} from '@uform/antd' +import { Button } from 'antd' +import Printer from '@uform/printer' +import 'antd/dist/antd.css' + +const App = () => { + const [state, setState] = useState({ editable: true }) + return ( + + console.log(v)}> +
normal
+ + ​提交重置​ + +
sticky
+ + ​提交​ + + 重置​ + +
+
+ ) +} +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> TextButton 组件 Props, 完全继承自 [ButtonProps](#ButtonProps) + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { TextButton } from '@uform/antd' +import 'antd/dist/antd.css' + +const App = () => ( + + content + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> CircleButton 组件 Props, 完全继承自 [ButtonProps](#ButtonProps) + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { CircleButton } from '@uform/antd' +import 'antd/dist/antd.css' + +const App = () => ( + + ok + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `(即将废弃,请使用SchemaMarkupField)` + +> 即将废弃,请使用[SchemaMarkupField](#SchemaMarkupField) + +### Type of SchemaMarkupField + +#### string + +* Schema Type : `string` +* Schema UI Component: Fusion-Next ``, ``, `` + +```typescript +interface IPasswordProps { + checkStrength: boolean + /** next input props **/ + // 当前值 + value?: string | number + + // 初始化值 + defaultValue?: string | number + + // 发生改变的时候触发的回调 + onChange?: (value: string, e: React.ChangeEvent) => void + + // 键盘按下的时候触发的回调 + onKeyDown?: (e: React.KeyboardEvent, opts: {}) => void + + // 禁用状态 + disabled?: boolean + + // 最大长度 + maxLength?: number + + // 是否展现最大长度样式 + hasLimitHint?: boolean + + // 当设置了maxLength时,是否截断超出字符串 + cutString?: boolean + + // 只读 + readOnly?: boolean + + // onChange返回会自动去除头尾空字符 + trim?: boolean + + // 输入提示 + placeholder?: string + + // 获取焦点时候触发的回调 + onFocus?: () => void + + // 失去焦点时候触发的回调 + onBlur?: () => void + + // 自定义字符串计算长度方式 + getValueLength?: (value: string) => number + + // 自定义class + className?: string + + // 自定义内联样式 + style?: React.CSSProperties + + // 原生type + htmlType?: string + + // name + name?: string + + // 状态 + state?: 'error' | 'loading' | 'success' + + // label + label?: React.ReactNode + + // 是否出现clear按钮 + hasClear?: boolean + + // 是否有边框 + hasBorder?: boolean + + // 尺寸 + size?: 'small' | 'medium' | 'large' + + // 按下回车的回调 + onPressEnter?: () => void + + // 水印 (Icon的type类型,和hasClear占用一个地方) + hint?: string + + // 文字前附加内容 + innerBefore?: React.ReactNode + + // 文字后附加内容 + innerAfter?: React.ReactNode + + // 输入框前附加内容 + addonBefore?: React.ReactNode + + // 输入框后附加内容 + addonAfter?: React.ReactNode + + // 输入框前附加文字 + addonTextBefore?: React.ReactNode + + // 输入框后附加文字 + addonTextAfter?: React.ReactNode + + // (原生input支持) + autoComplete?: string + + // 自动聚焦(原生input支持) + autoFocus?: boolean +} +``` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### number + +* Schema Type : `number` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### boolean + +* Schema Type : `boolean` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### date + +* Schema Type : `date` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### time + +* Schema Type : `time` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### range + +* Schema Type : `range` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### upload + +* Schema Type : `upload` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### checkbox + +* Schema Type : `checkbox` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### radio + +* Schema Type : `radio` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### rating + +* Schema Type : `rating` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### transfer + +* Schema Type : `transfer` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/antd' +import 'antd/dist/antd.css' + +const actions = createFormActions() +const App = () => { + return ( + + item.title + }} + x-component-props={{ + showSearch: true + }} + /> + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### API + +> 整体完全继承@uform/react, 下面只列举@uform/antd 的特有 API + +--- + +#### `createFormActions` + +> 创建一个 [IFormActions](#IFormActions) 实例 + +**签名** + +```typescript +createFormActions(): IFormActions +``` + +**用法** + +```typescript +import { createFormActions } from '@uform/antd' + +const actions = createFormActions() +console.log(actions.getFieldValue('username')) +``` + +#### `createAsyncFormActions` + +> 创建一个 [IFormAsyncActions](#IFormAsyncActions) 实例,成员方法 同[IFormActions](#IFormActions), +> 但是调用 API 返回的结果是异步的(promise)。 + +**签名** + +```typescript +createAsyncFormActions(): IFormAsyncActions +``` + +**用法** + +```typescript +import { createAsyncFormActions } from '@uform/antd' + +const actions = createAsyncFormActions() +actions.getFieldValue('username').then(val => console.log(val)) +``` + +#### `FormEffectHooks` + +> 返回包含所有 UForm 生命周期的钩子函数,可以被监听消费 + +**用法** + +```typescript +import SchemaForm, { FormEffectHooks } from '@uform/antd' +const { + /** + * Form LifeCycle + **/ + onFormWillInit$, // 表单预初始化触发 + onFormInit$, // 表单初始化触发 + onFormChange$, // 表单变化时触发 + onFormInputChange$, // 表单事件触发时触发,用于只监控人工操作 + onFormInitialValueChange$, // 表单初始值变化时触发 + onFormReset$, // 表单重置时触发 + onFormSubmit$, // 表单提交时触发 + onFormSubmitStart$, // 表单提交开始时触发 + onFormSubmitEnd$, // 表单提交结束时触发 + onFormMount$, // 表单挂载时触发 + onFormUnmount$, // 表单卸载时触发 + onFormValidateStart$, // 表单校验开始时触发 + onFormValidateEnd$, //表单校验结束时触发 + onFormValuesChange$, // 表单值变化时触发 + /** + * FormGraph LifeCycle + **/ + onFormGraphChange$, // 表单观察者树变化时触发 + /** + * Field LifeCycle + **/ + onFieldWillInit$, // 字段预初始化时触发 + onFieldInit$, // 字段初始化时触发 + onFieldChange$, // 字段变化时触发 + onFieldMount$, // 字段挂载时触发 + onFieldUnmount$, // 字段卸载时触发 + onFieldInputChange$, // 字段事件触发时触发,用于只监控人工操作 + onFieldValueChange$, // 字段值变化时触发 + onFieldInitialValueChange$ // 字段初始值变化时触发 +} = FormEffectHooks + +const App = () => { + return ( + { + onFormInit$().subscribe(() => { + console.log('初始化') + }) + }} + > + ... + + ) +} +``` + +#### createEffectHook + +> 自定义 hook + +**Usage** + +```jsx +import SchemaForm, { createEffectHook, createFormActions } from '@uform/antd' + +const actions = createFormActions() +const diyHook1$ = createEffectHook('diy1') +const diyHook2$ = createEffectHook('diy2') + +const App = () => { + return ( + { + diyHook1$().subscribe(payload => { + console.log('diy1 hook triggered', payload) + }) + + diyHook2$().subscribe(payload => { + console.log('diy2 hook triggered', payload) + }) + }} + > + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### connect + +> 包装字段组件,让字段组件只需要支持value/defaultValue/onChange属性即可快速接入表单 + +```typescript +type Connect = >(options?: IConnectOptions) => +(Target: T) => React.PureComponent +``` +**用法** + +```typescript +import {registerFormField,connect} from '@uform/antd' + +registerFormField( + 'string', + connect()(props => ) +) +``` + +#### registerFormField + +```typescript +type registerFormField( + name : string, //类型名称 + component : React.ComponentType, //类型组件 + noMiddleware: boolean //是否被middleware包装 +) +``` + +**用法** + +```jsx + +import SchemaForm, { SchemaMarkupField as Field, registerFormField, connect, createFormActions } from '@uform/antd' + +registerFormField( + 'custom-string', + connect()(props => ) +) +const actions = createFormActions() + +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### Interfaces + +> 整体完全继承@uform/react, 下面只列举@uform/antd 的特有的 Interfaces + +--- + +#### IForm + +> 通过 createForm 创建出来的 Form 实例对象 API + +```typescript +interface IForm { + /* + * 表单提交,如果回调参数返回Promise, + * 那么整个提交流程会hold住,同时loading为true, + * 等待Promise resolve才触发表单onFormSubmitEnd事件,同时loading为false + */ + submit( + onSubmit?: (values: IFormState['values']) => any | Promise + ): Promise<{ + validated: IFormValidateResult + payload: any //onSubmit回调函数返回值 + }> + + /* + * 清空错误消息,可以通过传FormPathPattern来批量或精确控制要清空的字段, + * 比如clearErrors("*(aa,bb,cc)") + */ + clearErrors: (pattern?: FormPathPattern) => void + + /* + * 获取状态变化情况,主要用于在表单生命周期钩子内判断当前生命周期中有哪些状态发生了变化, + * 比如hasChanged(state,'value.aa') + */ + hasChanged( + target: IFormState | IFieldState | IVirtualFieldState, + path: FormPathPattern + ): boolean + + /* + * 重置表单 + */ + reset(options?: { + //强制清空 + forceClear?: boolean + //强制校验 + validate?: boolean + //重置范围,用于批量或者精确控制要重置的字段 + selector?: FormPathPattern + }): Promise + + /* + * 校验表单 + */ + validate( + path?: FormPathPattern, + options?: { + //是否悲观校验,如果当前字段遇到第一个校验错误则停止后续校验流程 + first?: boolean + } + ): Promise + + /* + * 设置表单状态 + */ + setFormState( + //操作回调 + callback?: (state: IFormState) => any, + //是否不触发事件 + silent?: boolean + ): void + + /* + * 获取表单状态 + */ + getFormState( + //transformer + callback?: (state: IFormState) => any + ): any + + /* + * 设置字段状态 + */ + setFieldState( + //字段路径 + path: FormPathPattern, + //操作回调 + callback?: (state: IFieldState) => void, + //是否不触发事件 + silent?: boolean + ): void + + /* + * 获取字段状态 + */ + getFieldState( + //字段路径 + path: FormPathPattern, + //transformer + callback?: (state: IFieldState) => any + ): any + + /* + * 注册字段 + */ + registerField(props: { + //节点路径 + path?: FormPathPattern + //数据路径 + name?: string + //字段值 + value?: any + //字段多参值 + values?: any[] + //字段初始值 + initialValue?: any + //字段扩展属性 + props?: any + //字段校验规则 + rules?: ValidatePatternRules[] + //字段是否必填 + required?: boolean + //字段是否可编辑 + editable?: boolean + //字段是否走脏检查 + useDirty?: boolean + //字段状态计算容器,主要用于扩展核心联动规则 + computeState?: (draft: IFieldState, prevState: IFieldState) => void + }): IField + + /* + * 注册虚拟字段 + */ + registerVirtualField(props: { + //节点路径 + path?: FormPathPattern + //数据路径 + name?: string + //字段扩展属性 + props?: any + //字段是否走脏检查 + useDirty?: boolean + //字段状态计算容器,主要用于扩展核心联动规则 + computeState?: (draft: IFieldState, prevState: IFieldState) => void + }): IVirtualField + + /* + * 创建字段数据操作器,后面会详细解释返回的API + */ + createMutators(field: IField): IMutators + + /* + * 获取表单观察者树 + */ + getFormGraph(): IFormGraph + + /* + * 设置表单观察者树 + */ + setFormGraph(graph: IFormGraph): void + + /* + * 监听表单生命周期 + */ + subscribe( + callback?: ({ type, payload }: { type: string; payload: any }) => void + ): number + + /* + * 取消监听表单生命周期 + */ + unsubscribe(id: number): void + + /* + * 触发表单自定义生命周期 + */ + notify: (type: string, payload?: T) => void + + /* + * 设置字段值 + */ + setFieldValue(path?: FormPathPattern, value?: any): void + + /* + * 获取字段值 + */ + getFieldValue(path?: FormPathPattern): any + + /* + * 设置字段初始值 + */ + setFieldInitialValue(path?: FormPathPattern, value?: any): void + + /* + * 获取字段初始值 + */ + getFieldInitialValue(path?: FormPathPattern): any +} +``` + +#### ButtonProps + +```typescript +interface ButtonProps { + href: string; + target?: string; + onClick?: React.MouseEventHandler; + htmlType?: ButtonHTMLType; + onClick?: React.MouseEventHandler; +} +``` + +#### CardProps + +```typescript +interface CardProps extends HTMLAttributesWeak, CommonProps { + prefixCls?: string; + title?: React.ReactNode; + extra?: React.ReactNode; + bordered?: boolean; + headStyle?: React.CSSProperties; + bodyStyle?: React.CSSProperties; + style?: React.CSSProperties; + loading?: boolean; + noHovering?: boolean; + hoverable?: boolean; + children?: React.ReactNode; + id?: string; + className?: string; + size?: CardSize; + type?: CardType; + cover?: React.ReactNode; + actions?: React.ReactNode[]; + tabList?: CardTabListType[]; + tabBarExtraContent?: React.ReactNode | null; + onTabChange?: (key: string) => void; + activeTabKey?: string; + defaultActiveTabKey?: string; +} +``` + +#### ICompatItemProps + +```typescript +interface ICompatItemProps + extends Exclude, + Partial { + labelCol?: number | { span: number; offset?: number } + wrapperCol?: number | { span: number; offset?: number } +} +``` + + +#### IFieldState + +```typescript +interface IFieldState { + /**只读属性**/ + + //状态名称,FieldState + displayName?: string + //数据路径 + name: string + //节点路径 + path: string + //是否已经初始化 + initialized: boolean + //是否处于原始态,只有value===intialValues时的时候该状态为true + pristine: boolean + //是否处于合法态,只要errors长度大于0的时候valid为false + valid: boolean + //是否处于非法态,只要errors长度大于0的时候valid为true + invalid: boolean + //是否处于校验态 + validating: boolean + //是否被修改,如果值发生变化,该属性为true,同时在整个字段的生命周期内都会为true + modified: boolean + //是否被触碰 + touched: boolean + //是否被激活,字段触发onFocus事件的时候,它会被触发为true,触发onBlur时,为false + active: boolean + //是否访问过,字段触发onBlur事件的时候,它会被触发为true + visited: boolean + + /**可写属性**/ + + //是否可见,注意:该状态如果为false,那么字段的值不会被提交,同时UI不会显示 + visible: boolean + //是否展示,注意:该状态如果为false,那么字段的值会提交,UI不会展示,类似于表单隐藏域 + display: boolean + //是否可编辑 + editable: boolean + //是否处于loading状态,注意:如果字段处于异步校验时,loading为true + loading: boolean + //字段多参值,比如字段onChange触发时,给事件回调传了多参数据,那么这里会存储所有参数的值 + values: any[] + //字段错误消息 + errors: string[] + //字段告警消息 + warnings: string[] + //字段值,与values[0]是恒定相等 + value: any + //初始值 + initialValue: any + //校验规则,具体类型描述参考后面文档 + rules: ValidatePatternRules[] + //是否必填 + required: boolean + //是否挂载 + mounted: boolean + //是否卸载 + unmounted: boolean + //字段扩展属性 + props: FieldProps +} +``` + + +#### ISchemaFieldComponentProps + +```typescript +interface ISchemaFieldComponentProps extends IFieldState { + schema: Schema + mutators: IMutators + form: IForm + renderField: ( + addtionKey: string | number, + reactKey?: string | number + ) => React.ReactElement +} +``` + +#### ISchemaVirtualFieldComponentProps + +```typescript +interface ISchemaVirtualFieldComponentProps extends IVirtualFieldState { + schema: Schema + form: IForm + children: React.ReactElement[] + renderField: ( + addtionKey: string | number, + reactKey?: string | number + ) => React.ReactElement +} +``` + +#### ISchemaFieldWrapper + +```typescript +interface ISchemaFieldWrapper { + (Traget: ISchemaFieldComponent): + | React.FC + | React.ClassicComponent +} +``` + +#### ISchemaFieldComponent + +```typescript +declare type ISchemaFieldComponent = ComponentWithStyleComponent< + ISchemaFieldComponentProps +> & { + __WRAPPERS__?: ISchemaFieldWrapper[] +} +``` + +#### ISchemaVirtualFieldComponent + +```typescript +declare type ISchemaVirtualFieldComponent = ComponentWithStyleComponent< + ISchemaVirtualFieldComponentProps +> & { + __WRAPPERS__?: ISchemaFieldWrapper[] +} +``` + +#### ISchemaFormRegistry + +```typescript +interface ISchemaFormRegistry { + fields: { + [key: string]: ISchemaFieldComponent + } + virtualFields: { + [key: string]: ISchemaVirtualFieldComponent + } + wrappers?: ISchemaFieldWrapper[] + formItemComponent: React.JSXElementConstructor + formComponent: string | React.JSXElementConstructor +} +``` + +#### INextSchemaFieldProps + +```typescript +interface INextSchemaFieldProps { + name?: string; + /** ISchema **/ + title?: SchemaMessage; + description?: SchemaMessage; + default?: any; + readOnly?: boolean; + writeOnly?: boolean; + type?: 'string' | 'object' | 'array' | 'number' | string; + enum?: Array; + const?: any; + multipleOf?: number; + maximum?: number; + exclusiveMaximum?: number; + minimum?: number; + exclusiveMinimum?: number; + maxLength?: number; + minLength?: number; + pattern?: string | RegExp; + maxItems?: number; + minItems?: number; + uniqueItems?: boolean; + maxProperties?: number; + minProperties?: number; + required?: string[] | boolean; + format?: string; + properties?: { + [key: string]: ISchema; + }; + items?: ISchema | ISchema[]; + additionalItems?: ISchema; + patternProperties?: { + [key: string]: ISchema; + }; + additionalProperties?: ISchema; + editable?: boolean; + visible?: boolean; + display?: boolean; + ['x-props']?: { + [name: string]: any; + }; + ['x-index']?: number; + ['x-rules']?: ValidatePatternRules; + ['x-component']?: string; + ['x-component-props']?: { + [name: string]: any; + }; + ['x-render']?: (props: T & { + renderComponent: () => React.ReactElement; + }) => React.ReactElement; + ['x-effect']?: (dispatch: (type: string, payload: any) => void, option?: object) => { + [key: string]: any; + }; + +``` + +#### IPreviewTextProps + +```typescript +interface IPreviewTextProps { + className?: React.ReactText + dataSource?: any[] + value?: any + addonBefore?: React.ReactNode + innerBefore?: React.ReactNode + addonTextBefore?: React.ReactNode + addonTextAfter?: React.ReactNode + innerAfter?: React.ReactNode + addonAfter?: React.ReactNode +} + +``` + +#### IMutators + +```typescript +interface IMutators { + change: (value: V)=> void,//改变当前字段值 + dispatch: (name: string, payload : any) => void,//触发effect事件 + errors: (errors: string | Array, ...formatValues: Array) => void,//设置当前字段的错误消息 + push(value: V),//对当前字段的值做push操作 + pop(),//对当前字段的值做pop操作 + insert(index: number,value: V),//对当前字段的值做insert操作 + remove(name : string),//对当前字段的值做remove操作 + unshift(value : V),//对当前字段值做unshift操作 + shift(),//对当前字段值做shift操作 + move(fromIndex: number, toIndex: number)//对当前字段值做move操作 +} +``` + +#### IFieldProps + +```typescript +interface IFieldProps { + name : string //字段数据路径 + path : Array //字段数组数据路径 + value : V //字段值 + errors : Array //字段错误消息集合 + editable : boolean | ((name:string) => boolean) //字段是否可编辑 + locale : Locale //国际化文案对象 + loading : boolean //是否处于加载态 + schemaPath : Array //schema path,考虑到有些schema其实是不占数据路径的,所以这个路径是真实路径 + getSchema : (path: string) => ISchema //根据路径获取schema + renderField : (childKey: string, reactKey: string | number) => JSX.Element | string | null //根据childKey渲染当前字段的子字段 + renderComponent : React.FunctionComponent | undefined>,//渲染当前字段的组件,对于x-render来说,可以借助它快速实现渲染包装功能 + getOrderProperties : () => Array<{schema: ISchema, key: number, path: string, name: string }>,//根据properties里字段的x-index值求出排序后的properties + mutators : Mutators,//数据操作对象 + schema : ISchema +} + +``` + +```typescript + +interface IConnectOptions { + //控制表单组件 + valueName?: string + //事件名称 + eventName?: string + //默认props + defaultProps?: Partial + //取值函数,有些场景我们的事件函数取值并不是事件回调的第一个参数,需要做进一步的定制 + getValueFromEvent?: (props: IFieldProps['x-props'], event: Event, ...args: any[]) => any + //字段组件props transformer + getProps?: (connectProps: IConnectProps, fieldProps: IFieldProps) => IConnectProps + //字段组件component transformer + getComponent?: ( + target: T, + connectProps: IConnectProps, + fieldProps: IFieldProps + ) => T +} + +``` \ No newline at end of file diff --git a/packages/antd/package.json b/packages/antd/package.json index e90a996d84e..6004166ff98 100644 --- a/packages/antd/package.json +++ b/packages/antd/package.json @@ -1,6 +1,6 @@ { "name": "@uform/antd", - "version": "0.4.4", + "version": "1.0.0-alpha.5", "license": "MIT", "main": "lib", "module": "esm", @@ -24,17 +24,19 @@ }, "peerDependencies": { "@babel/runtime": "^7.4.4", + "@types/styled-components": "^4.1.19", "antd": "^3.14.1", "react": ">=16.8.0", "react-dom": ">=16.8.0" }, "dependencies": { - "@uform/react": "^0.4.4", - "@uform/types": "^0.4.4", - "@uform/utils": "^0.4.4", + "@uform/react-schema-renderer": "^1.0.0-alpha.5", + "@uform/react-shared-components": "^1.0.0-alpha.5", + "@uform/shared": "^1.0.0-alpha.5", "classnames": "^2.2.6", - "moveto": "^1.7.4", + "react-eva": "^1.0.0-alpha.0", "react-stikky": "^0.1.15", + "rxjs": "^6.5.1", "styled-components": "^4.1.1" }, "publishConfig": { diff --git a/packages/antd/src/compat/Form.tsx b/packages/antd/src/compat/Form.tsx new file mode 100644 index 00000000000..461bc0b91a5 --- /dev/null +++ b/packages/antd/src/compat/Form.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import { Form } from 'antd' +import { FormProps } from 'antd/lib/form' +import { IFormItemTopProps } from '../types' +import { FormItemProvider } from './context' +import { normalizeCol } from '../shared' +import { + PreviewText, + PreviewTextConfigProps +} from '@uform/react-shared-components' +export const CompatAntdForm: React.FC = props => { + return ( + + +
+ + + ) +} diff --git a/packages/antd/src/compat/FormItem.tsx b/packages/antd/src/compat/FormItem.tsx new file mode 100644 index 00000000000..dec52e1c9c0 --- /dev/null +++ b/packages/antd/src/compat/FormItem.tsx @@ -0,0 +1,109 @@ +import React, { createContext, useContext, createElement } from 'react' +import { Form } from 'antd' +import { useFormItem } from './context' +import { IFormItemTopProps, ICompatItemProps } from '../types' +import { normalizeCol } from '../shared' + +const computeStatus = (props: ICompatItemProps) => { + if (props.loading) { + return 'validating' + } + if (props.invalid) { + return 'error' + } + if (props.warnings && props.warnings.length) { + return 'warning' + } + return '' +} + +const computeHelp = (props: ICompatItemProps) => { + if (props.help) return props.help + const messages = [].concat(props.errors || [], props.warnings || []) + return messages.length + ? messages.map((message, index) => + createElement( + 'span', + { key: index }, + message, + messages.length - 1 > index ? ' ,' : '' + ) + ) + : props.schema && props.schema.description +} + +const computeLabel = (props: ICompatItemProps) => { + if (props.label) return props.label + if (props.schema && props.schema.title) { + return props.schema.title + } +} + +const computeExtra = (props: ICompatItemProps) => { + if (props.extra) return props.extra +} + +function pickProps(obj: T, ...keys: (keyof T)[]): Pick { + const result: Pick = {} as any + for (let i = 0; i < keys.length; i++) { + if (obj[keys[i]] !== undefined) { + result[keys[i]] = obj[keys[i]] + } + } + return result +} + +const computeSchemaExtendProps = ( + props: ICompatItemProps +): IFormItemTopProps => { + if (props.schema) { + return pickProps( + { + ...props.schema.getExtendsItemProps(), + ...props.schema.getExtendsProps() + }, + 'className', + 'prefix', + 'labelAlign', + 'labelTextAlign', + 'size', + 'labelCol', + 'wrapperCol' + ) + } +} + +const FormItemPropsContext = createContext({}) + +export const FormItemProps = ({ children, ...props }) => ( + + {children} + +) + +export const CompatAntdFormItem: React.FC = props => { + const { prefixCls, labelAlign, labelCol, wrapperCol } = useFormItem() + const help = computeHelp(props) + const label = computeLabel(props) + const status = computeStatus(props) + const extra = computeExtra(props) + const itemProps = computeSchemaExtendProps(props) + const outerFormItemProps = useContext(FormItemPropsContext) + return ( + {extra}

: undefined} + {...itemProps} + {...outerFormItemProps} + > + {props.children} +
+ ) +} diff --git a/packages/antd/src/compat/context.tsx b/packages/antd/src/compat/context.tsx new file mode 100644 index 00000000000..8905ea7ff08 --- /dev/null +++ b/packages/antd/src/compat/context.tsx @@ -0,0 +1,31 @@ +import React, { createContext, useContext } from 'react' +import { IFormItemTopProps } from '../types' + +const FormItemContext = createContext({}) + +export const FormItemProvider: React.FC = ({ + children, + prefixCls, + labelAlign, + labelCol, + inline, + wrapperCol +}) => ( + + {children} + +) + +FormItemProvider.displayName = 'FormItemProvider' + +export const useFormItem = () => { + return useContext(FormItemContext) +} diff --git a/packages/antd/src/compat/index.ts b/packages/antd/src/compat/index.ts new file mode 100644 index 00000000000..8afea05f282 --- /dev/null +++ b/packages/antd/src/compat/index.ts @@ -0,0 +1,10 @@ +import { + registerFormComponent, + registerFormItemComponent +} from '@uform/react-schema-renderer' +import { CompatAntdForm } from './Form' +import { CompatAntdFormItem } from './FormItem' + +registerFormComponent(CompatAntdForm) + +registerFormItemComponent(CompatAntdFormItem) diff --git a/packages/antd/src/components/Button.tsx b/packages/antd/src/components/Button.tsx new file mode 100644 index 00000000000..d8f707ce804 --- /dev/null +++ b/packages/antd/src/components/Button.tsx @@ -0,0 +1,92 @@ +import React from 'react' +import { FormSpy, LifeCycleTypes, createVirtualBox } from '@uform/react-schema-renderer' +import { Button } from 'antd' +import { ButtonProps } from 'antd/lib/button' +import { ISubmitProps, IResetProps } from '../types' + +export const TextButton: React.FC = props => ( + + ) + }} + + ) +} + +Submit.defaultProps = { + showLoading: true +} + +export const Reset: React.FC = ({ + children, + forceClear, + validate, + ...props +}) => { + return ( + + {({ form }) => { + return ( + + ) + }} + + ) +} + +createVirtualBox('reset', Reset) +createVirtualBox('text-button', TextButton) +createVirtualBox('submit', Submit) +createVirtualBox('circle-button', CircleButton) diff --git a/packages/antd/src/components/FormBlock.tsx b/packages/antd/src/components/FormBlock.tsx new file mode 100644 index 00000000000..ab870a2a4cd --- /dev/null +++ b/packages/antd/src/components/FormBlock.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import { createVirtualBox } from '@uform/react-schema-renderer' +import { Card } from 'antd' +import { CardProps } from 'antd/lib/card' +import styled from 'styled-components' + +export const FormBlock = createVirtualBox( + 'block', + styled(({ children, className, ...props }) => { + return ( + + {children} + + ) + })` + margin-bottom: 10px !important; + &.ant-card { + border: none; + box-shadow: none; + } + ` +) diff --git a/packages/antd/src/components/FormButtonGroup.tsx b/packages/antd/src/components/FormButtonGroup.tsx new file mode 100644 index 00000000000..850e2a222a0 --- /dev/null +++ b/packages/antd/src/components/FormButtonGroup.tsx @@ -0,0 +1,161 @@ +import React, { useRef } from 'react' +import { Row, Col } from 'antd' +import Sticky from 'react-stikky' +import cls from 'classnames' +import styled from 'styled-components' +import { useFormItem } from '../compat/context' +import { IFormButtonGroupProps } from '../types' +import { createVirtualBox } from '@uform/react-schema-renderer' + +export interface IOffset { + top: number | string + right: number | string + bottom: number | string + left: number | string +} + +const getAlign = align => { + if (align === 'start' || align === 'end') return align + if (align === 'left' || align === 'top') return 'flex-start' + if (align === 'right' || align === 'bottom') return 'flex-end' + return align +} + +const isElementInViewport = ( + rect: ClientRect, + { + offset = 0, + threshold = 0 + }: { + offset?: IOffset | number + threshold?: number + } = {} +): boolean => { + const { top, right, bottom, left, width, height } = rect + const intersection = { + t: bottom, + r: window.innerWidth - left, + b: window.innerHeight - top, + l: right + } + + const elementThreshold = { + x: threshold * width, + y: threshold * height + } + + return ( + intersection.t >= + ((offset as IOffset).top || (offset as number) + elementThreshold.y) && + intersection.r >= + ((offset as IOffset).right || (offset as number) + elementThreshold.x) && + intersection.b >= + ((offset as IOffset).bottom || (offset as number) + elementThreshold.y) && + intersection.l >= + ((offset as IOffset).left || (offset as number) + elementThreshold.x) + ) +} + +export const FormButtonGroup = styled( + (props: React.PropsWithChildren) => { + const { + span, + zIndex, + sticky, + style, + offset, + className, + children, + triggerDistance, + itemStyle + } = props + const { inline } = useFormItem() + const selfRef = useRef() + const renderChildren = () => { + return ( +
+ + + +
+ {children} +
+ + +
+
+ ) + } + const getStickyBoundaryHandler = () => { + return () => { + if (selfRef.current && selfRef.current.parentElement) { + const container = selfRef.current.parentElement + return isElementInViewport(container.getBoundingClientRect()) + } + return true + } + } + + const content = ( +
+ {renderChildren()} +
+ ) + + if (sticky) { + return ( +
+ +
+ {content} +
+
+
+ ) + } + + return content + } +)` + ${(props: IFormButtonGroupProps) => + props.align ? `display:flex;justify-content: ${getAlign(props.align)}` : ''} + &.is-inline { + display: inline-block; + flex-grow: 3; + } + .button-group { + .inline { + display: inline-block; + .inline-view { + & > * { + margin-right: 10px; + margin-left: 0px; + display: inline-block; + } + & > *:last-child { + margin-right: 0 !important; + } + } + } + } +` + +createVirtualBox>( + 'button-group', + FormButtonGroup, +) \ No newline at end of file diff --git a/packages/antd/src/components/FormCard.tsx b/packages/antd/src/components/FormCard.tsx new file mode 100644 index 00000000000..69c6b071c16 --- /dev/null +++ b/packages/antd/src/components/FormCard.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import { createVirtualBox } from '@uform/react-schema-renderer' +import { Card } from 'antd' +import { CardProps } from 'antd/lib/card' +import styled from 'styled-components' + +export const FormCard = createVirtualBox( + 'card', + styled(({ children, className, ...props }) => { + return ( + + {children} + + ) + })` + margin-bottom: 10px !important; + ` +) diff --git a/packages/antd/src/components/FormItemGrid.tsx b/packages/antd/src/components/FormItemGrid.tsx new file mode 100644 index 00000000000..65888cfa91b --- /dev/null +++ b/packages/antd/src/components/FormItemGrid.tsx @@ -0,0 +1,87 @@ +import React, { Fragment } from 'react' +import { CompatAntdFormItem } from '../compat/FormItem' +import { createVirtualBox } from '@uform/react-schema-renderer' +import { toArr } from '@uform/shared' +import { Row, Col } from 'antd' +import { RowProps, ColProps } from 'antd/lib/grid' +import { FormItemProps as ItemProps } from 'antd/lib/form' +import { IFormItemGridProps, IItemProps } from '../types' +import { normalizeCol } from '../shared' + +export const FormItemGrid = createVirtualBox< + React.PropsWithChildren +>('grid', props => { + const { + cols: rawCols, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + title, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + description, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + help, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + extra, + ...selfProps + } = props + const children = toArr(props.children) + const cols = toArr(rawCols).map(col => normalizeCol(col)) + const childNum = children.length + + if (cols.length < childNum) { + let offset: number = childNum - cols.length + let lastSpan: number = + 24 - + cols.reduce((buf, col) => { + return ( + buf + + Number(col.span ? col.span : 0) + + Number(col.offset ? col.offset : 0) + ) + }, 0) + for (let i = 0; i < offset; i++) { + cols.push({ span: Math.floor(lastSpan / offset) }) + } + } + const grids = ( + + {children.reduce((buf, child, key) => { + return child + ? buf.concat( + + {child} + + ) + : buf + }, [])} + + ) + + if (title) { + return ( + + {grids} + + ) + } + return {grids} +}) + +export const FormGridRow = createVirtualBox( + 'grid-row', + props => { + const { title, description, extra } = props + const grids = {props.children} + if (title) { + return ( + + {grids} + + ) + } + return grids + } +) + +export const FormGridCol = createVirtualBox('grid-col', props => { + return {props.children} +}) diff --git a/packages/antd/src/components/FormLayout.tsx b/packages/antd/src/components/FormLayout.tsx new file mode 100644 index 00000000000..35f242caf7e --- /dev/null +++ b/packages/antd/src/components/FormLayout.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import { FormItemProvider, useFormItem } from '../compat/context' +import { createVirtualBox } from '@uform/react-schema-renderer' +import cls from 'classnames' +import { IFormItemTopProps } from '../types' + +export const FormLayout = createVirtualBox( + 'layout', + props => { + const { inline } = useFormItem() + const isInline = props.inline || inline + const children = + isInline || props.className || props.style ? ( +
+ {props.children} +
+ ) : ( + props.children + ) + return {children} + } +) diff --git a/packages/antd/src/components/FormStep.tsx b/packages/antd/src/components/FormStep.tsx new file mode 100644 index 00000000000..e1c5f058af7 --- /dev/null +++ b/packages/antd/src/components/FormStep.tsx @@ -0,0 +1,100 @@ +import React, { useMemo, useRef, Fragment } from 'react' +import { + createControllerBox, + ISchemaVirtualFieldComponentProps, + createEffectHook, + useFormEffects, + useFieldState +} from '@uform/react-schema-renderer' +import { toArr } from '@uform/shared' +import { Steps } from 'antd' +import { IFormStep } from '../types' + +enum StateMap { + ON_FORM_STEP_NEXT = 'onFormStepNext', + ON_FORM_STEP_PREVIOUS = 'onFormStepPrevious', + ON_FORM_STEP_GO_TO = 'onFormStepGoto', + ON_FORM_STEP_CURRENT_CHANGE = 'onFormStepCurrentChange' +} +const EffectHooks = { + onStepNext$: createEffectHook(StateMap.ON_FORM_STEP_NEXT), + onStepPrevious$: createEffectHook(StateMap.ON_FORM_STEP_PREVIOUS), + onStepGoto$: createEffectHook(StateMap.ON_FORM_STEP_GO_TO), + onStepCurrentChange$: createEffectHook<{ + value: number + preValue: number + }>(StateMap.ON_FORM_STEP_CURRENT_CHANGE) +} + +type StepComponentExtendsProps = StateMap + +export const FormStep: React.FC & + StepComponentExtendsProps = createControllerBox( + 'step', + ({ form, schema, children }: ISchemaVirtualFieldComponentProps) => { + const [{ current }, setFieldState] = useFieldState({ + current: 0 + }) + const ref = useRef(current) + const { dataSource, ...stepProps } = schema.getExtendsComponentProps() + const items = toArr(dataSource) + const update = (cur: number) => { + form.notify(StateMap.ON_FORM_STEP_CURRENT_CHANGE, { + value: cur, + preValue: current + }) + setFieldState({ + current: cur + }) + } + useFormEffects(($, { setFieldState }) => { + items.forEach(({ name }, index) => { + setFieldState(name, (state: any) => { + state.display = index === current + }) + }) + $(StateMap.ON_FORM_STEP_CURRENT_CHANGE).subscribe(({ value }) => { + items.forEach(({ name }, index) => { + if (!name) + throw new Error('FormStep dataSource must include `name` property') + setFieldState(name, (state: any) => { + state.display = index === value + }) + }) + }) + + $(StateMap.ON_FORM_STEP_NEXT).subscribe(() => { + form.validate().then(({ errors }) => { + if (errors.length === 0) { + update( + ref.current + 1 > items.length - 1 ? ref.current : ref.current + 1 + ) + } + }) + }) + + $(StateMap.ON_FORM_STEP_PREVIOUS).subscribe(() => { + update(ref.current - 1 < 0 ? ref.current : ref.current - 1) + }) + + $(StateMap.ON_FORM_STEP_GO_TO).subscribe(payload => { + if (!(payload < 0 || payload > items.length)) { + update(payload) + } + }) + }) + ref.current = current + return ( + + + {items.map((props, key) => { + return + })} + {' '} + {children} + + ) + } +) as any + +Object.assign(FormStep, StateMap, EffectHooks) diff --git a/packages/antd/src/components/FormTextBox.tsx b/packages/antd/src/components/FormTextBox.tsx new file mode 100644 index 00000000000..771177680f0 --- /dev/null +++ b/packages/antd/src/components/FormTextBox.tsx @@ -0,0 +1,123 @@ +import React, { useRef, useLayoutEffect } from 'react' +import { createControllerBox } from '@uform/react-schema-renderer' +import { IFormTextBox } from '../types' +import { toArr } from '@uform/shared' +import { CompatAntdFormItem } from '../compat/FormItem' +import styled from 'styled-components' + +export const FormTextBox = createControllerBox( + 'text-box', + styled(({ props, className, children }) => { + const { + title, + help, + text, + name, + extra, + gutter, + style, + ...componentProps + } = Object.assign( + { + gutter: 5 + }, + props['x-component-props'] + ) + const ref: React.RefObject = useRef() + const arrChildren = toArr(children) + const split = text.split('%s') + let index = 0 + useLayoutEffect(() => { + if (ref.current) { + const elements = ref.current.querySelectorAll('.text-box-field') + const syncLayouts = Array.prototype.map.call( + elements, + (el: HTMLElement) => { + return [ + el, + () => { + const ctrl = el.querySelector( + '.ant-form-item-control:first-child' + ) + if (ctrl) { + el.style.width = ctrl.getBoundingClientRect().width + 'px' + } + } + ] + } + ) + syncLayouts.forEach(([el, handler]) => { + el.addEventListener('DOMSubtreeModified', handler) + }) + + return () => { + syncLayouts.forEach(([el, handler]) => { + el.removeEventListener('DOMSubtreeModified', handler) + }) + } + } + }, []) + const newChildren = split.reduce((buf, item, key) => { + return buf.concat( + item ? ( +

+ {item} +

+ ) : null, + arrChildren[key] ? ( +
+ {arrChildren[key]} +
+ ) : null + ) + }, []) + + const textChildren = ( +
+ {newChildren} +
+ ) + + if (!title) return textChildren + + return ( + + {textChildren} + + ) + })` + display: flex; + .text-box-words:nth-child(1) { + margin-left: 0; + } + .text-box-field { + display: inline-block; + } + .next-form-item { + margin-bottom: 0 !important; + } + .preview-text { + text-align: center !important; + } + ` +) diff --git a/packages/antd/src/components/Select.tsx b/packages/antd/src/components/Select.tsx new file mode 100644 index 00000000000..969410d5ec8 --- /dev/null +++ b/packages/antd/src/components/Select.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import { Select as AntSelect } from 'antd' +import { SelectProps as AntSelectProps } from 'antd/lib/select' +import styled from 'styled-components' + +type SelectOption = { + label: React.ReactText + value: any + [key: string]: any +} + +type SelectProps = AntSelectProps & { + dataSource?: SelectOption[] +} + +export const Select: React.FC = styled((props: SelectProps) => { + const { dataSource = [], ...others } = props + const children = dataSource.map((item, index) => { + const { label, value, ...others } = item + return ( + + {label} + + ) + }) + return ( + + {children} + + ) +})` + min-width: 100px; + width: 100%; +` diff --git a/packages/antd/src/components/button.tsx b/packages/antd/src/components/button.tsx deleted file mode 100644 index 966e4d6c90e..00000000000 --- a/packages/antd/src/components/button.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react' -import { FormConsumer } from '@uform/react' -import { Button } from 'antd' -import { ISubmitProps } from '../type' - -export const Submit = ({ showLoading, ...props }: ISubmitProps) => { - return ( - - {({ status }) => { - return ( - - ) - }} - - ) -} - -export const Reset: React.FC> = props => { - return ( - - {({ reset }) => { - return ( - - ) - }} - - ) -} diff --git a/packages/antd/src/components/formButtonGroup.tsx b/packages/antd/src/components/formButtonGroup.tsx deleted file mode 100644 index 2ddcc01dda7..00000000000 --- a/packages/antd/src/components/formButtonGroup.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import React, { Component } from 'react' -import ReactDOM from 'react-dom' -import { Row, Col } from './grid' -import { FormLayoutConsumer } from '../form' -import { IFormButtonGroupProps } from '../type' -import Sticky from 'react-stikky' -import cls from 'classnames' -import styled from 'styled-components' - -const getAlign = align => { - if (align === 'start' || align === 'end') { - return align - } - if (align === 'left' || align === 'top') { - return 'flex-start' - } - if (align === 'right' || align === 'bottom') { - return 'flex-end' - } - return align -} - -export interface IOffset { - top: number | string - right: number | string - bottom: number | string - left: number | string -} - -const isElementInViewport = ( - rect: ClientRect, - { - offset = 0, - threshold = 0 - }: { - offset?: IOffset | number - threshold?: number - } = {} -) => { - const { top, right, bottom, left, width, height } = rect - const intersection = { - t: bottom, - r: window.innerWidth - left, - b: window.innerHeight - top, - l: right - } - - const elementThreshold = { - x: threshold * width, - y: threshold * height - } - - return ( - intersection.t >= - ((offset as IOffset).top || (offset as number) + elementThreshold.y) && - intersection.r >= - ((offset as IOffset).right || (offset as number) + elementThreshold.x) && - intersection.b >= - ((offset as IOffset).bottom || (offset as number) + elementThreshold.y) && - intersection.l >= - ((offset as IOffset).left || (offset as number) + elementThreshold.x) - ) -} - -export const FormButtonGroup: React.FC = styled( - class FormButtonGroup extends Component { - public static defaultProps = { - span: 24 - } - - private formNode: HTMLElement - - public render() { - const { sticky, style, className } = this.props - - const content = ( - - {({ inline } = {}) => ( -
- {this.renderChildren()} -
- )} -
- ) - - if (sticky) { - return ( -
- - {({ FormRef } = {}) => { - if (!FormRef) { - return - } - return ( - -
- {content} -
-
- ) - }} -
-
- ) - } - - return content - } - - private renderChildren() { - const { children, itemStyle, offset, span } = this.props - return ( -
- - - -
- {children} -
- - -
-
- ) - } - - private getStickyBoundaryHandler(ref) { - return () => { - // eslint-disable-next-line react/no-find-dom-node - this.formNode = this.formNode || ReactDOM.findDOMNode(ref.current) - if (this.formNode) { - return isElementInViewport(this.formNode.getBoundingClientRect()) - } - return true - } - } - } -)` - ${props => - props.align ? `display:flex;justify-content: ${getAlign(props.align)}` : ''} - &.is-inline { - display: inline-block; - flex-grow: 3; - } - .button-group { - .inline { - display: inline-block; - .inline-view { - & > * { - margin-right: 10px; - margin-left: 0px; - display: inline-block; - } - & > *:last-child { - margin-right: 0 !important; - } - } - } - } -` diff --git a/packages/antd/src/components/grid.tsx b/packages/antd/src/components/grid.tsx deleted file mode 100644 index e9773c591a1..00000000000 --- a/packages/antd/src/components/grid.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import React, { Component, Children, cloneElement } from 'react' -import cx from 'classnames' -import { toArr } from '@uform/utils' -import { IColProps, IRowProps } from '../type' - -export class Row extends Component { - public static defaultProps = { - prefix: 'ant-', - pure: false, - fixed: false, - gutter: 0, - wrap: false, - component: 'div' - } - - public render() { - /* eslint-disable @typescript-eslint/no-unused-vars */ - const { - prefix, - pure, - wrap, - fixed, - gutter, - fixedWidth, - align, - justify, - hidden, - className, - component: Tag, - children, - ...others - } = this.props - /* eslint-enable @typescript-eslint/no-unused-vars */ - - let hiddenClassObj - if (hidden === true) { - hiddenClassObj = { [`${prefix}row-hidden`]: true } - } else if (typeof hidden === 'string') { - hiddenClassObj = { [`${prefix}row-${hidden}-hidden`]: !!hidden } - } else if (Array.isArray(hidden)) { - hiddenClassObj = hidden.reduce((ret, point) => { - ret[`${prefix}row-${point}-hidden`] = !!point - return ret - }, {}) - } - - const newClassName = cx({ - [`${prefix}row`]: true, - [`${prefix}row-wrap`]: wrap, - [`${prefix}row-fixed`]: fixed, - [`${prefix}row-fixed-${fixedWidth}`]: !!fixedWidth, - [`${prefix}row-justify-${justify}`]: !!justify, - [`${prefix}row-align-${align}`]: !!align, - ...hiddenClassObj, - [className]: !!className - }) - - let newChildren = toArr(children) - const gutterNumber = parseInt(gutter, 10) - - if (gutterNumber !== 0) { - const halfGutterString = `${gutterNumber / 2}px` - others.style = { - marginLeft: `-${halfGutterString}`, - marginRight: `-${halfGutterString}`, - ...(others.style || {}) - } - newChildren = Children.map(children, (child: React.ReactElement) => { - if ( - child && - child.type && - typeof child.type === 'function' && - (child.type as any).isNextCol - ) { - const newChild = cloneElement(child, { - style: { - paddingLeft: halfGutterString, - paddingRight: halfGutterString, - ...(child.props.style || {}) - } - }) - return newChild - } - - return child - }) - } - - return ( - - {newChildren} - - ) - } -} - -const breakPoints = ['xxs', 'xs', 's', 'm', 'l', 'xl'] - -export class Col extends Component { - public static isNextCol = true - - public static defaultProps = { - prefix: 'ant-', - pure: false, - component: 'div' - } - - public render() { - const { - prefix, - pure, - span, - offset, - fixedSpan, - fixedOffset, - hidden, - align, - xxs, - xs, - s, - m, - l, - xl, - component: Tag, - className, - children, - ...others - } = this.props - - const pointClassObj = breakPoints.reduce((ret, point) => { - let pointProps: { span?: string; offset?: string } = {} - if (typeof this.props[point] === 'object') { - pointProps = this.props[point] - } else { - pointProps.span = this.props[point] - } - - ret[`${prefix}col-${point}-${pointProps.span}`] = !!pointProps.span - ret[ - `${prefix}col-${point}-offset-${pointProps.offset}` - ] = !!pointProps.offset - return ret - }, {}) - - let hiddenClassObj - if (hidden === true) { - hiddenClassObj = { [`${prefix}col-hidden`]: true } - } else if (typeof hidden === 'string') { - hiddenClassObj = { [`${prefix}col-${hidden}-hidden`]: !!hidden } - } else if (Array.isArray(hidden)) { - hiddenClassObj = hidden.reduce((ret, point) => { - ret[`${prefix}col-${point}-hidden`] = !!point - return ret - }, {}) - } - - const classes = cx({ - [`${prefix}col`]: true, - [`${prefix}col-${span}`]: !!span, - [`${prefix}col-fixed-${fixedSpan}`]: !!fixedSpan, - [`${prefix}col-offset-${offset}`]: !!offset, - [`${prefix}col-offset-fixed-${fixedOffset}`]: !!fixedOffset, - [`${prefix}col-${align}`]: !!align, - ...pointClassObj, - ...hiddenClassObj, - [className]: className - }) - - return ( - - {children} - - ) - } -} diff --git a/packages/antd/src/components/index.ts b/packages/antd/src/components/index.ts new file mode 100644 index 00000000000..1015143d9c7 --- /dev/null +++ b/packages/antd/src/components/index.ts @@ -0,0 +1,8 @@ +export * from './Button' +export * from './FormButtonGroup' +export * from './FormLayout' +export * from './FormItemGrid' +export * from './FormCard' +export * from './FormBlock' +export * from './FormTextBox' +export * from './FormStep' diff --git a/packages/antd/src/components/layout.tsx b/packages/antd/src/components/layout.tsx deleted file mode 100644 index c8f65a40d4c..00000000000 --- a/packages/antd/src/components/layout.tsx +++ /dev/null @@ -1,343 +0,0 @@ -import React, { Component, useLayoutEffect, useRef } from 'react' -import { createVirtualBox, createControllerBox } from '@uform/react' -import { toArr } from '@uform/utils' -import { IFormItemGridProps, IFormItemProps } from '@uform/types' -import { Card, Row, Col } from 'antd' -import styled from 'styled-components' -import cls from 'classnames' - -import { FormLayoutConsumer, FormItem, FormLayoutProvider } from '../form' -import { - IFormTextBox, - IFormCardProps, - IFormBlockProps, - IFormLayoutProps, - TFormCardOrFormBlockProps, - IFormItemGridProps as IFormItemGridPropsAlias -} from '../type' - -const normalizeCol = ( - col: { span: number; offset?: number } | number, - defaultValue: { span: number } = { span: 0 } -): { span: number; offset?: number } => { - if (!col) { - return defaultValue - } else { - return typeof col === 'object' ? col : { span: col } - } -} - -export const FormLayoutItem: React.FC = function(props) { - return React.createElement( - FormLayoutConsumer, - {}, - ({ - labelAlign, - labelTextAlign, - labelCol, - wrapperCol, - size, - autoAddColon - }) => { - return React.createElement( - FormItem, - { - labelAlign, - labelTextAlign, - labelCol, - wrapperCol, - autoAddColon, - size, - ...props - }, - props.children - ) - } - ) -} - -export const FormLayout = createVirtualBox( - 'layout', - ({ children, ...props }) => { - return ( - - {value => { - const newValue = { ...value, ...props } - const child = - newValue.inline || newValue.className || newValue.style ? ( -
- {children} -
- ) : ( - children - ) - return ( - {child} - ) - }} -
- ) - } -) - -export const FormItemGrid = createVirtualBox( - 'grid', - class extends Component { - public render() { - const { title } = this.props - if (title) { - return this.renderFormItem(this.renderGrid()) - } else { - return this.renderGrid() - } - } - - private renderFormItem(children) { - const { title, help, name, extra, ...props } = this.props - return React.createElement( - FormLayoutItem, - { - label: title, - noMinHeight: true, - id: name, - extra, - help, - ...props - } as IFormItemGridProps, - children - ) - } - - private renderGrid() { - const { - children: rawChildren, - cols: rawCols, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - title, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - description, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - help, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - extra, - ...props - } = this.props - - const children = toArr(rawChildren) - const childNum = children.length - const cols = toArr(rawCols).map(col => normalizeCol(col)) - - if (cols.length < childNum) { - const offset: number = childNum - cols.length - const lastSpan: number = - 24 - - cols.reduce((buf, col) => { - return ( - buf + - Number(col.span ? col.span : 0) + - Number(col.offset ? col.offset : 0) - ) - }, 0) - - for (let i = 0; i < offset; i++) { - cols.push({ span: Math.floor(lastSpan / offset) }) - } - } - - // cols = toArr(cols).map(col => normalizeCol(col)) - - return ( - - {children.reduce((buf, child, key) => { - return child - ? buf.concat( - - {child} - - ) - : buf - }, [])} - - ) - } - } -) - -export const FormCard = createVirtualBox( - 'card', - styled( - class extends Component { - public static defaultProps = { - // bodyHeight: 'auto' - } - public render() { - const { children, className, ...props } = this.props - return ( - - {children} - - ) - } - } - )` - margin-bottom: 30px; - .ant-card-body { - padding-top: 30px; - padding-bottom: 0 !important; - } - &.ant-card { - display: block; - margin-bottom: 30px; - } - ` -) - -export const FormBlock = createVirtualBox( - 'block', - styled( - class extends Component { - public static defaultProps = { - // bodyHeight: 'auto' - } - - public render() { - const { children, className, ...props } = this.props - return ( - - {children} - - ) - } - } - )` - margin-bottom: 0px; - .ant-card-body { - padding-top: 20px; - padding-bottom: 0 !important; - } - &.ant-card { - border: none; - padding: 0 15px; - padding-bottom: 15px; - display: block; - box-shadow: none; - } - .ant-card-head { - padding: 0 !important; - min-height: 24px; - font-weight: normal; - } - .ant-card-head-title { - padding: 0; - } - ` -) - -export const FormTextBox = createControllerBox( - 'text-box', - styled(({ children, schema, className }) => { - const { title, help, text, name, extra, ...props } = schema['x-props'] - const ref: React.RefObject = useRef() - const arrChildren = toArr(children) - const split = String(text).split('%s') - useLayoutEffect(() => { - if (ref.current) { - const elements = ref.current.querySelectorAll('.text-box-field') - const syncLayouts = Array.prototype.map.call( - elements, - (el: HTMLElement) => { - return [ - el, - () => { - const ctrl = el.querySelector( - '.ant-form-item-control:first-child' - ) - if (ctrl) { - el.style.width = ctrl.getBoundingClientRect().width + 'px' - } - } - ] - } - ) - syncLayouts.forEach(([el, handler]) => { - el.addEventListener('DOMSubtreeModified', handler) - }) - - return () => { - syncLayouts.forEach(([el, handler]) => { - el.removeEventListener('DOMSubtreeModified', handler) - }) - } - } - }, []) - - let index = 0 - const newChildren = split.reduce((buf, item, key) => { - return buf.concat( - item ? ( - - {item} - - ) : null, - arrChildren[key] ? ( -
- {arrChildren[key]} -
- ) : null - ) - }, []) - - if (!title) { - return ( -
- {newChildren} -
- ) - } - - return React.createElement( - FormLayoutItem, - { - label: title, - noMinHeight: true, - id: name, - extra, - help, - ...props - }, -
- {newChildren} -
- ) - })` - display: flex; - .text-box-words { - font-size: 14px; - line-height: 34px; - color: #333; - ${props => { - const { editable, schema } = props - const { gutter } = schema['x-props'] - if (!editable) { - return { - margin: 0 - } - } - return { - margin: `0 ${gutter === 0 || gutter ? gutter : 10}px` - } - }} - } - .text-box-words:nth-child(1) { - margin-left: 0; - } - .text-box-field { - display: inline-block; - } - ` -) diff --git a/packages/antd/src/fields/array.tsx b/packages/antd/src/fields/array.tsx deleted file mode 100644 index 52feca6fd41..00000000000 --- a/packages/antd/src/fields/array.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import React from 'react' -import { registerFormField, createArrayField } from '@uform/react' -import { Icon } from 'antd' -import styled, { css } from 'styled-components' - -export const CircleButton = styled['div'].attrs({ className: 'cricle-btn' })` - ${props => (!props.hasText ? 'width:30px; height:30px;' : '')} - margin-right:10px; - border-radius: ${props => (!props.hasText ? '100px' : 'none')}; - border: ${props => (!props.hasText ? '1px solid #eee' : 'none')}; - margin-bottom: 20px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - ${props => - !props.hasText - ? `&:hover{ - background:#f7f4f4; - }` - : ''} - .op-name { - margin-left: 3px; - } -` - -export const TextButton = styled['div'].attrs({ - className: 'ant-btn-text' -})` - display: inline-block; - height: 20px; - line-height: 20px; - cursor: pointer; - .op-name { - margin-left: 4px; - } - ${props => - props.inline && - css` - display: inline-block; - width: auto; - `} -` - -export const ArrayField = createArrayField({ - CircleButton, - TextButton, - AddIcon: () => , - RemoveIcon: () => , - MoveDownIcon: () => , - MoveUpIcon: () => -}) - -registerFormField( - 'array', - styled( - class extends ArrayField { - public render() { - const { className, name, value, renderField } = this.props - const cls = this.getProps('className') - const style = this.getProps('style') - return ( -
- {value.map((item, index) => { - return ( -
-
- {index + 1} -
-
{renderField(index)}
-
- {this.renderRemove(index, item)} - {this.renderMoveDown(index, item)} - {this.renderMoveUp(index)} - {this.renderExtraOperations(index)} -
-
- ) - })} - {this.renderEmpty()} - {value.length > 0 && this.renderAddition()} -
- ) - } - } - )` - border: 1px solid #eee; - min-width: 400px; - .array-item { - padding: 20px; - padding-bottom: 0; - padding-top: 30px; - border-bottom: 1px solid #eee; - position: relative; - &:nth-child(even) { - background: #fafafa; - } - .array-index { - position: absolute; - top: 0; - left: 0; - display: block; - span { - position: absolute; - color: rgb(255, 255, 255); - z-index: 1; - font-size: 12px; - top: 3px; - left: 3px; - line-height: initial; - } - &::after { - content: ''; - display: block; - border-top: 20px solid transparent; - border-left: 20px solid transparent; - border-bottom: 20px solid transparent; - border-right: 20px solid #888; - transform: rotate(45deg); - position: absolute; - z-index: 0; - top: -20px; - left: -20px; - } - } - .array-item-operator { - display: flex; - border-top: 1px solid #eee; - padding-top: 20px; - } - } - .array-empty-wrapper { - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - &.disabled { - cursor: default; - } - .array-empty { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - margin: 20px; - img { - display: block; - height: 80px; - } - .ant-btn-text { - color: #999; - i { - margin-right: 3px; - } - } - } - } - .array-item-wrapper { - margin: 0 -20px; - } - .array-item-addition { - padding: 10px 20px; - line-height: normal !important; - background: #fbfbfb; - .ant-btn-text { - color: #888; - i { - margin-right: 3px; - } - } - } - ` -) diff --git a/packages/antd/src/fields/boolean.tsx b/packages/antd/src/fields/boolean.tsx index eb28ca25b26..cfea566fe57 100644 --- a/packages/antd/src/fields/boolean.tsx +++ b/packages/antd/src/fields/boolean.tsx @@ -1,6 +1,6 @@ import { Switch } from 'antd' -import { connect, registerFormField } from '@uform/react' -import { acceptEnum, mapStyledProps } from '../utils' +import { connect, registerFormField } from '@uform/react-schema-renderer' +import { acceptEnum, mapStyledProps } from '../shared' registerFormField( 'boolean', diff --git a/packages/antd/src/fields/cards.tsx b/packages/antd/src/fields/cards.tsx index a48422b0f22..c1a29afb2c0 100644 --- a/packages/antd/src/fields/cards.tsx +++ b/packages/antd/src/fields/cards.tsx @@ -1,132 +1,132 @@ -import React, { Fragment, ReactElement } from 'react' +import React, { Fragment } from 'react' +import { + registerFormField, + ISchemaFieldComponentProps, + SchemaField +} from '@uform/react-schema-renderer' +import { toArr, isFn, FormPath } from '@uform/shared' +import { ArrayList } from '@uform/react-shared-components' +import { CircleButton, TextButton } from '../components/Button' +import { Card, Icon } from 'antd' import styled from 'styled-components' -import { registerFormField } from '@uform/react' -import { Card } from 'antd' -import { toArr } from '@uform/utils' -import { ArrayField } from './array' -const FormCardsField = styled( - class extends ArrayField { - public renderOperations(item, index) { - return ( - - {this.renderRemove(index, item)} - {this.renderMoveDown(index, item)} - {this.renderMoveUp(index)} - {this.renderExtraOperations(index)} - - ) - } +const ArrayComponents = { + CircleButton, + TextButton, + AdditionIcon: () => , + RemoveIcon: () => , + MoveDownIcon: () => , + MoveUpIcon: () => +} - public renderCardEmpty = (title: string | ReactElement) => { - return ( - - {this.renderEmpty()} - - ) +const FormCardsField = styled( + (props: ISchemaFieldComponentProps & { className: string }) => { + const { value, schema, className, editable, path, mutators } = props + const { + renderAddition, + renderRemove, + renderMoveDown, + renderMoveUp, + renderEmpty, + renderExtraOperations, + ...componentProps + } = schema.getExtendsComponentProps() || {} + const onAdd = () => { + const items = Array.isArray(schema.items) + ? schema.items[schema.items.length - 1] + : schema.items + mutators.push(items.getEmptyValue()) } - - public render() { - const { value, className, schema, renderField } = this.props - const { - title, - style, - className: cls, - renderAddition, - renderRemove, - renderEmpty, - renderMoveDown, - renderMoveUp, - renderOperations, - ...others - } = this.getProps() || ({} as any) - - return ( -
+ {toArr(value).map((item, index) => { return ( - {index + 1}. {title || schema.title} + {index + 1}. {componentProps.title || schema.title} } - className="card-list" - key={index} - extra={this.renderOperations(item, index)} + extra={ + + mutators.remove(index)} + /> + mutators.moveDown(index)} + /> + mutators.moveUp(index)} + /> + {isFn(renderExtraOperations) + ? renderExtraOperations(index) + : renderExtraOperations} + + } > - {renderField(index)} + ) })} - {value.length === 0 && this.renderCardEmpty(title)} -
- {value.length > 0 && this.renderAddition()} -
-
- ) - } - } -)` - .ant-card-body { - padding-top: 30px; - padding-bottom: 0 !important; - } - .ant-card-head-main { - display: flex; - justify-content: space-between; - align-items: center; + + {({ children }) => { + return ( + +
{children}
+
+ ) + }} +
+ + {({ children, isEmpty }) => { + if (!isEmpty) { + return ( +
+ {children} +
+ ) + } + }} +
+ + + ) } +)` .ant-card { - display: block; - margin-bottom: 0px; - background: #fff; - .array-empty-wrapper { - display: flex; - justify-content: center; - cursor: pointer; - margin-bottom: 0px; - &.disabled { - cursor: default; - } - .array-empty { - display: flex; - flex-direction: column; - margin-bottom: 20px; - align-items: center; - img { - margin-bottom: 16px; - height: 85px; - } - .next-btn-text { - color: #888; - } - .next-icon:before { - width: 16px !important; - font-size: 16px !important; - margin-right: 5px; - } - } - } - - .next-card { + .ant-card { box-shadow: none; } - .card-list { - box-shadow: none; - border: 1px solid #eee; + .ant-card-body { + padding: 20px 10px 0 10px; } - - .array-item-addition { + .array-cards-addition { box-shadow: none; border: 1px solid #eee; transition: all 0.35s ease-in-out; @@ -134,40 +134,47 @@ const FormCardsField = styled( border: 1px solid #ccc; } } + .empty-wrapper { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 10px; + img { + height: 85px; + } + .ant-btn { + color: #888; + } + } } - .ant-card.card-list { - margin-top: 20px; + .card-list-empty.card-list-item { + cursor: pointer; } - .addition-wrapper .array-item-addition { - margin-top: 20px; + .array-cards-addition { + margin-top: 10px; margin-bottom: 3px; - } - .cricle-btn { - margin-bottom: 0; - } - .ant-card-extra { - display: flex; - } - .array-item-addition { background: #fff; display: flex; cursor: pointer; - padding: 10px 0; + padding: 5px 0; justify-content: center; box-shadow: 1px 1px 4px 0 rgba(0, 0, 0, 0.1); - .next-btn-text { - color: #888; - } - .next-icon:before { - width: 16px !important; - font-size: 16px !important; - margin-right: 5px; - } } - .card-list:first-child { + .card-list-item { + margin-top: 10px; + border: 1px solid #eee; + } + .card-list-item:first-child { margin-top: 0 !important; } + .ant-card-extra { + display: flex; + button { + margin-right: 8px; + } + } ` registerFormField('cards', FormCardsField) +registerFormField('array', FormCardsField) diff --git a/packages/antd/src/fields/checkbox.tsx b/packages/antd/src/fields/checkbox.tsx index de04757933e..e24d5874411 100644 --- a/packages/antd/src/fields/checkbox.tsx +++ b/packages/antd/src/fields/checkbox.tsx @@ -1,10 +1,10 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Checkbox } from 'antd' import { transformDataSourceKey, mapStyledProps, mapTextComponent -} from '../utils' +} from '../shared' const { Group: CheckboxGroup } = Checkbox diff --git a/packages/antd/src/fields/date.tsx b/packages/antd/src/fields/date.tsx index 5728c4154ab..d258487c464 100644 --- a/packages/antd/src/fields/date.tsx +++ b/packages/antd/src/fields/date.tsx @@ -1,52 +1,44 @@ import React from 'react' -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import moment from 'moment' -import { DatePicker as AntDatePicker } from 'antd' +import { DatePicker } from 'antd' import { mapStyledProps, mapTextComponent, - StateLoading, compose, isStr, isArr -} from '../utils' +} from '../shared' -const { - RangePicker: AntRangePicker, - WeekPicker: AntWeekPicker, - MonthPicker: AntMonthPicker -} = AntDatePicker +const { RangePicker, WeekPicker, MonthPicker } = DatePicker -class AntYearPicker extends React.Component { +class YearPicker extends React.Component { public render() { - return + return } } -const DatePicker = StateLoading(AntDatePicker) -const RangePicker = StateLoading(AntRangePicker) -const MonthPicker = StateLoading(AntMonthPicker) -const WeekPicker = StateLoading(AntWeekPicker) -const YearPicker = StateLoading(AntYearPicker) - const transformMoment = (value, format = 'YYYY-MM-DD HH:mm:ss') => { return value && value.format ? value.format(format) : value } -const mapMomentValue = (props, fieldProps) => { - const { value, showTime = false } = props +const mapMomentValue = props => { + const { value, showTime = false, disabled = false } = props try { - if (!fieldProps.editable) return props - if (isStr(value)) { - props.value = value - ? moment(value, showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD') - : null - } else if (isArr(value) && value.length) { - props.value = value.map(item => - item - ? moment(item, showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD') - : null - ) + if (!disabled) { + if (isStr(value) && value) { + props.value = moment( + value, + showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD' + ) + } else if (isArr(value) && value.length) { + props.value = value.map( + item => + (item && + moment(item, showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD')) || + '' + ) + } } } catch (e) { throw new Error(e) diff --git a/packages/antd/src/fields/index.ts b/packages/antd/src/fields/index.ts new file mode 100644 index 00000000000..59c23266c82 --- /dev/null +++ b/packages/antd/src/fields/index.ts @@ -0,0 +1,15 @@ +import './string' +import './number' +import './boolean' +import './date' +import './time' +import './range' +import './upload' +import './checkbox' +import './radio' +import './rating' +import './transfer' +import './cards' +import './table' +import './textarea' +import './password' \ No newline at end of file diff --git a/packages/antd/src/fields/number.tsx b/packages/antd/src/fields/number.tsx index 2a36d250716..e85d868452a 100644 --- a/packages/antd/src/fields/number.tsx +++ b/packages/antd/src/fields/number.tsx @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { InputNumber } from 'antd' -import { acceptEnum, mapStyledProps, mapTextComponent } from '../utils' +import { acceptEnum, mapStyledProps, mapTextComponent } from '../shared' registerFormField( 'number', diff --git a/packages/antd/src/fields/password.tsx b/packages/antd/src/fields/password.tsx index 926ae0e1ffe..02e057f15da 100644 --- a/packages/antd/src/fields/password.tsx +++ b/packages/antd/src/fields/password.tsx @@ -1,179 +1,53 @@ import React, { useState } from 'react' -import styled from 'styled-components' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Input } from 'antd' -import { InputProps } from 'antd/es/input' -import { connect, registerFormField } from '@uform/react' -import { mapStyledProps } from '../utils' +import { PasswordProps } from 'antd/lib/input' +import { PasswordStrength } from '@uform/react-shared-components' +import styled from 'styled-components' +import { mapStyledProps } from '../shared' -var isNum = function(c) { - return c >= 48 && c <= 57 -} -var isLower = function(c) { - return c >= 97 && c <= 122 -} -var isUpper = function(c) { - return c >= 65 && c <= 90 -} -var isSymbol = function(c) { - return !(isLower(c) || isUpper(c) || isNum(c)) -} -var isLetter = function(c) { - return isLower(c) || isUpper(c) +export interface IPasswordProps extends PasswordProps { + checkStrength: boolean } -const getStrength = val => { - if (!val) { - return 0 - } - let num = 0 - let lower = 0 - let upper = 0 - let symbol = 0 - let MNS = 0 - let rep = 0 - let repC = 0 - let consecutive = 0 - let sequential = 0 - const len = () => num + lower + upper + symbol - const require = () => { - var re = num > 0 ? 1 : 0 - re += lower > 0 ? 1 : 0 - re += upper > 0 ? 1 : 0 - re += symbol > 0 ? 1 : 0 - if (re > 2 && len() >= 8) { - return re + 1 - } else { - return 0 - } - } - for (var i = 0; i < val.length; i++) { - var c = val.charCodeAt(i) - if (isNum(c)) { - num++ - if (i !== 0 && i !== val.length - 1) { - MNS++ - } - if (i > 0 && isNum(val.charCodeAt(i - 1))) { - consecutive++ - } - } else if (isLower(c)) { - lower++ - if (i > 0 && isLower(val.charCodeAt(i - 1))) { - consecutive++ - } - } else if (isUpper(c)) { - upper++ - if (i > 0 && isUpper(val.charCodeAt(i - 1))) { - consecutive++ - } - } else { - symbol++ - if (i !== 0 && i !== val.length - 1) { - MNS++ - } - } - var exists = false - for (var j = 0; j < val.length; j++) { - if (val[i] === val[j] && i !== j) { - exists = true - repC += Math.abs(val.length / (j - i)) - } - } - if (exists) { - rep++ - var unique = val.length - rep - repC = unique ? Math.ceil(repC / unique) : Math.ceil(repC) - } - if (i > 1) { - var last1 = val.charCodeAt(i - 1) - var last2 = val.charCodeAt(i - 2) - if (isLetter(c)) { - if (isLetter(last1) && isLetter(last2)) { - var v = val.toLowerCase() - var vi = v.charCodeAt(i) - var vi1 = v.charCodeAt(i - 1) - var vi2 = v.charCodeAt(i - 2) - if (vi - vi1 === vi1 - vi2 && Math.abs(vi - vi1) === 1) { - sequential++ - } - } - } else if (isNum(c)) { - if (isNum(last1) && isNum(last2)) { - if (c - last1 === last1 - last2 && Math.abs(c - last1) === 1) { - sequential++ - } - } - } else { - if (isSymbol(last1) && isSymbol(last2)) { - if (c - last1 === last1 - last2 && Math.abs(c - last1) === 1) { - sequential++ +const Password: React.FC = styled((props: IPasswordProps) => { + const { className, checkStrength, onChange, ...others } = props + const [value, setValue] = useState(props.value || props.defaultValue) + return ( +
+ { + setValue(event.target.value) + if (onChange) { + onChange(event) } - } - } - } - } - let sum = 0 - let length = len() - sum += 4 * length - if (lower > 0) { - sum += 2 * (length - lower) - } - if (upper > 0) { - sum += 2 * (length - upper) - } - if (num !== length) { - sum += 4 * num - } - sum += 6 * symbol - sum += 2 * MNS - sum += 2 * require() - if (length === lower + upper) { - sum -= length - } - if (length === num) { - sum -= num - } - sum -= repC - sum -= 2 * consecutive - sum -= 3 * sequential - sum = sum < 0 ? 0 : sum - sum = sum > 100 ? 100 : sum - - if (sum >= 80) { - return 100 - } else if (sum >= 60) { - return 80 - } else if (sum >= 40) { - return 60 - } else if (sum >= 20) { - return 40 - } else { - return 20 - } -} - -interface IStrengthProps { - strength: number - className?: string -} - -// 校验强度 UI -const StrengthFC = styled(({ strength, className }: IStrengthProps) => ( -
-
-
-
-
-
-
+ {checkStrength && ( + + {score => { + return ( +
+
+
+
+
+
+
+ ) + }} + + )}
-
-))` + ) +})` .password-strength-wrapper { background: #e0e0e0; margin-bottom: 3px; @@ -210,42 +84,9 @@ const StrengthFC = styled(({ strength, className }: IStrengthProps) => ( } ` -export interface IPasswordProps extends Omit { - checkStrength: boolean // 是否启用密码强度校验 - onChange: (value: InputProps['value']) => void -} - -const PasswordFC = (props: Partial) => { - const [strength, setStrength] = useState(0) - - const { checkStrength, ...others } = props - - const onChangeHandler = (e: React.ChangeEvent) => { - const value = e.target.value - - // 开启才计算 - checkStrength && setStrength(getStrength(value)) - - // 回调 - props.onChange && props.onChange(value) - } - - return ( - - - - {checkStrength && } - - ) -} - registerFormField( 'password', connect({ getProps: mapStyledProps - })(PasswordFC) + })(Password) ) diff --git a/packages/antd/src/fields/radio.tsx b/packages/antd/src/fields/radio.tsx index f21dc2c8dee..3c510b49489 100644 --- a/packages/antd/src/fields/radio.tsx +++ b/packages/antd/src/fields/radio.tsx @@ -1,10 +1,10 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Radio } from 'antd' import { transformDataSourceKey, mapStyledProps, mapTextComponent -} from '../utils' +} from '../shared' const { Group: RadioGroup } = Radio diff --git a/packages/antd/src/fields/range.tsx b/packages/antd/src/fields/range.tsx index edab597790a..03c5fa1f730 100644 --- a/packages/antd/src/fields/range.tsx +++ b/packages/antd/src/fields/range.tsx @@ -1,7 +1,7 @@ import React from 'react' import { Slider } from 'antd' -import { connect, registerFormField } from '@uform/react' -import { mapStyledProps } from '../utils' +import { connect, registerFormField } from '@uform/react-schema-renderer' +import { mapStyledProps } from '../shared' export interface ISliderMarks { [key: number]: diff --git a/packages/antd/src/fields/rating.tsx b/packages/antd/src/fields/rating.tsx index 5b3c8cac7df..044cd5fbc9e 100644 --- a/packages/antd/src/fields/rating.tsx +++ b/packages/antd/src/fields/rating.tsx @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Rate } from 'antd' -import { mapStyledProps } from '../utils' +import { mapStyledProps } from '../shared' registerFormField( 'rating', diff --git a/packages/antd/src/fields/string.tsx b/packages/antd/src/fields/string.tsx index 6e6ba694e33..ffd041c0d9d 100644 --- a/packages/antd/src/fields/string.tsx +++ b/packages/antd/src/fields/string.tsx @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Input } from 'antd' -import { acceptEnum, mapStyledProps, mapTextComponent } from '../utils' +import { acceptEnum, mapStyledProps, mapTextComponent } from '../shared' registerFormField( 'string', diff --git a/packages/antd/src/fields/table.tsx b/packages/antd/src/fields/table.tsx index 0f5e7950ed2..19a0c896190 100644 --- a/packages/antd/src/fields/table.tsx +++ b/packages/antd/src/fields/table.tsx @@ -1,355 +1,165 @@ -import React, { Component } from 'react' +import React from 'react' +import { + registerFormField, + ISchemaFieldComponentProps, + SchemaField, + Schema +} from '@uform/react-schema-renderer' +import { toArr, isFn, isArr, FormPath } from '@uform/shared' +import { ArrayList } from '@uform/react-shared-components' +import { CircleButton, TextButton } from '../components/Button' +import { Table, Form, Icon } from 'antd' +import { FormItemProps } from '../compat/FormItem' import styled from 'styled-components' -import { registerFormField } from '@uform/react' -import { isFn, toArr } from '@uform/utils' -import { ArrayField } from './array' -export interface IColumnProps { - title?: string - dataIndex?: string - width?: string | number - cell: (item?: any, index?: number) => React.ReactElement +const ArrayComponents = { + CircleButton, + TextButton, + AdditionIcon: () => , + RemoveIcon: () => , + MoveDownIcon: () => , + MoveUpIcon: () => } -class Column extends Component { - public static displayName = '@schema-table-column' - public render() { - return this.props.children - } -} - -registerFormField( - 'table', - styled( - class extends ArrayField { - public createFilter(key, payload) { - const { schema } = this.props - const columnFilter: (key: string, payload: any) => boolean = - schema['x-props'] && schema['x-props'].columnFilter - - return (render, otherwise) => { - if (isFn(columnFilter)) { - return columnFilter(key, payload) - ? isFn(render) - ? render() - : render - : isFn(otherwise) - ? otherwise() - : otherwise - } else { - return render() - } - } - } - - public render() { - const { - value, - schema, - locale, - className, - renderField, - getOrderProperties - } = this.props - const cls = this.getProps('className') - const style = this.getProps('style') - const operationsWidth = this.getProps('operationsWidth') - return ( -
-
- - {getOrderProperties(schema.items).reduce( - (buf, { key, schema }) => { - const filter = this.createFilter(key, schema) - const res = filter( - () => { - return buf.concat( - { - return renderField([index, key]) - }} - /> - ) - }, - () => { - return buf - } - ) - return res - }, - [] - )} - - { - return ( -
- {this.renderRemove(index, item)} - {this.renderMoveDown(index, item)} - {this.renderMoveUp(index)} - {this.renderExtraOperations(index)} -
- ) - }} - /> -
- {this.renderAddition()} -
-
- ) - } +const FormTableField = styled( + (props: ISchemaFieldComponentProps & { className: string }) => { + const { value, schema, className, editable, path, mutators } = props + const { + renderAddition, + renderRemove, + renderMoveDown, + renderMoveUp, + renderEmpty, + renderExtraOperations, + operations, + ...componentProps + } = schema.getExtendsComponentProps() || {} + const onAdd = () => { + const items = Array.isArray(schema.items) + ? schema.items[schema.items.length - 1] + : schema.items + mutators.push(items.getEmptyValue()) } - )` - display: inline-block; - .array-item-addition { - line-height: normal !important; - padding: 10px; - background: #fbfbfb; - border-left: 1px solid #dcdee3; - border-right: 1px solid #dcdee3; - border-bottom: 1px solid #dcdee3; - .ant-btn-text { - color: #888; - i { - margin-right: 3px; + const renderColumns = (items: Schema) => { + return items.mapProperties((props, key) => { + const itemProps = { + ...props.getExtendsItemProps(), + ...props.getExtendsProps() } - } - } - .ant-table-cell-wrapper > .ant-form-item { - margin-bottom: 0; - } - .array-item-operator { - display: flex; - } - ` -) - -export interface ITableProps { - className?: string - dataSource: any -} - -/** - * 轻量级Table - */ -const Table = styled( - class Table extends Component { - public renderCell({ record, col, rowIndex }) { - return ( -
- {isFn(col.cell) - ? col.cell( - record ? record[col.dataIndex] : undefined, - rowIndex, - record - ) - : record - ? record[col.dataIndex] - : undefined} -
- ) - } - - public renderTable(columns, dataSource) { - return ( -
- - - - {columns.map((col, index) => { - return ( - - ) - })} - - - - {dataSource.map((record, rowIndex) => { - return ( - - {columns.map((col, colIndex) => { - return ( - - ) - })} - - ) - })} - {this.renderPlacehodler(dataSource, columns)} - -
-
- {col.title} -
-
- {this.renderCell({ - record, - col, - rowIndex - })} -
-
- ) + return { + title: props.title, + ...itemProps, + key, + dataIndex: key, + render: (value: any, record: any, index: number) => { + return ( + + + + ) + } + } + }) } - - public renderPlacehodler(dataSource, columns) { - if (dataSource.length === 0) { - return ( - - -
- { + return buf.concat(renderColumns(items)) + }, []) + : renderColumns(schema.items) + if (editable) { + columns.push({ + ...operations, + key: 'operations', + dataIndex: 'operations', + render: (value: any, record: any, index: number) => { + return ( + +
+ mutators.remove(index)} + /> + mutators.moveDown(index)} + /> + mutators.moveUp(index)} /> + {isFn(renderExtraOperations) + ? renderExtraOperations(index) + : renderExtraOperations}
- - - ) - } - } - - public getColumns(children) { - const columns: IColumnProps[] = [] - React.Children.forEach>( - children, - child => { - if (React.isValidElement(child)) { - if ( - child.type === Column || - child.type.displayName === '@schema-table-column' - ) { - columns.push(child.props) - } - } +
+ ) } - ) - - return columns - } - - public render() { - const columns = this.getColumns(this.props.children) - const dataSource = toArr(this.props.dataSource) - return ( -
-
-
- {this.renderTable(columns, dataSource)} -
-
-
- ) + }) } + return ( +
+ +
+ + {({ children }) => { + return ( +
+ {children} +
+ ) + }} +
+
+
+ ) } )` - .ant-table { - position: relative; - } - - .ant-table, - .ant-table *, - .ant-table :after, - .ant-table :before { - -webkit-box-sizing: border-box; - box-sizing: border-box; - } - - .ant-table table { - border-collapse: collapse; - border-spacing: 0; - width: 100%; - background: #fff; - display: table !important; - margin: 0 !important; - } - - .ant-table table tr:first-child td { - border-top-width: 0; - } - - .ant-table th { - padding: 0; - background: #ebecf0; - color: #333; - text-align: left; - font-weight: 400; - min-width: 200px; - border: 1px solid #dcdee3; + min-width: 600px; + margin-bottom: 10px; + table { + margin-bottom: 0 !important; + } + .array-table-addition { + background: #fbfbfb; + cursor: pointer; + margin-top: 3px; + border-radius: 3px; + .next-btn-text { + color: #888; + } + .next-icon:before { + width: 16px !important; + font-size: 16px !important; + margin-right: 5px; + } } - - .ant-table th .ant-table-cell-wrapper { - padding: 12px 16px; - overflow: hidden; - text-overflow: ellipsis; - word-break: break-all; + .ant-btn { + color: #888; } - - .ant-table td { - padding: 0; - border: 1px solid #dcdee3; - } - - .ant-table td .ant-table-cell-wrapper { - padding: 12px 16px; - overflow: hidden; - text-overflow: ellipsis; - word-break: break-all; + .array-item-operator { display: flex; - } - - .ant-table.zebra tr:nth-child(odd) td { - background: #fff; - } - - .ant-table.zebra tr:nth-child(2n) td { - background: #f7f8fa; - } - - .ant-table-empty { - color: #a0a2ad; - padding: 32px 0; - text-align: center; - } - - .ant-table-row { - -webkit-transition: all 0.3s ease; - transition: all 0.3s ease; - background: #fff; - color: #333; - border: none !important; - } - - .ant-table-row.hidden { - display: none; - } - - .ant-table-row.hovered, - .ant-table-row.selected { - background: #f2f3f7; - color: #333; - } - - .ant-table-body, - .ant-table-header { - overflow: auto; + button { + margin-right: 8px; + } } ` + +registerFormField('table', FormTableField) diff --git a/packages/antd/src/fields/textarea.tsx b/packages/antd/src/fields/textarea.tsx index b8b8d1cbe8a..97eb7bbc996 100644 --- a/packages/antd/src/fields/textarea.tsx +++ b/packages/antd/src/fields/textarea.tsx @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Input } from 'antd' -import { acceptEnum, mapStyledProps, mapTextComponent } from '../utils' +import { acceptEnum, mapStyledProps, mapTextComponent } from '../shared' const { TextArea } = Input diff --git a/packages/antd/src/fields/time.tsx b/packages/antd/src/fields/time.tsx index ca235c2fbed..fa314bf049f 100644 --- a/packages/antd/src/fields/time.tsx +++ b/packages/antd/src/fields/time.tsx @@ -1,15 +1,15 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import moment from 'moment' import { TimePicker } from 'antd' -import { mapStyledProps, mapTextComponent } from '../utils' +import { mapStyledProps, mapTextComponent } from '../shared' registerFormField( 'time', connect({ getValueFromEvent(_, value) { - return value ? value : null + return value }, - getProps: (props, fieldProps) => { + getProps: (props: any, fieldProps) => { const { value, disabled = false } = props try { if (!disabled && value) { diff --git a/packages/antd/src/fields/transfer.tsx b/packages/antd/src/fields/transfer.tsx index 2c30691b498..b7761ccb845 100644 --- a/packages/antd/src/fields/transfer.tsx +++ b/packages/antd/src/fields/transfer.tsx @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Transfer } from 'antd' -import { mapStyledProps } from '../utils' +import { mapStyledProps } from '../shared' registerFormField( 'transfer', diff --git a/packages/antd/src/fields/upload.tsx b/packages/antd/src/fields/upload.tsx index d3ec524d0b7..896e59ed4e8 100644 --- a/packages/antd/src/fields/upload.tsx +++ b/packages/antd/src/fields/upload.tsx @@ -1,8 +1,8 @@ import React from 'react' -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Button, Upload, Icon } from 'antd' import styled from 'styled-components' -import { toArr, isArr, isEqual, mapStyledProps } from '../utils' +import { toArr, isArr, isEqual, mapStyledProps } from '../shared' const { Dragger: UploadDragger } = Upload diff --git a/packages/antd/src/form.tsx b/packages/antd/src/form.tsx deleted file mode 100644 index 13857383c1f..00000000000 --- a/packages/antd/src/form.tsx +++ /dev/null @@ -1,481 +0,0 @@ -import React from 'react' -import classNames from 'classnames' -import { Row, Col, Popover, Icon } from 'antd' -import styled from 'styled-components' -import { registerFormWrapper, registerFieldMiddleware } from '@uform/react' -import { IFormItemProps, IFormProps } from '@uform/types' - -import LOCALE from './locale' -import { isFn, moveTo, isStr, stringLength } from './utils' - -/** - * 轻量级 Form,不包含任何数据管理能力 - */ - -export const { - Provider: FormLayoutProvider, - Consumer: FormLayoutConsumer -} = React.createContext(undefined) - -const normalizeCol = col => { - return typeof col === 'object' ? col : { span: col } -} - -const getParentNode = (node, selector) => { - if (!node || (node && !node.matches)) { - return - } - if (node.matches(selector)) { - return node - } else { - return getParentNode(node.parentNode || node.parentElement, selector) - } -} - -const isPopDescription = (description, maxTipsNum = 30) => { - if (isStr(description)) { - return stringLength(description) > maxTipsNum - } else { - return React.isValidElement(description) - } -} - -export const FormItem = styled( - class FormItem extends React.Component { - public static defaultProps = { - prefix: 'ant-' - } - - public render() { - /* eslint-disable @typescript-eslint/no-unused-vars */ - const { - className, - labelAlign, - labelTextAlign, - style, - prefix, - wrapperCol, - labelCol, - size, - help, - extra, - noMinHeight, - isTableColItem, - validateState, - autoAddColon, - maxTipsNum, - required, - type, - schema, - ...others - } = this.props - /* eslint-enable @typescript-eslint/no-unused-vars */ - const itemClassName = classNames({ - [`${prefix}form-item`]: true, - [`${prefix}${labelAlign}`]: labelAlign, - [`has-${validateState}`]: !!validateState, - [`${prefix}${size}`]: !!size, - [`${className}`]: !!className, - [`field-${type}`]: !!type - }) - - // 垂直模式并且左对齐才用到 - const Tag = (wrapperCol || labelCol) && labelAlign !== 'top' ? Row : 'div' - const label = labelAlign === 'inset' ? null : this.getItemLabel() - return ( - - {label} - {this.getItemWrapper()} - - ) - } - - private getItemLabel() { - const { - id, - required, - label, - labelCol, - wrapperCol, - prefix, - extra, - labelAlign, - labelTextAlign, - autoAddColon, - isTableColItem, - maxTipsNum - } = this.props - - if (!label || isTableColItem) { - return null - } - - const ele = ( - // @ts-ignore - - ) - - const cls = classNames({ - [`${prefix}form-item-label`]: true, - [`${prefix}${labelTextAlign}`]: !!labelTextAlign - }) - - if ((wrapperCol || labelCol) && labelAlign !== 'top') { - return ( - - {ele} - {isPopDescription(extra, maxTipsNum) && this.renderHelper()} - - ) - } - - return ( -
- {ele} - {isPopDescription(extra, maxTipsNum) && this.renderHelper()} -
- ) - } - - private getItemWrapper() { - const { - labelCol, - wrapperCol, - children, - extra, - label, - labelAlign, - help, - prefix, - noMinHeight, - size, - isTableColItem, - maxTipsNum - } = this.props - - const message = ( -
- {help &&
{help}
} - {!help && !isPopDescription(extra, maxTipsNum) && ( -
{extra}
- )} -
- ) - const ele = ( -
- {React.cloneElement(children, { size })} - {message} -
- ) - if ( - (wrapperCol || labelCol) && - labelAlign !== 'top' && - !isTableColItem && - label - ) { - return ( - - {ele} - - ) - } - - return {ele} - } - - private renderHelper() { - return ( - - {/* TODO antd 没有 size 属性 */} - - - ) - } - } -)` - margin-bottom: 0 !important; - .ant-form-item-control-wrapper { - line-height: 32px; - } - .ant-form-item-control { - line-height: 32px; - } - &.field-table { - .ant-form-item-control { - overflow: auto; - } - } - .antd-uploader { - display: block; - } - .ant-form-item-msg { - &.ant-form-item-space { - min-height: 18px; - margin-bottom: 2px; - .ant-form-item-help, - .ant-form-item-extra { - margin-top: 0; - line-height: 1.5; - } - } - } - .ant-form-tips { - margin-left: -5px; - margin-right: 10px; - transform: translateY(1px); - } - .ant-form-item-extra { - color: #888; - font-size: 12px; - line-height: 1.7; - } - .ant-col { - padding-right: 0; - } - .ant-card-head { - background: none; - } - .ant-form-item-label label { - color: #666; - font-size: 14px; - &.no-colon:after { - content: ''; - } - } - ul { - padding: 0; - li { - margin: 0; - & + li { - margin: 0; - } - } - } -` - -const toArr = val => (Array.isArray(val) ? val : val ? [val] : []) - -registerFormWrapper(OriginForm => { - OriginForm = styled(OriginForm)` - &.ant-form-inline, - .ant-form-inline { - display: flex; - .rs-uform-content { - margin-right: 15px; - } - .ant-form-item { - display: inline-block; - vertical-align: top; - } - .ant-form-item:not(:last-child) { - margin-right: 20px; - } - - .ant-form-item.ant-left .ant-form-item-control { - display: inline-block; - display: table-cell\0; - vertical-align: top; - line-height: 0; - } - } - .ant-form-item-label { - line-height: 32px; - } - .ant-form-item-label label[required]:before { - margin-right: 4px; - content: '*'; - color: #ff3000; - } - .ant-form-item-help { - margin-top: 4px; - font-size: 12px; - line-height: 1.5; - color: #999; - } - .ant-form-item.has-error .ant-form-item-help { - color: #ff3000; - } - - .ant-table { - table { - table-layout: auto; - } - } - ` - - class Form extends React.Component { - public static defaultProps = { - component: 'form', - prefix: 'ant-', - size: 'default', - labelAlign: 'left', - layout: 'horizontal', - locale: LOCALE, - autoAddColon: true - } - - public static displayName = 'SchemaForm' - public static LOCALE = LOCALE - private FormRef = React.createRef() - - public render() { - const { - className, - inline, - size, - labelAlign, - labelTextAlign, - autoAddColon, - children, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - labelCol, - layout, - wrapperCol, - maxTipsNum, - style, - prefix, - ...others - } = this.props - - const isInline = inline || layout === 'line' - const formClassName = classNames({ - [`${prefix}form`]: true, - [`${prefix}form-${isInline ? 'inline' : layout}`]: true, - [`${prefix}${size}`]: size, - [`${prefix}form-${labelAlign}`]: !!labelAlign, - [className]: !!className - }) - return ( - - - {children} - - - ) - } - - private validateFailedHandler(onValidateFailed) { - return (...args) => { - if (isFn(onValidateFailed)) { - onValidateFailed(...args) - } - const container = this.FormRef.current as HTMLElement - if (container) { - const errors = container.querySelectorAll('.ant-form-item-help') - if (errors && errors.length) { - const node = getParentNode(errors[0], '.ant-form-item') - if (node) { - moveTo(node) - } - } - } - } - } - } - - Form.LOCALE = LOCALE - - return Form -}) - -const isTableColItem = (path, getSchema) => { - const schema = getSchema(path) - return schema && schema.type === 'array' && schema['x-component'] === 'table' -} - -registerFieldMiddleware(Field => { - return props => { - const { - name, - errors, - editable, - path, - required, - schema, - schemaPath, - getSchema - } = props - if (path.length === 0) { - // 根节点是不需要包FormItem的 - return React.createElement(Field, props) - } - return React.createElement( - FormLayoutConsumer, - {}, - ({ - labelAlign, - labelTextAlign, - labelCol, - maxTipsNum, - wrapperCol, - size, - autoAddColon - }) => { - return React.createElement( - FormItem, - { - labelAlign, - labelTextAlign, - labelCol, - maxTipsNum, - wrapperCol, - autoAddColon, - size, - required: editable === false ? false : required, - ...schema['x-item-props'], - label: schema.title, - noMinHeight: schema.type === 'object' && !schema['x-component'], - isTableColItem: isTableColItem( - schemaPath.slice(0, schemaPath.length - 2), - getSchema - ), - type: schema['x-component'] || schema.type, - id: name, - validateState: toArr(errors).length ? 'error' : undefined, - extra: schema.description, - help: - toArr(errors).join(' , ') || - (schema['x-item-props'] && schema['x-item-props'].help) - }, - React.createElement(Field, props) - ) - } - ) - } -}) diff --git a/packages/antd/src/index.tsx b/packages/antd/src/index.tsx index 8534b7ef68b..2142e852b6a 100644 --- a/packages/antd/src/index.tsx +++ b/packages/antd/src/index.tsx @@ -1,44 +1,47 @@ -import './form' -import './fields/string' -import './fields/number' -import './fields/boolean' -import './fields/date' -import './fields/time' -import './fields/range' -import './fields/upload' -import './fields/checkbox' -import './fields/radio' -import './fields/rating' -import './fields/transfer' -import './fields/array' -import './fields/table' -import './fields/textarea' -import './fields/password' -import './fields/cards' - -export * from '@uform/react' -export * from './components/formButtonGroup' -export * from './components/button' -export * from './components/layout' -import React from 'react' +import React, { useRef } from 'react' import { - SchemaForm as InternalSchemaForm, - Field as InternalField -} from '@uform/react' -import { SchemaFormProps, FieldProps } from './type' - -export { mapStyledProps, mapTextComponent } from './utils' - -export default class SchemaForm extends React.Component> { - render() { - return - } -} + SchemaMarkupForm, + SchemaMarkupField +} from '@uform/react-schema-renderer' +import { IAntdSchemaFormProps, IAntdSchemaFieldProps } from './types' +import './fields' +import './compat' +export * from '@uform/react-schema-renderer' +export * from './components' +export * from './types' +export { mapStyledProps, mapTextComponent } from './shared' +export const SchemaForm: React.FC = props => { + const formRef = useRef() -export class Field extends React.Component< - FieldProps -> { - render() { - return - } + return ( +
+ { + if (props.onValidateFailed) { + props.onValidateFailed(result) + } + if (formRef.current) { + setTimeout(() => { + const elements = formRef.current.querySelectorAll( + '.ant-form-item-control.has-error' + ) + if (elements && elements.length) { + if (!elements[0].scrollIntoView) return + elements[0].scrollIntoView({ + behavior: 'smooth', + inline: 'center', + block: 'center' + }) + } + }, 30) + } + }} + > + {props.children} + +
+ ) } +export const Field: React.FC = SchemaMarkupField +export default SchemaForm diff --git a/packages/antd/src/locale.ts b/packages/antd/src/locale.ts deleted file mode 100644 index aa28ff56c6e..00000000000 --- a/packages/antd/src/locale.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable @typescript-eslint/camelcase */ -export default { - addItem: '添加', - array_invalid_minItems: '条目数不允许小于%s条', - array_invalid_maxItems: '条目数不允许大于%s条', - operations: '操作' -} diff --git a/packages/antd/src/shared.ts b/packages/antd/src/shared.ts new file mode 100644 index 00000000000..0a7b22e9b74 --- /dev/null +++ b/packages/antd/src/shared.ts @@ -0,0 +1,75 @@ +import React from 'react' +import { PreviewText } from '@uform/react-shared-components' +import { + IConnectProps, + MergedFieldComponentProps +} from '@uform/react-schema-renderer' +import { Select } from './components/Select' +export * from '@uform/shared' + +export const mapTextComponent = ( + Target: React.JSXElementConstructor, + props: any = {}, + fieldProps: any = {} +): React.JSXElementConstructor => { + const { editable } = fieldProps + if (editable !== undefined) { + if (editable === false) { + return PreviewText + } + } + if (Array.isArray(props.dataSource)) { + return Select + } + return Target +} + +export const acceptEnum = (component: React.JSXElementConstructor) => { + return ({ dataSource, ...others }) => { + if (dataSource) { + return React.createElement(Select, { dataSource, ...others }) + } else { + return React.createElement(component, others) + } + } +} + +export const transformDataSourceKey = (component, dataSourceKey) => { + return ({ dataSource, ...others }) => { + return React.createElement(component, { + [dataSourceKey]: dataSource, + ...others + }) + } +} + +export const normalizeCol = ( + col: { span: number; offset?: number } | number, + defaultValue?: { span: number } +): { span: number; offset?: number } => { + if (!col) { + return defaultValue + } else { + return typeof col === 'object' ? col : { span: Number(col) } + } +} + +export const mapStyledProps = ( + props: IConnectProps, + fieldProps: MergedFieldComponentProps +) => { + const { loading, errors } = fieldProps + if (loading) { + props.state = props.state || 'loading' + } else if (errors && errors.length) { + props.state = 'error' + } +} + +export const compose = (...args: any[]) => { + return (payload: any, ...extra: any[]) => { + return args.reduce((buf, fn) => { + return buf !== undefined ? fn(buf, ...extra) : fn(payload, ...extra) + }, payload) + } +} diff --git a/packages/antd/src/type.tsx b/packages/antd/src/type.tsx deleted file mode 100644 index 18b6114d416..00000000000 --- a/packages/antd/src/type.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import { ColProps } from 'antd/es/col' -import { CardProps } from 'antd/es/card' -import { BaseButtonProps } from 'antd/es/button/button' -import { - IFormActions, - ISchema, - IEffects, - IFieldError, - TextAlign, - Size, - Layout, - TextEl, - LabelAlign, - IAsyncFormActions -} from '@uform/types' -import { SwitchProps } from 'antd/lib/switch' -import { CheckboxGroupProps } from 'antd/lib/checkbox' -import { - DatePickerProps, - RangePickerProps, - MonthPickerProps, - WeekPickerProps -} from 'antd/lib/date-picker/interface' -import { InputNumberProps } from 'antd/lib/input-number' -import { IPasswordProps } from './fields/password' -import { RadioGroupProps } from 'antd/lib/radio' -import { ISliderProps } from './fields/range' -import { RateProps } from 'antd/lib/rate' -import { InputProps } from 'antd/lib/input' -import { TextAreaProps } from 'antd/es/input' -import { TimePickerProps } from 'antd/lib/time-picker' -import { TransferProps } from 'antd/lib/transfer' -import { IUploaderProps } from './fields/upload' -import { SelectProps } from 'antd/lib/select' - -type ColSpanType = number | string - -export interface ColSize { - span?: ColSpanType - offset?: ColSpanType -} - -export interface ILocaleMessages { - [key: string]: string | ILocaleMessages -} - -export interface IFormLayoutProps { - className?: string - inline?: boolean - labelAlign?: LabelAlign - wrapperCol?: ColProps | number - labelCol?: ColProps | number - labelTextAlign?: TextAlign - size?: Size - style?: React.CSSProperties -} - -export interface IFormItemGridProps { - cols?: Array - description?: TextEl - gutter?: number - title?: TextEl -} - -export interface IRowProps { - prefix?: string - pure?: boolean - wrap?: boolean - fixed?: boolean - hidden?: boolean - className?: string - fixedWidth?: string | number - style?: React.CSSProperties - component?: keyof JSX.IntrinsicElements | React.ComponentType - gutter?: string - align?: string | number - justify?: string | number - children: React.ReactNode -} - -export interface IColProps extends ColProps { - prefix?: string - pure?: boolean - className?: string - fixedSpan?: string | number - fixedOffset?: string | number - hidden?: boolean - align?: any - component?: keyof JSX.IntrinsicElements | React.ComponentType - children?: React.ReactNode - xxs?: ColSpanType | ColSize - xs?: ColSpanType | ColSize - s?: ColSpanType | ColSize - m?: ColSpanType | ColSize - l?: ColSpanType | ColSize - xl?: ColSpanType | ColSize -} - -export interface IFormCardProps extends CardProps { - className?: string -} - -export interface IFormBlockProps extends CardProps { - className?: string -} - -export type TFormCardOrFormBlockProps = Omit - -export interface IFormTextBox { - text?: string - name?: string - title?: TextEl - description?: TextEl - gutter?: number - required?: boolean -} - -export interface IFormButtonGroupProps { - sticky?: boolean - style?: React.CSSProperties - itemStyle?: React.CSSProperties - className?: string - align?: 'left' | 'right' | 'start' | 'end' | 'top' | 'bottom' | 'center' - triggerDistance?: number - zIndex?: number - span?: ColSpanType - offset?: ColSpanType -} - -export interface ISubmitProps extends Omit { - showLoading?: boolean -} - -export interface SchemaFormProps { - actions?: IFormActions | IAsyncFormActions - initialValues?: V - defaultValue?: V - value?: V - editable?: boolean | ((name: string) => boolean) - effects?: IEffects - locale?: ILocaleMessages - schema?: ISchema - onChange?: (values: V) => void - onReset?: (values: V) => void - onSubmit?: (values: V) => void - onValidateFailed?: (fieldErrors: IFieldError[]) => void - autoAddColon?: boolean - className?: string - inline?: boolean - layout?: Layout - maxTipsNum?: number - labelAlign?: LabelAlign - labelTextAlign?: TextAlign - labelCol?: ColSize | number - wrapperCol?: ColSize | number - size?: Size - style?: React.CSSProperties - prefix?: string -} - -interface InternalFieldTypes { - boolean: SwitchProps | SelectProps - checkbox: CheckboxGroupProps - date: DatePickerProps - daterange: RangePickerProps - month: MonthPickerProps - week: WeekPickerProps - year: DatePickerProps - number: InputNumberProps | SelectProps - password: IPasswordProps - radio: RadioGroupProps - range: ISliderProps - rating: RateProps - string: InputProps | SelectProps - textarea: TextAreaProps | SelectProps - time: TimePickerProps - transfer: TransferProps - upload: IUploaderProps -} - -export interface FieldProps extends ISchema { - type?: T - name?: string - editable?: boolean - ['x-props']?: T extends keyof InternalFieldTypes ? InternalFieldTypes[T] : any -} diff --git a/packages/antd/src/types.ts b/packages/antd/src/types.ts new file mode 100644 index 00000000000..a37bdaf7f74 --- /dev/null +++ b/packages/antd/src/types.ts @@ -0,0 +1,92 @@ +import { ButtonProps } from 'antd/lib/button' +import { FormProps, FormItemProps as ItemProps } from 'antd/lib/form' +import { + StepsProps as StepProps, + StepProps as StepItemProps +} from 'antd/lib/steps' +import { + ISchemaFormProps, + IMarkupSchemaFieldProps, + ISchemaFieldComponentProps +} from '@uform/react-schema-renderer' +import { PreviewTextConfigProps } from '@uform/react-shared-components' +import { StyledComponent } from 'styled-components' + +type ColSpanType = number | string + +export type IAntdSchemaFormProps = FormProps & + IFormItemTopProps & + PreviewTextConfigProps & + ISchemaFormProps + +export type IAntdSchemaFieldProps = IMarkupSchemaFieldProps + +export interface ISubmitProps extends ButtonProps { + onSubmit?: ISchemaFormProps['onSubmit'] + showLoading?: boolean +} + +export interface IResetProps extends ButtonProps { + forceClear?: boolean + validate?: boolean +} + +export type IFormItemTopProps = React.PropsWithChildren< + Exclude< + Pick, + 'labelCol' | 'wrapperCol' + > & { + inline?: boolean + className?: string + style?: React.CSSProperties + labelCol?: number | { span: number; offset?: number } + wrapperCol?: number | { span: number; offset?: number } + } +> + +export interface ICompatItemProps + extends Omit, + Partial { + labelCol?: number | { span: number; offset?: number } + wrapperCol?: number | { span: number; offset?: number } +} + +export type StyledCP

= StyledComponent< + (props: React.PropsWithChildren

) => React.ReactElement, + any, + {}, + never +> + +export type StyledCC = StyledCP & Statics + +export interface IFormButtonGroupProps { + sticky?: boolean + style?: React.CSSProperties + itemStyle?: React.CSSProperties + className?: string + align?: 'left' | 'right' | 'start' | 'end' | 'top' | 'bottom' | 'center' + triggerDistance?: number + zIndex?: number + span?: ColSpanType + offset?: ColSpanType +} + +export interface IItemProps { + title?: React.ReactText + description?: React.ReactText +} + +export interface IFormItemGridProps extends IItemProps { + cols?: Array + gutter?: number +} + +export interface IFormTextBox extends IItemProps { + text?: string + gutter?: number +} + +export interface IFormStep extends StepProps { + dataSource: StepItemProps[] +} diff --git a/packages/antd/src/utils.tsx b/packages/antd/src/utils.tsx deleted file mode 100644 index b9bd9c728f8..00000000000 --- a/packages/antd/src/utils.tsx +++ /dev/null @@ -1,255 +0,0 @@ -import React from 'react' -import { Select as AntSelect, Icon } from 'antd' -import ReactDOM from 'react-dom' -import styled from 'styled-components' -import { isFn } from '@uform/utils' -import { IConnectProps, IFieldProps } from '@uform/react' -export * from '@uform/utils' - -export interface ISelectProps { - dataSource: any[] - className: string -} - -export interface IElement extends Element { - oldHTML?: string -} -const MoveTo = typeof window !== 'undefined' ? require('moveto') : null -const WrapSelect = styled( - class extends React.Component { - public render() { - const { dataSource = [], ...others } = this.props - const children = dataSource.map(item => { - const { label, value, ...others } = item - return ( - - {label} - - ) - }) - return ( - - {children} - - ) - } - } -)` - min-width: 100px; - width: 100%; -` - -const Text = styled(props => { - let value - if (props.dataSource && props.dataSource.length) { - const find = props.dataSource.filter(({ value }) => - Array.isArray(props.value) - ? props.value.some(val => val == value) - : props.value == value - ) - value = find.reduce((buf, item, index) => { - return buf.concat(item.label, index < find.length - 1 ? ', ' : '') - }, []) - } else { - value = Array.isArray(props.value) - ? props.value.join(' ~ ') - : String( - props.value === undefined || props.value === null ? '' : props.value - ) - } - return ( -

- {value || 'N/A'} - {props.innerAfter ? ' ' + props.innerAfter : ''} - {props.addonAfter ? ' ' + props.addonAfter : ''} -
- ) -})` - height: 32px; - line-height: 32px; - vertical-align: middle; - font-size: 13px; - color: #333; - &.small { - height: 24px; - line-height: 24px; - } - &.large { - height: 40px; - line-height: 40px; - } -` - -export interface IStateLoadingProps { - state?: string - dataSource: any[] -} - -const loadingSvg = - '' - -export const StateLoading = (Target: React.ComponentClass) => { - return class Select extends React.Component { - public wrapper: React.ReactInstance - public wrapperDOM: HTMLElement - public classList: string[] - - public componentDidMount() { - if (this.wrapper) { - this.wrapperDOM = ReactDOM.findDOMNode(this.wrapper) - this.mapState() - } - } - - public componentDidUpdate() { - this.mapState() - } - - public render() { - return ( - { - if (inst) { - this.wrapper = inst - } - }} - {...this.props} - /> - ) - } - - public mapState() { - const { state } = this.props - const loadingName = 'anticon-spin' - const iconSizeClassNames = [ - 'xxs', - 'xs', - 'small', - 'medium', - 'large', - 'xl', - 'xxl', - 'xxxl' - ] - this.classList = this.classList || [] - - if (this.wrapperDOM) { - const icon: IElement = this.wrapperDOM.querySelector('.anticon') - if (!icon || !icon.classList) { - return - } - - if (state === 'loading') { - icon.classList.forEach(className => { - if (className.indexOf('anticon-') > -1) { - if ( - className !== loadingName && - iconSizeClassNames.every(val => `anticon-${val}` !== className) - ) { - icon.classList.remove(className) - this.classList.push(className) - } - } - }) - if (icon.innerHTML) { - icon.oldHTML = icon.innerHTML - icon.innerHTML = loadingSvg - } - if (!icon.classList.contains(loadingName)) { - icon.classList.add(loadingName) - } - } else { - icon.classList.remove(loadingName) - this.classList.forEach(className => { - icon.classList.add(className) - }) - if (icon.oldHTML) { - icon.innerHTML = icon.oldHTML - } - this.classList = [] - } - } - } - } -} - -const Select = StateLoading(WrapSelect) - -export const acceptEnum = component => { - return ({ dataSource, ...others }) => { - if (dataSource || others.showSearch) { - return React.createElement(Select, { dataSource, ...others }) - } else { - return React.createElement(component, others) - } - } -} - -export const mapStyledProps = ( - props: IConnectProps, - { loading, size }: IFieldProps -) => { - if (loading) { - props.state = props.state || 'loading' - props.suffix = props.suffix || ( - - ) - } else { - props.suffix = props.suffix || - } - if (size) { - props.size = size - } -} - -export const mapTextComponent = ( - Target: React.ComponentClass, - props, - { - editable, - name - }: { editable: boolean | ((name: string) => boolean); name: string } -): React.ComponentClass => { - if (editable !== undefined) { - if (isFn(editable)) { - if (!editable(name)) { - return Text - } - } else if (editable === false) { - return Text - } - } - return Target -} - -export const compose = (...args) => { - return (payload, ...extra) => { - return args.reduce((buf, fn) => { - return buf !== undefined ? fn(buf, ...extra) : fn(payload, ...extra) - }, payload) - } -} - -export const transformDataSourceKey = (component, dataSourceKey) => { - return ({ dataSource, ...others }) => { - return React.createElement(component, { - [dataSourceKey]: dataSource, - ...others - }) - } -} - -export const moveTo = element => { - if (!element || !MoveTo) { - return - } - if (element.scrollIntoView) { - element.scrollIntoView({ - behavior: 'smooth', - inline: 'start', - block: 'start' - }) - } else { - new MoveTo().move(element.getBoundingClientRect().top) - } -} diff --git a/packages/core/README.md b/packages/core/README.md index 624c47a4d3a..60f09ca6b66 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -1,2 +1,893 @@ # @uform/core -> UForm 内核包 \ No newline at end of file + +English | [简体中文](./README.zh-cn.md) + +> The form state core management package does not rely on any third-party UI frameworks. This package will provide the following features: +> +> - Manage Form status +> - Manage Field status +> - Manage the Validator status +> - Manage dependencies between Form, Field, and Validator + +### Install + +```bash +npm install --save @uform/core +``` + +### Table Of Contents + + + +- [Backdrop](#backdrop) +- [Design Concept](#design-concept) +- [Core highlights](#core-highlights) +- [Architecture diagram](#architecture-diagram) +- [Terminology explanation](#terminology-explanation) +- [API](#api) + - [`createForm`](#createform) + - [`registerValidationFormats`](#registervalidationformats) + - [`registerValidationRules`](#registervalidationrules) + - [`registerValidationMTEngine`](#registervalidationmtengine) + - [`setValidationLanguage`](#setvalidationlanguage) + - [`setValidationLocale`](#setvalidationlocale) +- [Classes](#classes) + - [`new FormPath()`](#new-formpath) + - [`new FormLifeCyle()`](#new-formlifecyle) +- [Enums](#enums) + - [Lifecycletypes](#lifecycletypes) +- [Interfaces](#interfaces) + - [IFormCreatorOptions](#iformcreatoroptions) + - [IForm](#iform) + - [IMutators](#imutators) + - [Validation](#the-validator) + - [IFormState](#iformstate) + - [IFieldState](#ifieldstate) + - [IVirtualFieldState](#ivirtualfieldstate) + - [IField/IVirtualField](#ifieldivirtualfield) + + + +### Background + +There are two main scenarios in the middle and back-end field, **one is data entry, one is data query + data presentation**, whether it is data entry or data query, it is realized by means of form, from the perspective of implementation complexity, the complexity of them is similar, because the data rendering level will inevitably have extremely complex renderings (such as Tree Table, etc.), but the data rendering is easier to reuse and abstract, only the Form requirements will involve a lot of interactive logic. So, as long as we solve the Form problem fundamentally, for the mid- and back-stage scenes, most of the mid- and back-stage scene problems are solved. + +UForm is born for this purpose. + +### Design + +**Anything comes from Observable Graph.** + +### Core highlights + +- Time travel, with the help of the Observable Graph, can record the full state at any time, can also roll back the state to any time, such abilities will maximize the performance in heavy transaction applications and local debugging scenarios. +- Efficient update, accurate rendering, no full tree rendering required +- Built-in immer.js, intelligent degradation, no need to care about browser compatibility +- More complete life cycle hook +- More complete verification engine + +- - ValidateFirst verification + - Warning Verification (no blocking submission verification) + - Verification message template engine (a complex verification message solution that does not affect international copy storage) + - The verification rule can be extended, and the regular verification library can be extended. + +- More flexible path parsing, matching, evaluation, value engine + +- - Batch matching data path capability + - Deconstruct evaluation, deconstruct value ability + +- Provides state management capabilities beyond the basic form state model. + +### Architecture diagram + +![img](https://img.alicdn.com/tfs/TB18LXHlVP7gK0jSZFjXXc5aXXa-1428-926.png) + +### Terminology explanation + +**FormPath/FormPathPattern** Is an abstract data path form, FormPath is a path class, and FormPathPattern is a path form that can be parsed by FormPath. [Cool-path](https://github.com/janrywang/cool-path) Path parsing matching, ability to evaluate values + +**The virtual field** Is a special Field data structure. The difference between the Field and the Field is that it does not manage values. That is to say, it has no relevance to the value of the Form. Usually we use it, more importantly, it acts as a proxy for the status of a UI container. For example, the layout component FormBlock in UForm exists as an independent node in the whole Form Graph, however, this node type is a VirtualField, but when the final data is submitted, the FormBlock does not pollute the data structure of the submitted data. + +**Observable Graph** Form is a unique Observer Tree. With the help of the observer tree, many forms-related internal linkage logic can be implemented. + +**Data Path** Is the name attribute of Field/VirtualField, which exists as the data path. + +**Node Path** Is the path attribute of Field/VirtualField, which exists as the node path. + +For the data path and node path, we can look at the following figure: + +![img](https://img.alicdn.com/tfs/TB1.rAamG61gK0jSZFlXXXDKFXa-1496-898.png) + +If there exists such a tree, then: + +- The name attribute of field c is a.c, and the path attribute is a.b.c. +- The name attribute of field B is a.b, and the path attribute is a.b. +- The name attribute of field d is a.d, and the path attribute is a.d. +- The name attribute of field e is a.d.e, and the path attribute is a.d.e. + +After this explanation, we roughly understand that as long as VirtualField exists in a node path, its data path will skip VirtualField. However, for VirtualField itself, its name attribute contains its own node identification, which is why the name attribute of field B is a.b. + +### API + +#### `createForm` + +> Create a Form instance + +**Signature** + +``` +createForm(options?: IFormCreatorOptions): IForm +``` + +**Usage** + +``` + import { createForm } from '@uform/core' + + const form = createForm({ + values:{}, + initialValues:{}, + onChange:(values)=>{ + console.log(values) + } + }) + +const aa = form.registerField({ + path:"aa" +}) + +aa.setState(state=>{ + state.value = 123 +}) +console.log(form.getFormState(state=>state.values)) //{aa:123} +``` + +#### `registerValidationFormats` + +> Register a regular verification rule set + +**Signature** + +``` +registerValidationFormats(formats:{ + [formatName in string]: RegExp; +}) : void +``` + +**Usage** + +``` + import { createForm,registerValidationFormats } from '@uform/core' + + registerValidationFormats({ + number: /^[+-]?\d+(\.\d+)?$/ + }) + + const form = createForm({ + values:{}, + initialValues:{}, + onChange:(values)=>{ + console.log(values) + } +}) + +const aa = form.registerField({ + path:"aa", + rules:[{ + format:"number", + message:'This field is not a number.' + }] +}) + +aa.setState(state=>{ + state.value = 'hello world' +}) +form.validate() + +console.log(form.getFormState(state=>state.errors)) +/** +[{ + path: 'aa', + messages: [ 'This field is not a number.' ] +}] +**/ +``` + +#### `registerValidationRules` + +> The difference between registering a verification rule set and registering formats is that it can register complex verification rules, but the formats are just regular expressions. + +**Signature** + +``` +registerValidationRules( + rules:{ + [ruleName:string]:(value:any,rule:ValidateDescription)=>boolean + } +) : void +``` + +**Usage** + +``` + import { createForm,registerValidationRules } from '@uform/core' + + registerValidationRules({ + custom: value => { + return value === '123' ? 'This field can not be 123' : '' + } + }) + + const form = createForm({ + values: {}, + initialValues: {}, + onChange: values => { + console.log(values) + } +}) + +const aa = form.registerField({ + path: 'aa', + rules: [ + { + custom: true + } + ] +}) + +aa.setState(state => { + state.value = '123' +}) +form.validate() + +console.log(form.getFormState(state =>state.errors)) +/** +[{ + path: 'aa', + messages: ['This field can not be 123'] +}] +**/ +``` + +#### `registerValidationMTEngine` + +> Register a verification message template engine + +**Signature** + +``` +registerValidationMTEngine(callback:(message,context)=>any) : void +``` + +**Usage** + +``` + import { createForm,registerValidationMTEngine } from '@uform/core' + + registerValidationMTEngine((message,context)=>{ + return message.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, $0) => { + return FormPath.getIn(context, $0) + }) + }) + + const form = createForm({ + values: {}, + initialValues: {}, + onChange: values => { + console.log(values) + } +}) + +const aa = form.registerField({ + path: 'aa', + rules: [ + { + validator(value){ + return value === 123 : 'This field can not be 123 {{scope.outerVariable}}' + }, + scope:{ + outerVariable:'addonAfter' + } + } + ] +}) + +aa.setState(state => { + state.value = '123' +}) +form.validate() + +console.log(form.getFormState(state =>state.errors)) +/** +[{ + path: 'aa', + messages: ['This field can not be 123 addonAfter'] +}] +**/ +``` + +#### `setValidationLanguage` + +> Set the international language type + +**Signature** + +``` +setValidationLanguage(lang: string): void +``` + +**Usage** + +``` +import { setValidationLanguage } from '@uform/core' + +setValidationLanguage('en-US') +``` + +#### `setValidationLocale` + +> Set a language pack + +**Signature** + +``` +interface ILocaleMessages { + [key: string]: string | ILocaleMessages; +} +interface ILocales { + [lang: string]: ILocaleMessages; +} +setValidationLocale(locale: ILocales) => void +``` + +**Usage** + +``` +import { setValidationLocale } from '@uform/core' + +setValidationLocale({ + 'en-US':{ + required:"This field is required." + } +}) +``` + +### Classes + +#### `new FormPath()` + +> The form path engine is responsible for path analysis, matching, evaluation, value, deconstruction evaluation, and deconstruction value. + +For more information, see: [ https://github.com/janrywang/cool-path ](https://github.com/janrywang/cool-path) + +#### `new FormLifeCycle()` + +> Create a life cycle listener + +**Signature** + +``` +type FormLifeCycleHandler = (payload: T, context: any) => void + +new FormLifeCycle(handler: FormLifeCycleHandler) +new FormLifeCycle(...type: LifeCycleTypes, handler: FormLifeCycleHandler...) +new FormLifeCycle(handlerMap: { [key: LifeCycleTypes]: FormLifeCycleHandler }) +``` + +**Usage** + +```typescript + import { createForm,FormLifeCycle,LifeCycleTypes } from '@uform/core' + + const form = createForm({ + lifecycles:[ + new FormLifeCycle(({type:LifeCycleTypes,payload:IForm | IField | IVirtualField })=>{ + // God mode, full monitoring + }), + new FormLifeCycle( + LifeCycleTypes.ON_FORM_MOUNT, + (payload:IForm | IField | IVirtualField)=>{ + // Accurate monitoring + }), + new FormLifeCycle({ + [LifeCycleTypes.ON_FORM_MOUNT]:(payload:IForm | IField | IVirtualField)=>{ + // Object form accurate listener + } + }), + ] +}) +``` + +### Enums + +#### LifeCycleTypes + +```typescript +enum LifeCycleTypes { // Form pre-initialization trigger + /** + * Form LifeCycle + **/ ON_FORM_WILL_INIT = 'onFormWillInit', + + // Form initialization trigger + ON_FORM_INIT = 'onFormInit', + + // Triggered when the form changes + ON_FORM_CHANGE = 'onFormChange', + + // Triggered when the form is mounted + ON_FORM_MOUNT = 'onFormMount', + + // Triggered when the form is unloaded + ON_FORM_UNMOUNT = 'onFormUnmount', + + // Triggered when the form is submitted + ON_FORM_SUBMIT = 'onFormSubmit', + + // Triggered when the form is reset + ON_FORM_RESET = 'onFormReset', + + // Triggered when the form submission starts + ON_FORM_SUBMIT_START = 'onFormSubmitStart', + + // Triggered when the form submission ends + ON_FORM_SUBMIT_END = 'onFormSubmitEnd', + + // Triggered when the form value changes + ON_FORM_VALUES_CHANGE = 'onFormValuesChange', + + // Trigger when the form initial value changes + ON_FORM_INITIAL_VALUES_CHANGE = 'onFormInitialValuesChange', + + // Triggered when form validation begins + ON_FORM_VALIDATE_START = 'onFormValidateStart', + + // Triggered when the form validation ends + ON_FORM_VALIDATE_END = 'onFormValidateEnd', + + // Triggered when the form event is triggered, used to monitor only manual operations + ON_FORM_INPUT_CHANGE = 'onFormInputChange', // Triggered when the form observer tree changes + /** + * FormGraph LifeCycle + **/ ON_FORM_GRAPH_CHANGE = 'onFormGraphChange', // Triggered when pre-initialized + /** + * Field LifeCycle + **/ ON_FIELD_WILL_INIT = 'onFieldWillInit', + + // Triggered when the field is initialized + ON_FIELD_INIT = 'onFieldInit', + + // Triggered when the field changes + ON_FIELD_CHANGE = 'onFieldChange', + + // Triggered when the field event is triggered, used to monitor only manual operations + ON_FIELD_INPUT_CHANGE = 'onFieldInputChange', + + // Triggered when the field value changes + ON_FIELD_VALUE_CHANGE = 'onFieldValueChange', + + // Trigger when the initial value of the field changes + ON_FIELD_INITIAL_VALUE_CHANGE = 'onFieldInitialValueChange', + + // Triggered when the field is mounted + ON_FIELD_MOUNT = 'onFieldMount', + + // Trigger when the field is unloaded + ON_FIELD_UNMOUNT = 'onFieldUnmount', +} +``` + +### Interfaces + +#### IFormCreatorOptions + +> CreateForm parameter object protocol + +```typescript +interface IFormCreatorOptions { + // Form initial value + initialValues?: {} // Form value + + values?: {} // LifeCycle listener, here mainly introduced to the instantiated object of FormLifeCycle + + lifecycles?: FormLifeCycle[] // Is it editable, overall control in the Form dimension + + editable?: boolean | ((name: string) => boolean) // Whether to use the dirty check, the default will go immer accurate update + + useDirty?: boolean // Whether to go pessimistic check, stop the subsequent check when the first check fails + + validateFirst?: boolean // Form change event callback + + onChange?: (values: IFormState['values']) => void // Form submission event callback + + onSubmit?: (values: IFormState['values']) => any | Promise // Form reset event callback + + onReset?: () => void // Form verification failure event callback + + onValidateFailed?: (validated: IFormValidateResult) => void +} +``` + +#### IForm + +> Form instance object API created by using createForm + +```typescript +interface IForm { + /* + * Form submission, if the callback parameter returns Promise, + * Then the entire submission process will hold and load is true. + * Wait for Promise resolve to trigger the form onFormSubmitEnd event while loading is false + */ + submit( + onSubmit?: (values: IFormState['values']) => any | Promise + ): Promise<{ + Validated: IFormValidateResult + Payload: any //onSubmit callback function return value + }> + /* + * Clear the error message, you can pass the FormPathPattern to batch or precise control of the field to be cleared. + * For example, clearErrors("*(aa,bb,cc)") + */ + clearErrors: (pattern?: FormPathPattern) => void + /* + * Get status changes, mainly used to determine which states in the current life cycle have changed in the form lifecycle hook. + * For example, hasChanged(state,'value.aa') + */ + hasChanged( + target: IFormState | IFieldState | IVirtualFieldState, + path: FormPathPattern + ): boolean + /* + * Reset form + */ + reset(options?: { + // Forced to empty + forceClear?: boolean // Forced check + validate?: boolean // Reset range for batch or precise control of the field to be reset + selector?: FormPathPattern + }): Promise + /* + * Validation form + */ + validate( + path?: FormPathPattern, + options?: { + // Is it pessimistic check, if the current field encounters the first verification error, stop the subsequent verification process + first?: boolean + } + ): Promise + /* + * Set the form status + */ + setFormState( // Operation callback + callback?: (state: IFormState) => any, // No trigger the event + silent?: boolean + ): void + /* + * Get form status + */ + getFormState( //transformer + callback?: (state: IFormState) => any + ): any + /* + * Set the field status + */ + setFieldState( // Field path + path: FormPathPattern, // Operation callback + callback?: (state: IFieldState) => void, // No trigger the event + silent?: boolean + ): void + /* + * Get the field status + */ + getFieldState( // Field path + path: FormPathPattern, // Transformer + callback?: (state: IFieldState) => any + ): any + /* + * Registration field + */ + registerField(props: { + // Node path + path?: FormPathPattern // Data path + name?: string // Field value + value?: any // Field multi-value + values?: any[] // Field initial value + initialValue?: any // Field extension properties + visible?: boolean //Field initial visible status(Whether the data is visible) + display?: boolean //Field initial display status(Whether the style is visible) + props?: any // Field check rule + rules?: ValidatePatternRules[] // Field is required + required?: boolean // Is the field editable? + editable?: boolean // Whether the field is dirty check + useDirty?: boolean // Field state calculation container, mainly used to extend the core linkage rules + computeState?: (draft: IFieldState, prevState: IFieldState) => void + }): IField + /* + * Register virtual fields + */ + registerVirtualField(props: { + // Node path + path?: FormPathPattern // Data path + name?: string // Field extension properties + visible?: boolean //Field initial visible status(Whether the data and style is visible) + display?: boolean //Field initial display status(Whether the style is visible) + props?: any // Whether the field is dirty check + useDirty?: boolean // Field state calculation container, mainly used to extend the core linkage rules + computeState?: (draft: IFieldState, prevState: IFieldState) => void + }): IVirtualField + /* + * Create a field data operator, which will explain the returned API in detail later. + */ + createMutators(field: IField): IMutators + /* + * Get the form observer tree + */ + getFormGraph(): IFormGraph + /* + * Set the form observer tree + */ + setFormGraph(graph: IFormGraph): void + /* + * Listen to the form life cycle + */ + subscribe( + callback?: ({ type, payload }: { type: string; payload: any }) => void + ): number + /* + * Cancel the listening form life cycle + */ + unsubscribe(id: number): void + /* + * Trigger form custom life cycle + */ + notify: (type: string, payload?: T) => void + /* + * Set the field value + */ + setFieldValue(path?: FormPathPattern, value?: any): void + /* + * Get the field value + */ + getFieldValue(path?: FormPathPattern): any + /* + * Set the initial value of the field + */ + setFieldInitialValue(path?: FormPathPattern, value?: any): void + /* + * Get the initial value of the field + */ + getFieldInitialValue(path?: FormPathPattern): any +} +``` + +#### IMutators + +> The instance API created by crewikiutators is mainly used to operate field data. + +```typescript +interface IMutators { + // Changing the field value and multi parameter condition will store all parameters in values + change(...values: any[]): any + // Get focus, trigger active state change + focus(): void + // Lose focus, trigger active / visited status change + blur(): void + // Trigger current field verifier + validate(): Promise + // Whether the value of the current field exists in the values property of form + exist(index?: number | string): Boolean + + /**Array operation method**/ + + // Append data + push(value?: any): any[] + // Pop up tail data + pop(): any[] + // Insert data + insert(index: number, value: any): any[] + // Delete data + remove(index: number | string): any + // Head insertion + unshift(value: any): any[] + // Head ejection + shift(): any[] + // Move element + move($from: number, $to: number): any[] + // Move down + moveDown(index: number): any[] + // Move up + moveUp(index: number): any[] +} +``` + +#### Validation + +> Here we mainly list the intermediate type signatures related to verification. + +```typescript +type CustomValidator = ( + value: any, + rescription?: ValidateDescription +) => ValidateResponse +type SyncValidateResponse = + | null + | string + | boolean + | { + type?: 'error' | 'warning' + message: string + } +type AsyncValidateResponse = Promise +type ValidateResponse = SyncValidateResponse | AsyncValidateResponse + +interface IFormValidateResult { + errors: Array<{ + path: string + messages: string[] + }> + warnings: Array<{ + path: string + messages: string[] + }> +} + +type InternalFormats = + | 'url' + | 'email' + | 'ipv6' + | 'ipv4' + | 'idcard' + | 'taodomain' + | 'qq' + | 'phone' + | 'money' + | 'zh' + | 'date' + | 'zip' + | string + +interface ValidateDescription { + // Regular rule type + format?: InternalFormats + // Custom validator + validator?: CustomValidator + // Is it required? + required?: boolean + // Customize with regularity + pattern?: RegExp | string + // Maximum length rule + max?: number; + // Maximum numerical rule + maximum?: number + // Exclusive maximum numerical rule + exclusiveMaximum?: number + // Exclusive minimum numerical rules + exclusiveMinimum?: number + // Minimum value rule + minimum?: number + // Minimum length rule + min?: number + // Length rule + len?: number + // Whether to check the white space + whitespace?: boolean + // Enumeration check rules + enum?: any[] + // Custom error copy + message?: string + // Custom validation rules + [key: string]: any +} +``` + +#### IFormState + +> Form the core state + +```typescript +interface IFormState { + /**Read-only attribute**/ + // Is it in the original state, pristine is true only when values === initialValues + pristine: boolean // Is it legal, as long as the error length is greater than 0, the valid is false + valid: boolean // Is it illegal, as long as the error length is greater than 0, the valid is true + invalid: boolean // Is it in the check state, it will only be set when calling the validate API + validating: boolean // Is it in the commit state, it will only be set when the submit API is called + submitting: boolean //Error message list + errors: string[] //Alarm message list + warnings: string[] /** writable property**/ // Is it in the loaded state, writable state, as long as validating is true, the state will also be true, the same as false + loading: boolean // Is it in the initial state? + initialized: boolean // Is it editable? + editable: boolean | ((name: string) => boolean) // form value + values: {} // form initial value + initialValues: {} // form mount, the life cycle hook mentioned earlier, must be triggered by setting the state, the default will not trigger + mounted: boolean // Form unmount, the life cycle hook mentioned earlier, must be triggered by setting the state, the default will not trigger + unmounted: boolean // Form extension properties + props: FormProps +} +``` + +#### IFieldState + +> CORE Field status + +```typescript +interface IFieldState { + /**Read-only attribute**/ + // State name, FieldState + displayName?: string // Data path + name: string // Node path + path: string // Has been initialized + initialized: boolean // Is it in the original state, the state is true only when value===initialValues + pristine: boolean // Is it in a legal state, as long as the error length is greater than 0, the valid is false + valid: boolean // Is it illegal, as long as the error length is greater than 0, the valid is true + invalid: boolean // Is it in check state? + validating: boolean // Is it modified, if the value changes, the property is true, and will be true throughout the life of the field + modified: boolean // Is it touched? + touched: boolean // Is it activated, when the field triggers the onFocus event, it will be triggered to true, when onBlur is triggered, it is false + active: boolean // Have you ever visited, when the field triggers the onBlur event, it will be triggered to true + visited: boolean /** writable property**/ // Is it visible, note: if the state is false, then the value of the field will not be submitted, and the UI will not display + visible: boolean // Whether to show, note: if the state is false, then the value of the field will be submitted, the UI will not display, similar to the form hidden field + display: boolean // Is it editable? + editable: boolean // Is it in the loading state, note: if the field is in asynchronous verification, loading is true + loading: boolean // Field multi-parameter value, such as when the field onChange trigger, the event callback passed multi-parameter data, then the value of all parameters will be stored here + values: any[] // Field error message + errors: string[] // Field alert message + warnings: string[] // Field value, is equal to values[0] + value: any // Initial value + initialValue: any // Check the rules, the specific type description refers to the following documents + rules: ValidatePatternRules[] // Is it required? + required: boolean // Whether to mount + mounted: boolean // Whether to uninstall + unmounted: boolean // field extension properties + props: FieldProps +} +``` + +#### IVirtualFieldState + +> Virtual Field core status + +```typescript +interface IVirtualFieldState { + /**Read-only status**/ + // State name, VirtualFieldState + displayName: string // Field data path + name: string // Field node path + path: string // Has been initialized + initialized: boolean /** writable status**/ // Is it visible, note: if the state is false, the UI will not be displayed, the data will not be submitted (because it is a VirtualField) + visible: boolean // Whether to show, note: if the state is false, the UI will not display, the data will not be submitted (because it is VirtualField) + display: boolean // Is it mounted? + mounted: boolean // Has been uninstalled + unmounted: boolean // field extension properties + props: FieldProps +} +``` + +#### IField/IVirtualField + +> The instance API created by using registerField/registerVirtualField + +```typescript +interface IField/IVirtualField { +   // Batch update container +   batch: (callback?: () => void) => void +   // Get the status +   getState: (callback?: (state: IFieldState) => any) => any +   // Set the status +   setState: ( +      callback?: (state: IFieldState | Draft) => void, +      silent?: boolean +   ) => void +   // Get the source status +   unsafe_getSourceState: (callback?: (state: IFieldState) => any) => any +   // Set the source state +   unsafe_setSourceState: (callback?: (state: IFieldState) => void) => void +   // Get status changes +   hasChanged: (key?: string) => boolean +   // Get the state dirty +   isDirty: (key?: string) => boolean +   // Get state dirty information +   getDirtyInfo: () => StateDirtyMap +} +``` diff --git a/packages/core/README.zh-cn.md b/packages/core/README.zh-cn.md new file mode 100644 index 00000000000..f627f75d4e3 --- /dev/null +++ b/packages/core/README.zh-cn.md @@ -0,0 +1,980 @@ +# @uform/core + +> 表单状态核心管理包(不依赖任何第三方 UI 框架),在该包中,它主要做了: +> +> - 管理 Form 状态 +> - 管理 Field 状态 +> - 管理 Validator 状态 +> - 管理 Form/Field/Validator 之间的依赖关系 + +### 安装 + +```bash +npm install --save @uform/core +``` + +### 目录 + + + +- [背景](#%E8%83%8C%E6%99%AF) +- [设计理念](#%E8%AE%BE%E8%AE%A1%E7%90%86%E5%BF%B5) +- [核心亮点](#%E6%A0%B8%E5%BF%83%E4%BA%AE%E7%82%B9) +- [架构图](#%E6%9E%B6%E6%9E%84%E5%9B%BE) +- [术语解释](#%E6%9C%AF%E8%AF%AD%E8%A7%A3%E9%87%8A) +- [API](#api) + - [`createForm`](#createform) + - [`registerValidationFormats`](#registervalidationformats) + - [`registerValidationRules`](#registervalidationrules) + - [`registerValidationMTEngine`](#registervalidationmtengine) + - [`setValidationLanguage`](#setvalidationlanguage) + - [`setValidationLocale`](#setvalidationlocale) +- [Classes](#classes) + - [`new FormPath()`](#new-formpath) + - [`new FormLifeCyle()`](#new-formlifecyle) +- [Enums](#enums) + - [LifeCycleTypes](#lifecycletypes) +- [Interfaces](#interfaces) + - [IFormCreatorOptions](#iformcreatoroptions) + - [IForm](#iform) + - [IMutators](#imutators) + - [Validator](#validator) + - [IFormState](#iformstate) + - [IFieldState](#ifieldstate) + - [IVirtualFieldState](#ivirtualfieldstate) + - [IField/IVirtualField](#ifieldivirtualfield) + + + +### 背景 + + +中后台领域,核心就是两种场景,**一个是数据录入,一个是数据查询+数据展现**,不管 +是数据录入还是数据查询,都是借助表单来实现的,从实现复杂度来看,两者复杂度相差不 +多,因为数据呈现层面难免会有极度复杂的呈现形式(比如 Tree Table 等等),但是,数据 +呈现却是最容易复用和抽象的,只有表单,会涉及大量的交互逻辑,所以,只要我们根本上 +解决了表单问题,对于中后台场景,基本上解决了大部分中后台场景问题,UForm,就是为 +此而诞生的。 + +### 设计理念 + +**Anything comes from Observable Graph.** + +### 核心亮点 + + +- 时间旅行,借助首创 Observable Graph,可以记录任意时刻的全量状态,也可以将状态 + 回滚至任意时刻,这样的能力在,重事务型应用与本地调试上可以发挥出最大价值 +- 高效更新,精确渲染,无需整树渲染 +- 内置 immer.js,智能降级,无需关心浏览器兼容性 +- 更加完备的生命周期钩子 +- 更加完备的校验引擎 + - validateFirst 校验 + - warning 校验(不阻塞提交校验) + - 校验消息模板引擎(不影响国际化文案存储的复杂校验文案消息解决方案) + - 校验规则可扩展,正则校验库可扩展 +- 更加灵活的路径解析,匹配,求值,取值引擎 + - 批量匹配数据路径能力 + - 解构求值,解构取值能力 +- 提供了基础表单状态模型之外的状态管理能力 + +### 架构图 + +![](https://img.alicdn.com/tfs/TB18LXHlVP7gK0jSZFjXXc5aXXa-1428-926.png) + +### 术语解释 + + +**FormPath/FormPathPattern** 是一个抽象数据路径形式,FormPath 是路径类 +,FormPathPattern 是可以被 FormPath 解析的路径形式,在这里主要使用了 +[cool-path](https://github.com/janrywang/cool-path) 路径解析匹配,求值取值能力 + +**VirtualField** 是一个特殊的 Field 数据结构,它与 Field 的差异就是,它不管理 +value,也就是说,它与 Form 的 value 是没有关联性的,通常我们使用它,更多的是作为 +代理一个 UI 容器的状态,比如:UForm 中的布局组件 FormBlock,它会在整个 Form +Graph 中作为一个独立节点而存在,但是这个节点类型就是一个 VirtualField,但是最终 +数据提交的时候,FormBlock 并不会污染提交数据的数据结构。 + +**Observable Graph** 是 Form 独有的观察者树,借助观察者树,可以实现很多表单相关 +的内部联动逻辑 + +**Data Path** 是 Field/VirtualField 的 name 属性,它是作为数据路径而存在 + +**Node Path** 是 Field/VirtualField 的 path 属性,它是作为节点路径而存在 + +对于数据路径和节点路径,我们可以看下面这张图: + +![](https://img.alicdn.com/tfs/TB1.rAamG61gK0jSZFlXXXDKFXa-1496-898.png) + +如果存在这样一棵树的话,那么: + +- c 字段的 name 属性则是 a.c,path 属性是 a.b.c +- b 字段的 name 属性是 a.b,path 属性是 a.b +- d 字段的 name 属性是 a.d,path 属性是 a.d +- e 字段的 name 属性是 a.d.e,path 属性是 a.d.e + + +这一来解释之后,我们就大概明白了,只要在某个节点路径中,存在 VirtualField,那么 +它的数据路径就会略过 VirtualField,但是,对于 VirtualField 自身这个节点,它的 +name 属性,是包含它自身的节点标识的,这就是为什么 b 字段的 name 属性是 a.b 的原 +因 + +### API + +--- + +#### `createForm` + +> 创建一个 Form 实例 + +**签名** + +```typescript +createForm(options?: IFormCreatorOptions): IForm +``` + +**用法** + +```typescript +import { createForm } from '@uform/core' + +const form = createForm({ + values: {}, + initialValues: {}, + onChange: values => { + console.log(values) + }, +}) + +const aa = form.registerField({ + path: 'aa' +}) + +aa.setState(state => { + state.value = 123 +}) +console.log(form.getFormState(state => state.values)) //{aa:123} +``` + +#### `registerValidationFormats` + +> 注册正则校验规则集 + +**签名** + +```typescript +registerValidationFormats(formats:{ + [formatName in string]: RegExp; +}) : void +``` + +**用法** + +```typescript +import { createForm, registerValidationFormats } from '@uform/core' + +registerValidationFormats({ + number: /^[+-]?\d+(\.\d+)?$/, +}) + +const form = createForm({ + values: {}, + initialValues: {}, + onChange: values => { + console.log(values) + }, +}) + +const aa = form.registerField({ + path: 'aa', + rules: [ + { + format: 'number', + message: 'This field is not a number.' + } + ] +}) + +aa.setState(state => { + state.value = 'hello world' +}) +form.validate() + +console.log(form.getFormState(state => state.errors)) +/** +[{ + path: 'aa', + messages: [ 'This field is not a number.' ] +}] +**/ +``` + +#### `registerValidationRules` + +> 注册校验规则集,与注册 formats 的差别是,它可以注册复杂校验规则,但是 formats +> 只是正则表达式 + +**签名** + +```typescript +registerValidationRules( + rules:{ + [ruleName:string]:(value:any,rule:ValidateDescription)=>boolean + } +) : void +``` + +**用法** + +```typescript +import { createForm, registerValidationRules } from '@uform/core' + +registerValidationRules({ + custom: value => { + return value === '123' ? 'This field can not be 123' : '' + }, +}) + +const form = createForm({ + values: {}, + initialValues: {}, + onChange: values => { + console.log(values) + }, +}) + +const aa = form.registerField({ + path: 'aa', + rules: [ + { + custom: true, + }, + ], +}) + +aa.setState(state => { + state.value = '123' +}) +form.validate() + +console.log(form.getFormState(state => state.errors)) +/** +[{ + path: 'aa', + messages: ['This field can not be 123'] +}] +**/ +``` + +#### `registerValidationMTEngine` + +> 注册校验消息模板引擎 + +**签名** + +```typescript +registerValidationMTEngine(callback:(message,context)=>any) : void +``` + +**用法** + +```javascript +import { createForm,registerValidationMTEngine } from '@uform/core' + +registerValidationMTEngine((message,context)=>{ + return message.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, $0) => { + return FormPath.getIn(context, $0) + }) +}) + +const form = createForm({ + values: {}, + initialValues: {}, + onChange: values => { + console.log(values) + } +}) + +const aa = form.registerField({ + path: 'aa', + rules: [ + { + validator(value){ + return value === 123 : 'This field can not be 123 {{scope.outerVariable}}' + }, + scope:{ + outerVariable:'addonAfter' + } + } + ] +}) + +aa.setState(state => { + state.value = '123' +}) +form.validate() + +console.log(form.getFormState(state =>state.errors)) +/** +[{ + path: 'aa', + messages: ['This field can not be 123 addonAfter'] +}] +**/ +``` + +#### `setValidationLanguage` + +> 设置国际化语言类型 + +**签名** + +```typescript +setValidationLanguage(lang: string): void +``` + +**用法** + +```javascript +import { setValidationLanguage } from '@uform/core' + +setValidationLanguage('en-US') +``` + +#### `setValidationLocale` + +> 设置语言包 + +**签名** + +```typescript +interface ILocaleMessages { + [key: string]: string | ILocaleMessages; +} +interface ILocales { + [lang: string]: ILocaleMessages; +} +setValidationLocale(locale: ILocales) => void +``` + +**用法** + +```javascript +import { setValidationLocale } from '@uform/core' + +setValidationLocale({ + 'en-US': { + required: 'This field is required.' + } +}) +``` + +### Classes + +#### `new FormPath()` + +> 表单路径引擎,核心负责路径解析,匹配,求值,取值,解构求值,解构取值 + +具体文档参考:https://github.com/janrywang/cool-path + +#### `new FormLifeCyle()` + +> 创建一个生命周期监听器 + +**签名** + +```typescript +type FormLifeCycleHandler = (payload: T, context: any) => void + +new FormLifeCycle(handler: FormLifeCycleHandler) +new FormLifeCycle(...type: LifeCycleTypes, handler: FormLifeCycleHandler...) +new FormLifeCycle(handlerMap: { [key: LifeCycleTypes]: FormLifeCycleHandler }) +``` + +**用法** + +```typescript +import { createForm,FormLifeCycle,LifeCycleTypes } from '@uform/core' + +const form = createForm({ + lifecycles:[ + new FormLifeCycle(({type:LifeCycleTypes,payload:IForm | IField | IVirtualField })=>{ + //上帝模式,全量监听 + }), + new FormLifeCycle( + LifeCycleTypes.ON_FORM_MOUNT, + (payload:IForm | IField | IVirtualField)=>{ + //精确监听 + }), + new FormLifeCycle({ + [LifeCycleTypes.ON_FORM_MOUNT]:(payload:IForm | IField | IVirtualField)=>{ + //对象形式精确监听 + } + }), + ] +}) +``` + +### Enums + +--- + +#### LifeCycleTypes + +```typescript +enum LifeCycleTypes { + /** + * Form LifeCycle + **/ + + ON_FORM_WILL_INIT = 'onFormWillInit', //表单预初始化触发 + ON_FORM_INIT = 'onFormInit', //表单初始化触发 + ON_FORM_CHANGE = 'onFormChange', //表单变化时触发 + ON_FORM_MOUNT = 'onFormMount', //表单挂载时触发 + ON_FORM_UNMOUNT = 'onFormUnmount', //表单卸载时触发 + ON_FORM_SUBMIT = 'onFormSubmit', //表单提交时触发 + ON_FORM_RESET = 'onFormReset', //表单重置时触发 + ON_FORM_SUBMIT_START = 'onFormSubmitStart', //表单提交开始时触发 + ON_FORM_SUBMIT_END = 'onFormSubmitEnd', //表单提交结束时触发 + ON_FORM_VALUES_CHANGE = 'onFormValuesChange', //表单值变化时触发 + ON_FORM_INITIAL_VALUES_CHANGE = 'onFormInitialValuesChange', //表单初始值变化时触发 + ON_FORM_VALIDATE_START = 'onFormValidateStart', //表单校验开始时触发 + ON_FORM_VALIDATE_END = 'onFormValidateEnd', //表单校验结束时触发 + ON_FORM_INPUT_CHANGE = 'onFormInputChange', //表单事件触发时触发,用于只监控人工操作 + /** + * FormGraph LifeCycle + **/ + ON_FORM_GRAPH_CHANGE = 'onFormGraphChange', //表单观察者树变化时触发 + + /** + * Field LifeCycle + **/ + + ON_FIELD_WILL_INIT = 'onFieldWillInit', //字段预初始化时触发 + ON_FIELD_INIT = 'onFieldInit', //字段初始化时触发 + ON_FIELD_CHANGE = 'onFieldChange', //字段变化时触发 + ON_FIELD_INPUT_CHANGE = 'onFieldInputChange', //字段事件触发时触发,用于只监控人工操作 + ON_FIELD_VALUE_CHANGE = 'onFieldValueChange', //字段值变化时触发 + ON_FIELD_INITIAL_VALUE_CHANGE = 'onFieldInitialValueChange', //字段初始值变化时触发 + ON_FIELD_MOUNT = 'onFieldMount', //字段挂载时触发 + ON_FIELD_UNMOUNT = 'onFieldUnmount' //字段卸载时触发 +} +``` + +### Interfaces + +--- + +#### IFormCreatorOptions + +> createForm 参数对象协议 + +```typescript +interface IFormCreatorOptions { + //初始值 + initialValues?: {} + //值 + values?: {} + //生命周期监听器,在这里主要传入FormLifeCycle的实例化对象 + lifecycles?: FormLifeCycle[] + //是否可编辑,在Form维度整体控制 + editable?: boolean | ((name: string) => boolean) + //是否使用脏检查,默认会走immer精确更新 + useDirty?: boolean + //是否走悲观校验,遇到第一个校验失败就停止后续校验 + validateFirst?: boolean + //表单变化事件回调 + onChange?: (values: IFormState['values']) => void + //表单提交事件回调 + onSubmit?: (values: IFormState['values']) => any | Promise + //表单重置事件回调 + onReset?: () => void + //表单校验失败事件回调 + onValidateFailed?: (validated: IFormValidateResult) => void +} +``` + +#### IForm + +> 通过 createForm 创建出来的 Form 实例对象 API + +```typescript +interface IForm { + /* + * 表单提交,如果回调参数返回Promise, + * 那么整个提交流程会hold住,同时loading为true, + * 等待Promise resolve才触发表单onFormSubmitEnd事件,同时loading为false + */ + submit( + onSubmit?: (values: IFormState['values']) => any | Promise + ): Promise<{ + validated: IFormValidateResult + payload: any //onSubmit回调函数返回值 + }> + + /* + * 清空错误消息,可以通过传FormPathPattern来批量或精确控制要清空的字段, + * 比如clearErrors("*(aa,bb,cc)") + */ + clearErrors: (pattern?: FormPathPattern) => void + + /* + * 获取状态变化情况,主要用于在表单生命周期钩子内判断当前生命周期中有哪些状态发生了变化, + * 比如hasChanged(state,'value.aa') + */ + hasChanged( + target: IFormState | IFieldState | IVirtualFieldState, + path: FormPathPattern + ): boolean + + /* + * 重置表单 + */ + reset(options?: { + //强制清空 + forceClear?: boolean + //强制校验 + validate?: boolean + //重置范围,用于批量或者精确控制要重置的字段 + selector?: FormPathPattern + }): Promise + + /* + * 校验表单 + */ + validate( + path?: FormPathPattern, + options?: { + //是否悲观校验,如果当前字段遇到第一个校验错误则停止后续校验流程 + first?: boolean + } + ): Promise + + /* + * 设置表单状态 + */ + setFormState( + //操作回调 + callback?: (state: IFormState) => any, + //是否不触发事件 + silent?: boolean + ): void + + /* + * 获取表单状态 + */ + getFormState( + //transformer + callback?: (state: IFormState) => any + ): any + + /* + * 设置字段状态 + */ + setFieldState( + //字段路径 + path: FormPathPattern, + //操作回调 + callback?: (state: IFieldState) => void, + //是否不触发事件 + silent?: boolean + ): void + + /* + * 获取字段状态 + */ + getFieldState( + //字段路径 + path: FormPathPattern, + //transformer + callback?: (state: IFieldState) => any + ): any + + /* + * 注册字段 + */ + registerField(props: { + //节点路径 + path?: FormPathPattern + //数据路径 + name?: string + //字段值 + value?: any + //字段多参值 + values?: any[] + //字段初始值 + initialValue?: any + //数据与样式是否可见 + visible?: boolean + //样式是否可见 + display?: boolean + //字段扩展属性 + props?: any + //字段校验规则 + rules?: ValidatePatternRules[] + //字段是否必填 + required?: boolean + //字段是否可编辑 + editable?: boolean + //字段是否走脏检查 + useDirty?: boolean + //字段状态计算容器,主要用于扩展核心联动规则 + computeState?: (draft: IFieldState, prevState: IFieldState) => void + }): IField + + /* + * 注册虚拟字段 + */ + registerVirtualField(props: { + //节点路径 + path?: FormPathPattern + //数据路径 + name?: string + //字段扩展属性 + props?: any + //字段是否走脏检查 + useDirty?: boolean + //字段状态计算容器,主要用于扩展核心联动规则 + computeState?: (draft: IFieldState, prevState: IFieldState) => void + }): IVirtualField + + /* + * 创建字段数据操作器,后面会详细解释返回的API + */ + createMutators(field: IField): IMutators + + /* + * 获取表单观察者树 + */ + getFormGraph(): IFormGraph + + /* + * 设置表单观察者树 + */ + setFormGraph(graph: IFormGraph): void + + /* + * 监听表单生命周期 + */ + subscribe( + callback?: ({ type, payload }: { type: string; payload: any }) => void + ): number + + /* + * 取消监听表单生命周期 + */ + unsubscribe(id: number): void + + /* + * 触发表单自定义生命周期 + */ + notify: (type: string, payload?: T) => void + + /* + * 设置字段值 + */ + setFieldValue(path?: FormPathPattern, value?: any): void + + /* + * 获取字段值 + */ + getFieldValue(path?: FormPathPattern): any + + /* + * 设置字段初始值 + */ + setFieldInitialValue(path?: FormPathPattern, value?: any): void + + /* + * 获取字段初始值 + */ + getFieldInitialValue(path?: FormPathPattern): any +} +``` + +#### IMutators + +> 通过 createMutators 创建出来的实例 API,主要用于操作字段数据 + +```typescript +interface IMutators { + //改变字段值,多参情况,会将所有参数存在values中 + change(...values: any[]): any + //获取焦点,触发active状态改变 + focus(): void + //失去焦点,触发active/visited状态改变 + blur(): void + //触发当前字段校验器 + validate(): Promise + //当前字段的值是否在Form的values属性中存在 + exist(index?: number | string): boolean + + /**数组操作方法**/ + + //追加数据 + push(value?: any): any[] + //弹出尾部数据 + pop(): any[] + //插入数据 + insert(index: number, value: any): any[] + //删除数据 + remove(index: number | string): any + //头部插入 + unshift(value: any): any[] + //头部弹出 + shift(): any[] + //移动元素 + move($from: number, $to: number): any[] + //下移 + moveDown(index: number): any[] + //上移 + moveUp(index: number): any[] +} +``` + +#### Validator + +> 这里主要列举校验相关的中间类型签名 + +```typescript +type CustomValidator = ( + value: any, + rescription?: ValidateDescription +) => ValidateResponse +type SyncValidateResponse = + | null + | string + | boolean + | { + type?: 'error' | 'warning' + message: string + } +type AsyncValidateResponse = Promise +type ValidateResponse = SyncValidateResponse | AsyncValidateResponse + +interface IFormValidateResult { + errors: Array<{ + path: string + messages: string[] + }> + warnings: Array<{ + path: string + messages: string[] + }> +} + +type InternalFormats = + | 'url' + | 'email' + | 'ipv6' + | 'ipv4' + | 'idcard' + | 'taodomain' + | 'qq' + | 'phone' + | 'money' + | 'zh' + | 'date' + | 'zip' + | string + +interface ValidateDescription { + //正则规则类型 + format?: InternalFormats + //自定义校验规则 + validator?: CustomValidator + //是否必填 + required?: boolean + //自定以正则 + pattern?: RegExp | string + //最大长度规则 + max?: number + //最大数值规则 + maximum?: number + //封顶数值规则 + exclusiveMaximum?: number + //封底数值规则 + exclusiveMinimum?: number + //最小数值规则 + minimum?: number + //最小长度规则 + min?: number + //长度规则 + len?: number + //是否校验空白符 + whitespace?: boolean + //枚举校验规则 + enum?: any[] + //自定义错误文案 + message?: string + //自定义校验规则 + [key: string]: any +} +``` + +#### IFormState + +> Form 核心状态 + +```typescript +interface IFormState { + /**只读属性**/ + + //是否处于原始态,只有values===initialValues时,pristine为true + pristine: boolean + //是否合法,只要errors长度大于0的时候valid为false + valid: boolean + //是否非法,只要errors长度大于0的时候valid为true + invalid: boolean + //是否处于校验态,只有在调用validate API的时候才会被设置 + validating: boolean + //是否处于提交态,只有在调用submit API的时候才会被设置 + submitting: boolean + //错误消息了列表 + errors: string[] + //告警消息列表 + warnings: string[] + + /**可写属性**/ + + //是否处于加载态,可写状态,只要validating为true时,该状态也会为true,为false时同理 + loading: boolean + //是否处于初始态 + initialized: boolean + //是否可编辑 + editable: boolean | ((name: string) => boolean) + //表单值 + values: {} + //表单初始值 + initialValues: {} + //表单挂载,前面讲到的生命周期钩子,是必须通过设置该状态来触发的,默认不会触发 + mounted: boolean + //表单卸载,前面讲到的生命周期钩子,是必须通过设置该状态来触发的,默认不会触发 + unmounted: boolean + //表单扩展属性 + props: FormProps +} +``` + +#### IFieldState + +> 核心 Field 状态 + +```typescript +interface IFieldState { + /**只读属性**/ + + //状态名称,FieldState + displayName?: string + //数据路径 + name: string + //节点路径 + path: string + //是否已经初始化 + initialized: boolean + //是否处于原始态,只有value===intialValues时的时候该状态为true + pristine: boolean + //是否处于合法态,只要errors长度大于0的时候valid为false + valid: boolean + //是否处于非法态,只要errors长度大于0的时候valid为true + invalid: boolean + //是否处于校验态 + validating: boolean + //是否被修改,如果值发生变化,该属性为true,同时在整个字段的生命周期内都会为true + modified: boolean + //是否被触碰 + touched: boolean + //是否被激活,字段触发onFocus事件的时候,它会被触发为true,触发onBlur时,为false + active: boolean + //是否访问过,字段触发onBlur事件的时候,它会被触发为true + visited: boolean + + /**可写属性**/ + + //是否可见,注意:该状态如果为false,那么字段的值不会被提交,同时UI不会显示 + visible: boolean + //是否展示,注意:该状态如果为false,那么字段的值会提交,UI不会展示,类似于表单隐藏域 + display: boolean + //是否可编辑 + editable: boolean + //是否处于loading状态,注意:如果字段处于异步校验时,loading为true + loading: boolean + //字段多参值,比如字段onChange触发时,给事件回调传了多参数据,那么这里会存储所有参数的值 + values: any[] + //字段错误消息 + errors: string[] + //字段告警消息 + warnings: string[] + //字段值,与values[0]是恒定相等 + value: any + //初始值 + initialValue: any + //校验规则,具体类型描述参考后面文档 + rules: ValidatePatternRules[] + //是否必填 + required: boolean + //是否挂载 + mounted: boolean + //是否卸载 + unmounted: boolean + //字段扩展属性 + props: FieldProps +} +``` + +#### IVirtualFieldState + +> 虚拟 Field 核心状态 + +```typescript +interface IVirtualFieldState { + /**只读状态**/ + + //状态名称,VirtualFieldState + displayName: string + //字段数据路径 + name: string + //字段节点路径 + path: string + //是否已经初始化 + initialized: boolean + + /**可写状态**/ + + //是否可见,注意:该状态如果为false,UI不会显示,数据也不会提交(因为它是VirtualField) + visible: boolean + //是否展示,注意:该状态如果为false,UI不会显示,数据也不会提交(因为它是VirtualField) + display: boolean + //是否已挂载 + mounted: boolean + //是否已卸载 + unmounted: boolean + //字段扩展属性 + props: FieldProps +} +``` + +#### IField/IVirtualField + +> 通过 registerField/registerVirtualField 创建出来的实例 API + +```typescript +interface IField/IVirtualField { + //批量更新容器 + batch: (callback?: () => void) => void + //获取状态 + getState: (callback?: (state: IFieldState) => any) => any + //设置状态 + setState: (callback?: (state: IFieldState | Draft) => void, silent?: boolean) => void + //获取源状态 + unsafe_getSourceState: (callback?: (state: IFieldState) => any) => any + //设置源状态 + unsafe_setSourceState: (callback?: (state: IFieldState) => void) => void + //获取状态变化情况 + hasChanged: (key?: string) => boolean + //获取状态脏 + isDirty: (key?: string) => boolean + //获取状态脏信息 + getDirtyInfo: () => StateDirtyMap +} +``` diff --git a/packages/core/package.json b/packages/core/package.json index 762f749b705..8ee9ef89d84 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@uform/core", - "version": "0.4.4", + "version": "1.0.0-alpha.5", "license": "MIT", "main": "lib", "repository": { @@ -16,7 +16,7 @@ "npm": ">=3.0.0" }, "scripts": { - "build": "tsc --declaration" + "build": "rm -rf lib && tsc --declaration" }, "devDependencies": { "typescript": "^3.5.2" @@ -26,11 +26,10 @@ "scheduler": ">=0.11.2" }, "dependencies": { - "@uform/types": "^0.4.4", - "@uform/utils": "^0.4.4", - "@uform/validator": "^0.4.4", - "dot-match": "^0.1.18", - "rxjs": "^6.3.3" + "@uform/shared": "^1.0.0-alpha.5", + "@uform/types": "^0.4.0", + "@uform/validator": "^1.0.0-alpha.5", + "immer": "^3.2.0" }, "publishConfig": { "access": "public" diff --git a/packages/core/src/__test__/form.spec.js b/packages/core/src/__test__/form.spec.js deleted file mode 100644 index 097cf68678f..00000000000 --- a/packages/core/src/__test__/form.spec.js +++ /dev/null @@ -1,46 +0,0 @@ -import { createForm } from '../index' - -test('Increase lastValidateValue value processing during initialization', async () => { - const inpueFieldValidate = jest.fn() - const requriedFieldValidate = jest.fn() - - const form = createForm({ - initialValues: { - requriedField: 'defaultValue' - } - }) - - form.registerField('inpueField', { - props: { - requried: true, - 'x-rules': () => - new Promise(resolve => { - inpueFieldValidate() - - resolve() - }) - } - }) - - form.registerField('requriedField', { - props: { - requried: true, - 'x-rules': () => - new Promise(resolve => { - requriedFieldValidate() - - resolve() - }) - } - }) - - form.setValue('inpueField', 1111) - await sleep(1000) - expect(inpueFieldValidate).toHaveBeenCalledTimes(1) - expect(requriedFieldValidate).toHaveBeenCalledTimes(0) - - form.setValue('requriedField', 2222) - await sleep(1000) - expect(inpueFieldValidate).toHaveBeenCalledTimes(1) - expect(requriedFieldValidate).toHaveBeenCalledTimes(1) -}) diff --git a/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap b/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap new file mode 100644 index 00000000000..ee40161fe86 --- /dev/null +++ b/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap @@ -0,0 +1,5434 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`createForm initialValue 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": 111, + "bb": 222, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": 111, + "bb": 222, + }, + "warnings": Array [], + }, +} +`; + +exports[`createForm initialValues after init 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": 111, + "bb": 222, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": 111, + "bb": 222, + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 111, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 111, + "values": Array [ + 111, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 222, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "bb", + "path": "bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 222, + "values": Array [ + 222, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`createForm initialValues on init 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": 111, + "bb": 222, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": 111, + "bb": 222, + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 111, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 111, + "values": Array [ + 111, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 222, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "bb", + "path": "bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 222, + "values": Array [ + 222, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`createForm lifecycles 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": 111, + "bb": 222, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": "change aa", + "bb": "change bb", + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 111, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "change aa", + "values": Array [ + "change aa", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 222, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "bb", + "path": "bb", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "change bb", + "values": Array [ + "change bb", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`createForm merge values and initialValues 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "a": "x", + "b": "y", + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "a": 1, + "b": 2, + }, + "warnings": Array [], + }, +} +`; + +exports[`createForm values 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": 111, + "bb": 222, + }, + "warnings": Array [], + }, +} +`; + +exports[`createMutators blur 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object {}, + "warnings": Array [], + }, + "a": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "a", + "path": "a", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": undefined, + "values": Array [ + undefined, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`createMutators blur 2`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object {}, + "warnings": Array [], + }, + "a": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "a", + "path": "a", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": undefined, + "values": Array [ + undefined, + ], + "visible": true, + "visited": true, + "warnings": Array [], + }, +} +`; + +exports[`createMutators focus 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object {}, + "warnings": Array [], + }, + "a": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "a", + "path": "a", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": undefined, + "values": Array [ + undefined, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`createMutators focus 2`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object {}, + "warnings": Array [], + }, + "a": Object { + "active": true, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "a", + "path": "a", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": undefined, + "values": Array [ + undefined, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`graph getFormGraph 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": 111, + "bb": 222, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": "change aa", + "bb": "change bb", + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 111, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "change aa", + "values": Array [ + "change aa", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 222, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "bb", + "path": "bb", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "change bb", + "values": Array [ + "change bb", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`graph setFormGraph 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": 111, + "bb": 222, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "bb": "change bb", + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 111, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "change aa", + "values": Array [ + "change aa", + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, + "bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 222, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "bb", + "path": "bb", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "change bb", + "values": Array [ + "change bb", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`major sences deep nested visible with VirtualField 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object {}, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "cc": 123, + }, + "values": Array [ + Object { + "cc": 123, + }, + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "display": true, + "displayName": "VirtualFieldState", + "initialized": true, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "props": Object {}, + "unmounted": false, + "visible": false, + }, + "aa.bb.cc": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.cc", + "path": "aa.bb.cc", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 123, + "values": Array [ + 123, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`major sences deep nested visible(middle part) 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Object {}, + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": Object { + "cc": 123, + }, + }, + "values": Array [ + Object { + "bb": Object { + "cc": 123, + }, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "cc": 123, + }, + "values": Array [ + Object { + "cc": 123, + }, + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, + "aa.bb.cc": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.cc", + "path": "aa.bb.cc", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 123, + "values": Array [ + 123, + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`major sences deep nested visible(root) 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object {}, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": Object { + "cc": 123, + }, + }, + "values": Array [ + Object { + "bb": Object { + "cc": 123, + }, + }, + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "cc": 123, + }, + "values": Array [ + Object { + "cc": 123, + }, + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, + "aa.bb.cc": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.cc", + "path": "aa.bb.cc", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 123, + "values": Array [ + 123, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`major sences dynamic remove with intialValues 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": Array [ + Object { + "aa": 123, + "bb": 321, + }, + Object { + "aa": 345, + "bb": 678, + }, + ], + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Array [ + Object { + "aa": 123, + "bb": 321, + }, + Object { + "aa": "change aa", + "bb": 678, + }, + ], + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Array [ + Object { + "aa": 123, + "bb": 321, + }, + Object { + "aa": 345, + "bb": 678, + }, + ], + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [ + Object { + "aa": 123, + "bb": 321, + }, + Object { + "aa": "change aa", + "bb": 678, + }, + ], + "values": Array [ + Array [ + Object { + "aa": 123, + "bb": 321, + }, + Object { + "aa": "change aa", + "bb": 678, + }, + ], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 123, + "bb": 321, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.0", + "path": "aa.0", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": 123, + "bb": 321, + }, + "values": Array [ + Object { + "aa": 123, + "bb": 321, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 123, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.0.aa", + "path": "aa.0.aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 123, + "values": Array [ + 123, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 321, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.0.bb", + "path": "aa.0.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 321, + "values": Array [ + 321, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.1": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 345, + "bb": 678, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.1", + "path": "aa.1", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": "change aa", + "bb": 678, + }, + "values": Array [ + Object { + "aa": "change aa", + "bb": 678, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.1.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 345, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.1.aa", + "path": "aa.1.aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "change aa", + "values": Array [ + "change aa", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.1.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 678, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.1.bb", + "path": "aa.1.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 678, + "values": Array [ + 678, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`major sences dynamic remove with intialValues 2`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": Array [ + Object { + "aa": 123, + "bb": 321, + }, + Object { + "aa": 345, + "bb": 678, + }, + ], + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Array [ + Object { + "aa": "change aa", + "bb": 678, + }, + ], + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Array [ + Object { + "aa": 123, + "bb": 321, + }, + Object { + "aa": 345, + "bb": 678, + }, + ], + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [ + Object { + "aa": "change aa", + "bb": 678, + }, + ], + "values": Array [ + Array [ + Object { + "aa": "change aa", + "bb": 678, + }, + ], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 123, + "bb": 321, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.0", + "path": "aa.0", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": "change aa", + "bb": 678, + }, + "values": Array [ + Object { + "aa": "change aa", + "bb": 678, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 123, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.0.aa", + "path": "aa.0.aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "change aa", + "values": Array [ + "change aa", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 321, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.0.bb", + "path": "aa.0.bb", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 678, + "values": Array [ + 678, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`major sences dynamic remove with intialValues 3`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": Array [ + Object { + "aa": 123, + "bb": 321, + }, + Object { + "aa": 345, + "bb": 678, + }, + ], + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Array [ + Object { + "aa": 123, + "bb": 321, + }, + Object { + "aa": "change aa", + "bb": 678, + }, + ], + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Array [ + Object { + "aa": 123, + "bb": 321, + }, + Object { + "aa": 345, + "bb": 678, + }, + ], + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [ + Object { + "aa": 123, + "bb": 321, + }, + Object { + "aa": "change aa", + "bb": 678, + }, + ], + "values": Array [ + Array [ + Object { + "aa": 123, + "bb": 321, + }, + Object { + "aa": "change aa", + "bb": 678, + }, + ], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 123, + "bb": 321, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.0", + "path": "aa.0", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": 123, + "bb": 321, + }, + "values": Array [ + Object { + "aa": 123, + "bb": 321, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 123, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.0.aa", + "path": "aa.0.aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 123, + "values": Array [ + 123, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 321, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.0.bb", + "path": "aa.0.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 321, + "values": Array [ + 321, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.1": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 345, + "bb": 678, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.1", + "path": "aa.1", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": "change aa", + "bb": 678, + }, + "values": Array [ + Object { + "aa": "change aa", + "bb": 678, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.1.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 345, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.1.aa", + "path": "aa.1.aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "change aa", + "values": Array [ + "change aa", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.1.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 678, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.1.bb", + "path": "aa.1.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 678, + "values": Array [ + 678, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`major sences nested dynamic remove 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Array [ + undefined, + Object { + "aa": "change aa", + }, + ], + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [ + undefined, + Object { + "aa": "change aa", + }, + ], + "values": Array [ + Array [ + undefined, + Object { + "aa": "change aa", + }, + ], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.0", + "path": "aa.0", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": undefined, + "values": Array [ + undefined, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.0.aa", + "path": "aa.0.aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": undefined, + "values": Array [ + undefined, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.0.bb", + "path": "aa.0.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": undefined, + "values": Array [ + undefined, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.1": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.1", + "path": "aa.1", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": "change aa", + }, + "values": Array [ + Object { + "aa": "change aa", + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.1.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.1.aa", + "path": "aa.1.aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "change aa", + "values": Array [ + "change aa", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.1.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.1.bb", + "path": "aa.1.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": undefined, + "values": Array [ + undefined, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`major sences nested dynamic remove 2`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Array [ + Object { + "aa": "change aa", + }, + ], + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [ + Object { + "aa": "change aa", + }, + ], + "values": Array [ + Array [ + Object { + "aa": "change aa", + }, + ], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.0", + "path": "aa.0", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": "change aa", + }, + "values": Array [ + Object { + "aa": "change aa", + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.0.aa", + "path": "aa.0.aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "change aa", + "values": Array [ + "change aa", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.0.bb", + "path": "aa.0.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": undefined, + "values": Array [ + undefined, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`major sences nested dynamic remove 3`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Array [ + undefined, + Object { + "aa": "change aa", + }, + ], + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [ + undefined, + Object { + "aa": "change aa", + }, + ], + "values": Array [ + Array [ + undefined, + Object { + "aa": "change aa", + }, + ], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.0", + "path": "aa.0", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": undefined, + "values": Array [ + undefined, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.0.aa", + "path": "aa.0.aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": undefined, + "values": Array [ + undefined, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.0.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.0.bb", + "path": "aa.0.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": undefined, + "values": Array [ + undefined, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.1": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.1", + "path": "aa.1", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": "change aa", + }, + "values": Array [ + Object { + "aa": "change aa", + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.1.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.1.aa", + "path": "aa.1.aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "change aa", + "values": Array [ + "change aa", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.1.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.1.bb", + "path": "aa.1.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": undefined, + "values": Array [ + undefined, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`major sences nested visible 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": Object { + "bb": 123, + "cc": 222, + }, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object {}, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "bb": 123, + "cc": 222, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": 123, + "cc": 222, + }, + "values": Array [ + Object { + "bb": 123, + "cc": 222, + }, + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 123, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 123, + "values": Array [ + 123, + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, + "aa.cc": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 222, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.cc", + "path": "aa.cc", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 222, + "values": Array [ + 222, + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`major sences nested visible 2`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": Object { + "bb": 123, + "cc": 222, + }, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object {}, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "bb": 123, + "cc": 222, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": 123, + "cc": 222, + }, + "values": Array [ + Object { + "bb": 123, + "cc": 222, + }, + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 123, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 123, + "values": Array [ + 123, + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, + "aa.cc": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 222, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.cc", + "path": "aa.cc", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 222, + "values": Array [ + 222, + ], + "visible": false, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`major sences nested visible 3`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": Object { + "bb": 123, + "cc": 222, + }, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Object { + "bb": 123, + "cc": 222, + }, + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "bb": 123, + "cc": 222, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": 123, + "cc": 222, + }, + "values": Array [ + Object { + "bb": 123, + "cc": 222, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 123, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 123, + "values": Array [ + 123, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.cc": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 222, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.cc", + "path": "aa.cc", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 222, + "values": Array [ + 222, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`reset array reset forceclear 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + "values": Array [ + Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + "values": Array [ + Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.0": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 123, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.0", + "path": "aa.bb.0", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": 123, + }, + "values": Array [ + Object { + "aa": 123, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.0.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 123, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.0.aa", + "path": "aa.bb.0.aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 123, + "values": Array [ + 123, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.1": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 321, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.1", + "path": "aa.bb.1", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": 321, + }, + "values": Array [ + Object { + "aa": 321, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.1.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 321, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.1.aa", + "path": "aa.bb.1.aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 321, + "values": Array [ + 321, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`reset array reset forceclear 2`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Object { + "bb": Array [], + }, + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": Array [], + }, + "values": Array [ + Object { + "bb": Array [], + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [], + "values": Array [ + Array [], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`reset array reset no forceclear(initial values) 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + "values": Array [ + Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + "values": Array [ + Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.0": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 123, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.0", + "path": "aa.bb.0", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": 123, + }, + "values": Array [ + Object { + "aa": 123, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.0.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 123, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.0.aa", + "path": "aa.bb.0.aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 123, + "values": Array [ + 123, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.1": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 321, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.1", + "path": "aa.bb.1", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": 321, + }, + "values": Array [ + Object { + "aa": 321, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.1.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 321, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.1.aa", + "path": "aa.bb.1.aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 321, + "values": Array [ + 321, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`reset array reset no forceclear(initial values) 2`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Object { + "bb": Array [ + Object { + "aa": "aa changed", + }, + Object { + "aa": 321, + }, + ], + }, + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": Array [ + Object { + "aa": "aa changed", + }, + Object { + "aa": 321, + }, + ], + }, + "values": Array [ + Object { + "bb": Array [ + Object { + "aa": "aa changed", + }, + Object { + "aa": 321, + }, + ], + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [ + Object { + "aa": "aa changed", + }, + Object { + "aa": 321, + }, + ], + "values": Array [ + Array [ + Object { + "aa": "aa changed", + }, + Object { + "aa": 321, + }, + ], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.0": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 123, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.bb.0", + "path": "aa.bb.0", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": "aa changed", + }, + "values": Array [ + Object { + "aa": "aa changed", + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.0.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 123, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.bb.0.aa", + "path": "aa.bb.0.aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "aa changed", + "values": Array [ + "aa changed", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.1": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 321, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.1", + "path": "aa.bb.1", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": 321, + }, + "values": Array [ + Object { + "aa": 321, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.1.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 321, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.1.aa", + "path": "aa.bb.1.aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 321, + "values": Array [ + 321, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`reset array reset no forceclear(initial values) 3`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object { + "aa": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + }, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": true, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + "values": Array [ + Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + "values": Array [ + Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.0": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 123, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.0", + "path": "aa.bb.0", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": 123, + }, + "values": Array [ + Object { + "aa": 123, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.0.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 123, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.0.aa", + "path": "aa.bb.0.aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 123, + "values": Array [ + 123, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.1": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": Object { + "aa": 321, + }, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.1", + "path": "aa.bb.1", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": 321, + }, + "values": Array [ + Object { + "aa": 321, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.1.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": 321, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.1.aa", + "path": "aa.bb.1.aa", + "pristine": true, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 321, + "values": Array [ + 321, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`reset array reset no forceclear(values) 1`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + "values": Array [ + Object { + "bb": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + "values": Array [ + Array [ + Object { + "aa": 123, + }, + Object { + "aa": 321, + }, + ], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.0": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.0", + "path": "aa.bb.0", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": 123, + }, + "values": Array [ + Object { + "aa": 123, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.0.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.0.aa", + "path": "aa.bb.0.aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 123, + "values": Array [ + 123, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.1": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.1", + "path": "aa.bb.1", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": 321, + }, + "values": Array [ + Object { + "aa": 321, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.1.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.1.aa", + "path": "aa.bb.1.aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 321, + "values": Array [ + 321, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`reset array reset no forceclear(values) 2`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Object { + "bb": Array [ + Object { + "aa": "aa changed", + }, + Object { + "aa": 321, + }, + ], + }, + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": Array [ + Object { + "aa": "aa changed", + }, + Object { + "aa": 321, + }, + ], + }, + "values": Array [ + Object { + "bb": Array [ + Object { + "aa": "aa changed", + }, + Object { + "aa": 321, + }, + ], + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [ + Object { + "aa": "aa changed", + }, + Object { + "aa": 321, + }, + ], + "values": Array [ + Array [ + Object { + "aa": "aa changed", + }, + Object { + "aa": 321, + }, + ], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.0": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.bb.0", + "path": "aa.bb.0", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": "aa changed", + }, + "values": Array [ + Object { + "aa": "aa changed", + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.0.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.bb.0.aa", + "path": "aa.bb.0.aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": "aa changed", + "values": Array [ + "aa changed", + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.1": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.1", + "path": "aa.bb.1", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "aa": 321, + }, + "values": Array [ + Object { + "aa": 321, + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb.1.aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": false, + "mounted": false, + "name": "aa.bb.1.aa", + "path": "aa.bb.1.aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": 321, + "values": Array [ + 321, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; + +exports[`reset array reset no forceclear(values) 3`] = ` +Object { + "": Object { + "displayName": "FormState", + "editable": undefined, + "errors": Array [], + "initialValues": Object {}, + "initialized": true, + "invalid": false, + "loading": false, + "mounted": false, + "pristine": false, + "props": Object {}, + "submitting": false, + "unmounted": false, + "valid": true, + "validating": false, + "values": Object { + "aa": Object { + "bb": Array [], + }, + }, + "warnings": Array [], + }, + "aa": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa", + "path": "aa", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Object { + "bb": Array [], + }, + "values": Array [ + Object { + "bb": Array [], + }, + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, + "aa.bb": Object { + "active": false, + "display": true, + "displayName": "FieldState", + "editable": true, + "effectErrors": Array [], + "effectWarnings": Array [], + "errors": Array [], + "formEditable": undefined, + "initialValue": undefined, + "initialized": true, + "invalid": false, + "loading": false, + "modified": true, + "mounted": false, + "name": "aa.bb", + "path": "aa.bb", + "pristine": false, + "props": Object {}, + "required": false, + "ruleErrors": Array [], + "ruleWarnings": Array [], + "rules": Array [], + "selfEditable": undefined, + "touched": false, + "unmounted": false, + "valid": true, + "validating": false, + "value": Array [], + "values": Array [ + Array [], + ], + "visible": true, + "visited": false, + "warnings": Array [], + }, +} +`; diff --git a/packages/core/src/__tests__/field.state.spec.ts b/packages/core/src/__tests__/field.state.spec.ts new file mode 100644 index 00000000000..329b075a788 --- /dev/null +++ b/packages/core/src/__tests__/field.state.spec.ts @@ -0,0 +1,408 @@ +import { FieldState } from '../state/field' + +test('computeState setValues', () => { + const state = new FieldState({ useDirty: false }) + expect(state.getState().visible).toEqual(true) + expect(state.getState().value).toEqual(undefined) + //如果是隐藏状态,则禁止修改值 + state.setState((draft) => { + draft.visible = false + draft.value = 123 + draft.initialValue = 456 + }) + expect(state.getState().unmounted).toEqual(false) + expect(state.getState().visible).toEqual(false) + expect(state.getState().value).toEqual(undefined) + expect(state.getState().initialValue).toEqual(undefined) + state.setState((draft) => { + draft.visible = true + draft.unmounted = true + draft.value = 123 + draft.initialValue = 456 + }) + expect(state.getState().visible).toEqual(true) + expect(state.getState().unmounted).toEqual(true) + expect(state.getState().value).toEqual(undefined) + expect(state.getState().initialValue).toEqual(undefined) + state.setState((draft) => { + draft.visible = true + draft.unmounted = false + draft.value = 123 + draft.initialValue = 456 + }) + expect(state.getState().unmounted).toEqual(false) + expect(state.getState().visible).toEqual(true) + expect(state.getState().value).toEqual(123) + expect(state.getState().initialValue).toEqual(456) +}) + +test('computeState value and readValues', () => { + const state = new FieldState({ useDirty: false }) + expect(state.getState().value).toEqual(undefined) + expect(state.getState().values).toEqual([]) + expect(state.getState().initialized).toEqual(false) + expect(state.getState().modified).toEqual(false) + + // modified depends on initialized and value change + // invalid values and + state.setState((draft) => { + draft.values = undefined + draft.value = 123 + draft.initialized = true + }) + + expect(state.getState().value).toEqual(123) + expect(state.getState().values).toEqual([123]) + expect(state.getState().initialized).toEqual(true) + expect(state.getState().modified).toEqual(false) + + // valid values + state.setState((draft) => { + draft.values = [1,2,3] + draft.value = 456 + }) + expect(state.getState().modified).toEqual(true) + expect(state.getState().value).toEqual(456) + expect(state.getState().values).toEqual([456,2,3]) +}) + +test('computeState editable', () => { + const state = new FieldState({ useDirty: false }) + expect(state.getState().editable).toEqual(true) + expect(state.getState().selfEditable).toEqual(undefined) + expect(state.getState().formEditable).toEqual(undefined) + + // selfEditable + state.setState((draft) => { + draft.editable = false + }) + expect(state.getState().editable).toEqual(false) + expect(state.getState().selfEditable).toEqual(false) + expect(state.getState().formEditable).toEqual(undefined) + + // priority: selfEditable > formEditable > true + const formEditable = () => true + state.setState((draft) => { + draft.editable = undefined + draft.formEditable = formEditable + }) + expect(state.getState().editable).toEqual(true) + expect(state.getState().selfEditable).toEqual(undefined) + expect(state.getState().formEditable).toEqual(formEditable) + + // priority: selfEditable > formEditable > true + state.setState((draft) => { + draft.editable = undefined + draft.formEditable = undefined + }) + expect(state.getState().editable).toEqual(true) + expect(state.getState().selfEditable).toEqual(undefined) + expect(state.getState().formEditable).toEqual(undefined) +}) + +test('computeState erors/warning', () => { + const state = new FieldState({ useDirty: false }) + expect(state.getState().invalid).toEqual(false) + expect(state.getState().valid).toEqual(true) + expect(state.getState().errors).toEqual([]) + expect(state.getState().effectErrors).toEqual([]) + expect(state.getState().warnings).toEqual([]) + expect(state.getState().effectWarnings).toEqual([]) + expect(state.getState().ruleErrors).toEqual([]) + expect(state.getState().ruleWarnings).toEqual([]) + + // invalid setting + state.setState((draft) => { + draft.errors = ['', undefined, null] + draft.warnings = ['', undefined, null] + draft.ruleErrors = ['', undefined, null] + draft.ruleWarnings = ['', undefined, null] + }) + expect(state.getState().invalid).toEqual(false) + expect(state.getState().valid).toEqual(true) + expect(state.getState().errors).toEqual([]) + expect(state.getState().effectErrors).toEqual([]) + expect(state.getState().warnings).toEqual([]) + expect(state.getState().effectWarnings).toEqual([]) + expect(state.getState().ruleErrors).toEqual([]) + expect(state.getState().ruleWarnings).toEqual([]) + + // errors = ruleErrors effectErrors + // warnings = ruleWarnings warnings + const errors = ['error1', 'error2'] + const warnings = ['warning1', 'warning2'] + const ruleErrors = ['ruleError1', 'ruleError2'] + const ruleWarnings = ['ruleWarning1', 'ruleWarning2'] + state.setState((draft) => { + draft.errors = errors + draft.warnings = warnings + draft.ruleErrors = ruleErrors + draft.ruleWarnings = ruleWarnings + }) + expect(state.getState().invalid).toEqual(true) + expect(state.getState().valid).toEqual(false) + expect(state.getState().errors).toEqual([...ruleErrors, ...errors]) + expect(state.getState().effectErrors).toEqual(errors) + expect(state.getState().warnings).toEqual([...ruleWarnings, ...warnings]) + expect(state.getState().effectWarnings).toEqual(warnings) + expect(state.getState().ruleErrors).toEqual(ruleErrors) + expect(state.getState().ruleWarnings).toEqual(ruleWarnings) + + // 以下几种情况清理错误和警告信息 + // 1. 字段设置为不可编辑 + // 2. 字段隐藏 + // 3. 字段被卸载 + state.setState((draft) => { + draft.visible = false + }) + expect(state.getState().invalid).toEqual(false) + expect(state.getState().valid).toEqual(true) + expect(state.getState().errors).toEqual([]) + expect(state.getState().effectErrors).toEqual([]) + expect(state.getState().warnings).toEqual([]) + expect(state.getState().effectWarnings).toEqual([]) + expect(state.getState().ruleErrors).toEqual(ruleErrors) + expect(state.getState().ruleWarnings).toEqual(ruleWarnings) + + state.setState((draft) => { + draft.visible = true + }) + + expect(state.getState().invalid).toEqual(true) + expect(state.getState().valid).toEqual(false) + expect(state.getState().errors).toEqual(ruleErrors) + expect(state.getState().effectErrors).toEqual([]) + expect(state.getState().warnings).toEqual(ruleWarnings) + expect(state.getState().effectWarnings).toEqual([]) + expect(state.getState().ruleErrors).toEqual(ruleErrors) + expect(state.getState().ruleWarnings).toEqual(ruleWarnings) + + state.setState((draft) => { + draft.unmounted = true + }) + expect(state.getState().invalid).toEqual(false) + expect(state.getState().valid).toEqual(true) + expect(state.getState().errors).toEqual([]) + expect(state.getState().effectErrors).toEqual([]) + expect(state.getState().warnings).toEqual([]) + expect(state.getState().effectWarnings).toEqual([]) + expect(state.getState().ruleErrors).toEqual(ruleErrors) + expect(state.getState().ruleWarnings).toEqual(ruleWarnings) + + state.setState((draft) => { + draft.unmounted = false + }) + expect(state.getState().invalid).toEqual(true) + expect(state.getState().valid).toEqual(false) + expect(state.getState().errors).toEqual(ruleErrors) + expect(state.getState().effectErrors).toEqual([]) + expect(state.getState().warnings).toEqual(ruleWarnings) + expect(state.getState().effectWarnings).toEqual([]) + expect(state.getState().ruleErrors).toEqual(ruleErrors) + expect(state.getState().ruleWarnings).toEqual(ruleWarnings) + + state.setState((draft) => { + draft.editable = false + }) + expect(state.getState().invalid).toEqual(false) + expect(state.getState().valid).toEqual(true) + expect(state.getState().errors).toEqual([]) + expect(state.getState().effectErrors).toEqual([]) + expect(state.getState().warnings).toEqual([]) + expect(state.getState().effectWarnings).toEqual([]) + expect(state.getState().ruleErrors).toEqual(ruleErrors) + expect(state.getState().ruleWarnings).toEqual(ruleWarnings) + + state.setState((draft) => { + draft.editable = false + }) + expect(state.getState().invalid).toEqual(true) + expect(state.getState().valid).toEqual(false) + expect(state.getState().errors).toEqual(ruleErrors) + expect(state.getState().effectErrors).toEqual([]) + expect(state.getState().warnings).toEqual(ruleWarnings) + expect(state.getState().effectWarnings).toEqual([]) + expect(state.getState().ruleErrors).toEqual(ruleErrors) + expect(state.getState().ruleWarnings).toEqual(ruleWarnings) + +}) + + +test('computeState comon', () => { + // pristine depends on whether values to be equal initialvalues + const state = new FieldState({ useDirty: false }) + expect(state.getState().pristine).toEqual(true) + state.setState(draft => { + draft.pristine = false + }) + expect(state.getState().pristine).toEqual(true) + state.setState(draft => { + draft.value = { change: true } + }) + expect(state.getState().value).toEqual({ change: true }) + expect(state.getState().pristine).toEqual(false) + + // loading depends on validating + expect(state.getState().loading).toEqual(false) + state.setState(draft => { + draft.validating = true + }) + expect(state.getState().validating).toEqual(true) + expect(state.getState().loading).toEqual(true) + + // cannot set invalid props + expect(state.getState().props).toEqual({}) + state.setState((draft) => { + draft.props = { hello: 'world' } + }) + expect(state.getState().props).toEqual({ hello: 'world' }) + state.setState((draft) => { + draft.props = undefined + }) + expect(state.getState().props).toEqual({ hello: 'world' }) + + // mounted and unmounted + expect(state.getState().mounted).toEqual(false) + expect(state.getState().unmounted).toEqual(false) + state.setState(draft => { + draft.mounted = true + }) + expect(state.getState().mounted).toEqual(true) + expect(state.getState().unmounted).toEqual(false) + state.setState(draft => { + draft.unmounted = true + }) + expect(state.getState().mounted).toEqual(false) + expect(state.getState().unmounted).toEqual(true) +}) + +test('subscribe/unsubscribe', () => { + const state = new FieldState({ useDirty: false }) + const cb = jest.fn() + const idx = state.subscribe(cb) + const paylaod = state.getState() + state.notify(paylaod) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(paylaod) + state.unsubscribe(idx) + state.notify(paylaod) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(paylaod) +}) +test('batch', () => { + const state = new FieldState({ useDirty: false }) + const cb = jest.fn() + state.batch(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith() + // force run getState + const susCb = jest.fn() + state.subscribe(susCb) + state.dirtyNum = 1 + state.batch(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith() + expect(susCb).toBeCalledTimes(1) + expect(susCb).toBeCalledWith(state.state) +}) +test('getState', () => { + const state = new FieldState({ useDirty: false }) + const cb = jest.fn() + state.getState(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(state.state) + const syncState = state.getState() + expect(syncState).toEqual(state.state) + + state.controller.publishState = () => null + state.getState(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith(null) +}) +test('setState', () => { + const state = new FieldState({ useDirty: false }) + const susCb = jest.fn() + state.subscribe(susCb) + const cb1 = (draft) => { draft.value = { change: true } } + const cb2 = (draft) => { draft.value = { withNotify: true } } + const cb3 = (draft) => { draft.value = { withBatching: true } } + const cb4 = (draft) => { draft.value = { ...draft.value, withBatching2: true } } + const prevState1 = state.getState() + expect(prevState1.value).toEqual(undefined) + + // 默认 slient = false, 触发notify 通知UI更新 + state.setState(cb1) + expect(state.getState().value).toEqual({ change: true }) + expect(state.getState()).toEqual({ ...prevState1, pristine: false, value: { change: true }, values: [{ change: true }] }) + expect(susCb).toBeCalledTimes(1) + expect(susCb).toBeCalledWith({ ...prevState1, pristine: false, value: { change: true }, values: [{ change: true }] }) + + // slient = true 不触发notify + const prevState2 = state.getState() + expect(prevState2.value.withNotify).toEqual(undefined) + state.setState(cb2, true) + expect(state.getState().value.withNotify).toEqual(true) + expect(state.getState()).toEqual({ ...prevState2, value: { withNotify: true }, values: [{ withNotify: true }]}) + expect(susCb).toBeCalledTimes(1) + + // batching 相当于slient = true + const prevState3 = state.getState() + expect(prevState3.value.withBatching).toEqual(undefined) + expect(prevState3.value.withBatching2).toEqual(undefined) + state.batch(() => { + state.setState(cb3) + state.setState(cb4) + }) + + expect(state.getState().value.withBatching).toEqual(true) + expect(state.getState().value.withBatching2).toEqual(true) + expect(state.getState()).toEqual({ ...prevState3, + value: { withBatching: true, withBatching2: true }, + values: [{ withBatching: true, withBatching2: true }] + }) + // 这次notify是由batch批处理结束调用的 + expect(susCb).toBeCalledTimes(2) + expect(susCb).toBeCalledWith({ ...prevState3, + value: { withBatching: true, withBatching2: true }, + values: [{ withBatching: true, withBatching2: true }] + }) +}) +test('getSourceState', () => { + const state = new FieldState({ useDirty: false }) + const cb = jest.fn() + state.unsafe_getSourceState(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(state.state) + const syncState = state.unsafe_getSourceState() + expect(syncState).toEqual(state.state) + + state.controller.publishState = () => null + state.unsafe_getSourceState(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith(state.state) +}) + +test('setSourceState', () => { + const state = new FieldState({ useDirty: false }) + const cb1 = (draft) => draft.change = true + const prevState1 = state.unsafe_getSourceState() + expect(prevState1.change).toEqual(undefined) + + state.unsafe_setSourceState(cb1) + expect(state.unsafe_getSourceState()).toEqual({ ...prevState1, change: true }) +}) + +test('isDirty', () => { + const state = new FieldState({ useDirty: true }) + expect(state.dirtyNum).toEqual(0) + expect(state.isDirty()).toEqual(false) + state.dirtyNum = 1 + expect(state.isDirty()).toEqual(true) + state.dirtyNum = 0 + expect(state.isDirty()).toEqual(false) + state.dirtys.validating = true + expect(state.isDirty()).toEqual(false) + expect(state.isDirty('validating')).toEqual(true) +}) + + diff --git a/packages/core/src/__tests__/form.state.spec.ts b/packages/core/src/__tests__/form.state.spec.ts new file mode 100644 index 00000000000..05bcdb21926 --- /dev/null +++ b/packages/core/src/__tests__/form.state.spec.ts @@ -0,0 +1,213 @@ +import { FormState } from '../state/form' + +test('computeState', () => { + const state = new FormState({ useDirty: false }) + expect(state.getState()).toEqual({ + displayName: 'FormState', + editable: undefined, + pristine: true, + valid: true, + invalid: false, + loading: false, + validating: false, + initialized: false, + submitting: false, + errors: [], + warnings: [], + values: {}, + initialValues: {}, + mounted: false, + unmounted: false, + props: {} + }) + + // can not set invalid errors, warnings + state.setState(draft => { + draft.errors = [undefined, null, ''] + draft.warnings = [undefined, null, ''] + }) + expect(state.getState().warnings).toEqual([]) + expect(state.getState().errors).toEqual([]) + + // set errors will change invalid and valid + expect(state.getState().invalid).toEqual(false) + expect(state.getState().valid).toEqual(true) + state.setState(draft => { + draft.invalid = false + draft.valid = true + }) + expect(state.getState().invalid).toEqual(false) + expect(state.getState().valid).toEqual(true) + + state.setState(draft => { + draft.errors = ['sth wrong'] + }) + expect(state.getState().errors).toEqual(['sth wrong']) + expect(state.getState().invalid).toEqual(true) + expect(state.getState().valid).toEqual(false) + + // pristine depends on whether values to be equal initialvalues + expect(state.getState().pristine).toEqual(true) + state.setState(draft => { + draft.pristine = false + }) + expect(state.getState().pristine).toEqual(true) + state.setState(draft => { + draft.values = { change: true } + }) + expect(state.getState().values).toEqual({ change: true }) + expect(state.getState().pristine).toEqual(false) + + // cannot set invalid props + expect(state.getState().props).toEqual({}) + state.setState((draft) => { + draft.props = { hello: 'world' } + }) + expect(state.getState().props).toEqual({ hello: 'world' }) + state.setState((draft) => { + draft.props = undefined + }) + expect(state.getState().props).toEqual({ hello: 'world' }) + + // loading depends on validating + expect(state.getState().loading).toEqual(false) + state.setState(draft => { + draft.validating = true + }) + expect(state.getState().validating).toEqual(true) + expect(state.getState().loading).toEqual(true) + + // mounted and unmounted + expect(state.getState().mounted).toEqual(false) + expect(state.getState().unmounted).toEqual(false) + state.setState(draft => { + draft.mounted = true + }) + expect(state.getState().mounted).toEqual(true) + expect(state.getState().unmounted).toEqual(false) + state.setState(draft => { + draft.unmounted = true + }) + expect(state.getState().mounted).toEqual(false) + expect(state.getState().unmounted).toEqual(true) +}) +test('subscribe/unsubscribe', () => { + const state = new FormState({ useDirty: false }) + const cb = jest.fn() + const idx = state.subscribe(cb) + const paylaod = state.getState() + state.notify(paylaod) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(paylaod) + state.unsubscribe(idx) + state.notify(paylaod) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(paylaod) +}) +test('batch', () => { + const state = new FormState({ useDirty: false }) + const cb = jest.fn() + state.batch(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith() + // force run getState + const susCb = jest.fn() + state.subscribe(susCb) + state.dirtyNum = 1 + state.batch(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith() + expect(susCb).toBeCalledTimes(1) + expect(susCb).toBeCalledWith(state.state) +}) +test('getState', () => { + const state = new FormState({ useDirty: false }) + const cb = jest.fn() + state.getState(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(state.state) + const syncState = state.getState() + expect(syncState).toEqual(state.state) + + state.controller.publishState = () => null + state.getState(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith(null) +}) +test('setState', () => { + const state = new FormState({ useDirty: false }) + const susCb = jest.fn() + state.subscribe(susCb) + const cb1 = (draft) => { draft.values = { change: true } } + const cb2 = (draft) => { draft.values = { withNotify: true } } + const cb3 = (draft) => { draft.values = { withBatching: true } } + const cb4 = (draft) => { draft.values = { ...draft.values, withBatching2: true } } + const prevState1 = state.getState() + expect(prevState1.values.change).toEqual(undefined) + + // 默认 slient = false, 触发notify 通知UI更新 + state.setState(cb1) + expect(state.getState().values.change).toEqual(true) + expect(state.getState()).toEqual({ ...prevState1, pristine: false, values: { change: true } }) + expect(susCb).toBeCalledTimes(1) + expect(susCb).toBeCalledWith({ ...prevState1, pristine: false, values: { change: true } }) + + // slient = true 不触发notify + const prevState2 = state.getState() + expect(prevState2.values.withNotify).toEqual(undefined) + state.setState(cb2, true) + expect(state.getState().values.withNotify).toEqual(true) + expect(state.getState()).toEqual({ ...prevState2, values: { withNotify: true }}) + expect(susCb).toBeCalledTimes(1) + + // batching 相当于slient = true + const prevState3 = state.getState() + expect(prevState3.values.withBatching).toEqual(undefined) + expect(prevState3.values.withBatching2).toEqual(undefined) + state.batch(() => { + state.setState(cb3) + state.setState(cb4) + }) + + expect(state.getState().values.withBatching).toEqual(true) + expect(state.getState().values.withBatching2).toEqual(true) + expect(state.getState()).toEqual({ ...prevState3, values: { withBatching: true, withBatching2: true } }) + // 这次notify是由batch批处理结束调用的 + expect(susCb).toBeCalledTimes(2) + expect(susCb).toBeCalledWith({ ...prevState3, values: { withBatching: true, withBatching2: true } }) +}) +test('getSourceState', () => { + const state = new FormState({ useDirty: false }) + const cb = jest.fn() + state.unsafe_getSourceState(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(state.state) + const syncState = state.unsafe_getSourceState() + expect(syncState).toEqual(state.state) + + state.controller.publishState = () => null + state.unsafe_getSourceState(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith(state.state) +}) +test('setSourceState', () => { + const state = new FormState({ useDirty: false }) + const cb1 = (draft) => draft.change = true + const prevState1 = state.unsafe_getSourceState() + expect(prevState1.change).toEqual(undefined) + + state.unsafe_setSourceState(cb1) + expect(state.unsafe_getSourceState()).toEqual({ ...prevState1, change: true }) +}) +test('isDirty', () => { + const state = new FormState({ useDirty: true }) + expect(state.dirtyNum).toEqual(0) + expect(state.isDirty()).toEqual(false) + state.dirtyNum = 1 + expect(state.isDirty()).toEqual(true) + state.dirtyNum = 0 + expect(state.isDirty()).toEqual(false) + state.dirtys.validating = true + expect(state.isDirty()).toEqual(false) + expect(state.isDirty('validating')).toEqual(true) +}) diff --git a/packages/core/src/__tests__/graph.spec.ts b/packages/core/src/__tests__/graph.spec.ts new file mode 100644 index 00000000000..3a85de549b3 --- /dev/null +++ b/packages/core/src/__tests__/graph.spec.ts @@ -0,0 +1,357 @@ +import { FormGraph } from '../shared/graph' +import { FormPathPattern, FormPath } from '@uform/shared' +import { IField, IVirtualField } from '../types' +import createForm from '../' +import { FormState } from '../state/form' + +test('constructor with strategy',()=>{ + function matchStrategy( + pattern: FormPathPattern, + node: IField | IVirtualField + ) { + const matchPattern = FormPath.parse(pattern) + return node.unsafe_getSourceState( + state => matchPattern.match(state.name) || matchPattern.match(state.path) + ) + } + + const form = createForm() + const vf1 =form.registerVirtualField({ name: 'a' }) + const graph = new FormGraph({ matchStrategy }) + graph.appendNode("a", vf1) + expect(graph.select("a")).toEqual(vf1) +}) + +test('constructor without strategy',()=>{ + const form = createForm() + const vf1 =form.registerVirtualField({ name: 'a' }) + const graph = new FormGraph() + graph.appendNode(vf1.state.path, vf1) + expect(graph.select(vf1.state.path)).toEqual(vf1) +}) + +test('appendNode',()=>{ + const state = new FormState({}) + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf1Children = form.registerVirtualField({ name: 'a.a' }) + const vf2 = form.registerVirtualField({ name: 'b' }) + const vf2Children = form.registerVirtualField({ name: 'b.b' }) + const graph = new FormGraph() + graph.appendNode("", state) + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf2.state.path, vf2) + graph.appendNode(vf1Children.state.path, vf1Children) + graph.appendNode(vf2Children.state.path, vf2Children) + // DFS + expect(graph.selectChildren("")[0]).toEqual(vf1) + expect(graph.selectChildren("")[1]).toEqual(vf1Children) + expect(graph.selectChildren("")[2]).toEqual(vf2) + expect(graph.selectChildren("")[3]).toEqual(vf2Children) + expect(graph.selectChildren(vf1.state.path)).toEqual([vf1Children]) + expect(graph.selectChildren(vf2.state.path)).toEqual([vf2Children]) + expect(graph.selectChildren(vf1Children.state.path)).toEqual([]) + expect(graph.selectChildren(vf2Children.state.path)).toEqual([]) +}) + +test('appendNode disOreder',()=>{ + const state = new FormState({}) + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf1Children = form.registerVirtualField({ name: 'a.a' }) + const vf2 = form.registerVirtualField({ name: 'b' }) + const graph = new FormGraph() + // 极端情况,先加子节点,再加父节点不会生效,保证上层代码正常即可 + graph.appendNode(vf1Children.state.path, vf1Children) + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf2.state.path, vf2) + expect(graph.selectChildren(vf1.state.path)).toEqual([]) + expect(graph.selectChildren("")).toEqual([]) + graph.appendNode("", state) + expect(graph.selectChildren("")).toEqual([]) +}) + +test('select',()=>{ + const form = createForm() + const vf1 =form.registerVirtualField({ name: 'a' }) + const graph = new FormGraph() + graph.appendNode(vf1.state.path, vf1) + expect(graph.select(vf1.state.path)).toEqual(vf1) + expect(graph.select("b")).toEqual(undefined) +}) + +test('get',()=>{ + const form = createForm() + const vf1 =form.registerVirtualField({ name: 'a' }) + const graph = new FormGraph() + graph.appendNode(vf1.state.path, vf1) + expect(graph.get(vf1.state.path)).toEqual(vf1) + expect(graph.get("b")).toEqual(undefined) +}) + +test('exist',()=>{ + const form = createForm() + const vf1 =form.registerVirtualField({ name: 'a' }) + const graph = new FormGraph() + graph.appendNode(vf1.state.path, vf1) + expect(graph.exist(vf1.state.path)).toEqual(true) + expect(graph.exist("b")).toEqual(false) +}) + +test('replace',()=>{ + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf1Children = form.registerVirtualField({ name: 'a.a' }) + const vf2 = form.registerVirtualField({ name: 'b' }) + const graph = new FormGraph() + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf1Children.state.path, vf1Children) + expect(graph.get(vf1.state.path)).toEqual(vf1) + expect(graph.get(vf1Children.state.path)).toEqual(vf1Children) + graph.replace(vf1.state.path, vf2) + expect(graph.get(vf1.state.path)).toEqual(vf2) + // 完全打平, 更换parent不会影响子类 + expect(graph.get(vf1Children.state.path)).toEqual(vf1Children) +}) + +test('remove',()=>{ + const form = createForm() + const vf1 =form.registerVirtualField({ name: 'a' }) + const graph = new FormGraph() + graph.appendNode(vf1.state.path, vf1) + expect(graph.exist(vf1.state.path)).toEqual(true) + graph.remove(vf1.state.path) + expect(graph.exist(vf1.state.path)).toEqual(false) +}) + +test('remove deep', () => { + const state = new FormState({}) + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf1Children = form.registerVirtualField({ name: 'a.a' }) + const vf1GrandChildren = form.registerVirtualField({ name: 'a.a.a' }) + const graph = new FormGraph() + graph.appendNode("", state) + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf1Children.state.path, vf1Children) + graph.appendNode(vf1GrandChildren.state.path, vf1GrandChildren) + graph.remove(vf1.state.path) + expect(graph.exist(vf1.state.path)).toEqual(false) + expect(graph.exist(vf1Children.state.path)).toEqual(false) + expect(graph.exist(vf1GrandChildren.state.path)).toEqual(false) +}) + +test('map',()=>{ + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf2 = form.registerVirtualField({ name: 'b' }) + const graph = new FormGraph() + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf2.state.path, vf2) + // nodes 是kv结构, map后也是kv结构 + const result = graph.map(node => node) + expect(result).toEqual({ a: vf1, b: vf2 }) +}) + +test('reduce',()=>{ + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf2 = form.registerVirtualField({ name: 'b' }) + const graph = new FormGraph() + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf2.state.path, vf2) + const result = graph.reduce((buf, node) => [...buf].concat(node), []) + expect(result).toEqual([vf1, vf2]) +}) + +test('selectChildren',()=>{ + const state = new FormState({}) + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf2 = form.registerVirtualField({ name: 'b' }) + const graph = new FormGraph() + graph.appendNode("", state) + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf2.state.path, vf2) + expect(graph.selectChildren("")).toEqual([vf1, vf2]) + expect(graph.selectChildren(vf1.state.path)).toEqual([]) + expect(graph.selectChildren(vf2.state.path)).toEqual([]) +}) + + +test('selectParent',()=>{ + const state = new FormState({}) + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf1Children = form.registerVirtualField({ name: 'a.a' }) + const vf2 = form.registerVirtualField({ name: 'b' }) + const vf2Children = form.registerVirtualField({ name: 'b.b' }) + const graph = new FormGraph() + graph.appendNode("", state) + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf2.state.path, vf2) + graph.appendNode(vf1Children.state.path, vf1Children) + graph.appendNode(vf2Children.state.path, vf2Children) + expect(graph.selectParent("")).toEqual(undefined) + expect(graph.selectParent(vf1.state.path)).toEqual(state) + expect(graph.selectParent(vf2.state.path)).toEqual(state) + expect(graph.selectParent(vf1Children.state.path)).toEqual(vf1) + expect(graph.selectParent(vf2Children.state.path)).toEqual(vf2) +}) + +test('eachChildren eacher',()=>{ + const state = new FormState({}) + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf1Children = form.registerVirtualField({ name: 'a.a' }) + const vf1GrandChildren = form.registerVirtualField({ name: 'a.a.a' }) + const graph = new FormGraph() + graph.appendNode("", state) + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf1Children.state.path, vf1Children) + graph.appendNode(vf1GrandChildren.state.path, vf1GrandChildren) + + // shallow mode: recursion = false + const shallowChilds = [] + graph.eachChildren((node, path) => { + shallowChilds.push({ node, path }) + }, false) + + expect(shallowChilds).toEqual([{ node: vf1, path: FormPath.getPath(vf1.state.path)}]) + + // deep mode: recursion = true + const deepChilds = [] + graph.eachChildren((node, path) => { + deepChilds.push({ node, path }) + }, true) + expect(deepChilds).toEqual([ + { node: vf1, path: FormPath.getPath(vf1.state.path)}, + { node: vf1Children, path: FormPath.getPath(vf1Children.state.path)}, + { node: vf1GrandChildren, path: FormPath.getPath(vf1GrandChildren.state.path)}, + ]) +}) + +test('eachChildren path eacher',()=>{ + const state = new FormState({}) + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf1Children = form.registerVirtualField({ name: 'a.a' }) + const vf1GrandChildren = form.registerVirtualField({ name: 'a.a.a' }) + const graph = new FormGraph() + graph.appendNode("", state) + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf1Children.state.path, vf1Children) + graph.appendNode(vf1GrandChildren.state.path, vf1GrandChildren) + + // deep mode: recursion = true + const deepChilds = [] + graph.eachChildren("a", (node, path) => { + deepChilds.push({ node, path }) + }, true) + expect(deepChilds).toEqual([ + { node: vf1Children, path: FormPath.getPath(vf1Children.state.path)}, + { node: vf1GrandChildren, path: FormPath.getPath(vf1GrandChildren.state.path)}, + ]) +}) + +test('eachChildren path selector eacher',()=>{ + const state = new FormState({}) + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf1Children = form.registerVirtualField({ name: 'a.a' }) + const vf1GrandChildren = form.registerVirtualField({ name: 'a.a.a' }) + const graph = new FormGraph() + graph.appendNode("", state) + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf1Children.state.path, vf1Children) + graph.appendNode(vf1GrandChildren.state.path, vf1GrandChildren) + + // shallow mode: recursion = false + const shallowChilds = [] + graph.eachChildren("a", "a.a.a", (node, path) => { + shallowChilds.push({ node, path }) + }, true) + expect(shallowChilds).toEqual([]) + + // deep mode: recursion = true + const deepChilds = [] + graph.eachChildren("a", "a.a", (node, path) => { + deepChilds.push({ node, path }) + }, true) + expect(deepChilds).toEqual([ + { node: vf1Children, path: FormPath.getPath(vf1Children.state.path)}, + ]) +}) + +test('eachParent',()=>{ + const state = new FormState({}) + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf1Children = form.registerVirtualField({ name: 'a.a' }) + const vf1GrandChildren = form.registerVirtualField({ name: 'a.a.a' }) + const graph = new FormGraph() + graph.appendNode("", state) + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf1Children.state.path, vf1Children) + graph.appendNode(vf1GrandChildren.state.path, vf1GrandChildren) + const rootEacher = jest.fn() + graph.eachParent("", rootEacher) + expect(rootEacher).toBeCalledTimes(0) + //expect(rootEacher).toBeCalledWith(state, FormPath.getPath("")) + + const vf1Eacher = jest.fn() + graph.eachParent(vf1.state.path, vf1Eacher) + expect(vf1Eacher).toBeCalledTimes(1) + + const vf1CEacher = jest.fn() + graph.eachParent(vf1Children.state.path, vf1CEacher) + expect(vf1CEacher).toBeCalledTimes(2) + + const vf1GEacher = jest.fn() + graph.eachParent(vf1GrandChildren.state.path, vf1GEacher) + expect(vf1GEacher).toBeCalledTimes(3) +}) + +test('getLatestParent',()=>{ + const state = new FormState({}) + const form = createForm() + const vf1 = form.registerVirtualField({ name: 'a' }) + const vf1Children = form.registerVirtualField({ name: 'a.a' }) + const vf1GrandChildren = form.registerVirtualField({ name: 'a.a.a' }) + const vf2 = form.registerVirtualField({ name: 'b' }) + const vf2Children = form.registerVirtualField({ name: 'b.b' }) + const graph = new FormGraph() + graph.appendNode("", state) + graph.appendNode(vf1.state.path, vf1) + graph.appendNode(vf2.state.path, vf2) + graph.appendNode(vf1Children.state.path, vf1Children) + graph.appendNode(vf1GrandChildren.state.path, vf1GrandChildren) + graph.appendNode(vf2Children.state.path, vf2Children) + expect(graph.getLatestParent("")).toEqual(undefined) + const rootPath = FormPath.getPath("") + const v1Path = FormPath.getPath(vf1.state.path) + const v2Path = FormPath.getPath(vf2.state.path) + const v1CPath = FormPath.getPath(vf1Children.state.path) + const v2CPath = FormPath.getPath(vf2Children.state.path) + const root = { + path: rootPath, + ref: { path: rootPath, children: [v1Path, v2Path] } + } + expect(graph.getLatestParent(vf1.state.path)).toEqual(root) + expect(graph.getLatestParent(vf2.state.path)).toEqual(root) + expect(graph.getLatestParent(vf1Children.state.path)).toEqual({ + path: v1Path, + ref: { path: v1Path, children: [v1CPath], parent: graph.getLatestParent(vf1.state.path).ref } + }) + expect(graph.getLatestParent(vf2Children.state.path)).toEqual({ + path: v2Path, + ref: { path: v2Path, children: [v2CPath], parent: graph.getLatestParent(vf2.state.path).ref } + }) + expect(graph.getLatestParent(vf1GrandChildren.state.path)).toEqual({ + path: v1CPath, + ref: { path: v1CPath, + children: [FormPath.getPath(vf1GrandChildren.state.path)], + parent: graph.getLatestParent(vf1Children.state.path).ref + } + }) +}) \ No newline at end of file diff --git a/packages/core/src/__tests__/index.spec.ts b/packages/core/src/__tests__/index.spec.ts new file mode 100644 index 00000000000..5302e0fbcd5 --- /dev/null +++ b/packages/core/src/__tests__/index.spec.ts @@ -0,0 +1,1559 @@ +import { isEqual } from '@uform/shared' +import { + createForm, + LifeCycleTypes, + FormLifeCycle, + FormPath, + registerValidationFormats, + registerValidationRules, + registerValidationMTEngine +} from '../index' +import { ValidateDescription, ValidatePatternRules } from '@uform/validator' + +// mock datasource +const testValues = { aa: 111, bb: 222 } +const testValues2 = { aa: '123', bb: '321' } +const changeValues = { aa: 'change aa', bb: 'change bb' } +const resetInitValues = { + aa: { + bb: [{ aa: 123 }, { aa: 321 }] + } +} +const deepValues = { + a: { + b: { c: { d: { e: 1 } } }, + c: { + e: 2 + } + }, + b: { + c: 3 + } +} + +describe('createForm', () => { + test('values', () => { + const form = createForm({ + values: testValues + }) + expect(form.getFormState(state => state.values)).toEqual(testValues) + expect(form.getFormState(state => state.pristine)).toEqual(false) + expect(form.getFormState(state => state.initialized)).toEqual(true) + expect(form.getFormGraph()).toMatchSnapshot() + }) + + test('initialValues on init', () => { + const form = createForm({ + initialValues: testValues + }) + const aa = form.registerField({ path: 'aa' }) + const bb = form.registerField({ path: 'bb' }) + expect(form.getFormState(state => state.values)).toEqual(testValues) + expect(form.getFormState(state => state.initialValues)).toEqual(testValues) + expect(form.getFormState(state => state.pristine)).toEqual(true) + expect(form.getFormState(state => state.initialized)).toEqual(true) + expect(aa.getState(state => state.value)).toEqual(testValues.aa) + expect(bb.getState(state => state.value)).toEqual(testValues.bb) + expect(form.getFormGraph()).toMatchSnapshot() + }) + + test('merge values and initialValues', () => { + const form = createForm({ + values: { a: 1, b: 2 }, + initialValues: { a: 'x', b: 'y' } + }) + expect(form.getFormState(state => state.values)).toEqual({ a: 1, b: 2 }) + expect(form.getFormGraph()).toMatchSnapshot() + }) + + test('initialValues after init', () => { + const form = createForm() + const aa = form.registerField({ path: 'aa' }) + const bb = form.registerField({ path: 'bb' }) + form.setFormState(state => { + state.initialValues = testValues + }) + expect(form.getFormState(state => state.values)).toEqual(testValues) + expect(form.getFormState(state => state.initialValues)).toEqual(testValues) + expect(form.getFormState(state => state.pristine)).toEqual(true) + expect(form.getFormState(state => state.initialized)).toEqual(true) + expect(aa.getState(state => state.value)).toEqual(testValues.aa) + expect(bb.getState(state => state.value)).toEqual(testValues.bb) + expect(form.getFormGraph()).toMatchSnapshot() + }) + + test('initialValue', () => { + const form = createForm({ + initialValues: testValues + }) + expect(form.getFormState(state => state.values)).toEqual(testValues) + expect(form.getFormState(state => state.initialValues)).toEqual(testValues) + expect(form.getFormState(state => state.pristine)).toEqual(true) + expect(form.getFormState(state => state.initialized)).toEqual(true) + expect(form.getFormGraph()).toMatchSnapshot() + }) + + test('lifecycles', () => { + const onFormInit = jest.fn() + const onFieldInit = jest.fn() + const onFieldChange = jest.fn() + const form = createForm({ + initialValues: testValues, + lifecycles: [ + new FormLifeCycle(LifeCycleTypes.ON_FORM_INIT, onFormInit), + new FormLifeCycle(LifeCycleTypes.ON_FIELD_INIT, onFieldInit), + new FormLifeCycle(LifeCycleTypes.ON_FIELD_CHANGE, onFieldChange) + ] + }) + + const aa = form.registerField({ path: 'aa', value: testValues2.aa }) + const bb = form.registerField({ path: 'bb', value: testValues2.bb }) + + // registerField will trigger ON_FIELD_CHANGE(because of initialized changed) + expect(onFormInit).toBeCalledTimes(1) + expect(onFieldInit).toBeCalledTimes(2) + expect(onFieldChange).toBeCalledTimes(2) + expect(form.getFormState(state => state.values)).toEqual(testValues2) + + // change field's value + aa.setState(state => (state.value = changeValues.aa)) + bb.setState(state => (state.value = changeValues.bb)) + expect(onFieldChange).toBeCalledTimes(4) + expect(form.getFormState(state => state.values)).toEqual(changeValues) + expect(aa.getState(state => state.value)).toEqual(changeValues.aa) + expect(bb.getState(state => state.value)).toEqual(changeValues.bb) + expect(form.getFormGraph()).toMatchSnapshot() + }) +}) + +describe('graph', () => { + test('getFormGraph', () => { + const form = createForm({ + initialValues: testValues + }) + + form.registerField({ path: 'aa', value: changeValues.aa }) + form.registerField({ path: 'bb', value: changeValues.bb }) + expect(form.getFormGraph()).toMatchSnapshot() + }) + + test('setFormGraph', () => { + const form = createForm({ + initialValues: testValues + }) + + form.registerField({ path: 'aa', value: changeValues.aa }) + form.registerField({ path: 'bb', value: changeValues.bb }) + const snapshot = form.getFormGraph() + form.setFieldState('aa', state => (state.visible = false)) + expect(form.getFormGraph()).toMatchSnapshot() + + // change graph or you can also call it 'time travel' + form.setFormGraph(snapshot) + expect(form.getFormGraph()).toEqual(snapshot) + }) +}) + +describe('submit', () => { + test('onSubmit', async () => { + const onSubmitContructor = jest.fn() + const onSubmitFn = jest.fn() + const changeSubmitPayload = values => ({ hello: 'world' }) + const form1 = createForm({ onSubmit: onSubmitContructor }) + const form2 = createForm() + + expect(onSubmitContructor).toBeCalledTimes(0) + expect(onSubmitContructor).toBeCalledTimes(0) + await form1.submit() + await form2.submit() + expect(onSubmitContructor).toBeCalledTimes(1) + expect(onSubmitFn).toBeCalledTimes(0) + + // priority: onSubmitFn > onSubmitContructor + await form1.submit(onSubmitFn) + await form2.submit(onSubmitFn) + expect(onSubmitContructor).toBeCalledTimes(1) + expect(onSubmitFn).toBeCalledTimes(2) + const result = await form2.submit(changeSubmitPayload) + expect(result).toEqual({ + validated: { + errors: [], + warnings: [] + }, + payload: { hello: 'world' } + }) + }) + + test('submitResult', async () => { + const sunmitFailed = jest.fn() + const form = createForm() + form.registerField({ + path: 'a', + rules: [ + value => { + return value === undefined + ? { type: 'error', message: 'a is required' } + : null + } + ] + }) + form.registerField({ + path: 'b', + rules: [ + value => { + return value === undefined + ? { type: 'warning', message: 'b is required' } + : null + } + ] + }) + + // error failed + try { + await form.submit() + } catch (errors) { + sunmitFailed() + expect(errors).toEqual([{ path: 'a', messages: ['a is required'] }]) + } + + // warning pass + form.setFieldValue('a', 1) + let validated + try { + const result = await form.submit() + validated = result.validated + } catch (errors) { + sunmitFailed() + } + + expect(validated.warnings).toEqual([ + { path: 'b', messages: ['b is required'] } + ]) + expect(validated.errors).toEqual([]) + expect(sunmitFailed).toHaveBeenCalledTimes(1) + }) + + test('repeat submit', async () => { + const form = createForm() + const result1 = form.submit() + const result2 = form.submit() + // reuse before result + expect(result1).toEqual(result2) + }) + + test('basic', async () => { + const onSubmitStart = jest.fn() + const onSubmitEnd = jest.fn() + const form = createForm({ + lifecycles: [ + new FormLifeCycle(LifeCycleTypes.ON_FORM_SUBMIT_START, onSubmitStart), + new FormLifeCycle(LifeCycleTypes.ON_FORM_SUBMIT_END, onSubmitEnd) + ], + onSubmit: () => { + expect(form.getFormState(state => state.submitting)).toEqual(true) + } + }) + expect(form.getFormState(state => state.submitting)).toEqual(false) + expect(onSubmitStart).toBeCalledTimes(0) + expect(onSubmitEnd).toBeCalledTimes(0) + await form.submit() + expect(form.getFormState(state => state.submitting)).toEqual(false) + expect(onSubmitStart).toBeCalledTimes(1) + expect(onSubmitEnd).toBeCalledTimes(1) + }) +}) + +describe('reset', () => { + test('array reset forceclear', async () => { + const form = createForm({ + initialValues: resetInitValues + }) + + form.registerField({ path: 'aa' }) + form.registerField({ path: 'aa.bb' }) + form.registerField({ path: 'aa.bb.0' }) + form.registerField({ path: 'aa.bb.1' }) + form.registerField({ path: 'aa.bb.0.aa' }) + form.registerField({ path: 'aa.bb.1.aa' }) + expect(form.getFormGraph()).toMatchSnapshot() + + form.setFieldState('aa.bb.0.aa', state => { + state.value = 'aa changed' + }) + await form.reset({ forceClear: true }) + expect(form.getFormGraph()).toMatchSnapshot() + expect(form.getFormState(state => state.values)).toEqual({ aa: { bb: [] } }) + expect(form.getFormState(state => state.initialValues)).toEqual( + resetInitValues + ) + }) + + test('array reset no forceclear(initial values)', async () => { + const form = createForm({ + initialValues: resetInitValues + }) + form.registerField({ path: 'aa' }) + form.registerField({ path: 'aa.bb' }) + form.registerField({ path: 'aa.bb.0' }) + form.registerField({ path: 'aa.bb.1' }) + form.registerField({ path: 'aa.bb.0.aa' }) + form.registerField({ path: 'aa.bb.1.aa' }) + expect(form.getFormGraph()).toMatchSnapshot() + form.setFieldState('aa.bb.0.aa', state => { + state.value = 'aa changed' + }) + expect(form.getFormGraph()).toMatchSnapshot() + await form.reset() + expect(form.getFormGraph()).toMatchSnapshot() + expect(form.getFormState(state => state.values)).toEqual(resetInitValues) + expect(form.getFormState(state => state.initialValues)).toEqual( + resetInitValues + ) + }) + + test('array reset no forceclear(values)', async () => { + const form = createForm({ + values: resetInitValues + }) + form.registerField({ path: 'aa' }) + form.registerField({ path: 'aa.bb' }) + form.registerField({ path: 'aa.bb.0' }) + form.registerField({ path: 'aa.bb.1' }) + form.registerField({ path: 'aa.bb.0.aa' }) + form.registerField({ path: 'aa.bb.1.aa' }) + expect(form.getFormGraph()).toMatchSnapshot() + form.setFieldState('aa.bb.0.aa', state => { + state.value = 'aa changed' + }) + expect(form.getFormGraph()).toMatchSnapshot() + await form.reset() + expect(form.getFormGraph()).toMatchSnapshot() + expect(form.getFormState(state => state.values)).toEqual({ aa: { bb: [] } }) + expect(form.getFormState(state => state.initialValues)).toEqual({}) + }) +}) + +describe('clearErrors', () => { + test('basic', async () => { + const form = createForm() + const warnMsg = ['warning msg'] + const errMsg = ['error msg'] + form.registerField({ + path: 'b', + rules: [ + v => + v === undefined + ? { type: 'warning', message: 'warning msg' } + : undefined + ] + }) // CustomValidator warning + form.registerField({ + path: 'c', + rules: [ + v => + v === undefined ? { type: 'error', message: 'error msg' } : undefined + ] + }) // CustomValidator error + const result1 = await form.validate() + expect(result1.warnings).toEqual([{ path: 'b', messages: warnMsg }]) + expect(result1.errors).toEqual([{ path: 'c', messages: errMsg }]) + + form.clearErrors('b') + expect(form.getFormState(state => state.warnings)).toEqual([]) + expect(form.getFormState(state => state.errors)).toEqual([ + { path: 'c', messages: errMsg } + ]) + + form.clearErrors('c') + expect(form.getFormState(state => state.warnings)).toEqual([]) + expect(form.getFormState(state => state.errors)).toEqual([]) + + const result2 = await form.validate() + expect(result2.warnings).toEqual([{ path: 'b', messages: warnMsg }]) + expect(result2.errors).toEqual([{ path: 'c', messages: errMsg }]) + + form.clearErrors() + expect(form.getFormState(state => state.warnings)).toEqual([]) + expect(form.getFormState(state => state.errors)).toEqual([]) + }) + + test('wildcard path', async () => {}) + + test('effect', async () => {}) +}) + +describe('validate', () => { + test('empty', async () => { + const form = createForm() + const { warnings, errors } = await form.validate() + expect(warnings).toEqual([]) + expect(errors).toEqual([]) + }) + + test('onValidateFailed', async () => { + const onValidateFailedTrigger = jest.fn() + const onValidateFailed = ({ warnings, errors }) => { + expect(warnings).toEqual([{ path: 'b', messages: ['warning msg'] }]) + expect(errors).toEqual([{ path: 'c', messages: ['error msg'] }]) + onValidateFailedTrigger() + } + const form = createForm({ + onValidateFailed + }) + form.registerField({ + path: 'b', + rules: [() => ({ type: 'warning', message: 'warning msg' })] + }) // CustomValidator warning + form.registerField({ + path: 'c', + rules: [() => ({ type: 'error', message: 'error msg' })] + }) // CustomValidator error + try { + await form.submit() + } catch (e) {} + expect(onValidateFailedTrigger).toBeCalledTimes(1) + }) + + test('validate basic', async () => { + const onValidateStart = jest.fn() + const onValidateEnd = jest.fn() + const form = createForm({ + lifecycles: [ + new FormLifeCycle( + LifeCycleTypes.ON_FORM_VALIDATE_START, + onValidateStart + ), + new FormLifeCycle(LifeCycleTypes.ON_FORM_VALIDATE_END, onValidateEnd) + ] + }) + form.registerField({ path: 'a', rules: ['number'] }) // string + form.registerField({ + path: 'b', + rules: [() => ({ type: 'warning', message: 'warning msg' })] + }) // CustomValidator warning + form.registerField({ + path: 'c', + rules: [() => ({ type: 'error', message: 'warning msg' })] + }) // CustomValidator error + form.registerField({ path: 'd', rules: [() => 'straight error msg'] }) // CustomValidator string + form.registerField({ + path: 'e', + rules: [{ required: true, message: 'desc msg' }] + }) // ValidateDescription + + expect(onValidateStart).toBeCalledTimes(0) + expect(onValidateEnd).toBeCalledTimes(0) + expect(form.getFormState(state => state.validating)).toEqual(false) + const validatePromise = form.validate() + expect(form.getFormState(state => state.validating)).toEqual(true) + expect(onValidateStart).toBeCalledTimes(1) + validatePromise.then(validated => { + expect(form.getFormState(state => state.validating)).toEqual(false) + expect(onValidateEnd).toBeCalledTimes(1) + const { warnings, errors } = validated + expect(warnings.length).toEqual(1) + expect(errors.length).toEqual(4) + }) + }) + + test('path', async () => { + const form = createForm() + form.registerField({ + path: 'b', + rules: [ + v => + v === undefined + ? { type: 'warning', message: 'warning msg' } + : undefined + ] + }) // CustomValidator warning + form.registerField({ + path: 'c', + rules: [ + v => + v === undefined ? { type: 'error', message: 'error msg' } : undefined + ] + }) // CustomValidator error + const bResult = await form.validate('b') + expect(bResult.warnings).toEqual([{ path: 'b', messages: ['warning msg'] }]) + expect(bResult.errors).toEqual([]) + expect(form.getFieldState('b', state => state.warnings)).toEqual([ + 'warning msg' + ]) + expect(form.getFieldState('c', state => state.errors)).toEqual([]) + expect(form.getFormState(state => state.warnings)).toEqual([ + { path: 'b', messages: ['warning msg'] } + ]) + expect(form.getFormState(state => state.errors)).toEqual([]) + + const cResult = await form.validate('c') + expect(cResult.warnings).toEqual([]) + expect(cResult.errors).toEqual([{ path: 'c', messages: ['error msg'] }]) + expect(form.getFieldState('b', state => state.warnings)).toEqual([ + 'warning msg' + ]) + expect(form.getFieldState('c', state => state.errors)).toEqual([ + 'error msg' + ]) + expect(form.getFormState(state => state.warnings)).toEqual([ + { path: 'b', messages: ['warning msg'] } + ]) + expect(form.getFormState(state => state.errors)).toEqual([ + { path: 'c', messages: ['error msg'] } + ]) + + form.setFieldValue('b', 1) + form.setFieldValue('c', 1) + const bResult2 = await form.validate('b') + const cResult2 = await form.validate('c') + expect(bResult2.warnings).toEqual([]) + expect(bResult2.errors).toEqual([]) + expect(cResult2.warnings).toEqual([]) + expect(cResult2.errors).toEqual([]) + expect(form.getFieldState('b', state => state.warnings)).toEqual([]) + expect(form.getFieldState('c', state => state.errors)).toEqual([]) + expect(form.getFormState(state => state.warnings)).toEqual([]) + expect(form.getFormState(state => state.errors)).toEqual([]) + }) +}) + +describe('setFormState', () => { + test('no callback', async () => { + const form = createForm() + const state = form.getFormState() + form.setFormState() + expect(form.getFormState()).toEqual(state) + }) + + test('set', async () => { + const form = createForm() + // pristine 依赖 draft.values === draft.initialValues + // invalid 依赖 errors.length === 0 + // valid 是 invalid的取反 + // loading 取决于validating + // mounted 和 unmounted 互为取反,先读mounted + // errors, warnings 无法被设置,会从最新的state中获取 + form.registerField({ + path: 'b', + rules: [ + v => + v === undefined + ? { type: 'warning', message: 'warning msg' } + : undefined + ] + }) // CustomValidator warning + form.registerField({ + path: 'c', + rules: [ + v => + v === undefined ? { type: 'error', message: 'error msg' } : undefined + ] + }) // CustomValidator error + const { errors, warnings } = await form.validate() + const invalid = errors.length > 0 + + const values = { b: '2' } + const initialValues = { a: '1' } + const validating = true + form.setFormState(state => { + state.pristine = false + state.valid = false + state.invalid = false + state.loading = false + state.validating = validating + state.submitting = true + state.initialized = false + state.editable = false + state.values = values + state.initialValues = initialValues + state.mounted = true + state.unmounted = true + state.props = { hello: 'world' } + }) + expect(form.getFormState()).toEqual({ + displayName: 'FormState', + pristine: isEqual(values, initialValues), + valid: !invalid, + invalid: invalid, + loading: validating, + validating: validating, + submitting: true, + initialized: false, + editable: false, + errors, + warnings, + values, + initialValues, + mounted: true, + unmounted: false, + props: { hello: 'world' } + }) + }) + + test('set with slient', async () => { + const fieldChange = jest.fn() + const formChange = jest.fn() + const form = createForm({ + lifecycles: [ + new FormLifeCycle(LifeCycleTypes.ON_FIELD_CHANGE, fieldChange), + new FormLifeCycle(LifeCycleTypes.ON_FORM_CHANGE, formChange) + ] + }) + + form.registerField({ path: 'a' }) + + form.setFormState(state => { + state.values = { a: '1234' } + }) + expect(form.getFormState(state => state.values)).toEqual({ a: '1234' }) + expect(fieldChange).toBeCalledTimes(2) + expect(formChange).toBeCalledTimes(2) + + form.setFormState(state => (state.values = { a: '5678' }), true) + expect(form.getFormState(state => state.values)).toEqual({ a: '5678' }) + expect(formChange).toBeCalledTimes(2) + expect(fieldChange).toBeCalledTimes(2) + }) +}) + +describe('getFormState', () => { + test('basic', async () => { + const form = createForm() + const state = form.getFormState() + expect(state).toEqual(form.getFormState(state => state)) + expect(form.getFormState(state => state.pristine)).toEqual(state.pristine) + expect(form.getFormState(state => state.valid)).toEqual(state.valid) + expect(form.getFormState(state => state.invalid)).toEqual(state.invalid) + expect(form.getFormState(state => state.loading)).toEqual(state.loading) + expect(form.getFormState(state => state.validating)).toEqual( + state.validating + ) + expect(form.getFormState(state => state.submitting)).toEqual( + state.submitting + ) + expect(form.getFormState(state => state.initialized)).toEqual( + state.initialized + ) + expect(form.getFormState(state => state.editable)).toEqual(state.editable) + expect(form.getFormState(state => state.errors)).toEqual(state.errors) + expect(form.getFormState(state => state.warnings)).toEqual(state.warnings) + expect(form.getFormState(state => state.values)).toEqual(state.values) + expect(form.getFormState(state => state.initialValues)).toEqual( + state.initialValues + ) + expect(form.getFormState(state => state.mounted)).toEqual(state.mounted) + expect(form.getFormState(state => state.unmounted)).toEqual(state.unmounted) + expect(form.getFormState(state => state.props)).toEqual(state.props) + }) +}) + +describe('setFieldState', () => { + test('no callback', async () => { + const form = createForm() + form.registerField({ path: 'a' }) + const state = form.getFieldState('a') + form.setFieldState('a') + expect(form.getFieldState('a')).toEqual(state) + }) + + test('set with slient', async () => { + const fieldChange = jest.fn() + const form = createForm({ + lifecycles: [ + new FormLifeCycle(LifeCycleTypes.ON_FIELD_CHANGE, fieldChange) + ] + }) + form.registerField({ path: 'a' }) + form.getFieldState('a') + form.setFieldState('a', state => (state.value = '1234')) + expect(form.getFieldState('a', state => state.value)).toEqual('1234') + expect(fieldChange).toBeCalledTimes(2) + + form.setFieldState('a', state => (state.value = '5678'), true) + expect(form.getFieldState('a', state => state.value)).toEqual('5678') + expect(fieldChange).toBeCalledTimes(2) + }) + + test('validating and loading', () => { + const form = createForm() + form.registerField({ + path: 'a', + rules: [ + v => + v === undefined + ? { type: 'warning', message: 'warning msg' } + : undefined, + v => + v === undefined ? { type: 'error', message: 'error msg' } : undefined + ] + }) + expect(form.getFieldState('a', state => state.validating)).toEqual(false) + expect(form.getFieldState('a', state => state.loading)).toEqual(false) + form.setFieldState('a', state => (state.validating = true)) + expect(form.getFieldState('a', state => state.loading)).toEqual(true) + form.setFieldState('a', state => (state.validating = false)) + expect(form.getFieldState('a', state => state.loading)).toEqual(false) + }) + + test('value, values and parseValues', () => { + const form = createForm() + form.registerField({ path: 'a' }) + expect(form.getFieldState('a', state => state.modified)).toEqual(false) + expect(form.getFieldState('a', state => state.value)).toEqual(undefined) + expect(form.getFieldState('a', state => state.values)).toEqual([undefined]) + const arr = [1, 2, 3] + form.setFieldState('a', state => (state.value = arr)) + expect(form.getFieldState('a', state => state.modified)).toEqual(true) + expect(form.getFieldState('a', state => state.value)).toEqual(arr) + expect(form.getFieldState('a', state => state.values)).toEqual([arr]) + form.setFieldState('a', state => (state.values = ['e', 'context'])) + // values 第一个参数会是value, 处理onChange的多参数一般会和values[0]同步,这里value测试极端情况 + expect(form.getFieldState('a', state => state.value)).toEqual(arr) + expect(form.getFieldState('a', state => state.values)).toEqual([ + arr, + 'context' + ]) + // visible为false或者已卸载的组件无法修改value + form.setFieldState('a', state => (state.visible = false)) + form.setFieldState('a', state => (state.value = [4, 5, 6])) + expect(form.getFieldState('a', state => state.value)).toEqual(arr) + form.setFieldState('a', state => { + state.visible = true + state.unmounted = true + }) + form.setFieldState('a', state => (state.value = [4, 5, 6])) + expect(form.getFieldState('a', state => state.value)).toEqual(arr) + }) + + test('mount and unmount', () => { + const form = createForm() + form.registerField({ path: 'a' }) + expect(form.getFieldState('a', state => state.mounted)).toEqual(false) + expect(form.getFieldState('a', state => state.unmounted)).toEqual(false) + form.setFieldState('a', state => (state.mounted = true)) + expect(form.getFieldState('a', state => state.mounted)).toEqual(true) + expect(form.getFieldState('a', state => state.unmounted)).toEqual(false) + }) + + test('rules, required and parseRules', () => { + const form = createForm() + form.registerField({ path: 'a' }) + expect(form.getFieldState('a', state => state.required)).toEqual(false) + expect(form.getFieldState('a', state => state.rules)).toEqual([]) + form.setFieldState('a', state => (state.required = true)) + expect(form.getFieldState('a', state => state.required)).toEqual(true) + const customValidator: ValidatePatternRules[] = [ + (v, _: ValidateDescription) => + v === undefined ? { type: 'warning', message: 'warning msg' } : null + ] + form.setFieldState('a', state => (state.rules = customValidator)) + const rules = form.getFieldState('a', state => state.rules) + expect(rules).toEqual([...customValidator]) + }) + + test('pristine', () => { + // 无法被修改,依赖value和initialValue的差别 + const form = createForm() + form.registerField({ path: 'a' }) + expect(form.getFieldState('a', state => state.pristine)).toEqual(true) + form.setFieldState('a', state => (state.pristine = false)) + expect(form.getFieldState('a', state => state.pristine)).toEqual(true) + form.setFieldState('a', state => (state.value = '1')) + expect(form.getFieldState('a', state => state.pristine)).toEqual(false) + form.setFieldState('a', state => (state.initialValue = '1')) + expect(form.getFieldState('a', state => state.pristine)).toEqual(true) + }) + + test('invalid 和 valid', () => { + // 无法被修改,依赖错误信息和告警信息 + const form = createForm() + form.registerField({ path: 'a' }) + expect(form.getFieldState('a', state => state.invalid)).toEqual(false) + expect(form.getFieldState('a', state => state.valid)).toEqual(true) + form.setFieldState('a', state => (state.invalid = true)) + form.setFieldState('a', state => (state.valid = false)) + expect(form.getFieldState('a', state => state.invalid)).toEqual(false) + expect(form.getFieldState('a', state => state.valid)).toEqual(true) + }) + + test('set errors and warnings', async () => { + const form = createForm() + const a = form.registerField({ + path: 'a', + rules: [ + v => + v === undefined + ? { type: 'warning', message: 'warning msg' } + : undefined, + v => + v === undefined ? { type: 'error', message: 'error msg' } : undefined + ] + }) + const state = form.getFieldState('a') + expect(state.effectErrors).toEqual([]) + expect(state.effectWarnings).toEqual([]) + expect(state.ruleErrors).toEqual([]) + expect(state.ruleWarnings).toEqual([]) + + const mutators = form.createMutators(a) + const result = await mutators.validate() + expect(result.errors).toEqual([{ path: 'a', messages: ['error msg'] }]) + expect(result.warnings).toEqual([{ path: 'a', messages: ['warning msg'] }]) + // 校验后影响的是ruleErrors, ruleWarnings + const state2 = form.getFieldState('a') + expect(state2.effectErrors).toEqual([]) + expect(state2.effectWarnings).toEqual([]) + expect(state2.ruleErrors).toEqual(['error msg']) + expect(state2.ruleWarnings).toEqual(['warning msg']) + expect(state2.errors).toEqual(['error msg']) + expect(state2.warnings).toEqual(['warning msg']) + + // effectError 和 effectWarnings 需要通过设置 errors 和 warning 进行设置 + // 看起来有问题,但是对于开发者并不透出effectErrors的概念,只让他感知这是errors + // errors = effectErrors + ruleErrors + // warnings = effectWarnings + ruleWarnings + form.setFieldState('a', state => (state.errors = ['effect errors msg'])) + form.setFieldState('a', state => (state.warnings = ['effect warning msg'])) + const state3 = form.getFieldState('a') + expect(state3.effectErrors).toEqual(['effect errors msg']) + expect(state3.effectWarnings).toEqual(['effect warning msg']) + expect(state3.errors).toEqual(['error msg', 'effect errors msg']) + expect(state3.warnings).toEqual(['warning msg', 'effect warning msg']) + + // 不可编辑,清空所有错误和警告信息 + form.setFieldState('a', state => (state.editable = false)) + const state4 = form.getFieldState('a') + expect(state4.effectErrors).toEqual([]) + expect(state4.effectWarnings).toEqual([]) + expect(state4.errors).toEqual([]) + expect(state4.warnings).toEqual([]) + form.setFieldState('a', state => (state.editable = true)) + + // 隐藏,清空所有错误和警告信息 + await mutators.validate() + form.setFieldState('a', state => (state.errors = ['effect errors msg'])) + form.setFieldState('a', state => (state.warnings = ['effect warning msg'])) + form.setFieldState('a', state => (state.visible = false)) + const state6 = form.getFieldState('a') + expect(state6.effectErrors).toEqual([]) + expect(state6.effectWarnings).toEqual([]) + expect(state6.errors).toEqual([]) + expect(state6.warnings).toEqual([]) + + // 卸载组件,清空所有错误和警告信息 + await mutators.validate() + form.setFieldState('a', state => (state.errors = ['effect errors msg'])) + form.setFieldState('a', state => (state.warnings = ['effect warning msg'])) + const state7 = form.getFieldState('a') + expect(state7.effectErrors).toEqual([]) + expect(state7.effectWarnings).toEqual([]) + expect(state7.errors).toEqual([]) + expect(state7.warnings).toEqual([]) + }) + + test('set editable', async () => { + const form = createForm() + form.registerField({ path: 'a' }) + const state = form.getFieldState('a') + + // 初始化 + expect(state.editable).toEqual(true) + expect(state.selfEditable).toEqual(undefined) + expect(state.formEditable).toEqual(undefined) + expect(form.getFieldState('a', state => state.editable)).toEqual(true) + // 简单设置 (editable会影响selfEditable) + form.setFieldState('a', state => (state.editable = false)) + expect(form.getFieldState('a', state => state.editable)).toEqual(false) + expect(form.getFieldState('a', state => state.selfEditable)).toEqual(false) + // 设置影响计算的值selfEditable + form.setFieldState('a', state => (state.selfEditable = true)) + expect(form.getFieldState('a', state => state.editable)).toEqual(true) + // 设置影响计算的值formEditable(selfEditable优先级高于formEditable) + form.setFieldState('a', state => (state.selfEditable = undefined)) + form.setFieldState('a', state => (state.formEditable = false)) + expect(form.getFieldState('a', state => state.editable)).toEqual(false) + // 支持函数(UI层传入) + form.setFieldState('a', state => (state.formEditable = () => true)) + expect(form.getFieldState('a', state => state.editable)).toEqual(true) + // editable会影响selfEditable, 设置顺序又因为 editable > selfEditable > formEditable + // 前两者都无效时,最终返回formEditable的值 + form.setFieldState('a', state => (state.editable = undefined)) + form.setFieldState('a', state => (state.formEditable = () => false)) + expect(form.getFieldState('a', state => state.selfEditable)).toEqual( + undefined + ) + expect(form.getFieldState('a', state => state.editable)).toEqual(false) + }) +}) + +describe('getFieldState', () => { + const form = createForm() + form.registerField({ path: 'a' }) + const state = form.getFieldState('a') + expect(state).toEqual(form.getFieldState('a', state => state)) + expect(form.getFieldState('a', state => state.name)).toEqual(state.name) + expect(form.getFieldState('a', state => state.initialized)).toEqual( + state.initialized + ) + expect(form.getFieldState('a', state => state.pristine)).toEqual( + state.pristine + ) + expect(form.getFieldState('a', state => state.valid)).toEqual(state.valid) + expect(form.getFieldState('a', state => state.touched)).toEqual(state.touched) + expect(form.getFieldState('a', state => state.invalid)).toEqual(state.invalid) + expect(form.getFieldState('a', state => state.visible)).toEqual(state.visible) + expect(form.getFieldState('a', state => state.display)).toEqual(state.display) + expect(form.getFieldState('a', state => state.editable)).toEqual( + state.editable + ) + expect(form.getFieldState('a', state => state.formEditable)).toEqual( + state.formEditable + ) + expect(form.getFieldState('a', state => state.loading)).toEqual(state.loading) + expect(form.getFieldState('a', state => state.modified)).toEqual( + state.modified + ) + expect(form.getFieldState('a', state => state.active)).toEqual(state.active) + expect(form.getFieldState('a', state => state.visited)).toEqual(state.visited) + expect(form.getFieldState('a', state => state.validating)).toEqual( + state.validating + ) + expect(form.getFieldState('a', state => state.errors)).toEqual(state.errors) + expect(form.getFieldState('a', state => state.values)).toEqual(state.values) + expect(form.getFieldState('a', state => state.effectErrors)).toEqual( + state.effectErrors + ) + expect(form.getFieldState('a', state => state.warnings)).toEqual( + state.warnings + ) + expect(form.getFieldState('a', state => state.effectWarnings)).toEqual( + state.effectWarnings + ) + expect(form.getFieldState('a', state => state.value)).toEqual(state.value) + expect(form.getFieldState('a', state => state.initialValue)).toEqual( + state.initialValue + ) + expect(form.getFieldState('a', state => state.rules)).toEqual(state.rules) + expect(form.getFieldState('a', state => state.required)).toEqual( + state.required + ) + expect(form.getFieldState('a', state => state.mounted)).toEqual(state.mounted) + expect(form.getFieldState('a', state => state.unmounted)).toEqual( + state.unmounted + ) + expect(form.getFieldState('a', state => state.props)).toEqual(state.props) +}) + +describe('setFieldValue', () => { + const form = createForm() + form.registerField({ path: 'a' }) + form.setFieldValue('a') + expect(form.getFieldValue('a')).toEqual(undefined) + expect(form.getFieldState('a', state => state.value)).toEqual(undefined) + expect(form.getFormState(state => state.values)).toEqual({ a: undefined }) + form.setFieldValue('a', 1) + expect(form.getFieldValue('a')).toEqual(1) + expect(form.getFieldState('a', state => state.value)).toEqual(1) + expect(form.getFormState(state => state.values)).toEqual({ a: 1 }) +}) + +describe('getFieldValue', () => { + test('normal path', async () => { + const form = createForm({ values: deepValues }) + form.registerField({ path: 'a' }) + form.registerField({ path: 'a.b' }) + form.registerField({ path: 'a.b.c' }) + form.registerField({ path: 'a.b.c.d' }) + form.registerField({ path: 'a.b.c.d.e' }) + + expect(form.getFieldValue('a')).toEqual(deepValues.a) + expect(form.getFieldValue('a.b')).toEqual(deepValues.a.b) + expect(form.getFieldValue('a.b.c')).toEqual(deepValues.a.b.c) + expect(form.getFieldValue('a.b.c.d')).toEqual(deepValues.a.b.c.d) + expect(form.getFieldValue('a.b.c.d.e')).toEqual(deepValues.a.b.c.d.e) + }) + + test('virtual path', async () => { + const form = createForm({ values: deepValues }) + form.registerField({ path: 'a' }) + form.registerVirtualField({ path: 'a.b' }) + form.registerField({ path: 'a.b.c' }) + form.registerVirtualField({ path: 'a.b.c.d' }) + form.registerField({ path: 'a.b.c.d.e' }) + + expect(form.getFieldValue('a')).toEqual(deepValues.a) + expect(form.getFieldValue('a.b')).toEqual(undefined) + expect(form.getFieldValue('a.b.c')).toEqual(deepValues.a.c) + expect(form.getFieldValue('a.b.c.d')).toEqual(undefined) + expect(form.getFieldValue('a.b.c.d.e')).toEqual(deepValues.a.c.e) + }) + + test('virtual path(head)', async () => { + const form = createForm({ values: deepValues }) + form.registerVirtualField({ path: 'a' }) + form.registerField({ path: 'a.b' }) + form.registerField({ path: 'a.b.c' }) + + expect(form.getFieldValue('a')).toEqual(undefined) + expect(form.getFieldValue('a.b')).toEqual(deepValues.b) + expect(form.getFieldValue('a.b.c')).toEqual(deepValues.b.c) + }) +}) + +describe('registerField', () => { + test('basic', async () => { + const form = createForm({ values: { a: 1 } }) + form.registerField({ path: 'a' }) + form.registerField({ path: 'b' }) + form.registerField({ path: 'c' }) + form.registerField({ path: 'd', editable: false }) + form.registerField({ path: 'e', editable: true }) + expect(form.getFieldValue('a')).toEqual(1) + expect(form.getFieldValue('b')).toEqual(undefined) + expect(form.getFieldState('a', state => state.values)).toEqual([1]) + expect(form.getFieldState('b', state => state.values)).toEqual([undefined]) + expect(form.getFieldState('c', state => state.editable)).toEqual(true) + expect(form.getFieldState('d', state => state.editable)).toEqual(false) + expect(form.getFieldState('e', state => state.editable)).toEqual(true) + }) + + test('merge', async () => { + const form = createForm({ values: { a: 1, b: 2, c: 3, d: 4 } }) + form.registerField({ path: 'a' }) + form.registerField({ path: 'b', value: 'x' }) + form.registerField({ path: 'c', initialValue: 'y' }) + form.registerField({ path: 'd', initialValue: 'z', value: 's' }) + expect(form.getFieldValue('a')).toEqual(1) + expect(form.getFieldValue('b')).toEqual('x') + expect(form.getFieldValue('c')).toEqual(3) // false, 得到y + expect(form.getFieldValue('d')).toEqual('s') + }) +}) + +describe('registerVirtualField', () => { + test('basic', async () => { + const vprops = { hello: 'world' } + const form = createForm({ values: { a: 1 } }) + form.registerVirtualField({ path: 'a' }) + form.registerVirtualField({ path: 'b' }) + form.registerVirtualField({ path: 'c', props: vprops }) + expect(form.getFieldValue('a')).toEqual(undefined) + expect(form.getFieldState('a', state => state.values)).toEqual(undefined) // 不存在这个属性 + expect(form.getFieldState('c', state => state.props)).toEqual(vprops) + expect(form.getFieldState('b', state => state.display)).toEqual(true) + expect(form.getFieldState('b', state => state.visible)).toEqual(true) + form.setFieldState('b', state => (state.display = false)) + expect(form.getFieldState('b', state => state.display)).toEqual(false) + form.setFieldState('b', state => (state.visible = false)) + expect(form.getFieldState('b', state => state.visible)).toEqual(false) + }) +}) + +describe('createMutators', () => { + const arr = ['a', 'b'] + test('change', async () => { + const form = createForm() + const a = form.registerField({ path: 'a' }) + const mutators = form.createMutators(a) + expect( + form.getFieldState('a', state => ({ + values: state.values, + value: state.value + })) + ).toEqual({ + value: undefined, + values: [undefined] + }) + mutators.change(1, 2, 3, 4) + expect( + form.getFieldState('a', state => ({ + values: state.values, + value: state.value + })) + ).toEqual({ + value: 1, + values: [1, 2, 3, 4] + }) + }) + + test('focus', async () => { + const form = createForm() + const a = form.registerField({ path: 'a' }) + const mutators = form.createMutators(a) + expect(form.getFormGraph()).toMatchSnapshot() + expect( + form.getFieldState('a', state => ({ + active: state.active, + visited: state.visited + })) + ).toEqual({ + active: false, + visited: false + }) + mutators.focus() + expect(form.getFormGraph()).toMatchSnapshot() + expect( + form.getFieldState('a', state => ({ + active: state.active, + visited: state.visited + })) + ).toEqual({ + active: true, + visited: false + }) + mutators.blur() + expect( + form.getFieldState('a', state => ({ + active: state.active, + visited: state.visited + })) + ).toEqual({ + active: false, + visited: true + }) + }) + + test('blur', async () => { + const form = createForm() + const a = form.registerField({ path: 'a' }) + const mutators = form.createMutators(a) + expect(form.getFormGraph()).toMatchSnapshot() + expect( + form.getFieldState('a', state => ({ + active: state.active, + visited: state.visited + })) + ).toEqual({ + active: false, + visited: false + }) + mutators.focus() + mutators.blur() + expect(form.getFormGraph()).toMatchSnapshot() + expect( + form.getFieldState('a', state => ({ + active: state.active, + visited: state.visited + })) + ).toEqual({ + active: false, + visited: true + }) + }) + + test('push', async () => { + const form = createForm() + const mm = form.registerField({ path: 'mm', value: [] }) + const mutators = form.createMutators(mm) + expect(form.getFieldValue('mm')).toEqual([]) + mutators.push({}) + expect(form.getFieldValue('mm')).toEqual([{}]) + }) + + test('pop', async () => { + const form = createForm() + const mm = form.registerField({ path: 'mm', value: [{}] }) + const mutators = form.createMutators(mm) + expect(form.getFieldValue('mm')).toEqual([{}]) + mutators.pop() + expect(form.getFieldValue('mm')).toEqual([]) + }) + + test('insert', async () => { + const form = createForm() + const mm = form.registerField({ path: 'mm', value: arr }) + const mutators = form.createMutators(mm) + expect(form.getFieldValue('mm')).toEqual(arr) + mutators.insert(1, 'x') + expect(form.getFieldValue('mm')).toEqual(['a', 'x', 'b']) + }) + + test('remove', async () => { + const form = createForm() + const mm = form.registerField({ path: 'mm', value: arr }) + const mutators = form.createMutators(mm) + expect(form.getFieldValue('mm')).toEqual(arr) + mutators.remove(1) + expect(form.getFieldValue('mm')).toEqual(arr.slice(0, 1)) + }) + + test('remove object key', async () => { + const form = createForm({ useDirty: true }) + const initialValue = { username: '1234' } + const user = form.registerField({ path: 'user', initialValue }) + form.registerField({ path: 'user.username' }) + const mutators = form.createMutators(user) + expect(form.getFieldValue('user')).toEqual(initialValue) + mutators.remove('username') + expect(form.getFieldValue('user')).toEqual({}) + }) + + test('exist', async () => { + const form = createForm() + const mm = form.registerField({ path: 'mm', value: arr }) + const mutators = form.createMutators(mm) + expect(mutators.exist(1)).toEqual(true) + }) + + test('shift', async () => { + const form = createForm() + const mm = form.registerField({ path: 'mm', value: arr }) + const mutators = form.createMutators(mm) + expect(form.getFieldValue('mm')).toEqual(arr) + mutators.shift() + expect(form.getFieldValue('mm')).toEqual(arr.slice(1)) + }) + + test('unshift', async () => { + const form = createForm() + const mm = form.registerField({ path: 'mm', value: arr }) + const mutators = form.createMutators(mm) + expect(form.getFieldValue('mm')).toEqual(arr) + mutators.unshift('x') + expect(form.getFieldValue('mm')).toEqual(['x', ...arr]) + }) + + test('move', async () => { + const form = createForm() + const mm = form.registerField({ path: 'mm', value: arr }) + const mutators = form.createMutators(mm) + expect(form.getFieldValue('mm')).toEqual(arr) + mutators.move(0, 1) + expect(form.getFieldValue('mm')).toEqual(arr.reverse()) + }) + + test('validate', async () => { + const form = createForm() + const mm = form.registerField({ + path: 'mm', + rules: [ + v => + v === undefined + ? { type: 'warning', message: 'warning msg' } + : undefined + ] + }) + const mutators = form.createMutators(mm) + const result = await mutators.validate() + expect(result.errors).toEqual([]) + expect(result.warnings).toEqual([{ path: 'mm', messages: ['warning msg'] }]) + mutators.change(1) + const result2 = await mutators.validate() + expect(result2.errors).toEqual([]) + expect(result2.warnings).toEqual([]) + }) +}) + +describe('transformDataPath', () => { + test('normal path', async () => { + const form = createForm() + form.registerField({ path: 'a' }) + form.registerField({ path: 'a.b' }) + form.registerField({ path: 'a.b.c' }) + form.registerField({ path: 'a.b.c.d' }) + form.registerField({ path: 'a.b.c.d.e' }) + + expect( + form.unsafe_do_not_use_transform_data_path(new FormPath('a')).toString() + ).toEqual('a') + expect( + form.unsafe_do_not_use_transform_data_path(new FormPath('a.b')).toString() + ).toEqual('a.b') + expect( + form + .unsafe_do_not_use_transform_data_path(new FormPath('a.b.c')) + .toString() + ).toEqual('a.b.c') + expect( + form + .unsafe_do_not_use_transform_data_path(new FormPath('a.b.c.d')) + .toString() + ).toEqual('a.b.c.d') + expect( + form + .unsafe_do_not_use_transform_data_path(new FormPath('a.b.c.d.e')) + .toString() + ).toEqual('a.b.c.d.e') + }) + + test('virtual path', async () => { + const form = createForm() + form.registerField({ path: 'a' }) + form.registerVirtualField({ path: 'a.b' }) + form.registerField({ path: 'a.b.c' }) + form.registerVirtualField({ path: 'a.b.c.d' }) + form.registerField({ path: 'a.b.c.d.e' }) + + expect( + form.unsafe_do_not_use_transform_data_path(new FormPath('a')).toString() + ).toEqual('a') + expect( + form.unsafe_do_not_use_transform_data_path(new FormPath('a.b')).toString() + ).toEqual('a.b') + expect( + form + .unsafe_do_not_use_transform_data_path(new FormPath('a.b.c')) + .toString() + ).toEqual('a.c') + expect( + form + .unsafe_do_not_use_transform_data_path(new FormPath('a.b.c.d')) + .toString() + ).toEqual('a.c.d') + expect( + form + .unsafe_do_not_use_transform_data_path(new FormPath('a.b.c.d.e')) + .toString() + ).toEqual('a.c.e') + }) + + test('virtual path(head)', async () => { + const form = createForm() + form.registerVirtualField({ path: 'a' }) + form.registerField({ path: 'a.b' }) + form.registerField({ path: 'a.b.c' }) + + expect( + form.unsafe_do_not_use_transform_data_path(new FormPath('a')).toString() + ).toEqual('a') + expect( + form.unsafe_do_not_use_transform_data_path(new FormPath('a.b')).toString() + ).toEqual('b') + expect( + form + .unsafe_do_not_use_transform_data_path(new FormPath('a.b.c')) + .toString() + ).toEqual('b.c') + }) +}) + +describe('major sences', () => { + test('dynamic remove with intialValues', async () => { + const form = createForm({ + initialValues: { + aa: [ + { aa: 123, bb: 321 }, + { aa: 345, bb: 678 } + ] + } + }) + const aa = form.registerField({ path: 'aa' }) + form.registerField({ path: 'aa.0' }) + form.registerField({ path: 'aa.0.aa' }) + form.registerField({ path: 'aa.0.bb' }) + form.registerField({ path: 'aa.1' }) + form.registerField({ path: 'aa.1.aa' }) + form.registerField({ path: 'aa.1.bb' }) + form.setFieldState('aa.1.aa', state => { + state.value = 'change aa' + }) + const mutators = form.createMutators(aa) + const snapshot = form.getFormGraph() + expect(snapshot).toMatchSnapshot() + mutators.remove(0) + expect(form.getFormGraph()).toMatchSnapshot() + form.setFormGraph(snapshot) + expect(form.getFormGraph()).toMatchSnapshot() + }) + + test('nested dynamic remove', () => { + const form = createForm({ + useDirty: true + }) + const aa = form.registerField({ path: 'aa', value: [] }) + form.registerField({ path: 'aa.0' }) + form.registerField({ path: 'aa.0.aa' }) + form.registerField({ path: 'aa.0.bb' }) + form.registerField({ path: 'aa.1' }) + form.registerField({ path: 'aa.1.aa' }) + form.registerField({ path: 'aa.1.bb' }) + form.setFieldState('aa.1.aa', state => { + state.value = 'change aa' + }) + + const mutators = form.createMutators(aa) + const snapshot = form.getFormGraph() + expect(snapshot).toMatchSnapshot() + mutators.remove(0) + expect(form.getFormGraph()).toMatchSnapshot() + form.setFormGraph(snapshot) + expect(form.getFormGraph()).toMatchSnapshot() + expect(form.getFormGraph()).toEqual(snapshot) + }) + + test('nested visible', () => { + const form = createForm() + form.registerField({ path: 'aa', value: {} }) + form.registerField({ path: 'aa.bb', initialValue: 123 }) + form.registerField({ path: 'aa.cc', initialValue: 222 }) + form.setFieldState('aa', state => (state.visible = false)) + expect(form.getFormState(state => state.values)).toEqual({}) + + expect(form.getFormGraph()).toMatchSnapshot() + form.setFieldState('aa.bb', state => (state.value = '123')) + expect(form.getFormGraph()).toMatchSnapshot() + + form.setFieldState('aa', state => (state.visible = true)) + expect(form.getFieldValue('aa')).toEqual({ bb: 123, cc: 222 }) + expect(form.getFormState(state => state.values)).toEqual({ + aa: { bb: 123, cc: 222 } + }) + expect(form.getFormGraph()).toMatchSnapshot() + }) + + test('deep nested visible(root)', () => { + const form = createForm() + form.registerField({ path: 'aa', value: {} }) + form.registerField({ path: 'aa.bb' }) + form.registerField({ path: 'aa.bb.cc', value: 123 }) + form.setFieldState('aa', state => (state.visible = false)) + expect(form.getFormGraph()).toMatchSnapshot() + expect(form.getFormState(state => state.values)).toEqual({}) + }) + + test('deep nested visible(middle part)', () => { + const form = createForm() + form.registerField({ path: 'aa', value: {} }) + form.registerField({ path: 'aa.bb' }) + form.registerField({ path: 'aa.bb.cc', value: 123 }) + form.setFieldState('aa.bb', state => (state.visible = false)) + expect(form.getFormGraph()).toMatchSnapshot() + expect(form.getFormState(state => state.values)).toEqual({ aa: {} }) + }) + + test('deep nested visible with VirtualField', () => { + const form = createForm() + form.registerField({ path: 'aa', value: {} }) + form.registerVirtualField({ path: 'aa.bb' }) + form.registerField({ path: 'aa.bb.cc', value: 123 }) + form.setFieldState('aa', state => { + state.visible = false + }) + expect(form.getFormGraph()).toMatchSnapshot() + }) +}) + +describe('validator', () => { + test('registerValidationFormats', async () => { + registerValidationFormats({ + number: /^[+-]?\d+(\.\d+)?$/ + }) + + const form = createForm({ + values: {}, + initialValues: {}, + onChange: values => { + // console.log(values) + } + }) + + const aa = form.registerField({ + path: 'aa', + rules: [ + { + format: 'number', + message: 'This field is not a number.' + } + ] + }) + + aa.setState(state => { + state.value = 'hello world' + }) + await form.validate() + + form.getFormState(state => + expect(state.errors).toEqual([ + { + path: 'aa', + messages: ['This field is not a number.'] + } + ]) + ) + }) + + test('registerValidationRules', async () => { + registerValidationRules({ + custom: value => { + return value === '123' ? 'This field can not be 123' : '' + } + }) + + const form = createForm({ + values: {}, + initialValues: {}, + onChange: values => { + // console.log(values) + } + }) + + const aa = form.registerField({ + path: 'aa', + rules: [ + { + custom: true + } + ] + }) + + aa.setState(state => { + state.value = '123' + }) + await form.validate() + + form.getFormState(state => + expect(state.errors).toEqual([ + { + path: 'aa', + messages: ['This field can not be 123'] + } + ]) + ) + }) + + test('registerValidationMTEngine', async () => { + registerValidationMTEngine((message, context) => { + return message.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, $0) => { + return FormPath.getIn(context, $0) + }) + }) + + const form = createForm({ + values: {}, + initialValues: {}, + onChange: values => { + // console.log(values) + } + }) + + const aa = form.registerField({ + path: 'aa', + rules: [ + { + validator(value) { + return value === 123 + ? 'This field can not be 123 {{scope.outerVariable}}' + : '' + }, + scope: { + outerVariable: 'addonAfter' + } + } + ] + }) + + aa.setState(state => { + state.value = 123 + }) + await form.validate() + + form.getFormState(state => + expect(state.errors).toEqual([ + { + path: 'aa', + messages: ['This field can not be 123 addonAfter'] + } + ]) + ) + }) +}) diff --git a/packages/core/src/__tests__/lifecycle.spec.ts b/packages/core/src/__tests__/lifecycle.spec.ts new file mode 100644 index 00000000000..3a886d2e004 --- /dev/null +++ b/packages/core/src/__tests__/lifecycle.spec.ts @@ -0,0 +1,169 @@ +import { FormHeart, FormLifeCycle } from '../shared/lifecycle' +import { LifeCycleTypes } from '../types' + +describe('FormLifeCycle', () => { + test('handler',()=>{ + const cb = jest.fn() + const lifeCycle = new FormLifeCycle(cb) + const data = { hello: 'world' } + lifeCycle.notify(LifeCycleTypes.ON_FORM_INIT, data) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith({ payload: data, type: LifeCycleTypes.ON_FORM_INIT }, undefined) + lifeCycle.notify(LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE, data) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith({ payload: data, type: LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE }, undefined) + }) + + test('handler with context',()=>{ + const cb = jest.fn() + const lifeCycle = new FormLifeCycle(cb) + const data = { hello: 'world' } + const context = { temp: true } + lifeCycle.notify(LifeCycleTypes.ON_FORM_INIT, data, context) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith({ payload: data, type: LifeCycleTypes.ON_FORM_INIT }, context) + lifeCycle.notify(LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE, data, context) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith({ payload: data, type: LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE }, context) + }) + + test('type/handler',()=>{ + const cb1 = jest.fn() + const cb2 = jest.fn() + const lifeCycle1= new FormLifeCycle(LifeCycleTypes.ON_FORM_INIT, cb1); + const lifeCycle2= new FormLifeCycle(LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE, cb2); + const data = { hello: 'world' } + lifeCycle1.notify(LifeCycleTypes.ON_FORM_INIT, data) + expect(cb1).toBeCalledTimes(1) + expect(cb2).toBeCalledTimes(0) + expect(cb1).toBeCalledWith(data, undefined) + lifeCycle2.notify(LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE, data) + expect(cb1).toBeCalledTimes(1) + expect(cb2).toBeCalledTimes(1) + expect(cb2).toBeCalledWith(data, undefined) + }) + + test('map',()=>{ + const cb1 = jest.fn() + const cb2 = jest.fn() + const lifeCycle = new FormLifeCycle({ + [LifeCycleTypes.ON_FORM_INIT]: cb1, + [LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE]: cb2, + }) + const data = { hello: 'world' } + lifeCycle.notify(LifeCycleTypes.ON_FORM_INIT, data) + expect(cb1).toBeCalledTimes(1) + expect(cb2).toBeCalledTimes(0) + expect(cb1).toBeCalledWith(data, undefined) + lifeCycle.notify(LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE, data) + expect(cb1).toBeCalledTimes(1) + expect(cb2).toBeCalledTimes(1) + expect(cb2).toBeCalledWith(data, undefined) + }) +}) + +describe('FormHeart', () => { + test('heart is instance of Subscribe',()=>{ + const heart = new FormHeart({}) + const cb = jest.fn() + const idx = heart.subscribe(cb) + const data = { hello: 'world' } + heart.notify(data) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(data) + heart.unsubscribe(idx) + heart.notify(data) + expect(cb).toBeCalledTimes(1) + }) + + test('lifecycles constructor handler',()=>{ + const cb = jest.fn() + const heart = new FormHeart({ + lifecycles: [ + new FormLifeCycle(cb), + ] + }) + const data = { hello: 'world' } + heart.publish(LifeCycleTypes.ON_FORM_INIT, data) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith({ payload: data, type: LifeCycleTypes.ON_FORM_INIT }, undefined) + heart.publish(LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE, data) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith({ payload: data, type: LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE }, undefined) + }) + + test('lifecycles constructor type/handler',()=>{ + const cb1 = jest.fn() + const cb2 = jest.fn() + const heart = new FormHeart({ + lifecycles: [ + new FormLifeCycle(LifeCycleTypes.ON_FORM_INIT, cb1), + new FormLifeCycle(LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE, cb2), + ] + }) + const data = { hello: 'world' } + heart.publish(LifeCycleTypes.ON_FORM_INIT, data) + expect(cb1).toBeCalledTimes(1) + expect(cb2).toBeCalledTimes(0) + expect(cb1).toBeCalledWith(data, undefined) + heart.publish(LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE, data) + expect(cb1).toBeCalledTimes(1) + expect(cb2).toBeCalledTimes(1) + expect(cb2).toBeCalledWith(data, undefined) + }) + + test('lifecycles constructor map',()=>{ + const cb1 = jest.fn() + const cb2 = jest.fn() + const heart = new FormHeart({ + lifecycles: [ + new FormLifeCycle({ + [LifeCycleTypes.ON_FORM_INIT]: cb1, + [LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE]: cb2, + }), + ] + }) + const data = { hello: 'world' } + heart.publish(LifeCycleTypes.ON_FORM_INIT, data) + expect(cb1).toBeCalledTimes(1) + expect(cb2).toBeCalledTimes(0) + expect(cb1).toBeCalledWith(data, undefined) + heart.publish(LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE, data) + expect(cb1).toBeCalledTimes(1) + expect(cb2).toBeCalledTimes(1) + expect(cb2).toBeCalledWith(data, undefined) + }) + + test('lifecycles with constructor context',()=>{ + const cb = jest.fn() + const context = { constructor: true } + const heart = new FormHeart({ + lifecycles: [ + new FormLifeCycle(LifeCycleTypes.ON_FORM_INIT, cb), + ], + context, + }) + const data = { hello: 'world' } + heart.publish(LifeCycleTypes.ON_FORM_INIT, data) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(data, context) + }) + + test('lifecycles with temp context',()=>{ + const cb = jest.fn() + const context = { constructor: true } + const heart = new FormHeart({ + lifecycles: [ + new FormLifeCycle(LifeCycleTypes.ON_FORM_INIT, cb), + ], + }) + const data = { hello: 'world' } + heart.publish(LifeCycleTypes.ON_FORM_INIT, data) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(data, undefined) + + heart.publish(LifeCycleTypes.ON_FORM_INIT, data, context) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith(data, context) + }) +}) diff --git a/packages/core/src/__tests__/model.spec.ts b/packages/core/src/__tests__/model.spec.ts new file mode 100644 index 00000000000..8a8e90c270e --- /dev/null +++ b/packages/core/src/__tests__/model.spec.ts @@ -0,0 +1,285 @@ +import { createStateModel } from '../shared/model' + +const displayName = 'TEST' +const defaultState = { type: 'controller defaultState' } +const defaultProps = { type: 'controller defaultProps' } + +class State { + static displayName = displayName + static defaultState = defaultState + static defaultProps = defaultProps + + name: string + state: any + props: any + + constructor(state, props) { + this.name = 'inner' + this.state = state + this.props = props + } + + dirtyCheck(dirtys) {} + computeState(state, prevState) {} +} +const StateModel = createStateModel(State) + +test('createStateModel', () => { + const params = { modelType: 'model defaultProps' } + const state1 = new StateModel(params) + // model properties + expect(state1.state).toEqual({ displayName, ...defaultState }) + expect(state1.props).toEqual({ ...defaultProps, ...params }) + expect(state1.dirtys).toEqual({}) + expect(state1.dirtyNum).toEqual(0) + expect(state1.batching).toEqual(false) + expect(state1.displayName).toEqual(displayName) + expect(state1.controller).toEqual({ + state: { displayName, ...defaultState }, + props: { ...defaultProps, ...params }, + name: 'inner', + }) +}) + +describe('proxy model', () => { + test('subscribe/unsubscribe', () => { + const state = new StateModel({ useDirty: false }) + const cb = jest.fn() + const idx = state.subscribe(cb) + const paylaod = { hello: 'world' } + state.notify(paylaod) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(paylaod) + state.unsubscribe(idx) + state.notify(paylaod) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(paylaod) + }) + test('batch with ', () => { + const state = new StateModel({ useDirty: false }) + const cb = jest.fn() + state.batch(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith() + // force run getState + const susCb = jest.fn() + state.subscribe(susCb) + state.dirtyNum = 1 + state.batch(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith() + expect(susCb).toBeCalledTimes(1) + expect(susCb).toBeCalledWith(state.state) + }) + test('getState', () => { + const state = new StateModel({ useDirty: false }) + const cb = jest.fn() + state.getState(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(state.state) + const syncState = state.getState() + expect(syncState).toEqual(state.state) + + state.controller.publishState = () => null + state.getState(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith(null) + }) + test('setState', () => { + const state = new StateModel({ useDirty: false }) + const susCb = jest.fn() + state.subscribe(susCb) + const cb1 = (draft) => draft.change = true + const cb2 = (draft) => draft.withNotify = true + const cb3 = (draft) => draft.withBatching = true + const cb4 = (draft) => draft.withBatching2 = true + const prevState1 = state.getState() + expect(prevState1.change).toEqual(undefined) + + // 默认 slient = false, 触发notify 通知UI更新 + state.setState(cb1) + expect(state.getState().change).toEqual(true) + expect(state.getState()).toEqual({ ...prevState1, change: true }) + expect(susCb).toBeCalledTimes(1) + expect(susCb).toBeCalledWith({ ...prevState1, change: true }) + + // slient = true 不触发notify + const prevState2 = state.getState() + expect(prevState2.withNotify).toEqual(undefined) + state.setState(cb2, true) + expect(state.getState().withNotify).toEqual(true) + expect(state.getState()).toEqual({ ...prevState2, withNotify: true }) + expect(susCb).toBeCalledTimes(1) + + // batching 相当于slient = true + const prevState3 = state.getState() + expect(prevState3.withBatching).toEqual(undefined) + expect(prevState3.withBatching2).toEqual(undefined) + state.batch(() => { + state.setState(cb3) + state.setState(cb4) + }) + + expect(state.getState().withBatching).toEqual(true) + expect(state.getState().withBatching2).toEqual(true) + expect(state.getState()).toEqual({ ...prevState3, withBatching: true, withBatching2: true }) + // 这次notify是由batch批处理结束调用的 + expect(susCb).toBeCalledTimes(2) + expect(susCb).toBeCalledWith({ ...prevState3, withBatching: true, withBatching2: true }) + }) + test('getSourceState', () => { + const state = new StateModel({ useDirty: false }) + const cb = jest.fn() + state.unsafe_getSourceState(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(state.state) + const syncState = state.unsafe_getSourceState() + expect(syncState).toEqual(state.state) + + state.controller.publishState = () => null + state.unsafe_getSourceState(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith(state.state) + }) + test('setSourceState', () => { + const state = new StateModel({ useDirty: false }) + const cb1 = (draft) => draft.change = true + const prevState1 = state.unsafe_getSourceState() + expect(prevState1.change).toEqual(undefined) + + state.unsafe_setSourceState(cb1) + expect(state.unsafe_getSourceState()).toEqual({ ...prevState1, change: true }) + }) + test('isDirty', () => { + const state = new StateModel({ useDirty: false }) + expect(state.dirtyNum).toEqual(0) + expect(state.isDirty()).toEqual(false) + state.dirtyNum = 1 + expect(state.isDirty()).toEqual(true) + state.dirtyNum = 0 + expect(state.isDirty()).toEqual(false) + state.dirtys.change = true + expect(state.isDirty()).toEqual(false) + expect(state.isDirty('change')).toEqual(true) + }) + +}) + +describe('dirty model', () => { + test('subscribe/unsubscribe', () => { + const state = new StateModel({ useDirty: true }) + const cb = jest.fn() + const idx = state.subscribe(cb) + const paylaod = { hello: 'world' } + state.notify(paylaod) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(paylaod) + state.unsubscribe(idx) + state.notify(paylaod) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(paylaod) + }) + test('batch', () => { + const state = new StateModel({ useDirty: true }) + const cb = jest.fn() + state.batch(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith() + // force run getState + const susCb = jest.fn() + state.subscribe(susCb) + state.dirtyNum = 1 + state.batch(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith() + expect(susCb).toBeCalledTimes(1) + expect(susCb).toBeCalledWith(state.state) + }) + test('getState', () => { + const state = new StateModel({ useDirty: true }) + const cb = jest.fn() + state.getState(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(state.state) + const syncState = state.getState() + expect(syncState).toEqual(state.state) + + state.controller.publishState = () => null + state.getState(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith(null) + }) + test('setState', () => { + const state = new StateModel({ useDirty: true }) + const susCb = jest.fn() + state.subscribe(susCb) + const cb1 = (draft) => draft.change = true + const cb2 = (draft) => draft.withNotify = true + const cb3 = (draft) => draft.withBatching = true + const cb4 = (draft) => draft.withBatching2 = true + const prevState1 = state.getState() + expect(prevState1.change).toEqual(undefined) + + // 默认 slient = false, 触发notify 通知UI更新 + state.setState(cb1) + expect(state.getState().change).toEqual(true) + expect(state.getState()).toEqual({ ...prevState1, change: true }) + expect(susCb).toBeCalledTimes(1) + expect(susCb).toBeCalledWith({ ...prevState1, change: true }) + + // slient = true 不触发notify + const prevState2 = state.getState() + expect(prevState2.withNotify).toEqual(undefined) + state.setState(cb2, true) + expect(state.getState().withNotify).toEqual(true) + expect(state.getState()).toEqual({ ...prevState2, withNotify: true }) + expect(susCb).toBeCalledTimes(1) + + // batching 相当于slient = true + const prevState3 = state.getState() + expect(prevState3.withBatching).toEqual(undefined) + expect(prevState3.withBatching2).toEqual(undefined) + state.batch(() => { + state.setState(cb3) + state.setState(cb4) + }) + + expect(state.getState().withBatching).toEqual(true) + expect(state.getState().withBatching2).toEqual(true) + expect(state.getState()).toEqual({ ...prevState3, withBatching: true, withBatching2: true }) + // 这次notify是由batch批处理结束调用的 + expect(susCb).toBeCalledTimes(2) + expect(susCb).toBeCalledWith({ ...prevState3, withBatching: true, withBatching2: true }) + }) + test('getSourceState', () => { + const state = new StateModel({ useDirty: true }) + const cb = jest.fn() + state.getState(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(state.state) + const syncState = state.getState() + expect(syncState).toEqual(state.state) + }) + test('setSourceState', () => { + const state = new StateModel({ useDirty: true }) + const cb1 = (draft) => draft.change = true + const prevState1 = state.unsafe_getSourceState() + expect(prevState1.change).toEqual(undefined) + + state.unsafe_setSourceState(cb1) + expect(state.unsafe_getSourceState()).toEqual({ ...prevState1, change: true }) + }) + test('hasChanged', () => { + const state = new StateModel({ useDirty: true }) + expect(state.dirtyNum).toEqual(0) + expect(state.isDirty()).toEqual(false) + state.dirtyNum = 1 + expect(state.isDirty()).toEqual(true) + state.dirtyNum = 0 + expect(state.isDirty()).toEqual(false) + state.dirtys.change = true + expect(state.isDirty()).toEqual(false) + expect(state.isDirty('change')).toEqual(true) + }) + +}) diff --git a/packages/core/src/__tests__/vfield.state.spec.ts b/packages/core/src/__tests__/vfield.state.spec.ts new file mode 100644 index 00000000000..970cd451d5a --- /dev/null +++ b/packages/core/src/__tests__/vfield.state.spec.ts @@ -0,0 +1,147 @@ +import { VirtualFieldState } from '../state/virtual-field' + +test('computeState', () => { + const state = new VirtualFieldState({ useDirty: false }) + state.setState((draft) => { + draft.unmounted = true + }) + expect(state.getState().mounted).toEqual(false) + expect(state.getState().unmounted).toEqual(true) + state.setState((draft) => { + draft.mounted = true + }) + expect(state.getState().mounted).toEqual(true) + expect(state.getState().unmounted).toEqual(false) + // cannot set invalid props + expect(state.getState().props).toEqual({}) + state.setState((draft) => { + draft.props = { hello: 'world' } + }) + expect(state.getState().props).toEqual({ hello: 'world' }) + state.setState((draft) => { + draft.props = undefined + }) + expect(state.getState().props).toEqual({ hello: 'world' }) +}) + + +test('subscribe/unsubscribe', () => { + const state = new VirtualFieldState({ useDirty: false }) + const cb = jest.fn() + const idx = state.subscribe(cb) + const paylaod = state.getState() + state.notify(paylaod) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(paylaod) + state.unsubscribe(idx) + state.notify(paylaod) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(paylaod) +}) +test('batch', () => { + const state = new VirtualFieldState({ useDirty: false }) + const cb = jest.fn() + state.batch(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith() + // force run getState + const susCb = jest.fn() + state.subscribe(susCb) + state.dirtyNum = 1 + state.batch(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith() + expect(susCb).toBeCalledTimes(1) + expect(susCb).toBeCalledWith(state.state) +}) +test('getState', () => { + const state = new VirtualFieldState({ useDirty: false }) + const cb = jest.fn() + state.getState(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(state.state) + const syncState = state.getState() + expect(syncState).toEqual(state.state) + + state.controller.publishState = () => null + state.getState(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith(null) +}) +test('setState', () => { + const state = new VirtualFieldState({ useDirty: false }) + const susCb = jest.fn() + state.subscribe(susCb) + const cb1 = (draft) => { draft.visible = false } + const cb2 = (draft) => { draft.display = false } + const cb3 = (draft) => { draft.initialized = true } + const cb4 = (draft) => { draft.mounted = true } + const prevState1 = state.getState() + expect(prevState1.visible).toEqual(true) + + // 默认 slient = false, 触发notify 通知UI更新 + state.setState(cb1) + expect(state.getState().visible).toEqual(false) + expect(state.getState()).toEqual({ ...prevState1, visible: false }) + expect(susCb).toBeCalledTimes(1) + expect(susCb).toBeCalledWith({ ...prevState1, visible: false }) + + // slient = true 不触发notify + const prevState2 = state.getState() + expect(prevState2.display).toEqual(true) + state.setState(cb2, true) + expect(state.getState().display).toEqual(false) + expect(state.getState()).toEqual({ ...prevState2, display: false }) + expect(susCb).toBeCalledTimes(1) + + // batching 相当于slient = true + const prevState3 = state.getState() + expect(prevState3.initialized).toEqual(false) + expect(prevState3.mounted).toEqual(false) + state.batch(() => { + state.setState(cb3) + state.setState(cb4) + }) + + expect(state.getState().initialized).toEqual(true) + expect(state.getState().mounted).toEqual(true) + expect(state.getState()).toEqual({ ...prevState3, initialized: true, mounted: true }) + // 这次notify是由batch批处理结束调用的 + expect(susCb).toBeCalledTimes(2) + expect(susCb).toBeCalledWith({ ...prevState3, initialized: true, mounted: true }) +}) +test('getSourceState', () => { + const state = new VirtualFieldState({ useDirty: false }) + const cb = jest.fn() + state.unsafe_getSourceState(cb) + expect(cb).toBeCalledTimes(1) + expect(cb).toBeCalledWith(state.state) + const syncState = state.unsafe_getSourceState() + expect(syncState).toEqual(state.state) + + state.controller.publishState = () => null + state.unsafe_getSourceState(cb) + expect(cb).toBeCalledTimes(2) + expect(cb).toBeCalledWith(state.state) +}) +test('setSourceState', () => { + const state = new VirtualFieldState({ useDirty: false }) + const cb1 = (draft) => draft.change = true + const prevState1 = state.unsafe_getSourceState() + expect(prevState1.change).toEqual(undefined) + + state.unsafe_setSourceState(cb1) + expect(state.unsafe_getSourceState()).toEqual({ ...prevState1, change: true }) +}) +test('isDirty', () => { + const state = new VirtualFieldState({ useDirty: true }) + expect(state.dirtyNum).toEqual(0) + expect(state.isDirty()).toEqual(false) + state.dirtyNum = 1 + expect(state.isDirty()).toEqual(true) + state.dirtyNum = 0 + expect(state.isDirty()).toEqual(false) + state.dirtys.visible = true + expect(state.isDirty()).toEqual(false) + expect(state.isDirty('visible')).toEqual(true) +}) diff --git a/packages/core/src/field.ts b/packages/core/src/field.ts deleted file mode 100644 index 09a60e5eb84..00000000000 --- a/packages/core/src/field.ts +++ /dev/null @@ -1,590 +0,0 @@ -import { - Broadcast, - publishFieldState, - isEqual, - clone, - isFn, - isBool, - toArr, - isStr, - hasRequired, - resolveFieldPath, - isEmpty -} from './utils' -import { - IFieldOptions, - IRuleDescription, - Path, - IField, - IFormPathMatcher, - IFieldState -} from '@uform/types' -import { Form } from './form' - -const filterSchema = (value: any, key: string): boolean => - key !== 'properties' && key !== 'items' - -export class Field implements IField { - public dirty: boolean - - public dirtyType: string - - public pristine: boolean - - public valid: boolean - - public invalid: boolean - - public visible: boolean - - public display: boolean - - public editable: boolean - - public loading: boolean - - public errors: string[] - - public effectErrors: string[] - - public name: string - - public value: any - - public hiddenFromParent: boolean - - public shownFromParent: boolean - - public initialValue: any - - public namePath: string[] - - public path: string[] - - public rules: IRuleDescription[] - - public required: boolean - - public props: any - - public lastValidateValue: any - - private context: Form - - private removed: boolean - - private destructed: boolean - - private alreadyHiddenBeforeUnmount: boolean - - private fieldbrd: Broadcast - - private unSubscribeOnChange: () => void - - constructor(context: Form, options: IFieldOptions) { - this.fieldbrd = new Broadcast() - this.context = context - this.dirty = false - this.pristine = true - this.valid = true - this.removed = false - this.invalid = false - this.visible = true - this.display = true - this.editable = true - this.destructed = false - this.loading = false - this.errors = [] - this.props = {} - this.effectErrors = [] - this.initialize(options) - } - - public initialize(options: IFieldOptions) { - const rules = this.getRulesFromProps(options.props) - this.value = !isEqual(this.value, options.value) - ? clone(options.value) - : this.value - this.name = !isEmpty(options.name) ? options.name : this.name || '' - this.namePath = resolveFieldPath(this.name) - - this.path = resolveFieldPath( - !isEmpty(options.path) ? options.path : this.path || [] - ) - this.rules = !isEmpty(rules) ? rules : this.rules - this.required = hasRequired(this.rules) - - if (isEmpty(options.props)) { - this.initialValue = !isEmpty(options.initialValue) - ? options.initialValue - : this.initialValue - } else { - this.initialValue = !isEqual(this.initialValue, options.initialValue) - ? options.initialValue - : !isEmpty(this.initialValue) - ? this.initialValue - : this.getInitialValueFromProps(options.props) - this.props = !isEmpty(this.props) - ? { ...this.props, ...clone(options.props) } - : clone(options.props) - const editable = this.getEditableFromProps(options.props) - this.editable = !isEmpty(editable) ? editable : this.getContextEditable() - } - - if (options.initialValue) { - this.lastValidateValue = options.initialValue - } - - if ( - this.pristine && - !isEmpty(this.initialValue) && - ((isEmpty(this.value) && this.visible) || - (this.removed && !this.shownFromParent)) - ) { - this.value = clone(this.initialValue) - this.context.setIn(this.name, this.value) - } - - this.mount() - - if (isFn(options.onChange)) { - this.onChange(options.onChange) - } - - this.context.syncUpdate(() => { - this.context.dispatchEffect('onFieldInit', this.publishState()) - }) - } - - public getInitialValueFromProps(props: any) { - if (props) { - if (!isEqual(this.initialValue, props.default)) { - return props.default - } - } - } - - public getContextEditable() { - return this.getEditable(this.context.editable) - } - - public getEditableFromProps(props: any) { - if (props) { - if (!isEmpty(props.editable)) { - return this.getEditable(props.editable) - } else { - if (props['x-props'] && !isEmpty(props['x-props'].editable)) { - return this.getEditable(props['x-props'].editable) - } - } - } - } - - public getRulesFromProps(props: any) { - if (props) { - const rules = toArr(props['x-rules']) - if (props.required && !rules.some(rule => rule.required)) { - rules.push({ required: true }) - } - return clone(rules) - } - return this.rules - } - - public getRequiredFromProps(props: any) { - if (!isEmpty(props.required)) { - return props.required - } - } - - public getEditable(editable: boolean | ((name: string) => boolean)): boolean { - if (isFn(editable)) { - return editable(this.name) - } - if (isBool(editable)) { - return editable - } - return this.editable - } - - public onChange(fn: (payload: any) => void) { - if (isFn(fn)) { - if (this.unSubscribeOnChange) { - this.unSubscribeOnChange() - } - fn(this.publishState()) - this.unSubscribeOnChange = this.subscribe(fn) - } - } - - public pathEqual(path: Path | IFormPathMatcher): boolean { - if (isStr(path)) { - if (path === this.name) { - return true - } - } - - path = resolveFieldPath(path) - - if (path.length === this.path.length) { - for (let i = 0; i < path.length; i++) { - if (path[i] !== this.path[i]) { - return false - } - } - return true - } else if (path.length === this.namePath.length) { - for (let i = 0; i < path.length; i++) { - if (path[i] !== this.namePath[i]) { - return false - } - } - return true - } - - return false - } - - public match(path: Path | IFormPathMatcher) { - if (isFn(path)) { - return path(this) - } - if (isStr(path)) { - if (path === this.name) { - return true - } - } - - path = resolveFieldPath(path) - - if (path.length === this.path.length) { - for (let i = 0; i < path.length; i++) { - if (path[i] !== this.path[i]) { - return false - } - } - return true - } else if (path.length === this.namePath.length) { - for (let i = 0; i < path.length; i++) { - if (path[i] !== this.namePath[i]) { - return false - } - } - return true - } - - return false - } - - public publishState() { - return publishFieldState(this) - } - - public syncContextValue() { - if (this.visible) { - const contextValue = this.context.getValue(this.name, true) - const contextInitialValue = this.context.getInitialValue( - this.name, - this.path - ) - if (!isEqual(this.value, contextValue)) { - this.value = contextValue - } - if (!isEqual(this.initialValue, contextInitialValue)) { - this.initialValue = contextInitialValue - } - } - } - - public subscribe(callback) { - return this.fieldbrd.subscribe(callback) - } - - public notify(force?: boolean) { - if (!this.dirty && !force) { - return - } - this.fieldbrd.notify(this.publishState()) - this.dirty = false - this.dirtyType = '' - } - - public unsubscribe() { - this.fieldbrd.unsubscribe() - } - - public changeProps(props: any, force?: boolean) { - const lastProps = this.props - if (isEmpty(props)) { - return - } - if (force || !isEqual(lastProps, props, filterSchema)) { - this.props = clone(props, filterSchema) - const editable = this.getEditableFromProps(this.props) - if (!isEmpty(editable)) { - this.editable = this.getEditableFromProps(this.props) - } - const rules = this.getRulesFromProps(this.props) - if (!isEmpty(rules)) { - this.rules = rules - } - this.dirty = true - this.notify() - } - } - - public changeEditable(editable: boolean | ((name: string) => boolean)): void { - if (!this.props || !isEmpty(this.props.editable)) { - return - } - if (this.props['x-props'] && !isEmpty(this.props['x-props'].editable)) { - return - } - this.editable = this.getEditable(editable) - this.dirty = true - this.notify() - } - - public mount() { - if (this.removed) { - if (!this.alreadyHiddenBeforeUnmount && !this.visible) { - this.visible = true - } - this.removed = false - this.context.dispatchEffect('onFieldChange', this.publishState()) - } - } - - public unmount() { - if (!this.visible) { - this.alreadyHiddenBeforeUnmount = true - } else { - this.alreadyHiddenBeforeUnmount = false - } - this.visible = false - this.removed = true - if (!this.context) { - return - } - if (!this.hiddenFromParent) { - this.context.deleteIn(this.name) - } - } - - public checkState(published = this.publishState()) { - if (!isEqual(published.value, this.value)) { - this.value = published.value - this.pristine = false - this.context.setIn(this.name, this.value) - this.context.updateChildrenValue(this) - this.dirtyType = 'value' - this.dirty = true - } - - if (!isEqual(published.initialValue, this.initialValue)) { - this.initialValue = published.initialValue - this.context.setInitialValueIn(this.name, this.value) - this.context.updateChildrenInitalValue(this) - this.dirtyType = 'initialValue' - this.dirty = true - } - - const editable = this.getEditable(published.editable) - if (!isEqual(editable, this.editable)) { - this.editable = editable - this.dirtyType = 'editable' - this.dirty = true - } else { - const prevEditable = this.getEditableFromProps(this.props) - const propsEditable = this.getEditableFromProps(published.props) - if ( - !isEmpty(propsEditable) && - !isEqual(propsEditable, this.editable) && - !isEqual(prevEditable, propsEditable) - ) { - this.editable = propsEditable - this.dirtyType = 'editable' - this.dirty = true - } - } - - published.errors = toArr(published.errors).filter(v => !!v) - - if (!isEqual(published.errors, this.effectErrors)) { - this.effectErrors = published.errors - this.valid = this.effectErrors.length > 0 && this.errors.length > 0 - this.invalid = !this.valid - this.dirtyType = 'errors' - this.dirty = true - } - if (!isEqual(published.rules, this.rules)) { - this.rules = published.rules - this.errors = [] - this.valid = true - if (hasRequired(this.rules)) { - this.required = true - published.required = true - } - this.invalid = false - this.dirtyType = 'rules' - this.dirty = true - } else { - const prePropsRules = this.getRulesFromProps(this.props) - const propsRules = this.getRulesFromProps(published.props) - if ( - !isEmpty(propsRules) && - !isEqual(prePropsRules, propsRules) && - !isEqual(propsRules, this.rules) - ) { - this.rules = propsRules - this.errors = [] - if (hasRequired(this.rules)) { - this.required = true - published.required = true - } - this.valid = true - this.invalid = false - this.dirtyType = 'rules' - this.dirty = true - } - } - if (!isEqual(published.required, this.required)) { - this.required = !!published.required - if (this.required) { - if (!hasRequired(this.rules)) { - this.rules = toArr(this.rules).concat({ - required: true - }) - this.errors = [] - this.valid = true - this.invalid = false - } - } else { - this.rules = toArr(this.rules).filter(rule => { - if (rule && rule.required) { - return false - } - return true - }) - this.errors = [] - this.valid = true - this.invalid = false - } - this.dirty = true - } else { - const propsRequired = this.getRequiredFromProps(published.props) - const prevPropsRequired = this.getRequiredFromProps(this.props) - if ( - !isEmpty(propsRequired) && - !isEqual(propsRequired, prevPropsRequired) - ) { - this.required = !!propsRequired - this.errors = [] - if (this.required) { - if (!hasRequired(this.rules)) { - this.rules = toArr(this.rules).concat({ - required: true - }) - this.errors = [] - this.valid = true - this.invalid = false - } - } else { - this.rules = toArr(this.rules).filter(rule => { - if (rule && rule.required) { - return false - } - return true - }) - this.errors = [] - this.valid = true - this.invalid = false - } - this.dirty = true - } - } - - if (published.loading !== this.loading) { - this.loading = published.loading - this.dirtyType = 'loading' - this.dirty = true - } - - if (!isEqual(published.visible, this.visible)) { - this.visible = !!published.visible - if (this.visible) { - this.value = - this.value !== undefined ? this.value : clone(this.initialValue) - if (this.value !== undefined) { - this.context.setIn(this.name, this.value) - } - this.context.updateChildrenVisible(this, true) - } else { - this.context.deleteIn(this.name) - this.context.updateChildrenVisible(this, false) - } - this.dirtyType = 'visible' - this.dirty = true - } - - if (!isEqual(published.display, this.display)) { - this.display = !!published.display - this.context.updateChildrenDisplay(this, this.display) - this.dirtyType = 'display' - this.dirty = true - } - - if (!isEqual(published.props, this.props, filterSchema)) { - this.props = clone(published.props, filterSchema) - this.dirtyType = 'props' - this.dirty = true - } - if (this.editable === false) { - this.errors = [] - this.effectErrors = [] - } - } - - public updateState(reducer: (fieldStte: IFieldState) => void) { - if (!isFn(reducer)) { - return - } - if (this.removed) { - return - } - const published = { - name: this.name, - path: this.path, - props: clone(this.props, filterSchema), - value: clone(this.value), - initialValue: clone(this.initialValue), - valid: this.valid, - loading: this.loading, - editable: this.editable, - invalid: this.invalid, - pristine: this.pristine, - rules: clone(this.rules), - errors: clone(this.effectErrors), - visible: this.visible, - display: this.display, - required: this.required - } - reducer(published) - this.checkState(published) - } - - public destructor() { - if (this.destructed) { - return - } - this.destructed = true - if (this.value !== undefined) { - this.value = undefined - this.context.deleteIn(this.name) - } - this.context.updateChildrenVisible(this, false) - delete this.context - this.unsubscribe() - delete this.fieldbrd - } -} diff --git a/packages/core/src/form.ts b/packages/core/src/form.ts deleted file mode 100644 index 69928865b3c..00000000000 --- a/packages/core/src/form.ts +++ /dev/null @@ -1,847 +0,0 @@ -import { - Broadcast, - each, - reduce, - isEqual, - isFn, - isStr, - isArr, - setIn, - getIn, - deleteIn, - clone, - isEmpty, - toArr, - publishFormState, - raf, - caf, - isChildField, - getSchemaNodeFromPath, - BufferList, - defer -} from './utils' -import { Field } from './field' -import { runValidation, format } from '@uform/validator' -import { Subject } from 'rxjs/internal/Subject' -import { filter } from 'rxjs/internal/operators/filter' -import { FormPath } from './path' -import { - IFormOptions, - IFieldOptions, - IFieldState, - IField, - IFormPathMatcher, - IFormState, - ISchema, - Path, - IFieldMap, - ISubscribers -} from '@uform/types' - -type Editable = boolean | ((name: string) => boolean) - -const defaults = (opts: T): T => - ({ - initialValues: {}, - values: {}, - onSubmit: () => {}, - effects: () => {}, - ...opts - } as T) - -export class Form { - public editable: Editable - - private options: IFormOptions - - private publisher: Broadcast - - private state: IFormState - - private fields: IFieldMap - - private subscribes: ISubscribers - - private updateQueue: any[] - - private updateBuffer: BufferList - - private schema: ISchema - - private initialized: boolean - - private destructed: boolean - - private fieldSize: number - - private syncUpdateMode: boolean - - private updateRafId: any - - private rafValidateId: any - - private batchUpdateField: boolean - - private validating: boolean - - private traverse: (schema: ISchema) => ISchema - - constructor(opts: IFormOptions) { - this.getFieldState = this.getFieldState.bind(this) - this.getFormState = this.getFormState.bind(this) - this.options = defaults(opts) - this.publisher = new Broadcast() - this.initialized = false - this.state = {} as IFormState - this.fields = {} - this.subscribes = opts.subscribes || {} - this.updateQueue = [] - this.updateBuffer = new BufferList() - this.editable = opts.editable - this.schema = opts.schema || {} - this.traverse = opts.traverse - this.initialize({ - values: this.options.values, - initialValues: this.options.initialValues - }) - this.initializeEffects() - this.initialized = true - this.destructed = false - this.fieldSize = 0 - } - - public changeValues(values: any) { - const lastValues = this.state.values - const lastDirty = this.state.dirty - this.state.values = values || {} - this.state.dirty = - lastDirty || (this.initialized ? !isEqual(values, lastValues) : false) - this.updateFieldsValue() - } - - public changeEditable(editable: Editable) { - this.editable = editable - each(this.fields, field => { - field.changeEditable(editable) - }) - } - - public isDirtyValues(values: any) { - return !isEmpty(values) && !isEqual(this.state.values, values) - } - - public setFieldState = ( - path: Path | IFormPathMatcher, - callback: (fieldState: IFieldState) => void - ): Promise => { - if (this.destructed) { - return - } - return new Promise(resolve => { - if (isStr(path) || isArr(path) || isFn(path)) { - this.updateQueue.push({ path, callback, resolve }) - } - if (this.syncUpdateMode) { - this.updateFieldStateFromQueue() - return resolve() - } else if (this.updateQueue.length > 0) { - if (this.updateRafId !== undefined) { - caf(this.updateRafId) - } - this.updateRafId = raf(() => { - if (this.destructed) { - return - } - this.updateFieldStateFromQueue() - }) - } else { - return resolve() - } - }) - } - - public getFieldState( - path: Path | IFormPathMatcher, - callback: (fieldState: IFieldState) => void - ): void - public getFieldState(path: Path | IFormPathMatcher): IFieldState - - public getFieldState( - path: Path | IFormPathMatcher, - callback?: (fieldState: IFieldState) => void - ): any { - let field: IField - each(this.fields, innerField => { - if (innerField.match(path)) { - field = innerField - return false - } - }) - if (field) { - field.syncContextValue() - return isFn(callback) - ? callback(field.publishState()) - : field.publishState() - } - } - - public getFormState(callback: (formState: IFormState) => void): void - public getFormState(): IFormState - public getFormState(callback?: any): any { - return isFn(callback) ? callback(this.publishState()) : this.publishState() - } - - public setFormState = (callback: (formState: IFormState) => void) => { - if (this.destructed) { - return - } - if (!isFn(callback)) { - return - } - const published = this.publishState() - callback(published) - return Promise.resolve(this.checkState(published)) - } - - public registerField(name: string, options: IFieldOptions) { - const value = this.getValue(name) - const initialValue = this.getInitialValue(name, options.path) - const field = this.fields[name] - if (field) { - field.initialize({ - path: options.path, - onChange: options.onChange, - value, - initialValue - } as IFieldOptions) - this.asyncUpdate(() => { - this.updateFieldStateFromBuffer(field) - }) - } else { - this.fields[name] = new Field(this, { - name, - value, - path: options.path, - initialValue, - props: this.traverse ? this.traverse(options.props) : options.props - }) - const field = this.fields[name] - if (options.onChange) { - this.asyncUpdate(() => { - this.updateFieldStateFromBuffer(field) - field.onChange(options.onChange) - }) - this.dispatchEffect('onFieldChange', field.publishState()) - } - this.fieldSize++ - } - return this.fields[name] - } - - public setIn(name: string, value: any) { - setIn(this.state.values, name, value, path => { - return getSchemaNodeFromPath(this.schema, path) - }) - } - - public setInitialValueIn(name: string, value: any) { - setIn(this.state.initialValues, name, value) - } - - public setValue(name: string, value: any) { - const field = this.fields[name] - if (field) { - field.updateState(state => { - state.value = value - }) - field.pristine = false - if (field.dirty) { - field.notify() - this.dispatchEffect('onFieldInputChange', field.publishState()) - this.internalValidate(this.state.values).then(() => { - this.formNotify(field.publishState()) - }) - } - } - } - - public setErrors(name: string, errors: string[] | string, ...args: string[]) { - errors = toArr(errors) - const field = this.fields[name] - if (field) { - const lastErrors = field.errors - if (!isEqual(lastErrors, errors)) { - field.errors = errors.map(msg => format(msg, ...args)) - if (errors.length) { - field.invalid = true - field.valid = false - } else { - field.invalid = false - field.valid = true - } - field.dirty = true - field.notify() - } - } - } - - public updateChildrenValue(parent: Field) { - if (!parent.path || this.batchUpdateField) { - return - } - each(this.fields, (field, $name) => { - if (isChildField(field, parent)) { - const newValue = this.getValue($name) - if (!isEqual(field.value, newValue)) { - field.dirty = true - field.value = newValue - field.notify() - this.dispatchEffect('onFieldChange', field.publishState()) - } - } - }) - } - - public updateChildrenInitalValue(parent: Field) { - if (!parent.path) { - return - } - each(this.fields, (field, $name) => { - if (isChildField(field, parent)) { - const newValue = this.getInitialValue($name) - if (!isEqual(field.initialValue, newValue)) { - field.dirty = true - field.initialValue = newValue - } - } - }) - } - - public updateFieldInitialValue(): Promise { - if (this.state.dirty && this.initialized) { - each(this.fields, (field, name) => { - const newValue = this.getInitialValue(name) - field.initialValue = newValue - }) - } - return Promise.resolve() - } - - public updateFieldsValue(validate = true): Promise { - const { promise, resolve } = defer() - const update = () => { - const updateList = [] - this.batchUpdateField = true - each(this.fields, (field, name) => { - const newValue = this.getValue(name) - field.updateState(state => { - state.value = newValue - }) - if (field.dirty) { - updateList.push( - new Promise(resolve => { - raf(() => { - if (this.destructed) { - return - } - field.notify() - this.dispatchEffect('onFieldChange', field.publishState()) - resolve() - }) - }) - ) - } - }) - this.batchUpdateField = false - resolve(Promise.all(updateList)) - } - if (this.state.dirty && this.initialized) { - if (validate) { - this.internalValidate(this.state.values, true).then(() => { - this.formNotify() - update() - }) - } else { - update() - } - } - - return promise - } - - public updateChildrenVisible(parent: Field, visible?: boolean) { - if (!parent.path) { - return - } - each(this.fields, (field, $name) => { - if ($name === parent.name) { - return - } - if (isChildField(field, parent)) { - if (!visible) { - this.deleteIn($name) - } else { - const value = - field.value !== undefined ? field.value : clone(field.initialValue) - if (field.value !== undefined) { - this.setIn($name, value) - } - } - if (field.visible !== visible) { - if (visible) { - if (field.hiddenFromParent) { - field.visible = visible - field.hiddenFromParent = false - field.shownFromParent = true - field.dirty = true - } - } else { - field.visible = visible - field.hiddenFromParent = true - field.shownFromParent = false - field.dirty = true - } - } - } - }) - } - - public updateChildrenDisplay(parent: Field, display?: boolean) { - if (!parent.path) { - return - } - each(this.fields, (field, $name) => { - if ($name === parent.name) { - return - } - if (isChildField(field, parent)) { - if (field.display !== display) { - if (display) { - if (field.hiddenFromParent) { - field.display = display - field.hiddenFromParent = false - field.shownFromParent = true - field.dirty = true - } - } else { - field.display = display - field.hiddenFromParent = true - field.shownFromParent = false - field.dirty = true - } - } - } - }) - } - - public getInitialValue(name: string, path?: Path) { - const initialValue = getIn(this.state.initialValues, name) - let schema: ISchema - let schemaDefault: any - if (initialValue === undefined) { - schema = path ? getSchemaNodeFromPath(this.schema, path) : undefined - schemaDefault = schema && schema.default - if (schemaDefault !== undefined) { - this.setIn(name, schemaDefault) - } - } - return initialValue !== undefined ? initialValue : schemaDefault - } - - public getValue(name?: string, copy?: boolean) { - return copy - ? clone(getIn(this.state.values, name)) - : getIn(this.state.values, name) - } - - public deleteIn(name: string) { - deleteIn(this.state.values, name) - } - - public deleteInitialValues(name: string) { - deleteIn(this.state.initialValues, name) - } - - public reset(forceClear?: boolean, validate: boolean = true) { - each(this.fields, (field, name) => { - const value = this.getValue(name) - const initialValue = this.getInitialValue(name, field.path) - if (!validate) { - if (field.errors.length > 0) { - field.errors = [] - field.dirty = true - } - if (field.effectErrors.length > 0) { - field.effectErrors = [] - field.dirty = true - } - } - if (!isEmpty(value) || !isEmpty(initialValue)) { - field.updateState(state => { - state.value = forceClear ? undefined : initialValue - }) - field.pristine = true - } - if (field.dirty) { - field.notify() - this.formNotify(field.publishState()) - } - }) - if (!validate) { - const formState = this.publishState() - this.dispatchEffect('onFormReset', formState) - if (isFn(this.options.onReset)) { - this.options.onReset({ formState }) - } - } else { - this.internalValidate(this.state.values, true).then(() => { - this.formNotify() - raf(() => { - if (this.destructed) { - return - } - const formState = this.publishState() - this.dispatchEffect('onFormReset', formState) - if (isFn(this.options.onReset)) { - this.options.onReset({ formState }) - } - }) - }) - } - } - - public publishState() { - return publishFormState(this.state) - } - - public formNotify(fieldState?: IFieldState) { - const formState = this.publishState() - if (isFn(this.options.onFieldChange)) { - this.options.onFieldChange({ formState, fieldState }) - } - if (fieldState) { - this.dispatchEffect('onFieldChange', fieldState) - } - if (this.state.dirty) { - this.publisher.notify({ formState, fieldState }) - } - this.state.dirty = false - return formState - } - - public validate(): Promise { - this.validating = true - return this.internalValidate(this.state.values, true).then(() => { - return new Promise((resolve, reject) => { - this.formNotify() - raf(() => { - this.validating = false - if (this.destructed) { - return - } - if (this.state.valid) { - resolve(this.publishState()) - } else { - if (this.options.onValidateFailed) { - this.options.onValidateFailed(this.state.errors) - } - reject(this.state.errors) - } - }) - }) - }) - } - - public submit() { - if (this.validating) - return new Promise(resolve => { - resolve(this.publishState()) - }) - return this.validate().then((formState: IFormState) => { - this.dispatchEffect('onFormSubmit', formState) - if (isFn(this.options.onSubmit)) { - this.options.onSubmit({ formState }) - } - return formState - }) - } - - public subscribe(callback: (payload: any) => void) { - return this.publisher.subscribe(callback) - } - - public destructor() { - if (this.destructed) { - return - } - this.destructed = true - this.publisher.unsubscribe() - each(this.subscribes, effect => { - effect.unsubscribe() - }) - each(this.fields, (field, key) => { - field.destructor() - delete this.fields[key] - }) - this.fieldSize = 0 - delete this.fields - delete this.publisher - } - - public dispatchEffect = (eventName: string, ...args: any[]) => { - if (this.subscribes[eventName]) { - this.subscribes[eventName].next(...args) - } - } - - public syncUpdate(fn: () => void) { - if (isFn(fn)) { - this.syncUpdateMode = true - fn() - this.syncUpdateMode = false - } - } - - public initialize({ - initialValues = this.state.initialValues, - values = this.state.values - }) { - const lastValues = this.state.values - const lastDirty = this.state.dirty - const currentInitialValues = clone(initialValues) || {} - const currentValues = isEmpty(values) - ? clone(currentInitialValues) - : clone(values) || {} - this.state = { - valid: true, - invalid: false, - errors: [], - pristine: true, - initialValues: currentInitialValues, - values: currentValues, - dirty: - lastDirty || - (this.initialized ? !isEqual(currentValues, lastValues) : false) - } - if (this.options.onFormChange && !this.initialized) { - this.subscribe(this.options.onFormChange) - this.options.onFormChange({ - formState: this.publishState() - }) - } - this.updateFieldsValue(false) - } - - public selectEffect = ( - eventName: string, - eventFilter: string | IFormPathMatcher - ) => { - if (!this.subscribes[eventName]) { - this.subscribes[eventName] = new Subject() - } - if (isStr(eventFilter) || isFn(eventFilter)) { - const predicate = isStr(eventFilter) - ? FormPath.match(eventFilter as string) - : (eventFilter as IFormPathMatcher) - return this.subscribes[eventName].pipe(filter(predicate)) as Subject - } - return this.subscribes[eventName] - } - - private initializeEffects() { - const { effects } = this.options - if (isFn(effects)) { - effects(this.selectEffect, { - setFieldState: this.setFieldState, - getFieldState: this.getFieldState, - getFormState: this.getFormState, - setFormState: this.setFormState - }) - } - } - - private checkState(published: any): Promise { - if (!isEqual(this.state.values, published.values)) { - this.state.values = published.values - this.state.dirty = true - return this.updateFieldsValue() - } - if (!isEqual(this.state.initialValues, published.initialValues)) { - this.state.initialValues = published.initialValues - this.state.dirty = true - return this.updateFieldInitialValue() - } - - return Promise.resolve() - } - - private asyncUpdate(fn: () => void) { - if (isFn(fn)) { - if (this.syncUpdateMode) { - this.syncUpdateMode = false - fn() - this.syncUpdateMode = true - } else { - fn() - } - } - } - - private updateFieldStateFromQueue() { - const failed = {} - const rafIdMap = {} - each(this.updateQueue, ({ path, callback, resolve }, i) => { - each(this.fields, field => { - if (path && (isFn(path) || isArr(path) || isStr(path))) { - if (isFn(path) ? path(field) : field.pathEqual(path)) { - field.updateState(callback) - if (this.syncUpdateMode) { - field.dirty = false - } - if ((path as IFormPathMatcher).hasWildcard) { - this.updateBuffer.push( - (path as IFormPathMatcher).pattern, - callback, - { path, resolve } - ) - } - if (field.dirty) { - const dirtyType = field.dirtyType - field.notify() - if (rafIdMap[field.name]) { - caf(rafIdMap[field.name]) - } - rafIdMap[field.name] = raf(() => { - if (this.destructed) { - return - } - if (dirtyType === 'value') { - this.internalValidate().then(() => { - this.formNotify(field.publishState()) - }) - } else { - this.formNotify(field.publishState()) - } - }) - } - } else { - failed[i] = failed[i] || 0 - failed[i]++ - if (this.fieldSize <= failed[i]) { - if (isArr(path)) { - this.updateBuffer.push(path.join('.'), callback, { - path, - resolve - }) - } else if (isStr(path)) { - this.updateBuffer.push(path, callback, { path, resolve }) - } else if (isFn(path) && (path as IFormPathMatcher).pattern) { - this.updateBuffer.push( - (path as IFormPathMatcher).pattern, - callback, - { - path, - resolve - } - ) - } - } - } - } - }) - if (resolve && isFn(resolve)) { - resolve() - } - }) - this.updateQueue = [] - } - - private updateFieldStateFromBuffer(field: IField) { - const rafIdMap = {} - this.updateBuffer.forEach(({ path, values, key }) => { - if (isFn(path) ? path(field) : field.pathEqual(path)) { - values.forEach(callback => field.updateState(callback)) - if (this.syncUpdateMode) { - field.dirty = false - } - if (field.dirty) { - const dirtyType = field.dirtyType - field.notify() - if (rafIdMap[field.name]) { - caf(rafIdMap[field.name]) - } - rafIdMap[field.name] = raf(() => { - if (this.destructed) { - return - } - if (dirtyType === 'value') { - this.internalValidate().then(() => { - this.formNotify(field.publishState()) - }) - } else { - this.formNotify(field.publishState()) - } - }) - } - if (!path.hasWildcard) { - this.updateBuffer.remove(key) - } - } - }) - } - - private internalValidate( - values: any = this.state.values, - forceUpdate?: boolean - ) { - if (this.destructed) { - return - } - return new Promise(resolve => { - if (this.rafValidateId) { - caf(this.rafValidateId) - } - this.rafValidateId = raf(() => { - if (this.destructed) { - return resolve() - } - return runValidation( - values || this.state.values, - this.fields, - forceUpdate - ) - .then(response => { - const lastValid = this.state.valid - const newErrors = reduce( - response, - (buf, { name, errors }) => { - if (!errors.length) { - return buf - } else { - return buf.concat({ name, errors }) - } - }, - [] - ) - this.state.valid = newErrors.length === 0 - this.state.invalid = !this.state.valid - this.state.errors = newErrors - if (this.state.valid !== lastValid) { - this.state.dirty = true - } - const lastPristine = this.state.pristine - if (!isEqual(this.state.values, this.state.initialValues)) { - this.state.pristine = false - } else { - this.state.pristine = true - } - if (lastPristine !== this.state.pristine) { - this.state.dirty = true - } - return response - }) - .then(resolve) - }) - }) - } -} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 64ed2905d4a..f5f12f69f47 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,94 +1,1141 @@ -import { IFormOptions, ISchema } from '@uform/types' import { - setLocale as setValidationLocale, - setLanguage as setValidationLanguage -} from '@uform/validator' - -import { Form } from './form' -import { - calculateSchemaInitialValues, isFn, + isEqual, + toArr, + isNum, + isArr, + clone, + isValid, + FormPath, + FormPathPattern, each, - isEmpty, - clone -} from './utils' - -export * from './path' - -export const createForm = ({ - initialValues, - values, - onSubmit, - onReset, - schema, - onFormChange, - onFieldChange, - onFormWillInit, - subscribes, - editable, - effects, - onValidateFailed, - traverse -}: IFormOptions) => { - let fields = [] - let calculatedValues = calculateSchemaInitialValues( - schema, - isEmpty(values) ? clone(initialValues) : clone(values), - ({ name, path, schemaPath }, schema: ISchema, value: any) => { - fields.push({ name, path, schemaPath, schema, value }) - } - ) - - if (isEmpty(values)) { - initialValues = calculatedValues - } else { - values = calculatedValues - } - - const form = new Form({ - initialValues, - values, - onSubmit, - onReset, - subscribes, - onFormChange, - onFieldChange, + isObj +} from '@uform/shared' +import { + FormValidator, + setValidationLanguage, + setValidationLocale, + ValidateFieldOptions +} from '@uform/validator' +import { FormHeart } from './shared/lifecycle' +import { FormGraph } from './shared/graph' +import { scheduler } from './shared/scheduler' +import { FormState } from './state/form' +import { VirtualFieldState } from './state/virtual-field' +import { FieldState } from './state/field' +import { + IFormState, + IFieldState, + IVirtualFieldState, + IFormCreatorOptions, + IFieldStateProps, + IVirtualFieldStateProps, + IForm, + IFormSubmitResult, + IFormValidateResult, + IFormResetOptions, + IField, + IVirtualField, + isField, + FormHeartSubscriber, + LifeCycleTypes, + isVirtualField, + isFormState, + isFieldState, + isVirtualFieldState +} from './types' +export * from './shared/lifecycle' +export * from './types' + +export function createForm( + options: IFormCreatorOptions = {} +): IForm { + function onGraphChange({ type, payload }) { + heart.publish(LifeCycleTypes.ON_FORM_GRAPH_CHANGE, graph) + if (type === 'GRAPH_NODE_WILL_UNMOUNT') { + validator.unregister(payload.path.toString()) + } + } + + function onFormChange(published: IFormState) { + heart.publish(LifeCycleTypes.ON_FORM_CHANGE, state) + const valuesChanged = state.isDirty('values') + const initialValuesChanged = state.isDirty('initialValues') + const unmountedChanged = state.isDirty('unmounted') + const mountedChanged = state.isDirty('mounted') + const initializedChanged = state.isDirty('initialized') + const editableChanged = state.isDirty('editable') + if (valuesChanged || initialValuesChanged) { + const updateFields = (field: IField | IVirtualField) => { + if (isField(field)) { + field.setState(state => { + if (state.visible) { + if (valuesChanged) { + const dataPath = FormPath.parse(state.name) + const parent = graph.getLatestParent(state.path) + const parentValue = getFormValuesIn(parent.path) + const value = getFormValuesIn(state.name) + /** + * https://github.com/alibaba/uform/issues/267 dynamic remove node + */ + let removed = false + if ( + isArr(parentValue) && + !dataPath.existIn(parentValue, parent.path) + ) { + if ( + !parent.path + .getNearestChildPathBy(state.path) + .existIn(parentValue, parent.path) + ) { + graph.remove(state.path) + removed = true + } + } else { + each(env.removeNodes, (_, name) => { + if (dataPath.includes(name)) { + graph.remove(state.path) + delete env.removeNodes[name] + removed = true + } + }) + } + if (removed) return + if (!isEqual(value, state.value)) { + state.value = value + } + } + if (initialValuesChanged) { + const initialValue = getFormInitialValuesIn(state.name) + if (!isEqual(initialValue, state.initialValue)) { + state.initialValue = initialValue + if (!isValid(state.value)) { + state.value = initialValue + } + } + } + } + }) + } + } + if (valuesChanged || initialValuesChanged) { + if (!env.leadingStage) { + const userUpdateFieldPath = + env.userUpdateFields[env.userUpdateFields.length - 1] + /* + * 考虑初始化的时候还没生成节点树 + * 两种数据同步策略, + * 1. 精确更新的时候(mutators/setFieldState),只遍历父节点与子节点,同时父节点静默处理,子节点通知渲染 + * 2. setFormState批量更新的时候,是会遍历所有节点,同时所有节点只要有变化就会被通知 + */ + if (userUpdateFieldPath && graph.get(userUpdateFieldPath)) { + graph.eachParentAndChildren(userUpdateFieldPath, updateFields) + } else { + graph.eachChildren(updateFields) + } + } else { + graph.eachChildren(updateFields) + } + } + if (valuesChanged) { + if (isFn(options.onChange)) { + options.onChange(clone(published.values)) + } + heart.publish(LifeCycleTypes.ON_FORM_VALUES_CHANGE, state) + } + if (initialValuesChanged) { + heart.publish(LifeCycleTypes.ON_FORM_INITIAL_VALUES_CHANGE, state) + } + } + + if (editableChanged) { + graph.eachChildren((field: IField | IVirtualField) => { + if (isField(field)) { + field.setState(state => { + state.formEditable = published.editable + }) + } + }) + } + + if (unmountedChanged && published.unmounted) { + heart.publish(LifeCycleTypes.ON_FORM_UNMOUNT, state) + } + if (mountedChanged && published.mounted) { + heart.publish(LifeCycleTypes.ON_FORM_MOUNT, state) + } + if (initializedChanged) { + heart.publish(LifeCycleTypes.ON_FORM_INIT, state) + } + } + + function onFieldChange({ field, path }) { + return (published: IFieldState) => { + const valueChanged = field.isDirty('value') + const initialValueChanged = field.isDirty('initialValue') + const visibleChanged = field.isDirty('visible') + const displayChanged = field.isDirty('display') + const unmountedChanged = field.isDirty('unmounted') + const mountedChanged = field.isDirty('mounted') + const initializedChanged = field.isDirty('initialized') + const warningsChanged = field.isDirty('warnings') + const errorsChanges = field.isDirty('errors') + const userUpdateFieldPath = + env.userUpdateFields[env.userUpdateFields.length - 1] + if (initializedChanged) { + heart.publish(LifeCycleTypes.ON_FIELD_INIT, field) + const isEmptyValue = !isValid(published.value) + const isEmptyInitialValue = !isValid(published.initialValue) + if (isEmptyValue || isEmptyInitialValue) { + field.setState((state: IFieldState) => { + if (isEmptyValue) state.value = getFormValuesIn(state.name) + if (isEmptyInitialValue) + state.initialValue = getFormInitialValuesIn(state.name) + }, true) + } + } + if (displayChanged || visibleChanged) { + if (visibleChanged) { + if (!published.visible) { + deleteFormValuesIn(path, true) + } else { + setFormValuesIn(path, published.value) + } + } + graph.eachChildren( + path, + childState => { + childState.setState((state: IFieldState) => { + if (visibleChanged) { + state.visible = published.visible + } + if (displayChanged) { + state.display = published.display + } + }, true) + }, + false + ) + } + if ( + unmountedChanged && + (published.display !== false || published.visible === false) + ) { + if (published.unmounted) { + deleteFormValuesIn(path, true) + } else { + setFormValuesIn(path, published.value) + } + } + if (mountedChanged && published.mounted) { + heart.publish(LifeCycleTypes.ON_FIELD_MOUNT, field) + } + if (valueChanged) { + setFormValuesIn(path, published.value) + heart.publish(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, field) + } + if (initialValueChanged) { + setFormInitialValuesIn(path, published.initialValue) + heart.publish(LifeCycleTypes.ON_FIELD_INITIAL_VALUE_CHANGE, field) + } + + if (errorsChanges) { + syncFormMessages('errors', published.name, published.errors) + } + + if (warningsChanged) { + syncFormMessages('warnings', published.name, published.warnings) + } + heart.publish(LifeCycleTypes.ON_FIELD_CHANGE, field) + if (userUpdateFieldPath && !env.leadingStage) { + if (FormPath.parse(path).match(userUpdateFieldPath)) { + return + } + if (FormPath.parse(userUpdateFieldPath).includes(path)) { + return false + } + } + } + } + + function onVirtualFieldChange({ field, path }) { + return (published: IVirtualFieldState) => { + const visibleChanged = field.isDirty('visible') + const displayChanged = field.isDirty('display') + const mountedChanged = field.isDirty('mounted') + const initializedChnaged = field.isDirty('initialized') + + if (initializedChnaged) { + heart.publish(LifeCycleTypes.ON_FIELD_INIT, field) + } + + if (visibleChanged || displayChanged) { + graph.eachChildren(path, childState => { + childState.setState( + (state: IVirtualFieldState) => { + if (visibleChanged) { + state.visible = published.visible + } + if (displayChanged) { + state.display = published.display + } + }, + true + ) + }) + } + + if (mountedChanged && published.mounted) { + heart.publish(LifeCycleTypes.ON_FIELD_MOUNT, field) + } + heart.publish(LifeCycleTypes.ON_FIELD_CHANGE, field) + } + } + + function registerVirtualField({ + name, + path, + props, + display, + visible, + computeState, + useDirty + }: IVirtualFieldStateProps): IVirtualField { + let nodePath = FormPath.parse(path || name) + let dataPath = transformDataPath(nodePath) + let field: IVirtualField + const createField = (field?: IVirtualField) => { + const alreadyHaveField = !!field + field = + field || + new VirtualFieldState({ + nodePath, + dataPath, + computeState, + useDirty: isValid(useDirty) ? useDirty : options.useDirty + }) + field.subscription = { + notify: onVirtualFieldChange({ field, path: nodePath }) + } + heart.publish(LifeCycleTypes.ON_FIELD_WILL_INIT, field) + if (!alreadyHaveField) { + graph.appendNode(nodePath, field) + } + field.batch(() => { + field.setState((state: IVirtualFieldState) => { + state.initialized = true + state.props = props + if (isValid(visible)) { + state.visible = visible + } + if (isValid(display)) { + state.display = display + } + }) + batchRunTaskQueue(field, nodePath) + }) + return field + } + if (graph.exist(nodePath)) { + field = graph.get(nodePath) + field = createField(field) + if (isField(field)) { + graph.replace(nodePath, field) + } + } else { + field = createField() + } + return field + } + + function registerField({ + path, + name, + value, + initialValue, + required, + rules, editable, - effects, - onValidateFailed, - schema, - traverse - }) + visible, + display, + computeState, + useDirty, + props + }: Exclude): IField { + let field: IField + let nodePath = FormPath.parse(path || name) + let dataPath = transformDataPath(nodePath) + const createField = (field?: IField) => { + const alreadyHaveField = !!field + field = + field || + new FieldState({ + nodePath, + dataPath, + computeState, + useDirty: isValid(useDirty) ? useDirty : options.useDirty + }) + field.subscription = { + notify: onFieldChange({ field, path: nodePath }) + } + heart.publish(LifeCycleTypes.ON_FIELD_WILL_INIT, field) + if (!alreadyHaveField) { + graph.appendNode(nodePath, field) + } + field.batch(() => { + field.setState((state: IFieldState) => { + const formValue = getFormValuesIn(dataPath) + const formInitialValue = getFormInitialValuesIn(dataPath) + state.initialized = true + if (isValid(value)) { + // value > formValue > initialValue + state.value = value + } else { + state.value = isValid(formValue) ? formValue : initialValue + } + // initialValue > formInitialValue + state.initialValue = isValid(initialValue) + ? initialValue + : formInitialValue + if (isValid(visible)) { + state.visible = visible + } + if (isValid(display)) { + state.display = display + } + state.props = props + state.required = required + state.rules = rules as any + state.selfEditable = editable + state.formEditable = options.editable + }) + batchRunTaskQueue(field, nodePath) + }) + validator.register(nodePath, validate => { + const { + value, + rules, + editable, + visible, + unmounted, + display + } = field.getState() + // 不需要校验的情况有: 非编辑态(editable),已销毁(unmounted), 逻辑上不可见(visible) + if ( + editable === false || + visible === false || + unmounted === true || + display === false + ) + return validate(value, []) + clearTimeout((field as any).validateTimer) + ;(field as any).validateTimer = setTimeout(() => { + field.setState(state => { + state.validating = true + }) + }, 60) + return validate(value, rules).then(({ errors, warnings }) => { + clearTimeout((field as any).validateTimer) + return new Promise(resolve => { + const syncState = () => { + field.setState((state: IFieldState) => { + state.validating = false + state.ruleErrors = errors + state.ruleWarnings = warnings + }) + resolve({ errors, warnings }) + } + if (graph.size < 100) { + syncState() + } else { + applyWithScheduler(syncState) + } + }) + }) + }) + return field + } + if (graph.exist(nodePath)) { + field = graph.get(nodePath) + field = createField(field) + if (isVirtualField(field)) { + graph.replace(nodePath, field) + } + } else { + field = createField() + } + return field + } + + //实时同步Form Messages + function syncFormMessages(type: string, path: string, messages: string[]) { + state.unsafe_setSourceState(state => { + let foundField = false + state[type] = state[type] || [] + state[type] = state[type].reduce((buf: any, item: any) => { + if (item.path === path) { + foundField = true + return messages.length ? buf.concat({ path, messages }) : buf + } else { + return buf.concat(item) + } + }, []) + if (!foundField && messages.length) { + state[type].push({ + path, + messages + }) + } + if (state.errors.length) { + state.invalid = true + state.valid = false + } else { + state.invalid = false + state.valid = true + } + }) + } + + function transformDataPath(path: FormPathPattern) { + const newPath = FormPath.getPath(path) + return newPath.reduce((path: FormPath, key: string, index: number) => { + if (index >= newPath.length - 1) return path.concat(key) + const realPath = newPath.slice(0, index + 1) + const dataPath = path.concat(key) + const selected = graph.get(realPath) + if (isVirtualField(selected)) { + return path + } + return dataPath + }, FormPath.getPath('')) + } + + function setFormIn( + path: FormPathPattern, + key: string, + value: any, + silent?: boolean + ) { + state.setState(state => { + FormPath.setIn(state[key], transformDataPath(path), value) + }, silent) + } + + function deleteFormIn(path: FormPathPattern, key: string, silent?: boolean) { + state.setState(state => { + FormPath.deleteIn(state[key], transformDataPath(path)) + }, silent) + } + + function deleteFormValuesIn(path: FormPathPattern, silent?: boolean) { + deleteFormIn(path, 'values', silent) + } + + function setFormValuesIn( + path: FormPathPattern, + value?: any, + silent?: boolean + ) { + return setFormIn(path, 'values', value, silent) + } + + function setFormInitialValuesIn( + path: FormPathPattern, + value?: any, + silent?: boolean + ) { + return setFormIn(path, 'initialValues', value, silent) + } + + function getFormIn(path: FormPathPattern, key?: string) { + return state.getState(state => + FormPath.getIn(state[key], transformDataPath(path)) + ) + } + + function getFormValuesIn(path: FormPathPattern) { + return getFormIn(path, 'values') + } + + function getFormInitialValuesIn(path: FormPathPattern) { + return getFormIn(path, 'initialValues') + } + + function createMutators(field: IField) { + if (!(field instanceof FieldState)) { + throw new Error( + 'The `createMutators` can only accept FieldState instance.' + ) + } + function setValue(...values: any[]) { + userUpdating(field, () => { + field.setState((state: IFieldState) => { + state.value = values[0] + state.values = values + }) + }) + heart.publish(LifeCycleTypes.ON_FIELD_INPUT_CHANGE, field) + heart.publish(LifeCycleTypes.ON_FORM_INPUT_CHANGE, state) + } + + function removeValue(key: string | number) { + const nodePath = field.unsafe_getSourceState(state => state.path) + if (isValid(key)) { + const childNodePath = FormPath.parse(nodePath).concat(key) + env.userUpdateFields.push(nodePath) + env.removeNodes[childNodePath.toString()] = true + deleteFormValuesIn(childNodePath) + field.notify(field.getState()) + env.userUpdateFields.pop() + } else { + const parent = graph.selectParent(nodePath) + env.removeNodes[nodePath.toString()] = true + const parentNodePath = + parent && parent.unsafe_getSourceState(state => state.path) + if (parentNodePath) { + env.userUpdateFields.push(parentNodePath) + } else { + env.userUpdateFields.push(nodePath) + } + deleteFormValuesIn(nodePath) + if (parent) { + parent.notify(parent.getState()) + } + env.userUpdateFields.pop() + } + heart.publish(LifeCycleTypes.ON_FIELD_VALUE_CHANGE, field) + heart.publish(LifeCycleTypes.ON_FIELD_INPUT_CHANGE, field) + heart.publish(LifeCycleTypes.ON_FORM_INPUT_CHANGE, state) + heart.publish(LifeCycleTypes.ON_FIELD_CHANGE, field) + } + + function getValue() { + return field.unsafe_getSourceState(state => state.value) + } + return { + change(...values: any[]) { + setValue(...values) + return values[0] + }, + focus() { + field.setState((state: IFieldState) => { + state.active = true + }) + }, + blur() { + field.setState((state: IFieldState) => { + state.active = false + state.visited = true + }) + }, + push(value?: any) { + const arr = toArr(getValue()).slice() + arr.push(value) + setValue(arr) + return arr + }, + pop() { + const arr = toArr(getValue()).slice() + arr.pop() + setValue(arr) + return arr + }, + insert(index: number, value: any) { + const arr = toArr(getValue()).slice() + arr.splice(index, 0, value) + setValue(arr) + return arr + }, + remove(index?: number | string) { + let val = getValue() + if (isNum(index) && isArr(val)) { + val = [].concat(val) + val.splice(index, 1) + setValue(val) + } else { + removeValue(index) + } + }, + exist(index?: number | string) { + const newPath = field.unsafe_getSourceState(state => + FormPath.parse(state.path) + ) + let val = getValue() + return (index !== undefined ? newPath.concat(index) : newPath).existIn( + val, + newPath + ) + }, + unshift(value: any) { + const arr = toArr(getValue()).slice() + arr.unshift(value) + setValue(arr) + return arr + }, + shift() { + const arr = toArr(getValue()).slice() + arr.shift() + setValue(arr) + return arr + }, + move($from: number, $to: number) { + const arr = toArr(getValue()).slice() + const item = arr[$from] + arr.splice($from, 1) + arr.splice($to, 0, item) + setValue(arr) + return arr + }, + moveUp(index: number) { + const arr = toArr(getValue()).slice() + const item = arr[index] + const len = arr.length + arr.splice(index, 1) + arr.splice(index - 1 < 0 ? len - 1 : index - 1, 0, item) + setValue(arr) + return arr + }, + moveDown(index: number) { + const arr = toArr(getValue()).slice() + const item = arr[index] + const len = arr.length + arr.splice(index, 1) + arr.splice(index + 1 > len ? 0 : index + 1, 0, item) + setValue(arr) + return arr + }, + validate() { + return validate(field.unsafe_getSourceState(state => state.path)) + } + } + } - if (isFn(onFormWillInit)) { - onFormWillInit(form) + function clearErrors(pattern: FormPathPattern = '*') { + // 1. 指定路径或全部子路径清理 + graph.eachChildren('', pattern, field => { + if (isField(field)) { + field.setState(state => { + state.ruleErrors = [] + state.ruleWarnings = [] + state.effectErrors = [] + state.effectWarnings = [] + }) + } + }) } - fields = fields.map(({ name, schemaPath, schema }) => { - return form.registerField(name || schemaPath.join('.'), { - path: schemaPath, - props: schema + async function reset({ + selector = '*', + forceClear = false, + validate = true + }: IFormResetOptions = {}): Promise { + let result: Promise + graph.eachChildren('', selector, field => { + field.setState((state: IFieldState) => { + state.modified = false + state.ruleErrors = [] + state.ruleWarnings = [] + state.effectErrors = [] + state.effectWarnings = [] + // forceClear仅对设置initialValues的情况下有意义 + if (forceClear || !isValid(state.initialValue)) { + if (isArr(state.value)) { + state.value = [] + } else if (!isObj(state.value)) { + state.value = undefined + } + } else { + const value = clone(state.initialValue) + if (isArr(state.value)) { + if (isArr(value)) { + state.value = value + } else { + state.value = [] + } + } else if (isObj(state.value)) { + if (isObj(value)) { + state.value = value + } else { + state.value = {} + } + } else { + state.value = value + } + } + }) }) - }) + if (isFn(options.onReset)) { + options.onReset() + } + if (validate) { + result = formApi.validate() + } - form.syncUpdate(() => { - form.dispatchEffect('onFormInit', form.publishState()) - each( - fields, - field => { - form.dispatchEffect('onFieldChange', field.publishState()) + return result + } + + async function submit( + onSubmit?: (values: IFormState['values']) => any | Promise + ): Promise { + // 重复提交,返回前一次的promise + if (state.getState(state => state.submitting)) return env.submittingTask + heart.publish(LifeCycleTypes.ON_FORM_SUBMIT_START, state) + onSubmit = onSubmit || options.onSubmit + state.setState(state => { + state.submitting = true + }) + env.submittingTask = validate() + .then(() => { + const validated = state.getState(({ errors, warnings }) => ({ + errors, + warnings + })) //因为要合并effectErrors/effectWarnings,所以不能直接读validate的结果 + if (validated.errors.length) { + state.setState(state => { + state.submitting = false + }) + heart.publish(LifeCycleTypes.ON_FORM_SUBMIT_END, state) + if (isFn(options.onValidateFailed)) { + options.onValidateFailed(validated) + } + return Promise.reject(validated.errors) + } + if (isFn(onSubmit)) { + return Promise.resolve( + onSubmit(state.getState(state => clone(state.values))) + ).then(payload => ({ validated, payload })) + } + return { validated, payload: undefined } + }) + .then(response => { + const { + validated: { errors, warnings } + } = response + state.setState(state => { + state.submitting = false + }) + + heart.publish(LifeCycleTypes.ON_FORM_SUBMIT_END, state) + if (errors.length) { + return Promise.reject(errors) + } + if (warnings.length) { + console.warn(warnings) + } + return response + }) + return env.submittingTask + } + + async function validate( + path?: FormPathPattern, + opts?: ValidateFieldOptions + ): Promise { + if (!state.getState(state => state.validating)) { + state.unsafe_setSourceState(state => { + state.validating = true + }) + // 渲染优化 + clearTimeout(env.validateTimer) + env.validateTimer = setTimeout(() => { + state.notify() + }, 60) + } + + heart.publish(LifeCycleTypes.ON_FORM_VALIDATE_START, state) + return validator.validate(path, opts).then(payload => { + clearTimeout(env.validateTimer) + state.setState(state => { + state.validating = false + }) + heart.publish(LifeCycleTypes.ON_FORM_VALIDATE_END, state) + return payload + }) + } + + function setFormState( + callback?: (state: IFormState) => any, + silent?: boolean + ) { + env.leadingStage = true + state.setState(callback, silent) + env.leadingStage = false + } + + function getFormState(callback?: (state: IFormState) => any) { + return state.getState(callback) + } + + function batchRunTaskQueue( + field: IField | IVirtualField, + nodePath: FormPath + ) { + env.taskQueue.forEach((task, index) => { + const { pattern, callbacks } = task + if (matchStrategy(pattern, nodePath)) { + callbacks.forEach(callback => { + userUpdating(field, () => { + field.setState(callback) + }) + }) + if (!pattern.isWildMatchPattern && !pattern.isMatchPattern) { + env.taskQueue.splice(index, 1) + env.taskQueue.forEach(({ pattern }, index) => { + if (pattern.toString() === nodePath.toString()) { + env.taskIndexes[nodePath.toString()] = index + } + }) + } + } + }) + } + + function setFieldState( + path: FormPathPattern, + callback?: (state: IFieldState) => void, + silent?: boolean + ) { + if (!isFn(callback)) return + let matchCount = 0 + let pattern = FormPath.getPath(path) + graph.select(pattern, field => { + userUpdating(field, () => { + field.setState(callback, silent) + }) + matchCount++ + }) + if (matchCount === 0 || pattern.isWildMatchPattern) { + let taskIndex = env.taskIndexes[pattern.toString()] + if (isValid(taskIndex)) { + if ( + env.taskQueue[taskIndex] && + !env.taskQueue[taskIndex].callbacks.some(fn => isEqual(fn, callback)) + ) { + env.taskQueue[taskIndex].callbacks.push(callback) + } + } else { + env.taskIndexes[pattern.toString()] = env.taskQueue.length + env.taskQueue.push({ + pattern, + callbacks: [callback] + }) + } + } + } + + function userUpdating(field: IField | IVirtualField, fn?: () => void) { + if (!field) return + const nodePath = field.unsafe_getSourceState(state => state.path) + if (nodePath) + env.userUpdateFields.push( + field.unsafe_getSourceState(state => state.path) + ) + if (isFn(fn)) { + fn() + } + env.userUpdateFields.pop() + } + + function setFieldValue(path: FormPathPattern, value?: any, silent?: boolean) { + setFieldState( + path, + state => { + state.value = value + }, + silent + ) + } + + function getFieldValue(path?: FormPathPattern) { + return getFieldState(path, state => { + return state.value + }) + } + + function setFieldInitialValue( + path?: FormPathPattern, + value?: any, + silent?: boolean + ) { + setFieldState( + path, + state => { + state.initialValue = value }, - true + silent + ) + } + + function getFieldInitialValue(path?: FormPathPattern) { + return getFieldState(path, state => { + return state.initialValue + }) + } + + function getFieldState( + path: FormPathPattern, + callback?: (state: IFieldState) => any + ) { + const field = graph.select(path) + return field && field.getState(callback) + } + + function getFormGraph() { + return graph.map(node => { + return node.getState() + }) + } + + function setFormGraph(nodes: {}) { + each( + nodes, + ( + node: IFieldState | IVirtualFieldState, + key + ) => { + let nodeState: any + if (graph.exist(key)) { + nodeState = graph.get(key) + nodeState.unsafe_setSourceState(state => { + Object.assign(state, node) + }) + } else { + if (node.displayName === 'VirtualFieldState') { + nodeState = registerVirtualField({ + path: key + }) + nodeState.unsafe_setSourceState(state => { + Object.assign(state, node) + }) + } else if (node.displayName === 'FieldState') { + nodeState = registerField({ + path: key + }) + nodeState.unsafe_setSourceState(state => { + Object.assign(state, node) + }) + } + } + if (nodeState) { + nodeState.notify(state.getState()) + } + } ) + } + + function matchStrategy(pattern: FormPathPattern, nodePath: FormPathPattern) { + const matchPattern = FormPath.parse(pattern) + const node = graph.get(nodePath) + if (!node) return false + return node.unsafe_getSourceState( + state => matchPattern.match(state.name) || matchPattern.match(state.path) + ) + } + + //在subscribe中必须同步使用,否则会监听不到变化 + function hasChanged(target: any, path: FormPathPattern): boolean { + if (!env.publishing) { + throw new Error( + 'The watch function must be used synchronously in the subscribe callback.' + ) + } + if (isFormState(target)) { + return state.hasChanged(path) + } else if (isFieldState(target) || isVirtualFieldState(target)) { + const node = graph.get(target.path) + return node && node.hasChanged(path) + } else { + throw new Error( + 'Illegal parameter,You must pass the correct state object(FormState/FieldState/VirtualFieldState).' + ) + } + } + + const state = new FormState(options) + const validator = new FormValidator({ + ...options, + matchStrategy + }) + const graph = new FormGraph({ + matchStrategy + }) + const applyWithScheduler = scheduler(options.validateConcurrentTimeMS) + const formApi = { + submit, + reset, + hasChanged, + clearErrors, + validate, + setFormState, + getFormState, + setFieldState, + getFieldState, + registerField, + registerVirtualField, + createMutators, + getFormGraph, + setFormGraph, + setFieldValue, + unsafe_do_not_use_transform_data_path: transformDataPath, //eslint-disable-line + getFieldValue, + setFieldInitialValue, + getFieldInitialValue, + subscribe: (callback?: FormHeartSubscriber) => { + return heart.subscribe(callback) + }, + unsubscribe: (id: number) => { + heart.unsubscribe(id) + }, + notify: (type: string, payload: T) => { + heart.publish(type, payload) + } + } + const heart = new FormHeart({ + ...options, + context: formApi, + beforeNotify: () => { + env.publishing = true + }, + afterNotify: () => { + env.publishing = false + } }) - return form + const env = { + validateTimer: null, + graphChangeTimer: null, + leadingStage: false, + publishing: false, + taskQueue: [], + userUpdateFields: [], + taskIndexes: {}, + removeNodes: {}, + submittingTask: undefined + } + heart.publish(LifeCycleTypes.ON_FORM_WILL_INIT, state) + state.subscription = { + notify: onFormChange + } + + graph.appendNode('', state) + state.setState((state: IFormState) => { + state.initialized = true + }) + graph.subscribe(onGraphChange) + return formApi } +export const registerValidationFormats = FormValidator.registerFormats + +export const registerValidationRules = FormValidator.registerRules + +export const registerValidationMTEngine = FormValidator.registerMTEngine + export { - setValidationLocale, setValidationLanguage, - Form, - calculateSchemaInitialValues + setValidationLocale, + FormPath, + FormPathPattern, + FormGraph } export default createForm diff --git a/packages/core/src/path.ts b/packages/core/src/path.ts deleted file mode 100644 index 8a60d3dcd9f..00000000000 --- a/packages/core/src/path.ts +++ /dev/null @@ -1,81 +0,0 @@ -import createMatcher from 'dot-match' -import { resolveFieldPath, isStr, isFn, isArr, reduce } from './utils' -import { IFormPathMatcher } from '@uform/types' -type Filter = (payload: any) => boolean - -const matchWithFilter = (result: boolean, filter: Filter, payload: any) => { - if (isFn(filter)) { - return result && filter(payload) - } - return result -} - -const wildcardRE = /\*/ - -export const FormPath = { - match( - pattern: string, - isRealPath?: boolean | Filter, - filter?: Filter - ): IFormPathMatcher { - pattern = pattern + '' - const match = createMatcher(pattern) - if (isFn(isRealPath)) { - filter = isRealPath as Filter - isRealPath = false - } - const matcher = (payload: any) => { - if (payload && payload.fieldState) { - return matchWithFilter( - match( - resolveFieldPath( - isRealPath ? payload.fieldState.path : payload.fieldState.name - ) - ), - filter, - payload.fieldState - ) - } else if (payload && payload.name && payload.path) { - return matchWithFilter( - match(resolveFieldPath(isRealPath ? payload.path : payload.name)), - filter, - payload - ) - } else if (isStr(payload)) { - return matchWithFilter(match(resolveFieldPath(payload)), filter, { - name: payload - }) - } else if (isArr(payload)) { - return matchWithFilter(match(payload), filter, { path: payload }) - } - return false - } - - matcher.hasWildcard = wildcardRE.test(pattern) - matcher.pattern = pattern - - return matcher - }, - exclude(matcher: IFormPathMatcher) { - return (path: any): boolean => - isFn(matcher) - ? !matcher(path) - : isStr(matcher) - ? !FormPath.match(matcher)(path) - : false - }, - transform( - path: string, - regexp: RegExp, - calllback: (...args: string[]) => string - ) { - const args = reduce( - resolveFieldPath(path), - (buf: string[], key: string) => { - return new RegExp(regexp).test(key) ? buf.concat(key) : buf - }, - [] - ) - return calllback(...args) - } -} diff --git a/packages/core/src/shared/graph.ts b/packages/core/src/shared/graph.ts new file mode 100644 index 00000000000..8fdd542e6ad --- /dev/null +++ b/packages/core/src/shared/graph.ts @@ -0,0 +1,314 @@ +import { + each, + reduce, + map, + isFn, + FormPath, + FormPathPattern, + Subscribable +} from '@uform/shared' +import { + FormGraphNodeRef, + FormGraphMatcher, + FormGraphEacher, + FormGraphProps +} from '../types' + +export class FormGraph extends Subscribable<{ + type: string + payload: FormGraphNodeRef +}> { + private refrences: { + [key in string]: FormGraphNodeRef + } + + private nodes: { + [key in string]: NodeType + } + + private buffer: { + path: FormPath + ref: FormGraphNodeRef + latestParent?: { + ref: FormGraphNodeRef + path: FormPath + } + }[] + + private matchStrategy: FormGraphProps['matchStrategy'] + + public size: number + + constructor(props: FormGraphProps = {}) { + super() + this.refrences = {} + this.nodes = {} + this.size = 0 + this.buffer = [] + this.matchStrategy = props.matchStrategy + } + + /** + * 模糊匹配API + * @param path + * @param matcher + */ + select(path: FormPathPattern, eacher?: FormGraphMatcher) { + const pattern = FormPath.parse(path) + if (!eacher) { + const node = this.get(pattern) + if (node) { + return node + } + } + for (let nodePath in this.nodes) { + const node = this.nodes[nodePath] + if ( + isFn(this.matchStrategy) + ? this.matchStrategy(pattern, nodePath) + : pattern.match(nodePath) + ) { + if (isFn(eacher)) { + const result = eacher(node, FormPath.parse(nodePath)) + if (result === false) { + return node + } + } else { + return node + } + } + } + } + + get(path: FormPathPattern) { + return this.nodes[FormPath.parse(path).toString()] + } + + selectParent(path: FormPathPattern) { + const selfPath = FormPath.parse(path) + const parentPath = FormPath.parse(path).parent() + if (selfPath.toString() === parentPath.toString()) return undefined + + return this.get(parentPath) + } + + selectChildren(path: FormPathPattern) { + const ref = this.refrences[FormPath.parse(path).toString()] + if (ref && ref.children) { + return reduce( + ref.children, + (buf, path) => { + return buf.concat(this.get(path)).concat(this.selectChildren(path)) + }, + [] + ) + } + return [] + } + + exist(path: FormPathPattern) { + return !!this.get(FormPath.parse(path)) + } + + /** + * 递归遍历所有children + * 支持模糊匹配 + */ + eachChildren(eacher: FormGraphEacher, recursion?: boolean): void + eachChildren( + path: FormPathPattern, + eacher: FormGraphEacher, + recursion?: boolean + ): void + eachChildren( + path: FormPathPattern, + selector: FormPathPattern, + eacher: FormGraphEacher, + recursion?: boolean + ): void + eachChildren( + path: any, + selector: any = true, + eacher: any = true, + recursion: any = true + ) { + if (isFn(path)) { + recursion = selector + eacher = path + path = '' + selector = '*' + } + if (isFn(selector)) { + recursion = eacher + eacher = selector + selector = '*' + } + + const ref = this.refrences[FormPath.parse(path).toString()] + if (ref && ref.children) { + return each(ref.children, path => { + if (isFn(eacher)) { + const node = this.get(path) + if ( + node && + (isFn(this.matchStrategy) + ? this.matchStrategy(selector, path) + : FormPath.parse(selector).match(path)) + ) { + eacher(node, path) + if (recursion) { + this.eachChildren(path, selector, eacher, recursion) + } + } + } + }) + } + } + + /** + * 递归遍历所有parent + */ + eachParent(path: FormPathPattern, eacher: FormGraphEacher) { + const selfPath = FormPath.parse(path) + const ref = this.refrences[selfPath.toString()] + if (isFn(eacher)) { + if (ref && ref.parent) { + const node = this.get(ref.parent.path) + this.eachParent(ref.parent.path, eacher) + eacher(node, ref.parent.path) + } + } + } + + /** + * 遍历所有父节点与所有子节点 + */ + eachParentAndChildren( + path: FormPathPattern, + eacher: FormGraphEacher + ) { + const selfPath = FormPath.parse(path) + const node = this.get(selfPath) + if (!node) return + this.eachParent(selfPath, eacher) + if (isFn(eacher)) { + eacher(node, selfPath) + this.eachChildren(selfPath, eacher) + } + } + + getLatestParent(path: FormPathPattern) { + const selfPath = FormPath.parse(path) + const parentPath = FormPath.parse(path).parent() + if (selfPath.toString() === parentPath.toString()) return undefined + if (this.refrences[parentPath.toString()]) + return { + ref: this.refrences[parentPath.toString()], + path: FormPath.parse(parentPath.toString()) + } + return this.getLatestParent(parentPath) + } + + map(mapper: (node: NodeType) => any) { + return map(this.nodes, mapper) + } + + reduce( + reducer: (buffer: T, node: NodeType, key: string) => T, + initial: T + ) { + return reduce(this.nodes, reducer, initial) + } + + appendNode(path: FormPathPattern, node: NodeType) { + const selfPath = FormPath.parse(path) + const parentPath = selfPath.parent() + const parentRef = this.refrences[parentPath.toString()] + const selfRef: FormGraphNodeRef = { + path: selfPath, + children: [] + } + if (this.get(selfPath)) return + this.nodes[selfPath.toString()] = node + this.refrences[selfPath.toString()] = selfRef + this.size++ + if (parentRef) { + parentRef.children.push(selfPath) + selfRef.parent = parentRef + } else { + const latestParent = this.getLatestParent(selfPath) + if (latestParent) { + latestParent.ref.children.push(selfPath) + selfRef.parent = latestParent.ref + this.buffer.push({ + path: selfPath, + ref: selfRef, + latestParent: latestParent + }) + } + } + this.buffer.forEach(({ path, ref, latestParent }, index) => { + if ( + path.parent().match(selfPath) || + (selfPath.includes(latestParent.path) && + path.includes(selfPath) && + selfPath.toString() !== path.toString()) + ) { + selfRef.children.push(path) + ref.parent = selfRef + latestParent.ref.children.splice( + latestParent.ref.children.indexOf(path), + 1 + ) + this.buffer.splice(index, 1) + } + }) + this.notify({ + type: 'GRAPH_NODE_DID_MOUNT', + payload: selfRef + }) + } + + remove(path: FormPathPattern) { + const selfPath = FormPath.parse(path) + const selfRef = this.refrences[selfPath.toString()] + if (!selfRef) return + this.notify({ + type: 'GRAPH_NODE_WILL_UNMOUNT', + payload: selfRef + }) + if (selfRef.children) { + selfRef.children.forEach(path => { + this.remove(path) + }) + } + this.buffer = this.buffer.filter(({ ref }) => { + return selfRef !== ref + }) + delete this.nodes[selfPath.toString()] + delete this.refrences[selfPath.toString()] + this.size-- + if (selfRef.parent) { + selfRef.parent.children.forEach((path, index) => { + if (path.match(selfPath)) { + selfRef.parent.children.splice(index, 0) + } + }) + } + } + + replace(path: FormPathPattern, node: NodeType) { + const selfPath = FormPath.parse(path) + const selfRef = this.refrences[selfPath.toString()] + if (!selfRef) return + this.notify({ + type: 'GRAPH_NODE_WILL_UNMOUNT', + payload: selfRef + }) + this.nodes[selfPath.toString()] = node + this.notify({ + type: 'GRAPH_NODE_DID_MOUNT', + payload: selfRef + }) + } +} diff --git a/packages/core/src/shared/lifecycle.ts b/packages/core/src/shared/lifecycle.ts new file mode 100644 index 00000000000..4bb8c16a864 --- /dev/null +++ b/packages/core/src/shared/lifecycle.ts @@ -0,0 +1,105 @@ +import { isFn, isStr, isArr, isObj, each, Subscribable } from '@uform/shared' +import { FormLifeCyclePayload, FormLifeCycleHandler } from '../types' + +export class FormLifeCycle { + private listener: FormLifeCyclePayload + + constructor(handler: FormLifeCycleHandler) + constructor(type: string, handler: FormLifeCycleHandler) + constructor(handlerMap: { [key: string]: FormLifeCycleHandler }) + constructor(...params: any[]) { + this.listener = this.buildListener(params) + } + buildListener(params: any[]) { + return function(payload: { type: string; payload: Payload }, ctx: any) { + for (let index = 0; index < params.length; index++) { + let item = params[index] + if (isFn(item)) { + item.call(this, payload, ctx) + } else if (isStr(item) && isFn(params[index + 1])) { + if (item === payload.type) { + params[index + 1].call(this, payload.payload, ctx) + } + index++ + } else if (isObj(item)) { + each(item, (handler, type) => { + if (isFn(handler) && isStr(type)) { + if (type === payload.type) { + handler.call(this, payload.payload, ctx) + return false + } + } + }) + } + } + } + } + + notify = (type: any, payload: Payload, ctx?: any) => { + if (isStr(type)) { + this.listener.call(ctx, { type, payload }, ctx) + } + } +} + +export class FormHeart extends Subscribable { + private lifecycles: FormLifeCycle[] + + private context: Context + + private beforeNotify?: (...args: any[]) => void + + private afterNotify?: (...args: any[]) => void + + constructor({ + lifecycles, + context, + beforeNotify, + afterNotify + }: { + lifecycles?: FormLifeCycle[] + context?: Context + beforeNotify?: (...args: any[]) => void + afterNotify?: (...args: any[]) => void + } = {}) { + super() + this.lifecycles = this.buildLifeCycles(lifecycles || []) + this.context = context + this.beforeNotify = beforeNotify + this.afterNotify = afterNotify + } + + buildLifeCycles(lifecycles: FormLifeCycle[]) { + return lifecycles.reduce((buf, item) => { + if (item instanceof FormLifeCycle) { + return buf.concat(item) + } else { + if (typeof item === 'object') { + this.context = item + return buf + } else if (isArr(item)) { + return this.buildLifeCycles(item) + } + return buf + } + }, []) + } + + publish = (type: any, payload: P, context?: C) => { + if (isStr(type)) { + if (isFn(this.beforeNotify)) { + this.beforeNotify(type, payload, context) + } + this.lifecycles.forEach(lifecycle => { + lifecycle.notify(type, payload, context || this.context) + }) + this.notify({ + type, + payload + }) + if (isFn(this.afterNotify)) { + this.afterNotify(type, payload, context) + } + } + } +} diff --git a/packages/core/src/shared/model.ts b/packages/core/src/shared/model.ts new file mode 100644 index 00000000000..d8336fe437a --- /dev/null +++ b/packages/core/src/shared/model.ts @@ -0,0 +1,224 @@ +import { + clone, + isEqual, + isFn, + each, + globalThisPolyfill, + Subscribable, + FormPath, + FormPathPattern +} from '@uform/shared' +import produce, { Draft, setAutoFreeze } from 'immer' +import { + IStateModelProvider, + IStateModelFactory, + StateDirtyMap, + IModel, + StateModel +} from '../types' +const hasProxy = !!globalThisPolyfill.Proxy + +setAutoFreeze(false) + +export const createStateModel = ( + Factory: IStateModelFactory +): IStateModelProvider => { + return class Model extends Subscribable + implements IModel { + public state: State & { displayName?: string } + public props: Props & + DefaultProps & { + useDirty?: boolean + computeState?: (draft: State, prevState: State) => void + } + public displayName?: string + public dirtyNum: number + public dirtys: StateDirtyMap + public prevState: State + public batching: boolean + public stackCount: number + public controller: StateModel + + constructor(defaultProps: DefaultProps) { + super() + this.state = { ...Factory.defaultState } + this.prevState = { ...Factory.defaultState } + this.props = { + ...Factory.defaultProps, + ...defaultProps + } + this.dirtys = {} + this.dirtyNum = 0 + this.stackCount = 0 + this.batching = false + this.controller = new Factory(this.state, this.props) + this.displayName = Factory.displayName + this.state.displayName = this.displayName + } + + batch = (callback?: () => void) => { + this.batching = true + if (isFn(callback)) { + callback() + } + if (this.dirtyNum > 0) { + this.notify(this.getState()) + } + this.dirtys = {} + this.dirtyNum = 0 + this.batching = false + } + + getState = (callback?: (state: State) => any) => { + if (isFn(callback)) { + return callback(this.getState()) + } else { + if (isFn(this.controller.publishState)) { + return this.controller.publishState(this.state) + } + + if (!hasProxy || this.props.useDirty) { + return clone(this.state) + } else { + return produce(this.state, () => {}) + } + } + } + + unsafe_getSourceState = (callback?: (state: State) => any) => { + if (isFn(callback)) { + return callback(this.state) + } else { + return this.state + } + } + + unsafe_setSourceState = (callback: (state: State) => void) => { + if (isFn(callback)) { + callback(this.state) + } + } + + setState = ( + callback: (state: State | Draft) => State | void, + silent = false + ) => { + if (isFn(callback)) { + this.stackCount++ + if (!hasProxy || this.props.useDirty) { + const draft = this.getState() + if (!this.batching) { + this.dirtys = {} + this.dirtyNum = 0 + } + callback(draft) + if (isFn(this.props.computeState)) { + this.props.computeState(draft, this.state) + } + if (isFn(this.controller.computeState)) { + this.controller.computeState(draft, this.state) + } + const draftKeys = Object.keys(draft || {}) + const stateKeys = Object.keys(this.state || {}) + + each( + draftKeys.length > stateKeys.length ? draft : this.state, + (value, key) => { + if (!isEqual(this.state[key], draft[key])) { + this.state[key] = draft[key] + this.dirtys[key] = true + this.dirtyNum++ + } + } + ) + if (isFn(this.controller.dirtyCheck)) { + const result = this.controller.dirtyCheck(this.dirtys) + if (result !== undefined) { + Object.assign(this.dirtys, result) + } + } + if (this.dirtyNum > 0 && !silent) { + if (this.batching) { + this.stackCount-- + return + } + this.notify(this.getState()) + this.dirtys = {} + this.dirtyNum = 0 + } + } else { + if (!this.batching) { + this.dirtys = {} + this.dirtyNum = 0 + } + //用proxy解决脏检查计算属性问题 + this.state = produce( + this.state, + draft => { + callback(draft) + if (isFn(this.props.computeState)) { + this.props.computeState(draft, this.state) + } + if (isFn(this.controller.computeState)) { + this.controller.computeState(draft, this.state) + } + }, + patches => { + patches.forEach(({ path, op, value }) => { + if (op === 'replace') { + if (!isEqual(this.state[path[0]], value)) { + this.dirtys[path[0]] = true + this.dirtyNum++ + } + } else { + this.dirtys[path[0]] = true + this.dirtyNum++ + } + }) + } + ) + if (isFn(this.controller.dirtyCheck)) { + const result = this.controller.dirtyCheck(this.dirtys) + if (result !== undefined) { + Object.assign(this.dirtys, result) + } + } + if (this.dirtyNum > 0 && !silent) { + if (this.batching) { + this.stackCount-- + return + } + this.notify(this.getState()) + this.dirtys = {} + this.dirtyNum = 0 + } + } + + this.stackCount-- + if (!this.stackCount) { + this.prevState = { ...this.state } + } + } + } + /** + * 当前操作的变化情况 + */ + isDirty = (key?: string) => + key ? this.dirtys[key] === true : this.dirtyNum > 0 + + getDirtyInfo = () => this.dirtys + + /** + * + *在一组操作过程中的变化情况 + */ + hasChanged = (path?: FormPathPattern) => { + return path + ? !isEqual( + FormPath.getIn(this.prevState, path), + FormPath.getIn(this.state, path) + ) + : !isEqual(this.prevState, this.state) + } + } as any +} diff --git a/packages/core/src/shared/scheduler.ts b/packages/core/src/shared/scheduler.ts new file mode 100644 index 00000000000..0114a6fffcd --- /dev/null +++ b/packages/core/src/shared/scheduler.ts @@ -0,0 +1,36 @@ +import { isFn } from '@uform/shared' + +export const scheduler = (concurrent = 360) => { + let operating = false + let buffer = [] + const applyQueue = () => { + operating = true + requestAnimationFrame(() => { + let expireTime = performance.now() + concurrent, + operateEndTime = 0 + while (true) { + if (expireTime < operateEndTime) break + const fn = buffer.shift() + if (fn) { + fn() + operateEndTime = performance.now() + } else { + break + } + } + + if (buffer.length > 0) { + applyQueue() + } else { + operating = false + } + }) + } + return (fn: any) => { + if (!isFn(fn)) return + buffer.push(fn) + if (!operating) { + applyQueue() + } + } +} diff --git a/packages/core/src/state/field.ts b/packages/core/src/state/field.ts new file mode 100644 index 00000000000..de542cd8fb3 --- /dev/null +++ b/packages/core/src/state/field.ts @@ -0,0 +1,249 @@ +import { createStateModel } from '../shared/model' +import { toArr, isValid, isEqual, FormPath, isFn } from '@uform/shared' +import { IFieldState, IFieldStateProps } from '../types' +/** + * 核心数据结构,描述表单字段的所有状态 + */ +export const FieldState = createStateModel( + class FieldState { + static displayName = 'FieldState' + static defaultState = { + name: '', + path: '', + initialized: false, + pristine: true, + valid: true, + modified: false, + touched: false, + active: false, + visited: false, + invalid: false, + visible: true, + display: true, + loading: false, + validating: false, + errors: [], + values: [], + ruleErrors: [], + ruleWarnings: [], + effectErrors: [], + warnings: [], + effectWarnings: [], + editable: true, + selfEditable: undefined, + formEditable: undefined, + value: undefined, + initialValue: undefined, + rules: [], + required: false, + mounted: false, + unmounted: false, + props: {} + } + + static defaultProps = { + path: '' + } + + private state: IFieldState + + private nodePath: FormPath + + private dataPath: FormPath + + constructor(state: IFieldState, props: IFieldStateProps) { + this.state = state + this.nodePath = FormPath.getPath(props.nodePath) + this.dataPath = FormPath.getPath(props.dataPath) + this.state.name = this.dataPath.entire + this.state.path = this.nodePath.entire + } + + readValues({ value, values }: IFieldStateProps) { + if (isValid(values)) { + values = toArr(values) + values[0] = value + } else { + if (isValid(value)) { + values = toArr(value) + } + } + return { + value, + values: toArr(values) + } + } + + readRequired(rules: any[]) { + for (let i = 0; i < rules.length; i++) { + if (rules[i].required !== undefined) { + return rules[i].required + } + } + } + + syncRulesByRequired(rules: any[], required: boolean) { + if (isValid(required)) { + if (rules.length) { + if (!rules.some(rule => rule && isValid(rule.required))) { + rules.push({ required }) + } else { + rules = rules.reduce((buf: any[], item: any) => { + const keys = Object.keys(item || {}) + if (item.required !== undefined) { + if (item.message !== undefined) { + if (keys.length > 2) { + return buf.concat({ + ...item, + required + }) + } + } else { + if (keys.length > 1) { + return buf.concat({ + ...item, + required + }) + } + } + } + if (isValid(item.required)) { + return buf.concat({ + ...item, + required + }) + } + return buf.concat(item) + }, []) + } + } else { + if (required === true) { + rules.push({ + required + }) + } + } + } + return rules + } + + readRules({ rules, required }: IFieldStateProps, prevState: IFieldState) { + let newRules = isValid(rules) ? toArr(rules) : this.state.rules + let newRequired = isValid(required) ? required : false + const currentRuleRequired = this.readRequired(newRules) + const prevRuleRequired = this.readRequired(prevState.rules) + const ruleRequiredChanged = currentRuleRequired !== prevRuleRequired + const requiredChanged = !isEqual(required, prevState.required) + + if (ruleRequiredChanged && !requiredChanged) { + if (isValid(currentRuleRequired)) { + newRequired = currentRuleRequired + } + } else if (requiredChanged && !ruleRequiredChanged) { + newRules = this.syncRulesByRequired(newRules, newRequired) + } else if (ruleRequiredChanged && requiredChanged) { + if (isValid(currentRuleRequired)) { + newRequired = currentRuleRequired + } + } else { + newRules = this.syncRulesByRequired(newRules, newRequired) + } + return { + rules: newRules, + required: newRequired + } + } + + computeState(draft: IFieldState, prevState: IFieldState) { + //如果是隐藏状态,则禁止修改值 + if (!draft.visible || draft.unmounted) { + draft.value = prevState.value + draft.initialValue = prevState.initialValue + } + //操作重定向 + if (!isEqual(draft.errors, prevState.errors)) { + draft.effectErrors = draft.errors + } + if (!isEqual(draft.warnings, prevState.warnings)) { + draft.effectWarnings = draft.warnings + } + //容错逻辑 + draft.rules = toArr(draft.rules).filter(v => !!v) + draft.effectWarnings = toArr(draft.effectWarnings).filter(v => !!v) + draft.effectErrors = toArr(draft.effectErrors).filter(v => !!v) + draft.ruleWarnings = toArr(draft.ruleWarnings).filter(v => !!v) + draft.ruleErrors = toArr(draft.ruleErrors).filter(v => !!v) + + draft.errors = draft.ruleErrors.concat(draft.effectErrors) + draft.warnings = draft.ruleWarnings.concat(draft.effectWarnings) + + if (!isEqual(draft.editable, prevState.editable)) { + draft.selfEditable = draft.editable + } + draft.editable = isValid(draft.selfEditable) + ? draft.selfEditable + : isValid(draft.formEditable) + ? isFn(draft.formEditable) + ? draft.formEditable(draft.name) + : draft.formEditable + : true + + const { value, values } = this.readValues(draft) + draft.value = value + draft.values = values + if ( + draft.initialized && + prevState.initialized && + !isEqual(draft.value, prevState.value) + ) { + draft.modified = true + } + if (isEqual(draft.value, draft.initialValue)) { + draft.pristine = true + } else { + draft.pristine = false + } + if (!isValid(draft.props)) { + draft.props = prevState.props + } + if (draft.validating !== prevState.validating) { + if (draft.validating === true) { + draft.loading = true + } else if (draft.validating === false) { + draft.loading = false + } + } + // 以下几种情况清理错误和警告信息 + // 1. 字段设置为不可编辑 + // 2. 字段隐藏 + // 3. 字段被卸载 + if ( + (draft.selfEditable !== prevState.selfEditable && + !draft.selfEditable) || + draft.visible === false || + draft.unmounted === true + ) { + draft.errors = [] + draft.effectErrors = [] + draft.warnings = [] + draft.effectWarnings = [] + } + if (draft.mounted === true && draft.mounted !== prevState.mounted) { + draft.unmounted = false + } + if (draft.unmounted === true && draft.unmounted !== prevState.unmounted) { + draft.mounted = false + } + if (draft.errors.length) { + draft.invalid = true + draft.valid = false + } else { + draft.invalid = false + draft.valid = true + } + const { rules, required } = this.readRules(draft, prevState) + draft.rules = rules + draft.required = required + } + } +) diff --git a/packages/core/src/state/form.ts b/packages/core/src/state/form.ts new file mode 100644 index 00000000000..6920e1c7e80 --- /dev/null +++ b/packages/core/src/state/form.ts @@ -0,0 +1,73 @@ +import { createStateModel } from '../shared/model' +import { toArr, clone, isEqual, isValid } from '@uform/shared' +import { IFormState, IFormStateProps } from '../types' +/** + * 核心数据结构,描述Form级别状态 + */ +export const FormState = createStateModel( + class FormState { + static displayName = 'FormState' + static defaultState = { + pristine: true, + valid: true, + invalid: false, + loading: false, + validating: false, + initialized: false, + submitting: false, + editable: true, + errors: [], + warnings: [], + values: {}, + initialValues: {}, + mounted: false, + unmounted: false, + props: {} + } + + static defaultProps = { + lifecycles: [] + } + + private state: IFormState + constructor(state: IFormState, props: IFormStateProps) { + this.state = state + this.state.initialValues = clone(props.initialValues || {}) + this.state.values = clone(props.values || props.initialValues || {}) + this.state.editable = props.editable + } + + computeState(draft: IFormState, prevState: IFormState) { + draft.errors = toArr(draft.errors).filter(v => !!v) + draft.warnings = toArr(draft.warnings).filter(v => !!v) + if (draft.errors.length) { + draft.invalid = true + draft.valid = false + } else { + draft.invalid = false + draft.valid = true + } + if (!isValid(draft.props)) { + draft.props = prevState.props + } + if (isEqual(draft.values, draft.initialValues)) { + draft.pristine = true + } else { + draft.pristine = false + } + if (draft.validating !== prevState.validating) { + if (draft.validating === true) { + draft.loading = true + } else if (draft.validating === false) { + draft.loading = false + } + } + if (draft.mounted === true && draft.mounted !== prevState.mounted) { + draft.unmounted = false + } + if (draft.unmounted === true && draft.unmounted !== prevState.unmounted) { + draft.mounted = false + } + } + } +) diff --git a/packages/core/src/state/virtual-field.ts b/packages/core/src/state/virtual-field.ts new file mode 100644 index 00000000000..4940529d2b4 --- /dev/null +++ b/packages/core/src/state/virtual-field.ts @@ -0,0 +1,59 @@ +import { createStateModel } from '../shared/model' +import { FormPath, isValid } from '@uform/shared' +import { IVirtualFieldState, IVirtualFieldStateProps } from '../types' + +/** + * UForm特有,描述一个虚拟字段, + * 它不占用数据空间,但是它拥有状态, + * 可以联动控制Field或者VirtualField的状态 + * 类似于现在UForm的Card之类的容器布局组件 + */ +export const VirtualFieldState = createStateModel< + IVirtualFieldState, + IVirtualFieldStateProps +>( + class VirtualFieldState { + static displayName = 'VirtualFieldState' + static defaultState = { + name: '', + path: '', + initialized: false, + visible: true, + display: true, + mounted: false, + unmounted: false, + props: {} + } + + static defaultProps = { + path: '', + props: {} + } + + private state: IVirtualFieldState + + private path: FormPath + + private dataPath: FormPath + + constructor(state: IVirtualFieldState, props: IVirtualFieldStateProps) { + this.state = state + this.path = FormPath.getPath(props.nodePath) + this.dataPath = FormPath.getPath(props.dataPath) + this.state.path = this.path.entire + this.state.name = this.dataPath.entire + } + + computeState(draft: IVirtualFieldState, prevState: IVirtualFieldState) { + if (draft.mounted === true) { + draft.unmounted = false + } + if (!isValid(draft.props)) { + draft.props = prevState.props + } + if (draft.unmounted === true) { + draft.mounted = false + } + } + } +) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts new file mode 100644 index 00000000000..045773d65a2 --- /dev/null +++ b/packages/core/src/types.ts @@ -0,0 +1,359 @@ +import { FormPath, FormPathPattern, isFn, Subscribable } from '@uform/shared' +import { + ValidatePatternRules, + ValidateNodeResult, + ValidateFieldOptions +} from '@uform/validator' +import { FormLifeCycle } from './shared/lifecycle' +import { Draft } from 'immer' + +export type FormLifeCycleHandler = (payload: T, context: any) => void + +export type FormHeartSubscriber = ({ + type, + payload +}: { + type: string + payload: any +}) => void + +export enum LifeCycleTypes { + /** + * Form LifeCycle + **/ + + ON_FORM_WILL_INIT = 'onFormWillInit', + ON_FORM_INIT = 'onFormInit', + ON_FORM_CHANGE = 'onFormChange', //ChangeType精准控制响应 + ON_FORM_MOUNT = 'onFormMount', + ON_FORM_UNMOUNT = 'onFormUnmount', + ON_FORM_SUBMIT = 'onFormSubmit', + ON_FORM_RESET = 'onFormReset', + ON_FORM_SUBMIT_START = 'onFormSubmitStart', + ON_FORM_SUBMIT_END = 'onFormSubmitEnd', + ON_FORM_VALUES_CHANGE = 'onFormValuesChange', + ON_FORM_INITIAL_VALUES_CHANGE = 'onFormInitialValuesChange', + ON_FORM_VALIDATE_START = 'onFormValidateStart', + ON_FORM_VALIDATE_END = 'onFormValidateEnd', + ON_FORM_INPUT_CHANGE = 'onFormInputChange', + /** + * FormGraph LifeCycle + **/ + ON_FORM_GRAPH_CHANGE = 'onFormGraphChange', + + /** + * Field LifeCycle + **/ + + ON_FIELD_WILL_INIT = 'onFieldWillInit', + ON_FIELD_INIT = 'onFieldInit', + ON_FIELD_CHANGE = 'onFieldChange', + ON_FIELD_INPUT_CHANGE = 'onFieldInputChange', + ON_FIELD_VALUE_CHANGE = 'onFieldValueChange', + ON_FIELD_INITIAL_VALUE_CHANGE = 'onFieldInitialValueChange', + ON_FIELD_MOUNT = 'onFieldMount', + ON_FIELD_UNMOUNT = 'onFieldUnmount' +} + +export interface FormGraphProps { + matchStrategy?: (pattern: FormPathPattern, field: any) => boolean +} + +export type FormGraphNodeMap = { + [key in string]: T +} + +export interface FormGraphVisitorOptions { + path: FormPath + exsist: boolean + append: (node: T) => void +} + +export type FormGraph = ( + node: T, + options: FormGraphVisitorOptions +) => void + +export interface FormGraphNodeRef { + parent?: FormGraphNodeRef + path: FormPath + children: FormPath[] +} + +export type FormGraphMatcher = (node: T, path: FormPath) => void | boolean + +export type FormGraphEacher = (node: T, path: FormPath) => void + +export type FormLifeCyclePayload = ( + params: { + type: string + payload: T + }, + context: any +) => void + +export type StateDirtyMap

= { + [key in keyof P]?: boolean +} + +export interface StateModel

{ + publishState?: (state: P) => P + dirtyCheck?: (dirtys: StateDirtyMap

) => StateDirtyMap

| void + computeState?: (state: Draft

, preState?: P) => Draft

| void +} + +export interface IStateModelFactory { + new (state: S, props: P): StateModel + defaultState?: S + defaultProps?: P + displayName?: string +} + +export interface IStateModelProvider { + new (props: P): IModel +} + +export interface IFieldState { + displayName?: string + name: string + path: string + initialized: boolean + pristine: boolean + valid: boolean + touched: boolean + invalid: boolean + visible: boolean + display: boolean + editable: boolean + selfEditable: boolean + formEditable: boolean | ((name: string) => boolean) + loading: boolean + modified: boolean + active: boolean + visited: boolean + validating: boolean + values: any[] + errors: string[] + effectErrors: string[] + ruleErrors: string[] + warnings: string[] + effectWarnings: string[] + ruleWarnings: string[] + value: any + initialValue: any + rules: ValidatePatternRules[] + required: boolean + mounted: boolean + unmounted: boolean + props: FieldProps + [key: string]: any +} +export type FieldStateDirtyMap = StateDirtyMap + +export interface IFieldStateProps { + path?: FormPathPattern + nodePath?: FormPathPattern + dataPath?: FormPathPattern + name?: string + value?: any + values?: any[] + initialValue?: any + props?: FieldProps + rules?: ValidatePatternRules[] + required?: boolean + editable?: boolean + visible?: boolean + display?: boolean + useDirty?: boolean + computeState?: (draft: IFieldState, prevState: IFieldState) => void +} + +export const isField = (target: any): target is IField => + target && + target.displayName === 'FieldState' && + isFn(target.getState) && + isFn(target.setState) + +export const isFieldState = (target: any): target is IFieldState => + target && target.displayName === 'FieldState' && target.name && target.path + +export const isFormState = (target: any): target is IFormState => + target && target.displayName === 'FormState' + +export const isVirtualField = (target: any): target is IVirtualField => + target && + target.displayName === 'VirtualFieldState' && + isFn(target.getState) && + isFn(target.setState) + +export const isVirtualFieldState = ( + target: any +): target is IVirtualFieldState => + target && target.displayName === 'VirtualFieldState' + +export const isStateModel = (target: any): target is IModel => + target && isFn(target.getState) + +export interface IFormState { + pristine: boolean + valid: boolean + invalid: boolean + loading: boolean + validating: boolean + submitting: boolean + initialized: boolean + editable: boolean | ((name: string) => boolean) + errors: string[] + warnings: string[] + values: {} + initialValues: {} + mounted: boolean + unmounted: boolean + props: FormProps + [key: string]: any +} + +export type FormStateDirtyMap = StateDirtyMap + +export interface IFormStateProps { + initialValues?: {} + values?: {} + lifecycles?: FormLifeCycle[] + useDirty?: boolean + editable?: boolean | ((name: string) => boolean) + validateFirst?: boolean +} + +export interface IFormCreatorOptions extends IFormStateProps { + onChange?: (values: IFormState['values']) => void + onSubmit?: (values: IFormState['values']) => any | Promise + onReset?: () => void + onValidateFailed?: (validated: IFormValidateResult) => void + validateConcurrentTimeMS?: number +} + +export interface IVirtualFieldState { + name: string + path: string + displayName?: string + initialized: boolean + visible: boolean + display: boolean + mounted: boolean + unmounted: boolean + props: FieldProps + [key: string]: any +} +export type VirtualFieldStateDirtyMap = StateDirtyMap + +export interface IVirtualFieldStateProps { + path?: FormPathPattern + dataPath?: FormPathPattern + nodePath?: FormPathPattern + display?: boolean + visible?: boolean + useDirty?: boolean + computeState?: ( + draft: IVirtualFieldState, + prevState: IVirtualFieldState + ) => void + name?: string + props?: FieldProps +} + +export type IFormValidateResult = ValidateNodeResult + +export interface IFormSubmitResult { + validated: IFormValidateResult + payload: any +} + +export interface IFormResetOptions { + forceClear?: boolean + validate?: boolean + selector?: FormPathPattern +} + +export interface IFormGraph { + [path: string]: IFormState | IFieldState | IVirtualFieldState +} + +export interface IMutators { + change(...values: any[]): any + focus(): void + blur(): void + push(value?: any): any[] + pop(): any[] + insert(index: number, value: any): any[] + remove(index: number | string): any + unshift(value: any): any[] + shift(): any[] + move($from: number, $to: number): any[] + moveDown(index: number): any[] + moveUp(index: number): any[] + validate(): Promise + exist(index?: number | string): boolean +} + +export interface IModel extends Subscribable { + state: S + props: P + displayName?: string + dirtyNum: number + dirtys: StateDirtyMap + prevState: S + batching: boolean + stackCount: number + controller: StateModel + batch: (callback?: () => void) => void + getState: (callback?: (state: S) => any) => any + setState: (callback?: (state: S | Draft) => void, silent?: boolean) => void + unsafe_getSourceState: (callback?: (state: S) => any) => any + unsafe_setSourceState: (callback?: (state: S) => void) => void + hasChanged: (path?: FormPathPattern) => boolean + isDirty: (key?: string) => boolean + getDirtyInfo: () => StateDirtyMap +} + +export type IField = IModel + +export type IVirtualField = IModel + +export type IFormInternal = IModel + +export interface IForm { + submit( + onSubmit?: (values: IFormState['values']) => any | Promise + ): Promise + clearErrors: (pattern?: FormPathPattern) => void + hasChanged(target: any, path: FormPathPattern): boolean + reset(options?: IFormResetOptions): Promise + validate( + path?: FormPathPattern, + options?: ValidateFieldOptions + ): Promise + setFormState(callback?: (state: IFormState) => any, silent?: boolean): void + getFormState(callback?: (state: IFormState) => any): any + setFieldState( + path: FormPathPattern, + callback?: (state: IFieldState) => void, + silent?: boolean + ): void + getFieldState( + path: FormPathPattern, + callback?: (state: IFieldState) => any + ): any + unsafe_do_not_use_transform_data_path(path: FormPathPattern): FormPathPattern //eslint-disable-line + registerField(props: IFieldStateProps): IField + registerVirtualField(props: IVirtualFieldStateProps): IVirtualField + createMutators(field: IField): IMutators + getFormGraph(): IFormGraph + setFormGraph(graph: IFormGraph): void + subscribe(callback?: FormHeartSubscriber): number + unsubscribe(id: number): void + notify: (type: string, payload?: T) => void + setFieldValue(path?: FormPathPattern, value?: any): void + getFieldValue(path?: FormPathPattern): any + setFieldInitialValue(path?: FormPathPattern, value?: any): void + getFieldInitialValue(path?: FormPathPattern): any +} diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts deleted file mode 100644 index 837927628d7..00000000000 --- a/packages/core/src/utils.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { Path, IFormPathMatcher } from '@uform/types' -import { - isArr, - isStr, - getPathSegments, - isEqual, - toArr, - clone, - isFn, - globalThisPolyfill -} from '@uform/utils' - -export * from '@uform/utils' - -const self = globalThisPolyfill - -const compactScheduler = ([raf, caf, priority], fresh: boolean) => { - return [fresh ? callback => raf(priority, callback) : raf, caf] -} - -const getScheduler = () => { - if (!self.requestAnimationFrame) { - return [self.setTimeout, self.clearTimeout] - } - try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const scheduler = require('scheduler') as any - return compactScheduler( - [ - scheduler.scheduleCallback || scheduler.unstable_scheduleCallback, - scheduler.cancelCallback || scheduler.unstable_cancelCallback, - scheduler.NormalPriority || scheduler.unstable_NormalPriority - ], - !!scheduler.unstable_requestPaint - ) - } catch (err) { - return [self.requestAnimationFrame, self.cancelAnimationFrame] - } -} - -export const [raf, caf] = getScheduler() - -export const resolveFieldPath = (path: Path | IFormPathMatcher): string[] => { - if (!isArr(path)) { - return isStr(path) ? resolveFieldPath(getPathSegments(path)) : undefined - } - return path.reduce((buf, key) => { - return buf.concat(getPathSegments(key)) - }, []) -} - -export const isChildField = (field, parent) => { - if (field && parent && field.path && parent.path) { - for (let i = 0; i < parent.path.length; i++) { - if (field.path[i] !== parent.path[i]) { - return false - } - } - return parent.path.length < field.path.length - } - return false -} - -export const hasRequired = rules => { - return toArr(rules).some(rule => { - return rule && rule.required - }) -} - -export const publishFormState = state => { - const { - values, - valid, - invalid, - initialValues, - errors, - pristine, - dirty - } = state - return { - values: clone(values), - valid, - invalid, - errors, - pristine, - dirty, - initialValues: clone(initialValues) - } -} - -export const publishFieldState = state => { - const { - value, - valid, - invalid, - errors, - visible, - display, - editable, - initialValue, - name, - path, - props, - effectErrors, - loading, - pristine, - required, - rules - } = state - return { - value: clone(value), - valid, - invalid, - editable, - visible, - display, - loading, - errors: errors.concat(effectErrors), - pristine, - initialValue: clone(initialValue), - name, - path, - props, - required, - rules - } -} - -export class BufferList { - public data = [] - public indexes = {} - public push(key: string, value: any, extra: any) { - if (!this.indexes[key]) { - const index = this.data.length - this.data.push({ ...extra, key, values: [value] }) - this.indexes[key] = index - } else { - const item = this.data[this.indexes[key]] - if (!item.values.some(callback => isEqual(callback, value))) { - item.values.push(value) - } - } - } - - public forEach(callback) { - for (let i = 0; i < this.data.length; i++) { - if (isFn(callback)) { - callback(this.data[i], this.data[i].key) - } - } - } - - public remove(key: string, value?: any) { - this.data = this.data.reduce((buf, item, index) => { - if (item.key === key) { - delete this.indexes[key] - return buf - } else { - this.indexes[key] = buf.length - return buf.concat(item) - } - }, []) - } -} diff --git a/packages/next/README.md b/packages/next/README.md index 3eb267bdc52..a55b5804149 100644 --- a/packages/next/README.md +++ b/packages/next/README.md @@ -1,2 +1,2970 @@ # @uform/next -> UForm Fusion Next组件插件包 \ No newline at end of file + +### Install + +```bash +npm install --save @uform/next +``` + +### Table Of Contents + + + +- [Quick-Start](#Quick-Start) +- [Components](#components) + - [``](#SchemaForm) + - [``](#SchemaMarkupField) + - [``](#Submit) + - [``](#Reset) + - [`(deprecated,please use )`](#) +- [Form List](#Array-Components) + - [`array`](#array) + - [`cards`](#cards) + - [`table`](#table) +- [Layout Components](#Layout-Components) + - [``](#FormCard) + - [``](#FormBlock) + - [``](#FormStep) + - [``](#FormLayout) + - [``](#FormItemGrid) + - [``](#FormTextBox) + - [``](#FormButtonGroup) + - [``](#TextButton) + - [``](#CircleButton) +- [Type of SchemaMarkupField](#Type-of-SchemaMarkupField) + - [`string`](#string) + - [`textarea`](#textarea) + - [`password`](#password) + - [`number`](#number) + - [`boolean`](#boolean) + - [`date`](#date) + - [`time`](#time) + - [`range`](#range) + - [`upload`](#upload) + - [`checkbox`](#checkbox) + - [`radio`](#radio) + - [`rating`](#rating) + - [`transfer`](#transfer) +- [API](#API) + - [`createFormActions`](#createFormActions) + - [`createAsyncFormActions`](#createAsyncFormActions) + - [`FormEffectHooks`](#FormEffectHooks) + - [`createEffectHook`](#createEffectHook) + - [`connect`](#connect) + - [`registerFormField`](#registerFormField) +- [Interfaces](#Interfaces) + - [`ButtonProps`](#ButtonProps) + - [`CardProps`](#CardProps) + - [`ICompatItemProps`](#ICompatItemProps) + - [`IFieldState`](#IFieldState) + - [`ISchemaFieldComponentProps`](#ISchemaFieldComponentProps) + - [`ISchemaVirtualFieldComponentProps`](#ISchemaVirtualFieldComponentProps) + - [`ISchemaFieldWrapper`](#ISchemaFieldWrapper) + - [`ISchemaFieldComponent`](#ISchemaFieldComponent) + - [`ISchemaVirtualFieldComponent`](#ISchemaVirtualFieldComponent) + - [`ISchemaFormRegistry`](#ISchemaFormRegistry) + - [`INextSchemaFieldProps`](#INextSchemaFieldProps) + - [`IPreviewTextProps`](#IPreviewTextProps) + - [`IMutators`](#IMutators) + - [`IFieldProps`](#IFieldProps) + - [`IConnectOptions`](#IConnectOptions) + + +### Quick-Start + +--- + +Example:develop with JSX + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import { Button } from '@alifd/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() + +const App = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +Example:develop with JSON Schema + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import { Button } from '@alifd/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() + +const App = () => { + const schema = { + type: 'object', + properties: { + radio: { + type: 'radio', + enum: ['1', '2', '3', '4'], + title: 'Radio' + }, + select: { + type: 'string', + enum: ['1', '2', '3', '4'], + title: 'Select', + required: true + }, + checkbox: { + type: 'checkbox', + enum: ['1', '2', '3', '4'], + title: 'Checkbox', + required: true + }, + textarea: { + type: 'string', + 'x-component': 'textarea', + title: 'TextArea' + }, + number: { + type: 'number', + title: 'number' + }, + boolean: { + type: 'boolean', + title: 'boolean' + }, + date: { + type: 'date', + title: 'date' + }, + daterange: { + type: 'daterange', + default: ['2018-12-19', '2018-12-19'], + title: 'daterange' + }, + year: { + type: 'year', + title: 'year' + }, + time: { + type: 'time', + title: 'time' + }, + upload: { + type: 'upload', + 'x-props': { + listType: 'card' + }, + title: 'upload(card)' + }, + upload2: { + type: 'upload', + 'x-props': { + listType: 'dragger' + }, + title: 'uplaod(dragger)' + }, + upload3: { + type: 'upload', + 'x-props': { + listType: 'text' + }, + title: 'upload(text)' + }, + range: { + type: 'range', + 'x-props': { + min: 0, + max: 1024, + marks: [0, 1024] + }, + title: 'range' + }, + transfer: { + type: 'transfer', + enum: [ + { + value: 1, + label: 'opt1' + }, + { + value: 2, + label: 'opt2' + } + ], + title: 'transfer' + }, + rating: { + type: 'rating', + title: 'rating' + }, + layout_btb_group: { + type: 'object', + 'x-component': 'button-group', + 'x-component-props': { + offset:7, + sticky: true, + }, + properties: { + submit_btn: { + type: 'object', + 'x-component': 'submit', + 'x-component-props': { + children: 'Submit', + }, + }, + reset_btn: { + type: 'object', + 'x-component': 'reset', + 'x-component-props': { + children: 'Reset', + }, + }, + } + }, + } + } + return +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### Components + +--- + +#### `` + +Base on `` of @uform/react-schema-renderer. Recommended for production environments. + +```typescript +interface INextSchemaFormProps { + // render by schema + schema?: ISchema; + fields?: ISchemaFormRegistry['fields']; + virtualFields?: ISchemaFormRegistry['virtualFields']; + // pre-registered Form Component + formComponent?: ISchemaFormRegistry['formComponent']; + // pre-registered FormItem Component + formItemComponent?: ISchemaFormRegistry['formItemComponent']; + // label column settiing + labelCol?: number | { span: number; offset?: number } + // FormItem column settiing + wrapperCol?: number | { span: number; offset?: number } + // custom placeholder when preivew + previewPlaceholder?: string | ((props: IPreviewTextProps) => string); + // prefix + prefix?: string; + // is it inline + inline?: boolean; + // The size of a single Item is customized, and takes precedence over the size of the Form, and when a component is used with an Item, the component itself does not set the size property. + size?: 'large' | 'medium' | 'small'; + // position of label + labelAlign?: 'top' | 'left' | 'inset'; + // aligment of label + labelTextAlign?: 'left' | 'right'; + // labelCol of FormItem + labelCol?: {}; + // wrapperCol of FormItem + wrapperCol?: {}; + children?: any; + className?: string; + style?: React.CSSProperties; + // type of component + component?: string | (() => void); + // form state value + value?: Value; + // form state defaultValue + defaultValue?: DefaultValue; + // form state initialValues + initialValues?: DefaultValue; + // FormActions instance + actions?: FormActions; + // IFormEffect instance + effects?: IFormEffect; + // form instance + form?: IForm; + // Form change event callback + onChange?: (values: Value) => void; + // triggered by `htmlType="submit"` or actions.submit时 + onSubmit?: (values: Value) => void | Promise; + // triggered by or actions.reset + onReset?: () => void; + // Form verification failure event callback + onValidateFailed?: (valideted: IFormValidateResult) => void; + children?: React.ReactElement | ((form: IForm) => React.ReactElement); + // Whether to use the dirty check, the default will go immer accurate update + useDirty?: boolean; + // Is it editable, overall control in the Form dimension + editable?: boolean | ((name: string) => boolean); + // Whether to go pessimistic check, stop the subsequent check when the first check fails + validateFirst?: boolean; +} +``` + +**Usage** + +Example1: Sync value of a and a-mirror + +```jsx +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + registerFormField, + Field, + connect, + createFormActions +} from '@uform/next' + +const actions = createFormActions() + +registerFormField( + 'string', + connect()(props => ) +) + +ReactDOM.render( + { + $('onFieldChange','a').subscribe((fieldState)=>{ + actions.setFieldState('a-mirror',state=>{ + state.value = fieldState.value + }) + }) + }}> + + + , + document.getElementById('root') +) +``` + + +Example:Layout + +```jsx +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + createFormActions, + FormLayout, + FormButtonGroup, + Submit, + Reset, +} from '@uform/next' + +const actions = createFormActions() + +ReactDOM.render( +

+
Basic Layout
+ + + + + + + + SubmitReset​ + + +
Inline Layout
+ + + + ​ + + SubmitReset​ + + +
editable = false
+ + + + ​ + + SubmitReset​ + + +
, + document.getElementById('root') +) +``` + +#### `` + +> Core components of @uform/next, used to describe form fields + +```typescript +interface IMarkupSchemaFieldProps { + name?: string + /** base json schema spec**/ + title?: SchemaMessage + description?: SchemaMessage + default?: any + readOnly?: boolean + writeOnly?: boolean + type?: 'string' | 'object' | 'array' | 'number' | string + enum?: Array + const?: any + multipleOf?: number + maximum?: number + exclusiveMaximum?: number + minimum?: number + exclusiveMinimum?: number + maxLength?: number + minLength?: number + pattern?: string | RegExp + maxItems?: number + minItems?: number + uniqueItems?: boolean + maxProperties?: number + minProperties?: number + required?: string[] | boolean + format?: string + /** nested json schema spec **/ + properties?: { + [key: string]: ISchema + } + items?: ISchema | ISchema[] + additionalItems?: ISchema + patternProperties?: { + [key: string]: ISchema + } + additionalProperties?: ISchema + /** extend json schema specs */ + editable?: boolean + visible?: boolean + display?: boolean + ['x-props']?: { [name: string]: any } + ['x-index']?: number + ['x-rules']?: ValidatePatternRules + ['x-component']?: string + ['x-component-props']?: { [name: string]: any } + ['x-render']?: ( + props: T & { + renderComponent: () => React.ReactElement + } + ) => React.ReactElement + ['x-effect']?: ( + dispatch: (type: string, payload: any) => void, + option?: object + ) => { [key: string]: any } +} +``` + +##### Usage + + +```jsx +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + FormSlot, + Field, + createFormActions, + FormLayout, + FormButtonGroup, + Submit, + Reset, +} from '@uform/next' + +const actions = createFormActions() + +ReactDOM.render( + +
required
+ + +
description
+ + +
default value
+ + +
readOnly
+ + +
visible = false
+ + +
display = false
+ + +
editable = false
+ +
, + document.getElementById('root') +) +``` + + +#### `` + +> Props of `` + +```typescript +interface ISubmitProps { + /** reset pops **/ + onSubmit?: ISchemaFormProps['onSubmit'] + showLoading?: boolean + /** nextBtnProps **/ + // type of btn + type?: 'primary' | 'secondary' | 'normal' + // size of btn + size?: 'small' | 'medium' | 'large' + // size of Icon + iconSize?: 'xxs' | 'xs' | 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' + // type of button when component = 'button' + htmlType?: 'submit' | 'reset' | 'button' + // typeof btn + component?: 'button' | 'a' + // Set the loading state of the button + loading?: boolean + // Whether it is a ghost button + ghost?: true | false | 'light' | 'dark' + // Whether it is a text button + text?: boolean + // Whether it is a warning button + warning?: boolean + // Whether it is disabled + disabled?: boolean + // Callback for button click + onClick?: (e: {}) => void + // Valid when Button component is set to 'a', which represents the URL of the linked page + href?: string + // Valid when Button component is set to 'a', which represents the way of open the linked document + target?: string +} +``` + +#### `` + +> Props of `` + +```typescript +interface IResetProps { + /** reset pops **/ + forceClear?: boolean + validate?: boolean + /** nextBtnProps **/ + // type of btn + type?: 'primary' | 'secondary' | 'normal' + // size of btn + size?: 'small' | 'medium' | 'large' + // size of Icon + iconSize?: 'xxs' | 'xs' | 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' + // type of button when component = 'button' + htmlType?: 'submit' | 'reset' | 'button' + // typeof btn + component?: 'button' | 'a' + // Set the loading state of the button + loading?: boolean + // Whether it is a ghost button + ghost?: true | false | 'light' | 'dark' + // Whether it is a text button + text?: boolean + // Whether it is a warning button + warning?: boolean + // Whether it is disabled + disabled?: boolean + // Callback for button click + onClick?: (e: {}) => void + // Valid when Button component is set to 'a', which represents the URL of the linked page + href?: string + // Valid when Button component is set to 'a', which represents the way of open the linked document + target?: string +} +``` + +#### `` + +> deprecated,please use [SchemaMarkupField](#SchemaMarkupField) + +### Array Components + +#### array + +```jsx +import React, { useState, useEffect } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + FormItemGrid, + FormButtonGroup, + Submit, + Reset, + FormBlock, + FormLayout +} from '@uform/next' +import '@alifd/next/dist/next.css' +import Printer from '@uform/printer' + +const App = () => { + const [value, setValues] = useState({}) + useEffect(() => { + setTimeout(() => { + setValues({ + array: [{ array2: [{ aa: '123', bb: '321' }] }] + }) + }, 1000) + }, []) + return ( + + console.log(v)}> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Submit + Reset + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### cards + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + FormItemGrid, + FormButtonGroup, + Submit, + Reset, + FormBlock, + FormLayout +} from '@uform/next' +import '@alifd/next/dist/next.css' +import Printer from '@uform/printer' + +const App = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### table + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + FormItemGrid, + FormButtonGroup, + Submit, + Reset, + FormBlock, + FormLayout +} from '@uform/next' +import '@alifd/next/dist/next.css' +import Printer from '@uform/printer' + +const App = () => ( + + + + Hello worldasdasdasdasd
+ }, + operationsWidth: 300 + }} + > + + + + + + + + + + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +### Layout Components + + +#### `` + +> Props of ``, fully inherited from [CardProps](#CardProps)。 +> The only difference between FormCard [FormBlock](#FormBlock) is a border on the style + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { FormCard, SchemaMarkupField as Field } from '@uform/next' +import '@alifd/next/dist/next.css' + +const App = () => ( + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of `` , fully inherited from [CardProps](#CardProps) + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { FormBlock, SchemaMarkupField as Field } from '@uform/next' +import '@alifd/next/dist/next.css' + +const App = () => ( + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of `` + +```typescript +interface IFormStep { + dataSource: StepItemProps[] + /** next step props**/ + // current + current?: number + // direction of step + direction?: 'hoz' | 'ver' + // Content arrangement in horizontal layout + labelPlacement?: 'hoz' | 'ver' + // shape of step + shape?: 'circle' | 'arrow' | 'dot' + readOnly?: boolean + // Whether to activate animation + animation?: boolean + className?: string + // Custom StepItem render + itemRender?: (index: number, status: string) => React.ReactNode +} +``` + +**Usage** + +```jsx +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + FormEffectHooks, + createFormActions, + FormGridRow, + FormItemGrid, + FormGridCol, + FormPath, + FormLayout, + FormBlock, + FormCard, + FormTextBox, + FormStep +} from '@uform/next' +import { Button } from '@alifd/next' +import '@alifd/next/dist/next.css' + +const { onFormInit$ } = FormEffectHooks + +const actions = createFormActions() + +let cache = {} + +export default () => ( + { + console.log('submit') + console.log(values) + }} + actions={actions} + labelCol={{ span: 8 }} + wrapperCol={{ span: 6 }} + validateFirst + effects={({ setFieldState, getFormGraph }) => { + onFormInit$().subscribe(() => { + setFieldState('col1', state => { + state.visible = false + }) + }) + }} + > + + + + + + + + + + + + Submit + + + + + + +) +``` + +#### `` + +> Props of `` + +```typescript +interface IFormItemTopProps { + inline?: boolean + className?: string + style?: React.CSSProperties + labelCol?: number | { span: number; offset?: number } + wrapperCol?: number | { span: number; offset?: number } +} +``` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormBlock, + FormLayout +} from '@uform/next' +import { Button } from '@alifd/next' +import Printer from '@uform/printer' +import '@alifd/next/dist/next.css' +const App = () => ( + + + + + + + + + SubmitReset​ + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of `` + +```typescript +interface IFormItemGridProps { + cols?: Array + gutter?: number + /** next Form.Item props**/ + // prefix od FormItem + prefix?: string + + // label od FormItem + label?: React.ReactNode + + // label layout setting, eg, {span: 8, offset: 16} + labelCol?: {} + wrapperCol?: {} + + // Custom prompt information, if not set, it will be automatically generated according to the verification rules. + help?: React.ReactNode + + // Additional prompt information, similar to help, can be used when error messages and prompt copy are required at the same time. Behind the error message. + extra?: React.ReactNode + + // Check status, if not set, it will be generated automatically according to check rules + validateState?: 'error' | 'success' | 'loading' + + // Used in conjunction with the validateState property, whether to display the success / loading validation status icon. Currently only Input supports + hasFeedback?: boolean + + // Custom inline style + + style?: React.CSSProperties + + // node or function(values) + children?: React.ReactNode | (() => void) + + // The size of a single Item is customized, and takes precedence over the size of the Form, and when a component is used with an Item, the component itself does not set the size property. + size?: 'large' | 'small' | 'medium' + + // Position of the label + labelAlign?: 'top' | 'left' | 'inset' + + // alignment of labels + labelTextAlign?: 'left' | 'right' + + className?: string + + // [validation] required + required?: boolean + + // whether required asterisks are displayed + asterisk?: boolean + + // required custom error message + requiredMessage?: string + + // required Custom trigger method + requiredTrigger?: string | Array + + // [validation] min + min?: number + + // [validation] max + max?: number + + // min/max error message + minmaxMessage?: string + + // min/max custom trigger method + minmaxTrigger?: string | Array + + // [validation] min length of string / min length of array + minLength?: number + + // [validation] max length of string / max length of array + maxLength?: number + + // minLength/maxLength custom error message + minmaxLengthMessage?: string + + // minLength/maxLength custom trigger method + minmaxLengthTrigger?: string | Array + + // [validation] length of string / length of array + length?: number + + // length custom error message + lengthMessage?: string + + // length custom trigger method + lengthTrigger?: string | Array + + // Regular pattern + pattern?: any + + // pattern custom error message + patternMessage?: string + + // pattern custom trigger method + patternTrigger?: string | Array + + // [validation] regular pattern + format?: 'number' | 'email' | 'url' | 'tel' + + // format custom error message + formatMessage?: string + + // format custom trigger method + formatTrigger?: string | Array + + // [validation] custom validator + validator?: () => void + + // validator custom trigger method + validatorTrigger?: string | Array + + // Whether to automatically trigger validate when data is modified + autoValidate?: boolean +} +``` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormBlock, + FormLayout +} from '@uform/next' +import { Button } from '@alifd/next' +import Printer from '@uform/printer' +import '@alifd/next/dist/next.css' + +const App = () => ( + + console.log(v)}> + + + + + + + + + + + + ​SubmitReset​ + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of `` + +```typescript +interface IFormTextBox { + text?: string + gutter?: number + title?: React.ReactText + description?: React.ReactText +} +``` + +**Usage** + +```jsx +import React, { useState } from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormTextBox, + FormCard, + FormLayout +} from '@uform/next' +import { Button } from '@alifd/next' +import Printer from '@uform/printer' +import '@alifd/next/dist/next.css' + +const App = () => { + return ( + + console.log(v)}> + + + + + + + + + + + + ) +} +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of `` + +```typescript +interface IFormButtonGroupProps { + sticky?: boolean + style?: React.CSSProperties + itemStyle?: React.CSSProperties + className?: string + align?: 'left' | 'right' | 'start' | 'end' | 'top' | 'bottom' | 'center' + triggerDistance?: number + zIndex?: number + span?: ColSpanType + offset?: ColSpanType +} +``` + +**Usage** + +```jsx +import React, { useState } from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormBlock, + FormLayout +} from '@uform/next' +import { Button } from '@alifd/next' +import Printer from '@uform/printer' +import '@alifd/next/dist/next.css' + +const App = () => { + const [state, setState] = useState({ editable: true }) + return ( + + console.log(v)}> +
normal
+ + ​SubmitReset​ + +
sticky
+ + ​Submit​ + + Reset​ + +
+
+ ) +} +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of ``, fully inherited from [ButtonProps](#ButtonProps) + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { TextButton } from '@uform/next' +import '@alifd/next/dist/next.css' + +const App = () => ( + + content + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> Props of ``, fully inherited from [ButtonProps](#ButtonProps) + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { CircleButton } from '@uform/next' +import '@alifd/next/dist/next.css' + +const App = () => ( + + ok + +) +ReactDOM.render(, document.getElementById('root')) +``` + + +### Type of SchemaMarkupField + +#### string + +* Schema Type : `string` +* Schema UI Component: Fusion-Next ``, ``, `` + +```typescript +interface IPasswordProps { + checkStrength: boolean + /** next input props **/ + // value + value?: string | number + + // default value + defaultValue?: string | number + + // callback triggered when value change + onChange?: (value: string, e: React.ChangeEvent) => void + + // callback triggered when keyboard is press + onKeyDown?: (e: React.KeyboardEvent, opts: {}) => void + + // Disabled + disabled?: boolean + + // Max length + maxLength?: number + + // Whether to show the maximum length style + hasLimitHint?: boolean + + // When maxLength is set, whether to truncate beyond the string + cutString?: boolean + + // readOnly + readOnly?: boolean + + // Automatically remove the leading and trailing blank characters when trigger onChange + trim?: boolean + + // placeholder + placeholder?: string + + // callback triggered when focus + onFocus?: () => void + + // callback triggered when blur + onBlur?: () => void + + // Custom string length calculation + getValueLength?: (value: string) => number + + className?: string + style?: React.CSSProperties + htmlType?: string + + // name of field + name?: string + + // state of field + state?: 'error' | 'loading' | 'success' + + // label + label?: React.ReactNode + + // whether to show clear + hasClear?: boolean + + // whether to show border + hasBorder?: boolean + + // size of field + size?: 'small' | 'medium' | 'large' + + // callback triggered when enter is press + onPressEnter?: () => void + + // Watermark (Icon type, shared with hasClear) + hint?: string + + // Append content before text + innerBefore?: React.ReactNode + + // Append content after text + innerAfter?: React.ReactNode + + // Append content before input + addonBefore?: React.ReactNode + + // Append content after input + addonAfter?: React.ReactNode + + // Append text before input + addonTextBefore?: React.ReactNode + + // Append text after input + addonTextAfter?: React.ReactNode + + // (Native supported by input) + autoComplete?: string + + // (Native supported by input) + autoFocus?: boolean +} +``` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### number + +* Schema Type : `number` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### boolean + +* Schema Type : `boolean` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### date + +* Schema Type : `date` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### time + +* Schema Type : `time` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### range + +* Schema Type : `range` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### upload + +* Schema Type : `upload` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### checkbox + +* Schema Type : `checkbox` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### radio + +* Schema Type : `radio` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### rating + +* Schema Type : `rating` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### transfer + +* Schema Type : `transfer` +* Schema UI Component: Fusion-Next `` + +**Usage** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### API + +> Fully inherited from @uform/react, The specific API of @uform/next is listed below. + +--- + +#### `createFormActions` + +> Return [IFormActions](#IFormActions) + +**Signature** + +```typescript +createFormActions(): IFormActions +``` + +**Usage** + +```typescript +import { createFormActions } from '@uform/next' + +const actions = createFormActions() +console.log(actions.getFieldValue('username')) +``` + +#### `createAsyncFormActions` + +> Return [IFormAsyncActions](#IFormAsyncActions) + +**Signature** + +```typescript +createAsyncFormActions(): IFormAsyncActions +``` + +**Usage** + +```typescript +import { createAsyncFormActions } from '@uform/next' + +const actions = createAsyncFormActions() +actions.getFieldValue('username').then(val => console.log(val)) +``` + +#### `FormEffectHooks` + +> Return all @uform/core lifeCycles hook which can be subscribe + +**Usage** + +```tsx +import { FormEffectHooks, Form } from '@uform/react' +const { + /** + * Form LifeCycle + **/ + // Form pre-initialization trigger + onFormWillInit$, + // Form initialization trigger + onFormInit$, + // Triggered when the form changes + onFormChange$, + // Triggered when the form event is triggered, used to monitor only manual operations + onFormInputChange$, + // Trigger when the form initial value changes + onFormInitialValueChange$, + // Triggered when the form is reset + onFormReset$, + // Triggered when the form is submitted + onFormSubmit$, + // Triggered when the form submission starts + onFormSubmitStart$, + // Triggered when the form submission ends + onFormSubmitEnd$, + // Triggered when the form is mounted + onFormMount$, + // Triggered when the form is unloaded + onFormUnmount$, + // Triggered when form validation begins + onFormValidateStart$, + // Triggered when the form validation ends + onFormValidateEnd$, + // Trigger when the form initial value changes + onFormValuesChange$, + /** + * FormGraph LifeCycle + **/ + // Triggered when the form observer tree changes + onFormGraphChange$, + /** + * Field LifeCycle + **/ + // Triggered when pre-initialized + onFieldWillInit$, + // Triggered when the field is initialized + onFieldInit$, + // Triggered when the field changes + onFieldChange$, + // Triggered when the field is mounted + onFieldMount$, + // Trigger when the field is unloaded + onFieldUnmount$, + // Triggered when the field event is triggered, used to monitor only manual operations + onFieldInputChange$, + // Triggered when the field value changes + onFieldValueChange$, + // Trigger when the initial value of the field changes + onFieldInitialValueChange$ +} = FormEffectHooks + +const App = () => { + return ( + { + onFormInit$().subscribe(() => { + console.log('initialized') + }) + }} + > + ... + + ) +} +``` + +#### createEffectHook + +> Custom your own hook by this api + +**Usage** + +```jsx +import SchemaForm, { createEffectHook, createFormActions } from '@uform/next' + +const actions = createFormActions() +const diyHook1$ = createEffectHook('diy1') +const diyHook2$ = createEffectHook('diy2') + +const App = () => { + return ( + { + diyHook1$().subscribe(payload => { + console.log('diy1 hook triggered', payload) + }) + + diyHook2$().subscribe(payload => { + console.log('diy2 hook triggered', payload) + }) + }} + > + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### connect + +> Wrap field components with value / defaultValue / onChange of field, which make it esaily make custom field + +```typescript +type Connect = >(options?: IConnectOptions) => +(Target: T) => React.PureComponent +``` +**Usage** + +```typescript +import {registerFormField,connect} from '@uform/next' + +registerFormField( + 'string', + connect()(props => ) +) +``` + +#### registerFormField + +```typescript +type registerFormField( + name : string, // name of field + component : React.ComponentType, // component of field + noMiddleware: boolean // whether use middleware +) +``` + +**Usage** + +```jsx + +import SchemaForm, { SchemaMarkupField as Field, registerFormField, connect, createFormActions } from '@uform/next' + +registerFormField( + 'custom-string', + connect()(props => ) +) +const actions = createFormActions() + +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### Interfaces + +> The Interfaces is fully inherited from @uform/react. The specific Interfaces of @uform/next is listed below. +--- + +#### IForm + +> Form instance object API created by using createForm + + + +```typescript +interface IForm { +  /* +   * Form submission, if the callback parameter returns Promise, +   * Then the entire submission process will hold and load is true. +   * Wait for Promise resolve to trigger the form onFormSubmitEnd event while loading is false +   */ +   submit( +      onSubmit?: (values: IFormState['values']) => any | Promise +    ): Promise<{ +       Validated: IFormValidateResult +       Payload: any //onSubmit callback function return value +   }> +    +   /* +    * Clear the error message, you can pass the FormPathPattern to batch or precise control of the field to be cleared. +    * For example, clearErrors("*(aa,bb,cc)") +    */ +   clearErrors: (pattern?: FormPathPattern) => void +    +   /* +    * Get status changes, mainly used to determine which states in the current life cycle have changed in the form lifecycle hook. +    * For example, hasChanged(state,'value.aa') +    */ +   hasChanged(target: IFormState | IFieldState | IVirtualFieldState, path: FormPathPattern): boolean +    +   /* +    * Reset form +    */ +   reset(options?: { +     // Forced to empty +     forceClear?: boolean +     // Forced check +     validate?: boolean +     // Reset range for batch or precise control of the field to be reset +     selector?: FormPathPattern +   }): Promise +    +   /* +    * Validation form +    */ +   validate(path?: FormPathPattern, options?: { +     // Is it pessimistic check, if the current field encounters the first verification error, stop the subsequent verification process +     first?:boolean +   }): Promise +    +   /* +    * Set the form status +    */ +   setFormState( +     // Operation callback +     callback?: (state: IFormState) => any, +     // No trigger the event +     silent?: boolean +   ): void +    +   /* +    * Get form status +    */ +   getFormState( +     //transformer +     callback?: (state: IFormState) => any +   ): any +    +   /* +    * Set the field status +    */ +   setFieldState( +     // Field path +     path: FormPathPattern, +     // Operation callback +     callback?: (state: IFieldState) => void, +     // No trigger the event +     silent?: boolean +   ): void +    +   /* +    * Get the field status +    */ +   getFieldState( +     // Field path +     path: FormPathPattern, +     // Transformer +     callback?: (state: IFieldState) => any +   ): any +    +   /* +    * Registration field +    */ +   registerField(props: { +    // Node path +    path?: FormPathPattern +    // Data path +    name?: string +    // Field value +    value?: any +    // Field multi-value +    values?: any[] +    // Field initial value +    initialValue?: any +    // Field extension properties +    props?: any +    // Field check rule +    rules?: ValidatePatternRules[] +    // Field is required +    required?: boolean +    // Is the field editable? +    editable?: boolean +    // Whether the field is dirty check +    useDirty?: boolean +    // Field state calculation container, mainly used to extend the core linkage rules +    computeState?: (draft: IFieldState, prevState: IFieldState) => void +  }): IField +   +  /* +   * Register virtual fields +   */ +  registerVirtualField(props: { +    // Node path +    path?: FormPathPattern +    // Data path +    name?: string +    // Field extension properties +    props?: any +    // Whether the field is dirty check +    useDirty?: boolean +    // Field state calculation container, mainly used to extend the core linkage rules +    computeState?: (draft: IFieldState, prevState: IFieldState) => void +  }): IVirtualField +   +  /* +   * Create a field data operator, which will explain the returned API in detail later. +   */ +  createMutators(field: IField): IMutators +   +  /* +   * Get the form observer tree +   */ +  getFormGraph(): IFormGraph +   +  /* +   * Set the form observer tree +   */ +  setFormGraph(graph: IFormGraph): void +   +  /* +   * Listen to the form life cycle +   */ +  subscribe(callback?: ({ +    type, +    payload +  }: { +    type: string +    payload: any +  }) => void): number +   +  /* +   * Cancel the listening form life cycle +   */ +  unsubscribe(id: number): void +   +  /* +   * Trigger form custom life cycle +   */ +  notify: (type: string, payload?: T) => void +   +  /* +   * Set the field value +   */ +  setFieldValue(path?: FormPathPattern, value?: any): void +   +  /* +   * Get the field value +   */ +  getFieldValue(path?: FormPathPattern): any +   +  /* +   * Set the initial value of the field +   */ +  setFieldInitialValue(path?: FormPathPattern, value?: any): void +   +  /* +   * Get the initial value of the field +   */ +  getFieldInitialValue(path?: FormPathPattern): any +} +``` + +#### ButtonProps + +```typescript +interface ButtonProps { + /** reset pops **/ + onSubmit?: ISchemaFormProps['onSubmit'] + showLoading?: boolean + /** nextBtnProps **/ + // type of btn + type?: 'primary' | 'secondary' | 'normal' + // size of btn + size?: 'small' | 'medium' | 'large' + // size of Icon + iconSize?: 'xxs' | 'xs' | 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' + // type of button when component = 'button' + htmlType?: 'submit' | 'reset' | 'button' + // typeof btn + component?: 'button' | 'a' + // Set the loading state of the button + loading?: boolean + // Whether it is a ghost button + ghost?: true | false | 'light' | 'dark' + // Whether it is a text button + text?: boolean + // Whether it is a warning button + warning?: boolean + // Whether it is disabled + disabled?: boolean + // Callback for button click + onClick?: (e: {}) => void + // Valid when Button component is set to 'a', which represents the URL of the linked page + href?: string + // Valid when Button component is set to 'a', which represents the way of open the linked document + target?: string +} +``` + +#### CardProps + +```typescript +interface CardProps extends HTMLAttributesWeak, CommonProps { + // media in card + media?: React.ReactNode + + // title of the card + title?: React.ReactNode + + // subTitle of the card + subTitle?: React.ReactNode + + // action button of the card + actions?: React.ReactNode + + // whether to show bullet of title + showTitleBullet?: boolean + + // whether to show divider of head + showHeadDivider?: boolean + contentHeight?: string | number + + // extra content of card + extra?: React.ReactNode + + // whether to set free mode, title, subtitle will be invalid when this options turns on + free?: boolean +} +``` + +#### ICompatItemProps + +```typescript +interface ICompatItemProps + extends Exclude, + Partial { + labelCol?: number | { span: number; offset?: number } + wrapperCol?: number | { span: number; offset?: number } +} +``` + + +#### IFieldState + +```typescript +interface IFieldState { + /**Read-only attribute**/ + // State name, FieldState + displayName?: string // Data path + name: string // Node path + path: string // Has been initialized + initialized: boolean // Is it in the original state, the state is true only when value===intialValues + pristine: boolean // Is it in a legal state, as long as the error length is greater than 0, the valid is false + valid: boolean // Is it illegal, as long as the error length is greater than 0, the valid is true + invalid: boolean // Is it in check state? + validating: boolean // Is it modified, if the value changes, the property is true, and will be true throughout the life of the field + modified: boolean // Is it touched? + touched: boolean // Is it activated, when the field triggers the onFocus event, it will be triggered to true, when onBlur is triggered, it is false + active: boolean // Have you ever visited, when the field triggers the onBlur event, it will be triggered to true + visited: boolean /** writable property**/ // Is it visible, note: if the state is false, then the value of the field will not be submitted, and the UI will not display + visible: boolean // Whether to show, note: if the state is false, then the value of the field will be submitted, the UI will not display, similar to the form hidden field + display: boolean // Is it editable? + editable: boolean // Is it in the loading state, note: if the field is in asynchronous verification, loading is true + loading: boolean // Field multi-parameter value, such as when the field onChange trigger, the event callback passed multi-parameter data, then the value of all parameters will be stored here + values: any[] // Field error message + errors: string[] // Field alert message + warnings: string[] // Field value, is equal to values[0] + value: any // Initial value + initialValue: any // Check the rules, the specific type description refers to the following documents + rules: ValidatePatternRules[] // Is it required? + required: boolean // Whether to mount + mounted: boolean // Whether to uninstall + unmounted: boolean // field extension properties + props: FieldProps +} +``` + + +#### ISchemaFieldComponentProps + +```typescript +interface ISchemaFieldComponentProps extends IFieldState { + schema: Schema + mutators: IMutators + form: IForm + renderField: ( + addtionKey: string | number, + reactKey?: string | number + ) => React.ReactElement +} +``` + +#### ISchemaVirtualFieldComponentProps + +```typescript +interface ISchemaVirtualFieldComponentProps extends IVirtualFieldState { + schema: Schema + form: IForm + children: React.ReactElement[] + renderField: ( + addtionKey: string | number, + reactKey?: string | number + ) => React.ReactElement +} +``` + +#### ISchemaFieldWrapper + +```typescript +interface ISchemaFieldWrapper { + (Traget: ISchemaFieldComponent): + | React.FC + | React.ClassicComponent +} +``` + +#### ISchemaFieldComponent + +```typescript +declare type ISchemaFieldComponent = ComponentWithStyleComponent< + ISchemaFieldComponentProps +> & { + __WRAPPERS__?: ISchemaFieldWrapper[] +} +``` + +#### ISchemaVirtualFieldComponent + +```typescript +declare type ISchemaVirtualFieldComponent = ComponentWithStyleComponent< + ISchemaVirtualFieldComponentProps +> & { + __WRAPPERS__?: ISchemaFieldWrapper[] +} +``` + +#### ISchemaFormRegistry + +```typescript +interface ISchemaFormRegistry { + fields: { + [key: string]: ISchemaFieldComponent + } + virtualFields: { + [key: string]: ISchemaVirtualFieldComponent + } + wrappers?: ISchemaFieldWrapper[] + formItemComponent: React.JSXElementConstructor + formComponent: string | React.JSXElementConstructor +} +``` + +#### INextSchemaFieldProps + +```typescript +interface INextSchemaFieldProps { + name?: string; + /** ISchema **/ + title?: SchemaMessage; + description?: SchemaMessage; + default?: any; + readOnly?: boolean; + writeOnly?: boolean; + type?: 'string' | 'object' | 'array' | 'number' | string; + enum?: Array; + const?: any; + multipleOf?: number; + maximum?: number; + exclusiveMaximum?: number; + minimum?: number; + exclusiveMinimum?: number; + maxLength?: number; + minLength?: number; + pattern?: string | RegExp; + maxItems?: number; + minItems?: number; + uniqueItems?: boolean; + maxProperties?: number; + minProperties?: number; + required?: string[] | boolean; + format?: string; + properties?: { + [key: string]: ISchema; + }; + items?: ISchema | ISchema[]; + additionalItems?: ISchema; + patternProperties?: { + [key: string]: ISchema; + }; + additionalProperties?: ISchema; + editable?: boolean; + visible?: boolean; + display?: boolean; + ['x-props']?: { + [name: string]: any; + }; + ['x-index']?: number; + ['x-rules']?: ValidatePatternRules; + ['x-component']?: string; + ['x-component-props']?: { + [name: string]: any; + }; + ['x-render']?: (props: T & { + renderComponent: () => React.ReactElement; + }) => React.ReactElement; + ['x-effect']?: (dispatch: (type: string, payload: any) => void, option?: object) => { + [key: string]: any; + }; + +``` + +#### IPreviewTextProps + +```typescript +interface IPreviewTextProps { + className?: React.ReactText + dataSource?: any[] + value?: any + addonBefore?: React.ReactNode + innerBefore?: React.ReactNode + addonTextBefore?: React.ReactNode + addonTextAfter?: React.ReactNode + innerAfter?: React.ReactNode + addonAfter?: React.ReactNode +} + +``` + + +#### IMutators + +> The instance API created by crewikiutators is mainly used to operate field data. + +```typescript +interface IMutators { + // Changing the field value and multi parameter condition will store all parameters in values + change(...values: any[]): any + // Get focus, trigger active state change + focus(): void + // Lose focus, trigger active / visited status change + blur(): void + // Trigger current field verifier + validate(): Promise + // Whether the value of the current field exists in the values property of form + exist(index?: number | string): Boolean + + /**Array operation method**/ + + // Append data + push(value?: any): any[] + // Pop up tail data + pop(): any[] + // Insert data + insert(index: number, value: any): any[] + // Delete data + remove(index: number | string): any + // Head insertion + unshift(value: any): any[] + // Head ejection + shift(): any[] + // Move element + move($from: number, $to: number): any[] + // Move down + moveDown(index: number): any[] + // Move up + moveUp(index: number): any[] +} +``` + +#### IFieldProps + +```typescript +interface IFieldProps { + name : string // Node path + path : Array // Data path + value : V // value + errors : Array // Field error message + editable : boolean | ((name:string) => boolean) // Is it editable? + locale : Locale // i18n locale + loading : boolean // Is it in the loading state, note: if the field is in asynchronous verification, loading is true + schemaPath : Array // schema path + getSchema : (path: string) => ISchema // get schema by path + renderField : (childKey: string, reactKey: string | number) => JSX.Element | string | null + renderComponent : React.FunctionComponent | undefined>, + getOrderProperties : () => Array<{schema: ISchema, key: number, path: string, name: string }>, + mutators : IMutators, + schema : ISchema +} + +``` + +#### IConnectOptions + +```typescript + +interface IConnectOptions { + // name of value property + valueName?: string + // name of event property + eventName?: string + // default props + defaultProps?: Partial + // In some case, the value of our event function is not the first parameter of the event callback, and further customization is required. + getValueFromEvent?: (props: IFieldProps['x-props'], event: Event, ...args: any[]) => any + // props transformer + getProps?: (connectProps: IConnectProps, fieldProps: IFieldProps) => IConnectProps + // component transformer + getComponent?: ( + target: T, + connectProps: IConnectProps, + fieldProps: IFieldProps + ) => T +} + +``` \ No newline at end of file diff --git a/packages/next/README.zh-cn.md b/packages/next/README.zh-cn.md new file mode 100644 index 00000000000..62a45464f58 --- /dev/null +++ b/packages/next/README.zh-cn.md @@ -0,0 +1,2966 @@ +# @uform/next + +### 安装 + +```bash +npm install --save @uform/next +``` + +### 目录 + + + +- [使用方式](#使用方式) + - [`快速开始`](#快速开始) +- [Components](#components) + - [``](#SchemaForm) + - [``](#SchemaMarkupField) + - [``](#Submit) + - [``](#Reset) + - [`(即将废弃,请使用)`](<#Field(即将废弃,请使用SchemaMarkupField)>) +- [表单List](#Array-Components) + - [`array`](#array) + - [`cards`](#cards) + - [`table`](#table) +- [布局组件](#Layout-Components) + - [``](#FormCard) + - [``](#FormBlock) + - [``](#FormStep) + - [``](#FormLayout) + - [``](#FormItemGrid) + - [``](#FormTextBox) + - [``](#FormButtonGroup) + - [``](#TextButton) + - [``](#CircleButton) +- [字段类型](#Type-of-SchemaMarkupField) + - [`string`](#string) + - [`textarea`](#textarea) + - [`password`](#password) + - [`number`](#number) + - [`boolean`](#boolean) + - [`date`](#date) + - [`time`](#time) + - [`range`](#range) + - [`upload`](#upload) + - [`checkbox`](#checkbox) + - [`radio`](#radio) + - [`rating`](#rating) + - [`transfer`](#transfer) +- [API](#API) + - [`createFormActions`](#createFormActions) + - [`createAsyncFormActions`](#createAsyncFormActions) + - [`FormEffectHooks`](#FormEffectHooks) + - [`createEffectHook`](#createEffectHook) + - [`connect`](#connect) + - [`registerFormField`](#registerFormField) +- [Interfaces](#Interfaces) + - [`ButtonProps`](#ButtonProps) + - [`CardProps`](#CardProps) + - [`ICompatItemProps`](#ICompatItemProps) + - [`IFieldState`](#IFieldState) + - [`ISchemaFieldComponentProps`](#ISchemaFieldComponentProps) + - [`ISchemaVirtualFieldComponentProps`](#ISchemaVirtualFieldComponentProps) + - [`ISchemaFieldWrapper`](#ISchemaFieldWrapper) + - [`ISchemaFieldComponent`](#ISchemaFieldComponent) + - [`ISchemaVirtualFieldComponent`](#ISchemaVirtualFieldComponent) + - [`ISchemaFormRegistry`](#ISchemaFormRegistry) + - [`INextSchemaFieldProps`](#INextSchemaFieldProps) + - [`IPreviewTextProps`](#IPreviewTextProps) + - [`IMutators`](#IMutators) + - [`IFieldProps`](#IFieldProps) + - [`IConnectOptions`](#IConnectOptions) + + +### 使用方式 + +--- + +#### 快速开始 + +例子:使用 JSX 开发 + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import { Button } from '@alifd/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() + +const App = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +例子:使用 schema 来开发 + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import { Button } from '@alifd/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() + +const App = () => { + const schema = { + type: 'object', + properties: { + radio: { + type: 'radio', + enum: ['1', '2', '3', '4'], + title: 'Radio' + }, + select: { + type: 'string', + enum: ['1', '2', '3', '4'], + title: 'Select', + required: true + }, + checkbox: { + type: 'checkbox', + enum: ['1', '2', '3', '4'], + title: 'Checkbox', + required: true + }, + textarea: { + type: 'string', + 'x-component': 'textarea', + title: 'TextArea' + }, + number: { + type: 'number', + title: '数字选择' + }, + boolean: { + type: 'boolean', + title: '开关选择' + }, + date: { + type: 'date', + title: '日期选择' + }, + daterange: { + type: 'daterange', + default: ['2018-12-19', '2018-12-19'], + title: '日期范围' + }, + year: { + type: 'year', + title: '年份' + }, + time: { + type: 'time', + title: '时间' + }, + upload: { + type: 'upload', + 'x-props': { + listType: 'card' + }, + title: '卡片上传文件' + }, + upload2: { + type: 'upload', + 'x-props': { + listType: 'dragger' + }, + title: '拖拽上传文件' + }, + upload3: { + type: 'upload', + 'x-props': { + listType: 'text' + }, + title: '普通上传文件' + }, + range: { + type: 'range', + 'x-props': { + min: 0, + max: 1024, + marks: [0, 1024] + }, + title: '范围选择' + }, + transfer: { + type: 'transfer', + enum: [ + { + value: 1, + label: '选项1' + }, + { + value: 2, + label: '选项2' + } + ], + title: '穿梭框' + }, + rating: { + type: 'rating', + title: '等级' + }, + layout_btb_group: { + type: 'object', + 'x-component': 'button-group', + 'x-component-props': { + offset:7, + sticky: true, + }, + properties: { + submit_btn: { + type: 'object', + 'x-component': 'submit', + 'x-component-props': { + children: '提交', + }, + }, + reset_btn: { + type: 'object', + 'x-component': 'reset', + 'x-component-props': { + children: '重置', + }, + }, + } + }, + } + } + return +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### Components + +--- + +#### `` + +基于@uform/react 的核心组件SchemaForm进一步扩展出来的SchemaForm组件,推荐生产环境下使用 + +```typescript +interface INextSchemaFormProps { + // 通过schema渲染 + schema?: ISchema; + fields?: ISchemaFormRegistry['fields']; + virtualFields?: ISchemaFormRegistry['virtualFields']; + // 全局注册Form渲染组件 + formComponent?: ISchemaFormRegistry['formComponent']; + // 全局注册FormItem渲染组件 + formItemComponent?: ISchemaFormRegistry['formItemComponent']; + // label布局控制 + labelCol?: number | { span: number; offset?: number } + // FormItem布局控制 + wrapperCol?: number | { span: number; offset?: number } + // 自定义预览placeholder + previewPlaceholder?: string | ((props: IPreviewTextProps) => string); + // 样式前缀 + prefix?: string; + // 内联表单 + inline?: boolean; + // 单个 Item 的 size 自定义,优先级高于 Form 的 size, 并且当组件与 Item 一起使用时,组件自身设置 size 属性无效。 + size?: 'large' | 'medium' | 'small'; + // 标签的位置 + labelAlign?: 'top' | 'left' | 'inset'; + // 标签的左右对齐方式 + labelTextAlign?: 'left' | 'right'; + // 控制第一级 Item 的 labelCol + labelCol?: {}; + // 控制第一级 Item 的 wrapperCol + wrapperCol?: {}; + // 扩展class + className?: string; + // 自定义内联样式 + style?: React.CSSProperties; + // 设置标签类型 + component?: string | (() => void); + // 全局value + value?: Value; + // 全局defaultValue + defaultValue?: DefaultValue; + // 全局initialValues + initialValues?: DefaultValue; + // FormActions实例 + actions?: FormActions; + // IFormEffect实例 + effects?: IFormEffect; + // 表单实例 + form?: IForm; + // 表单变化回调 + onChange?: (values: Value) => void; + // form内有 `htmlType="submit"` 或 actions.submit时 触发 + onSubmit?: (values: Value) => void | Promise; + // form内有 或 actions.reset时 触发 + onReset?: () => void; + // 校验失败时触发 + onValidateFailed?: (valideted: IFormValidateResult) => void; + children?: React.ReactElement | ((form: IForm) => React.ReactElement); + // 是否使用脏检查,默认会走immer精确更新 + useDirty?: boolean; + // 是否可编辑 + editable?: boolean | ((name: string) => boolean); + // 是否走悲观校验,遇到第一个校验失败就停止后续校验 + validateFirst?: boolean; +} +``` + +**用法** + +例子1: 将a-mirror的值设置为a的值。 + +```jsx +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + registerFormField, + Field, + connect, + createFormActions +} from '@uform/next' + +const actions = createFormActions() + +registerFormField( + 'string', + connect()(props => ) +) + +ReactDOM.render( + { + $('onFieldChange','a').subscribe((fieldState)=>{ + actions.setFieldState('a-mirror',state=>{ + state.value = fieldState.value + }) + }) + }}> + + + , + document.getElementById('root') +) +``` + + +例子2: 布局 + +```jsx +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + createFormActions, + FormLayout, + FormButtonGroup, + Submit, + Reset, +} from '@uform/next' + +const actions = createFormActions() + +ReactDOM.render( +
+
常规布局
+ + + + + + + + 提交重置​ + + +
Inline Layout
+ + + + ​ + + 提交重置​ + + +
editable = false
+ + + + ​ + + 提交重置​ + + +
, + document.getElementById('root') +) +``` + +#### `` + +> @uform/next 的核心组件,用于描述表单字段 + +```typescript +interface IMarkupSchemaFieldProps { + name?: string + /** base json schema spec**/ + title?: SchemaMessage + description?: SchemaMessage + default?: any + readOnly?: boolean + writeOnly?: boolean + type?: 'string' | 'object' | 'array' | 'number' | string + enum?: Array + const?: any + multipleOf?: number + maximum?: number + exclusiveMaximum?: number + minimum?: number + exclusiveMinimum?: number + maxLength?: number + minLength?: number + pattern?: string | RegExp + maxItems?: number + minItems?: number + uniqueItems?: boolean + maxProperties?: number + minProperties?: number + required?: string[] | boolean + format?: string + /** nested json schema spec **/ + properties?: { + [key: string]: ISchema + } + items?: ISchema | ISchema[] + additionalItems?: ISchema + patternProperties?: { + [key: string]: ISchema + } + additionalProperties?: ISchema + /** extend json schema specs */ + editable?: boolean + visible?: boolean + display?: boolean + ['x-props']?: { [name: string]: any } + ['x-index']?: number + ['x-rules']?: ValidatePatternRules + ['x-component']?: string + ['x-component-props']?: { [name: string]: any } + ['x-render']?: ( + props: T & { + renderComponent: () => React.ReactElement + } + ) => React.ReactElement + ['x-effect']?: ( + dispatch: (type: string, payload: any) => void, + option?: object + ) => { [key: string]: any } +} +``` + +##### 用法 + + +```jsx +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + FormSlot, + Field, + createFormActions, + FormLayout, + FormButtonGroup, + Submit, + Reset, +} from '@uform/next' + +const actions = createFormActions() + +ReactDOM.render( + +
required
+ + +
description
+ + +
default value
+ + +
readOnly
+ + +
visible = false
+ + +
display = false
+ + +
editable = false
+ +
, + document.getElementById('root') +) +``` + + +#### `` + +> Submit 组件 Props + +```typescript +interface ISubmitProps { + /** reset pops **/ + onSubmit?: ISchemaFormProps['onSubmit'] + showLoading?: boolean + /** nextBtnProps **/ + // 按钮的类型 + type?: 'primary' | 'secondary' | 'normal' + // 按钮的尺寸 + size?: 'small' | 'medium' | 'large' + // 按钮中 Icon 的尺寸,用于替代 Icon 的默认大小 + iconSize?: 'xxs' | 'xs' | 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' + // 当 component = 'button' 时,设置 button 标签的 type 值 + htmlType?: 'submit' | 'reset' | 'button' + // 设置标签类型 + component?: 'button' | 'a' + // 设置按钮的载入状态 + loading?: boolean + // 是否为幽灵按钮 + ghost?: true | false | 'light' | 'dark' + // 是否为文本按钮 + text?: boolean + // 是否为警告按钮 + warning?: boolean + // 是否禁用 + disabled?: boolean + // 点击按钮的回调 + onClick?: (e: {}) => void + // 在Button组件使用component属性值为a时有效,代表链接页面的URL + href?: string + // 在Button组件使用component属性值为a时有效,代表何处打开链接文档 + target?: string +} +``` + +#### `` + +> Reset 组件 Props + +```typescript +interface IResetProps { + /** reset pops **/ + forceClear?: boolean + validate?: boolean + /** nextBtnProps **/ + // 按钮的类型 + type?: 'primary' | 'secondary' | 'normal' + // 按钮的尺寸 + size?: 'small' | 'medium' | 'large' + // 按钮中 Icon 的尺寸,用于替代 Icon 的默认大小 + iconSize?: 'xxs' | 'xs' | 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' + // 当 component = 'button' 时,设置 button 标签的 type 值 + htmlType?: 'submit' | 'reset' | 'button' + // 设置标签类型 + component?: 'button' | 'a' + // 设置按钮的载入状态 + loading?: boolean + // 是否为幽灵按钮 + ghost?: true | false | 'light' | 'dark' + // 是否为文本按钮 + text?: boolean + // 是否为警告按钮 + warning?: boolean + // 是否禁用 + disabled?: boolean + // 点击按钮的回调 + onClick?: (e: {}) => void + // 在Button组件使用component属性值为a时有效,代表链接页面的URL + href?: string + // 在Button组件使用component属性值为a时有效,代表何处打开链接文档 + target?: string +} +``` + +### Array Components + +#### array + +```jsx +import React, { useState, useEffect } from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + FormItemGrid, + FormButtonGroup, + Submit, + Reset, + FormBlock, + FormLayout +} from '@uform/next' +import '@alifd/next/dist/next.css' +import Printer from '@uform/printer' + +const App = () => { + const [value, setValues] = useState({}) + useEffect(() => { + setTimeout(() => { + setValues({ + array: [{ array2: [{ aa: '123', bb: '321' }] }] + }) + }, 1000) + }, []) + return ( + + console.log(v)}> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 提交 + 重置 + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### cards + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + FormItemGrid, + FormButtonGroup, + Submit, + Reset, + FormBlock, + FormLayout +} from '@uform/next' +import '@alifd/next/dist/next.css' +import Printer from '@uform/printer' + +const App = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### table + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + Field, + FormItemGrid, + FormButtonGroup, + Submit, + Reset, + FormBlock, + FormLayout +} from '@uform/next' +import '@alifd/next/dist/next.css' +import Printer from '@uform/printer' + +const App = () => ( + + + + Hello worldasdasdasdasd
+ }, + operationsWidth: 300 + }} + > + + + + + + + + + + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +### Layout Components + + +#### `` + +> FormCard 组件 Props, 完全继承自 [CardProps](#CardProps)。 +> FormCard与[FormBlock](#FormBlock) 唯一区别是样式上是否有框 + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { FormCard, SchemaMarkupField as Field } from '@uform/next' +import '@alifd/next/dist/next.css' + +const App = () => ( + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> FormBlock 组件 Props, 完全继承自 [CardProps](#CardProps) + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { FormBlock, SchemaMarkupField as Field } from '@uform/next' +import '@alifd/next/dist/next.css' + +const App = () => ( + + + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> FormStep 组件 Props + +```typescript +interface IFormStep { + dataSource: StepItemProps[] + /** next step props**/ + // 当前步骤 + current?: number + // 展示方向 + direction?: 'hoz' | 'ver' + // 横向布局时的内容排列 + labelPlacement?: 'hoz' | 'ver' + // 类型 + shape?: 'circle' | 'arrow' | 'dot' + // 是否只读模式 + readOnly?: boolean + // 是否开启动效 + animation?: boolean + // 自定义样式名 + className?: string + // StepItem 的自定义渲染 + itemRender?: (index: number, status: string) => React.ReactNode +} +``` + +**用法** + +```jsx +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + FormEffectHooks, + createFormActions, + FormGridRow, + FormItemGrid, + FormGridCol, + FormPath, + FormLayout, + FormBlock, + FormCard, + FormTextBox, + FormStep +} from '@uform/next' +import { Button } from '@alifd/next' +import '@alifd/next/dist/next.css' + +const { onFormInit$ } = FormEffectHooks + +const actions = createFormActions() + +let cache = {} + +export default () => ( + { + console.log('提交') + console.log(values) + }} + actions={actions} + labelCol={{ span: 8 }} + wrapperCol={{ span: 6 }} + validateFirst + effects={({ setFieldState, getFormGraph }) => { + onFormInit$().subscribe(() => { + setFieldState('col1', state => { + state.visible = false + }) + }) + }} + > + + + + + + + + + + + + 提交 + + + + + + +) +``` + +#### `` + +> FormLayout 组件 Props + +```typescript +interface IFormItemTopProps { + inline?: boolean + className?: string + style?: React.CSSProperties + labelCol?: number | { span: number; offset?: number } + wrapperCol?: number | { span: number; offset?: number } +} +``` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormBlock, + FormLayout +} from '@uform/next' +import { Button } from '@alifd/next' +import Printer from '@uform/printer' +import '@alifd/next/dist/next.css' +const App = () => ( + + + + + + + + + 提交重置​ + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> FormItemGrid 组件 Props + +```typescript +interface IFormItemGridProps { + cols?: Array + gutter?: number + /** next Form.Item props**/ + // 样式前缀 + prefix?: string + + // label 标签的文本 + label?: React.ReactNode + + // label 标签布局,通 `` 组件,设置 span offset 值,如 {span: 8, offset: 16},该项仅在垂直表单有效 + labelCol?: {} + + // 需要为输入控件设置布局样式时,使用该属性,用法同 labelCol + wrapperCol?: {} + + // 自定义提示信息,如不设置,则会根据校验规则自动生成. + help?: React.ReactNode + + // 额外的提示信息,和 help 类似,当需要错误信息和提示文案同时出现时,可以使用这个。 位于错误信息后面 + extra?: React.ReactNode + + // 校验状态,如不设置,则会根据校验规则自动生成 + validateState?: 'error' | 'success' | 'loading' + + // 配合 validateState 属性使用,是否展示 success/loading 的校验状态图标, 目前只有Input支持 + hasFeedback?: boolean + + // 自定义内联样式 + style?: React.CSSProperties + + // node 或者 function(values) + children?: React.ReactNode | (() => void) + + // 单个 Item 的 size 自定义,优先级高于 Form 的 size, 并且当组件与 Item 一起使用时,组件自身设置 size 属性无效。 + size?: 'large' | 'small' | 'medium' + + // 标签的位置 + labelAlign?: 'top' | 'left' | 'inset' + + // 标签的左右对齐方式 + labelTextAlign?: 'left' | 'right' + + // 扩展class + className?: string + + // [表单校验] 不能为空 + required?: boolean + + // required 的星号是否显示 + asterisk?: boolean + + // required 自定义错误信息 + requiredMessage?: string + + // required 自定义触发方式 + requiredTrigger?: string | Array + + // [表单校验] 最小值 + min?: number + + // [表单校验] 最大值 + max?: number + + // min/max 自定义错误信息 + minmaxMessage?: string + + // min/max 自定义触发方式 + minmaxTrigger?: string | Array + + // [表单校验] 字符串最小长度 / 数组最小个数 + minLength?: number + + // [表单校验] 字符串最大长度 / 数组最大个数 + maxLength?: number + + // minLength/maxLength 自定义错误信息 + minmaxLengthMessage?: string + + // minLength/maxLength 自定义触发方式 + minmaxLengthTrigger?: string | Array + + // [表单校验] 字符串精确长度 / 数组精确个数 + length?: number + + // length 自定义错误信息 + lengthMessage?: string + + // length 自定义触发方式 + lengthTrigger?: string | Array + + // 正则校验 + pattern?: any + + // pattern 自定义错误信息 + patternMessage?: string + + // pattern 自定义触发方式 + patternTrigger?: string | Array + + // [表单校验] 四种常用的 pattern + format?: 'number' | 'email' | 'url' | 'tel' + + // format 自定义错误信息 + formatMessage?: string + + // format 自定义触发方式 + formatTrigger?: string | Array + + // [表单校验] 自定义校验函数 + validator?: () => void + + // validator 自定义触发方式 + validatorTrigger?: string | Array + + // 是否修改数据时自动触发校验 + autoValidate?: boolean +} +``` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormBlock, + FormLayout +} from '@uform/next' +import { Button } from '@alifd/next' +import Printer from '@uform/printer' +import '@alifd/next/dist/next.css' + +const App = () => ( + + console.log(v)}> + + + + + + + + + + + + ​提交重置​ + + + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> FormTextBox 组件 Props + +```typescript +interface IFormTextBox { + text?: string + gutter?: number + title?: React.ReactText + description?: React.ReactText +} +``` + +**用法** + +```jsx +import React, { useState } from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormTextBox, + FormCard, + FormLayout +} from '@uform/next' +import { Button } from '@alifd/next' +import Printer from '@uform/printer' +import '@alifd/next/dist/next.css' + +const App = () => { + return ( + + console.log(v)}> + + + + + + + + + + + + ) +} +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> FormButtonGroup 组件 Props + +```typescript +interface IFormButtonGroupProps { + sticky?: boolean + style?: React.CSSProperties + itemStyle?: React.CSSProperties + className?: string + align?: 'left' | 'right' | 'start' | 'end' | 'top' | 'bottom' | 'center' + triggerDistance?: number + zIndex?: number + span?: ColSpanType + offset?: ColSpanType +} +``` + +**用法** + +```jsx +import React, { useState } from 'react' +import ReactDOM from 'react-dom' +import { + SchemaForm, + Field, + FormButtonGroup, + Submit, + Reset, + FormItemGrid, + FormCard, + FormBlock, + FormLayout +} from '@uform/next' +import { Button } from '@alifd/next' +import Printer from '@uform/printer' +import '@alifd/next/dist/next.css' + +const App = () => { + const [state, setState] = useState({ editable: true }) + return ( + + console.log(v)}> +
normal
+ + ​提交重置​ + +
sticky
+ + ​提交​ + + 重置​ + +
+
+ ) +} +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> TextButton 组件 Props, 完全继承自 [ButtonProps](#ButtonProps) + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { TextButton } from '@uform/next' +import '@alifd/next/dist/next.css' + +const App = () => ( + + content + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `` + +> CircleButton 组件 Props, 完全继承自 [ButtonProps](#ButtonProps) + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { CircleButton } from '@uform/next' +import '@alifd/next/dist/next.css' + +const App = () => ( + + ok + +) +ReactDOM.render(, document.getElementById('root')) +``` + +#### `(即将废弃,请使用SchemaMarkupField)` + +> 即将废弃,请使用[SchemaMarkupField](#SchemaMarkupField) + +### Type of SchemaMarkupField + +#### string + +* Schema Type : `string` +* Schema UI Component: Fusion-Next ``, ``, `` + +```typescript +interface IPasswordProps { + checkStrength: boolean + /** next input props **/ + // 当前值 + value?: string | number + + // 初始化值 + defaultValue?: string | number + + // 发生改变的时候触发的回调 + onChange?: (value: string, e: React.ChangeEvent) => void + + // 键盘按下的时候触发的回调 + onKeyDown?: (e: React.KeyboardEvent, opts: {}) => void + + // 禁用状态 + disabled?: boolean + + // 最大长度 + maxLength?: number + + // 是否展现最大长度样式 + hasLimitHint?: boolean + + // 当设置了maxLength时,是否截断超出字符串 + cutString?: boolean + + // 只读 + readOnly?: boolean + + // onChange返回会自动去除头尾空字符 + trim?: boolean + + // 输入提示 + placeholder?: string + + // 获取焦点时候触发的回调 + onFocus?: () => void + + // 失去焦点时候触发的回调 + onBlur?: () => void + + // 自定义字符串计算长度方式 + getValueLength?: (value: string) => number + + // 自定义class + className?: string + + // 自定义内联样式 + style?: React.CSSProperties + + // 原生type + htmlType?: string + + // name + name?: string + + // 状态 + state?: 'error' | 'loading' | 'success' + + // label + label?: React.ReactNode + + // 是否出现clear按钮 + hasClear?: boolean + + // 是否有边框 + hasBorder?: boolean + + // 尺寸 + size?: 'small' | 'medium' | 'large' + + // 按下回车的回调 + onPressEnter?: () => void + + // 水印 (Icon的type类型,和hasClear占用一个地方) + hint?: string + + // 文字前附加内容 + innerBefore?: React.ReactNode + + // 文字后附加内容 + innerAfter?: React.ReactNode + + // 输入框前附加内容 + addonBefore?: React.ReactNode + + // 输入框后附加内容 + addonAfter?: React.ReactNode + + // 输入框前附加文字 + addonTextBefore?: React.ReactNode + + // 输入框后附加文字 + addonTextAfter?: React.ReactNode + + // (原生input支持) + autoComplete?: string + + // 自动聚焦(原生input支持) + autoFocus?: boolean +} +``` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### number + +* Schema Type : `number` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### boolean + +* Schema Type : `boolean` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### date + +* Schema Type : `date` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### time + +* Schema Type : `time` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### range + +* Schema Type : `range` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### upload + +* Schema Type : `upload` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### checkbox + +* Schema Type : `checkbox` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### radio + +* Schema Type : `radio` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### rating + +* Schema Type : `rating` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### transfer + +* Schema Type : `transfer` +* Schema UI Component: Fusion-Next `` + +**用法** + +```jsx +import React from 'react' +import ReactDOM from 'react-dom' +import SchemaForm, { + SchemaMarkupField as Field, + createFormActions, + FormBlock, + FormLayout, + FormButtonGroup, + Submit, + Reset +} from '@uform/next' +import '@alifd/next/dist/next.css' + +const actions = createFormActions() +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### API + +> 整体完全继承@uform/react, 下面只列举@uform/next 的特有 API + +--- + +#### `createFormActions` + +> 创建一个 [IFormActions](#IFormActions) 实例 + +**签名** + +```typescript +createFormActions(): IFormActions +``` + +**用法** + +```typescript +import { createFormActions } from '@uform/next' + +const actions = createFormActions() +console.log(actions.getFieldValue('username')) +``` + +#### `createAsyncFormActions` + +> 创建一个 [IFormAsyncActions](#IFormAsyncActions) 实例,成员方法 同[IFormActions](#IFormActions), +> 但是调用 API 返回的结果是异步的(promise)。 + +**签名** + +```typescript +createAsyncFormActions(): IFormAsyncActions +``` + +**用法** + +```typescript +import { createAsyncFormActions } from '@uform/next' + +const actions = createAsyncFormActions() +actions.getFieldValue('username').then(val => console.log(val)) +``` + +#### `FormEffectHooks` + +> 返回包含所有 UForm 生命周期的钩子函数,可以被监听消费 + +**用法** + +```typescript +import SchemaForm, { FormEffectHooks } from '@uform/next' +const { + /** + * Form LifeCycle + **/ + onFormWillInit$, // 表单预初始化触发 + onFormInit$, // 表单初始化触发 + onFormChange$, // 表单变化时触发 + onFormInputChange$, // 表单事件触发时触发,用于只监控人工操作 + onFormInitialValueChange$, // 表单初始值变化时触发 + onFormReset$, // 表单重置时触发 + onFormSubmit$, // 表单提交时触发 + onFormSubmitStart$, // 表单提交开始时触发 + onFormSubmitEnd$, // 表单提交结束时触发 + onFormMount$, // 表单挂载时触发 + onFormUnmount$, // 表单卸载时触发 + onFormValidateStart$, // 表单校验开始时触发 + onFormValidateEnd$, //表单校验结束时触发 + onFormValuesChange$, // 表单值变化时触发 + /** + * FormGraph LifeCycle + **/ + onFormGraphChange$, // 表单观察者树变化时触发 + /** + * Field LifeCycle + **/ + onFieldWillInit$, // 字段预初始化时触发 + onFieldInit$, // 字段初始化时触发 + onFieldChange$, // 字段变化时触发 + onFieldMount$, // 字段挂载时触发 + onFieldUnmount$, // 字段卸载时触发 + onFieldInputChange$, // 字段事件触发时触发,用于只监控人工操作 + onFieldValueChange$, // 字段值变化时触发 + onFieldInitialValueChange$ // 字段初始值变化时触发 +} = FormEffectHooks + +const App = () => { + return ( + { + onFormInit$().subscribe(() => { + console.log('初始化') + }) + }} + > + ... + + ) +} +``` + +#### createEffectHook + +> 自定义 hook + +**Usage** + +```jsx +import SchemaForm, { createEffectHook, createFormActions } from '@uform/next' + +const actions = createFormActions() +const diyHook1$ = createEffectHook('diy1') +const diyHook2$ = createEffectHook('diy2') + +const App = () => { + return ( + { + diyHook1$().subscribe(payload => { + console.log('diy1 hook triggered', payload) + }) + + diyHook2$().subscribe(payload => { + console.log('diy2 hook triggered', payload) + }) + }} + > + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +#### connect + +> 包装字段组件,让字段组件只需要支持value/defaultValue/onChange属性即可快速接入表单 + +```typescript +type Connect = >(options?: IConnectOptions) => +(Target: T) => React.PureComponent +``` +**用法** + +```typescript +import {registerFormField,connect} from '@uform/next' + +registerFormField( + 'string', + connect()(props => ) +) +``` + +#### registerFormField + +```typescript +type registerFormField( + name : string, //类型名称 + component : React.ComponentType, //类型组件 + noMiddleware: boolean //是否被middleware包装 +) +``` + +**用法** + +```jsx + +import SchemaForm, { SchemaMarkupField as Field, registerFormField, connect, createFormActions } from '@uform/next' + +registerFormField( + 'custom-string', + connect()(props => ) +) +const actions = createFormActions() + +const App = () => { + return ( + + + + ) +} + +ReactDOM.render(, document.getElementById('root')) +``` + +### Interfaces + +> 整体完全继承@uform/react, 下面只列举@uform/next 的特有的 Interfaces + +--- + +#### IForm + +> 通过 createForm 创建出来的 Form 实例对象 API + +```typescript +interface IForm { + /* + * 表单提交,如果回调参数返回Promise, + * 那么整个提交流程会hold住,同时loading为true, + * 等待Promise resolve才触发表单onFormSubmitEnd事件,同时loading为false + */ + submit( + onSubmit?: (values: IFormState['values']) => any | Promise + ): Promise<{ + validated: IFormValidateResult + payload: any //onSubmit回调函数返回值 + }> + + /* + * 清空错误消息,可以通过传FormPathPattern来批量或精确控制要清空的字段, + * 比如clearErrors("*(aa,bb,cc)") + */ + clearErrors: (pattern?: FormPathPattern) => void + + /* + * 获取状态变化情况,主要用于在表单生命周期钩子内判断当前生命周期中有哪些状态发生了变化, + * 比如hasChanged(state,'value.aa') + */ + hasChanged( + target: IFormState | IFieldState | IVirtualFieldState, + path: FormPathPattern + ): boolean + + /* + * 重置表单 + */ + reset(options?: { + //强制清空 + forceClear?: boolean + //强制校验 + validate?: boolean + //重置范围,用于批量或者精确控制要重置的字段 + selector?: FormPathPattern + }): Promise + + /* + * 校验表单 + */ + validate( + path?: FormPathPattern, + options?: { + //是否悲观校验,如果当前字段遇到第一个校验错误则停止后续校验流程 + first?: boolean + } + ): Promise + + /* + * 设置表单状态 + */ + setFormState( + //操作回调 + callback?: (state: IFormState) => any, + //是否不触发事件 + silent?: boolean + ): void + + /* + * 获取表单状态 + */ + getFormState( + //transformer + callback?: (state: IFormState) => any + ): any + + /* + * 设置字段状态 + */ + setFieldState( + //字段路径 + path: FormPathPattern, + //操作回调 + callback?: (state: IFieldState) => void, + //是否不触发事件 + silent?: boolean + ): void + + /* + * 获取字段状态 + */ + getFieldState( + //字段路径 + path: FormPathPattern, + //transformer + callback?: (state: IFieldState) => any + ): any + + /* + * 注册字段 + */ + registerField(props: { + //节点路径 + path?: FormPathPattern + //数据路径 + name?: string + //字段值 + value?: any + //字段多参值 + values?: any[] + //字段初始值 + initialValue?: any + //字段扩展属性 + props?: any + //字段校验规则 + rules?: ValidatePatternRules[] + //字段是否必填 + required?: boolean + //字段是否可编辑 + editable?: boolean + //字段是否走脏检查 + useDirty?: boolean + //字段状态计算容器,主要用于扩展核心联动规则 + computeState?: (draft: IFieldState, prevState: IFieldState) => void + }): IField + + /* + * 注册虚拟字段 + */ + registerVirtualField(props: { + //节点路径 + path?: FormPathPattern + //数据路径 + name?: string + //字段扩展属性 + props?: any + //字段是否走脏检查 + useDirty?: boolean + //字段状态计算容器,主要用于扩展核心联动规则 + computeState?: (draft: IFieldState, prevState: IFieldState) => void + }): IVirtualField + + /* + * 创建字段数据操作器,后面会详细解释返回的API + */ + createMutators(field: IField): IMutators + + /* + * 获取表单观察者树 + */ + getFormGraph(): IFormGraph + + /* + * 设置表单观察者树 + */ + setFormGraph(graph: IFormGraph): void + + /* + * 监听表单生命周期 + */ + subscribe( + callback?: ({ type, payload }: { type: string; payload: any }) => void + ): number + + /* + * 取消监听表单生命周期 + */ + unsubscribe(id: number): void + + /* + * 触发表单自定义生命周期 + */ + notify: (type: string, payload?: T) => void + + /* + * 设置字段值 + */ + setFieldValue(path?: FormPathPattern, value?: any): void + + /* + * 获取字段值 + */ + getFieldValue(path?: FormPathPattern): any + + /* + * 设置字段初始值 + */ + setFieldInitialValue(path?: FormPathPattern, value?: any): void + + /* + * 获取字段初始值 + */ + getFieldInitialValue(path?: FormPathPattern): any +} +``` + +#### ButtonProps + +```typescript +interface ButtonProps { + /** reset pops **/ + onSubmit?: ISchemaFormProps['onSubmit'] + showLoading?: boolean + /** nextBtnProps **/ + // 按钮的类型 + type?: 'primary' | 'secondary' | 'normal' + // 按钮的尺寸 + size?: 'small' | 'medium' | 'large' + // 按钮中 Icon 的尺寸,用于替代 Icon 的默认大小 + iconSize?: 'xxs' | 'xs' | 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' + // 当 component = 'button' 时,设置 button 标签的 type 值 + htmlType?: 'submit' | 'reset' | 'button' + // 设置标签类型 + component?: 'button' | 'a' + // 设置按钮的载入状态 + loading?: boolean + // 是否为幽灵按钮 + ghost?: true | false | 'light' | 'dark' + // 是否为文本按钮 + text?: boolean + // 是否为警告按钮 + warning?: boolean + // 是否禁用 + disabled?: boolean + // 点击按钮的回调 + onClick?: (e: {}) => void + // 在Button组件使用component属性值为a时有效,代表链接页面的URL + href?: string + // 在Button组件使用component属性值为a时有效,代表何处打开链接文档 + target?: string +} +``` + +#### CardProps + +```typescript +interface CardProps extends HTMLAttributesWeak, CommonProps { + // 卡片的上的图片 / 视频 + media?: React.ReactNode + + // 卡片的标题 + title?: React.ReactNode + + // 卡片的副标题 + subTitle?: React.ReactNode + + // 卡片操作组,位置在卡片底部 + actions?: React.ReactNode + + // 是否显示标题的项目符号 + showTitleBullet?: boolean + + // 是否展示头部的分隔线 + showHeadDivider?: boolean + + // 内容区域的固定高度 + contentHeight?: string | number + + // 标题区域的用户自定义内容 + extra?: React.ReactNode + + // 是否开启自由模式,开启后card 将使用子组件配合使用, 设置此项后 title, subtitle, 等等属性都将失效 + free?: boolean +} +``` + +#### ICompatItemProps + +```typescript +interface ICompatItemProps + extends Exclude, + Partial { + labelCol?: number | { span: number; offset?: number } + wrapperCol?: number | { span: number; offset?: number } +} +``` + + +#### IFieldState + +```typescript +interface IFieldState { + /**只读属性**/ + + //状态名称,FieldState + displayName?: string + //数据路径 + name: string + //节点路径 + path: string + //是否已经初始化 + initialized: boolean + //是否处于原始态,只有value===intialValues时的时候该状态为true + pristine: boolean + //是否处于合法态,只要errors长度大于0的时候valid为false + valid: boolean + //是否处于非法态,只要errors长度大于0的时候valid为true + invalid: boolean + //是否处于校验态 + validating: boolean + //是否被修改,如果值发生变化,该属性为true,同时在整个字段的生命周期内都会为true + modified: boolean + //是否被触碰 + touched: boolean + //是否被激活,字段触发onFocus事件的时候,它会被触发为true,触发onBlur时,为false + active: boolean + //是否访问过,字段触发onBlur事件的时候,它会被触发为true + visited: boolean + + /**可写属性**/ + + //是否可见,注意:该状态如果为false,那么字段的值不会被提交,同时UI不会显示 + visible: boolean + //是否展示,注意:该状态如果为false,那么字段的值会提交,UI不会展示,类似于表单隐藏域 + display: boolean + //是否可编辑 + editable: boolean + //是否处于loading状态,注意:如果字段处于异步校验时,loading为true + loading: boolean + //字段多参值,比如字段onChange触发时,给事件回调传了多参数据,那么这里会存储所有参数的值 + values: any[] + //字段错误消息 + errors: string[] + //字段告警消息 + warnings: string[] + //字段值,与values[0]是恒定相等 + value: any + //初始值 + initialValue: any + //校验规则,具体类型描述参考后面文档 + rules: ValidatePatternRules[] + //是否必填 + required: boolean + //是否挂载 + mounted: boolean + //是否卸载 + unmounted: boolean + //字段扩展属性 + props: FieldProps +} +``` + + +#### ISchemaFieldComponentProps + +```typescript +interface ISchemaFieldComponentProps extends IFieldState { + schema: Schema + mutators: IMutators + form: IForm + renderField: ( + addtionKey: string | number, + reactKey?: string | number + ) => React.ReactElement +} +``` + +#### ISchemaVirtualFieldComponentProps + +```typescript +interface ISchemaVirtualFieldComponentProps extends IVirtualFieldState { + schema: Schema + form: IForm + children: React.ReactElement[] + renderField: ( + addtionKey: string | number, + reactKey?: string | number + ) => React.ReactElement +} +``` + +#### ISchemaFieldWrapper + +```typescript +interface ISchemaFieldWrapper { + (Traget: ISchemaFieldComponent): + | React.FC + | React.ClassicComponent +} +``` + +#### ISchemaFieldComponent + +```typescript +declare type ISchemaFieldComponent = ComponentWithStyleComponent< + ISchemaFieldComponentProps +> & { + __WRAPPERS__?: ISchemaFieldWrapper[] +} +``` + +#### ISchemaVirtualFieldComponent + +```typescript +declare type ISchemaVirtualFieldComponent = ComponentWithStyleComponent< + ISchemaVirtualFieldComponentProps +> & { + __WRAPPERS__?: ISchemaFieldWrapper[] +} +``` + +#### ISchemaFormRegistry + +```typescript +interface ISchemaFormRegistry { + fields: { + [key: string]: ISchemaFieldComponent + } + virtualFields: { + [key: string]: ISchemaVirtualFieldComponent + } + wrappers?: ISchemaFieldWrapper[] + formItemComponent: React.JSXElementConstructor + formComponent: string | React.JSXElementConstructor +} +``` + +#### INextSchemaFieldProps + +```typescript +interface INextSchemaFieldProps { + name?: string; + /** ISchema **/ + title?: SchemaMessage; + description?: SchemaMessage; + default?: any; + readOnly?: boolean; + writeOnly?: boolean; + type?: 'string' | 'object' | 'array' | 'number' | string; + enum?: Array; + const?: any; + multipleOf?: number; + maximum?: number; + exclusiveMaximum?: number; + minimum?: number; + exclusiveMinimum?: number; + maxLength?: number; + minLength?: number; + pattern?: string | RegExp; + maxItems?: number; + minItems?: number; + uniqueItems?: boolean; + maxProperties?: number; + minProperties?: number; + required?: string[] | boolean; + format?: string; + properties?: { + [key: string]: ISchema; + }; + items?: ISchema | ISchema[]; + additionalItems?: ISchema; + patternProperties?: { + [key: string]: ISchema; + }; + additionalProperties?: ISchema; + editable?: boolean; + visible?: boolean; + display?: boolean; + ['x-props']?: { + [name: string]: any; + }; + ['x-index']?: number; + ['x-rules']?: ValidatePatternRules; + ['x-component']?: string; + ['x-component-props']?: { + [name: string]: any; + }; + ['x-render']?: (props: T & { + renderComponent: () => React.ReactElement; + }) => React.ReactElement; + ['x-effect']?: (dispatch: (type: string, payload: any) => void, option?: object) => { + [key: string]: any; + }; + +``` + +#### IPreviewTextProps + +```typescript +interface IPreviewTextProps { + className?: React.ReactText + dataSource?: any[] + value?: any + addonBefore?: React.ReactNode + innerBefore?: React.ReactNode + addonTextBefore?: React.ReactNode + addonTextAfter?: React.ReactNode + innerAfter?: React.ReactNode + addonAfter?: React.ReactNode +} + +``` + +#### IMutators + +```typescript +interface IMutators { + change: (value: V)=> void,//改变当前字段值 + dispatch: (name: string, payload : any) => void,//触发effect事件 + errors: (errors: string | Array, ...formatValues: Array) => void,//设置当前字段的错误消息 + push(value: V),//对当前字段的值做push操作 + pop(),//对当前字段的值做pop操作 + insert(index: number,value: V),//对当前字段的值做insert操作 + remove(name : string),//对当前字段的值做remove操作 + unshift(value : V),//对当前字段值做unshift操作 + shift(),//对当前字段值做shift操作 + move(fromIndex: number, toIndex: number)//对当前字段值做move操作 +} +``` + +#### IFieldProps + +```typescript +interface IFieldProps { + name : string //字段数据路径 + path : Array //字段数组数据路径 + value : V //字段值 + errors : Array //字段错误消息集合 + editable : boolean | ((name:string) => boolean) //字段是否可编辑 + locale : Locale //国际化文案对象 + loading : boolean //是否处于加载态 + schemaPath : Array //schema path,考虑到有些schema其实是不占数据路径的,所以这个路径是真实路径 + getSchema : (path: string) => ISchema //根据路径获取schema + renderField : (childKey: string, reactKey: string | number) => JSX.Element | string | null //根据childKey渲染当前字段的子字段 + renderComponent : React.FunctionComponent | undefined>,//渲染当前字段的组件,对于x-render来说,可以借助它快速实现渲染包装功能 + getOrderProperties : () => Array<{schema: ISchema, key: number, path: string, name: string }>,//根据properties里字段的x-index值求出排序后的properties + mutators : Mutators,//数据操作对象 + schema : ISchema +} + +``` + +```typescript + +interface IConnectOptions { + //控制表单组件 + valueName?: string + //事件名称 + eventName?: string + //默认props + defaultProps?: Partial + //取值函数,有些场景我们的事件函数取值并不是事件回调的第一个参数,需要做进一步的定制 + getValueFromEvent?: (props: IFieldProps['x-props'], event: Event, ...args: any[]) => any + //字段组件props transformer + getProps?: (connectProps: IConnectProps, fieldProps: IFieldProps) => IConnectProps + //字段组件component transformer + getComponent?: ( + target: T, + connectProps: IConnectProps, + fieldProps: IFieldProps + ) => T +} + +``` \ No newline at end of file diff --git a/packages/next/package.json b/packages/next/package.json index e25d0a3cecd..81f80184c0b 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "@uform/next", - "version": "0.4.4", + "version": "1.0.0-alpha.5", "license": "MIT", "main": "lib", "module": "esm", @@ -20,22 +20,27 @@ "build": "ts-node --project ../../tsconfig.build.json build.ts" }, "peerDependencies": { - "@alifd/next": "^1.13.1", + "@alifd/next": "^1.19.1", "@babel/runtime": "^7.4.4", + "@types/classnames": "^2.2.9", + "@types/styled-components": "^4.1.19", "react": ">=16.8.0", "react-dom": ">=16.8.0" }, "dependencies": { - "@uform/react": "^0.4.4", - "@uform/types": "^0.4.4", - "@uform/utils": "^0.4.4", + "@uform/react-schema-renderer": "^1.0.0-alpha.5", + "@uform/react-shared-components": "^1.0.0-alpha.5", + "@uform/shared": "^1.0.0-alpha.5", "classnames": "^2.2.6", - "moveto": "^1.7.4", + "react-eva": "^1.0.0-alpha.0", "react-stikky": "^0.1.15", + "rxjs": "^6.5.1", "styled-components": "^4.1.1" }, "devDependencies": { - "@alifd/next": "^1.13.1" + "@alifd/next": "^1.19.1", + "@types/classnames": "^2.2.9", + "@types/styled-components": "^4.1.19" }, "publishConfig": { "access": "public" diff --git a/packages/next/src/compat/Form.tsx b/packages/next/src/compat/Form.tsx new file mode 100644 index 00000000000..5ee05f200b7 --- /dev/null +++ b/packages/next/src/compat/Form.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import { Form } from '@alifd/next' +import { FormProps } from '@alifd/next/types/form' +import { IFormItemTopProps } from '../types' +import { FormItemProvider } from './context' +import { normalizeCol } from '../shared' +import { + PreviewText, + PreviewTextConfigProps +} from '@uform/react-shared-components' + +export const CompatNextForm: React.FC = props => { + return ( + + +
+ + + ) +} diff --git a/packages/next/src/compat/FormItem.tsx b/packages/next/src/compat/FormItem.tsx new file mode 100644 index 00000000000..56b9932ac34 --- /dev/null +++ b/packages/next/src/compat/FormItem.tsx @@ -0,0 +1,118 @@ +import React, { createContext, createElement } from 'react' +import { Form } from '@alifd/next' +import { useFormItem } from './context' +import { IFormItemTopProps, ICompatItemProps } from '../types' +import { normalizeCol } from '../shared' +import { useContext } from 'react' + +const computeStatus = (props: ICompatItemProps) => { + if (props.loading) { + return 'loading' + } + if (props.invalid) { + return 'error' + } + if (props.warnings && props.warnings.length) { + return 'warning' + } +} + +const computeHelp = (props: ICompatItemProps) => { + if (props.help) return props.help + const messages = [].concat(props.errors || [], props.warnings || []) + return messages.length + ? messages.map((message, index) => + createElement( + 'span', + { key: index }, + message, + messages.length - 1 > index ? ' ,' : '' + ) + ) + : props.schema && props.schema.description +} + +const computeLabel = (props: ICompatItemProps) => { + if (props.label) return props.label + if (props.schema && props.schema.title) { + return props.schema.title + } +} + +const computeExtra = (props: ICompatItemProps) => { + if (props.extra) return props.extra +} + +function pickProps(obj: T, ...keys: (keyof T)[]): Pick { + const result: Pick = {} as any + for (let i = 0; i < keys.length; i++) { + if (obj[keys[i]] !== undefined) { + result[keys[i]] = obj[keys[i]] + } + } + return result +} + +const computeSchemaExtendProps = ( + props: ICompatItemProps +): IFormItemTopProps => { + if (props.schema) { + return pickProps( + { + ...props.schema.getExtendsItemProps(), + ...props.schema.getExtendsProps() + }, + 'className', + 'prefix', + 'labelAlign', + 'labelTextAlign', + 'size', + 'labelCol', + 'wrapperCol' + ) + } +} + +const FormItemPropsContext = createContext({}) + +export const FormItemProps = ({ children, ...props }) => ( + + {children} + +) + +export const CompatNextFormItem: React.FC = props => { + const { + prefix, + labelAlign, + labelCol, + labelTextAlign, + wrapperCol, + size + } = useFormItem() + const formItemProps = useContext(FormItemPropsContext) + const help = computeHelp(props) + const label = computeLabel(props) + const status = computeStatus(props) + const extra = computeExtra(props) + const itemProps = computeSchemaExtendProps(props) + return ( + {extra}

} + {...itemProps} + {...formItemProps} + > + {props.children} +
+ ) +} diff --git a/packages/next/src/compat/context.tsx b/packages/next/src/compat/context.tsx new file mode 100644 index 00000000000..e3f38383c73 --- /dev/null +++ b/packages/next/src/compat/context.tsx @@ -0,0 +1,35 @@ +import React, { createContext, useContext } from 'react' +import { IFormItemTopProps } from '../types' + +const FormItemContext = createContext({}) + +export const FormItemProvider: React.FC = ({ + children, + prefix, + size, + labelAlign, + labelCol, + inline, + labelTextAlign, + wrapperCol +}) => ( + + {children} + +) + +FormItemProvider.displayName = 'FormItemProvider' + +export const useFormItem = () => { + return useContext(FormItemContext) +} diff --git a/packages/next/src/compat/index.ts b/packages/next/src/compat/index.ts new file mode 100644 index 00000000000..1eca2fb65ed --- /dev/null +++ b/packages/next/src/compat/index.ts @@ -0,0 +1,10 @@ +import { + registerFormComponent, + registerFormItemComponent +} from '@uform/react-schema-renderer' +import { CompatNextForm } from './Form' +import { CompatNextFormItem } from './FormItem' + +registerFormComponent(CompatNextForm) + +registerFormItemComponent(CompatNextFormItem) diff --git a/packages/next/src/components/Button.tsx b/packages/next/src/components/Button.tsx new file mode 100644 index 00000000000..1b167eebd3e --- /dev/null +++ b/packages/next/src/components/Button.tsx @@ -0,0 +1,107 @@ +import React from 'react' +import { FormSpy, LifeCycleTypes, createVirtualBox } from '@uform/react-schema-renderer' +import { Button } from '@alifd/next' +import { ButtonProps } from '@alifd/next/types/button' +import { ISubmitProps, IResetProps } from '../types' +import styled from 'styled-components' + +export const TextButton: React.FC = props => ( + + ) +})` + border-radius: 50% !important; + padding: 0 !important; + min-width: 28px; + &.next-large { + min-width: 40px; + } + &.next-small { + min-width: 20px; + } + &.has-text { + .next-icon { + margin-right: 5px; + } + background: none !important; + border: none !important; + } +` + +export const Submit = ({ showLoading, onSubmit, ...props }: ISubmitProps) => { + return ( + { + switch (action.type) { + case LifeCycleTypes.ON_FORM_SUBMIT_START: + return { + ...state, + submitting: true + } + case LifeCycleTypes.ON_FORM_SUBMIT_END: + return { + ...state, + submitting: false + } + default: + return state + } + }} + > + {({ state, form }) => { + return ( + + ) + }} + + ) +} + +Submit.defaultProps = { + showLoading: true +} + +export const Reset: React.FC = ({ + children, + forceClear, + validate, + ...props +}) => { + return ( + + {({ form }) => { + return ( + + ) + }} + + ) +} + +createVirtualBox('reset', Reset) +createVirtualBox('text-button', TextButton) +createVirtualBox('submit', Submit) +createVirtualBox('circle-button', CircleButton) diff --git a/packages/next/src/components/FormBlock.tsx b/packages/next/src/components/FormBlock.tsx new file mode 100644 index 00000000000..6468e5b03c6 --- /dev/null +++ b/packages/next/src/components/FormBlock.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import { createVirtualBox } from '@uform/react-schema-renderer' +import { Card } from '@alifd/next' +import { CardProps } from '@alifd/next/types/card' +import styled from 'styled-components' + +export const FormBlock = createVirtualBox( + 'block', + styled(({ children, className, ...props }) => { + return ( + + {children} + + ) + })` + margin-bottom: 0px; + .next-card-body { + padding-top: 20px; + padding-bottom: 0 !important; + } + &.next-card { + border: none; + padding-bottom: 15px; + } + ` +) diff --git a/packages/next/src/components/formButtonGroup.tsx b/packages/next/src/components/FormButtonGroup.tsx similarity index 52% rename from packages/next/src/components/formButtonGroup.tsx rename to packages/next/src/components/FormButtonGroup.tsx index 0af76eead94..f63d23eb80c 100644 --- a/packages/next/src/components/formButtonGroup.tsx +++ b/packages/next/src/components/FormButtonGroup.tsx @@ -1,12 +1,11 @@ -import React, { Component } from 'react' -import ReactDOM from 'react-dom' +import React, { useRef } from 'react' import { Grid } from '@alifd/next' import Sticky from 'react-stikky' import cls from 'classnames' import styled from 'styled-components' - -import { FormLayoutConsumer } from '../form' -import { IFormButtonGroupProps } from '../type' +import { useFormItem } from '../compat/context' +import { IFormButtonGroupProps } from '../types' +import { createVirtualBox } from '@uform/react-schema-renderer' const { Row, Col } = Grid @@ -59,17 +58,22 @@ const isElementInViewport = ( ) } -export const FormButtonGroup: React.FC = styled( - class FormButtonGroup extends Component { - static defaultProps = { - span: 24, - zIndex: 100 - } - - private formNode: HTMLElement - - private renderChildren() { - const { children, itemStyle, offset, span } = this.props +export const FormButtonGroup = styled( + (props: React.PropsWithChildren) => { + const { + span, + zIndex, + sticky, + style, + offset, + className, + children, + triggerDistance, + itemStyle + } = props + const { inline } = useFormItem() + const selfRef = useRef() + const renderChildren = () => { return (
@@ -84,69 +88,53 @@ export const FormButtonGroup: React.FC = styled(
) } - - getStickyBoundaryHandler(ref) { + const getStickyBoundaryHandler = () => { return () => { - this.formNode = this.formNode || ReactDOM.findDOMNode(ref.current) - if (this.formNode) { - return isElementInViewport(this.formNode.getBoundingClientRect()) + if (selfRef.current && selfRef.current.parentElement) { + const container = selfRef.current.parentElement + return isElementInViewport(container.getBoundingClientRect()) } return true } } - render() { - const { sticky, style, className } = this.props + const content = ( +
+ {renderChildren()} +
+ ) - const content = ( - - {({ inline } = {}) => ( -
- {this.renderChildren()} + if (sticky) { + return ( +
+ +
+ {content}
- )} - +
+
) - - if (sticky) { - return ( -
- - {({ FormRef } = {}) => { - if (!FormRef) return - return ( - -
- {content} -
-
- ) - }} -
-
- ) - } - - return content } + + return content } -)` - ${props => +)` + ${(props: IFormButtonGroupProps) => props.align ? `display:flex;justify-content: ${getAlign(props.align)}` : ''} &.is-inline { display: inline-block; @@ -168,3 +156,8 @@ export const FormButtonGroup: React.FC = styled( } } ` + +createVirtualBox>( + 'button-group', + FormButtonGroup, +) \ No newline at end of file diff --git a/packages/next/src/components/FormCard.tsx b/packages/next/src/components/FormCard.tsx new file mode 100644 index 00000000000..71cb0fbe68d --- /dev/null +++ b/packages/next/src/components/FormCard.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import { createVirtualBox } from '@uform/react-schema-renderer' +import { Card } from '@alifd/next' +import { CardProps } from '@alifd/next/types/card' +import styled from 'styled-components' + +export const FormCard = createVirtualBox( + 'card', + styled(({ children, className, ...props }) => { + return ( + + {children} + + ) + })` + margin-bottom: 30px; + .next-card-body { + padding-top: 30px; + padding-bottom: 0 !important; + } + ` +) diff --git a/packages/next/src/components/FormItemGrid.tsx b/packages/next/src/components/FormItemGrid.tsx new file mode 100644 index 00000000000..fbe30387150 --- /dev/null +++ b/packages/next/src/components/FormItemGrid.tsx @@ -0,0 +1,89 @@ +import React, { Fragment } from 'react' +import { CompatNextFormItem } from '../compat/FormItem' +import { createVirtualBox } from '@uform/react-schema-renderer' +import { toArr } from '@uform/shared' +import { Grid } from '@alifd/next' +import { RowProps, ColProps } from '@alifd/next/types/grid' +import { ItemProps } from '@alifd/next/types/form' +import { IFormItemGridProps, IItemProps } from '../types' +import { normalizeCol } from '../shared' +const { Row, Col } = Grid + +export const FormItemGrid = createVirtualBox( + 'grid', + props => { + const { + cols: rawCols, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + title, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + description, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + help, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + extra, + ...selfProps + } = props + const children = toArr(props.children) + const cols = toArr(rawCols).map(col => normalizeCol(col)) + const childNum = children.length + + if (cols.length < childNum) { + let offset: number = childNum - cols.length + let lastSpan: number = + 24 - + cols.reduce((buf, col) => { + return ( + buf + + Number(col.span ? col.span : 0) + + Number(col.offset ? col.offset : 0) + ) + }, 0) + for (let i = 0; i < offset; i++) { + cols.push({ span: Math.floor(lastSpan / offset) }) + } + } + const grids = ( + + {children.reduce((buf, child, key) => { + return child + ? buf.concat( + + {child} + + ) + : buf + }, [])} + + ) + + if (title) { + return ( + + {grids} + + ) + } + return {grids} + } +) + +export const FormGridRow = createVirtualBox( + 'grid-row', + props => { + const { title, description, extra } = props + const grids = {props.children} + if (title) { + return ( + + {grids} + + ) + } + return grids + } +) + +export const FormGridCol = createVirtualBox('grid-col', props => { + return {props.children} +}) diff --git a/packages/next/src/components/FormLayout.tsx b/packages/next/src/components/FormLayout.tsx new file mode 100644 index 00000000000..eb89eab6c43 --- /dev/null +++ b/packages/next/src/components/FormLayout.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import { FormItemProvider, useFormItem } from '../compat/context' +import { createVirtualBox } from '@uform/react-schema-renderer' +import cls from 'classnames' +import { IFormItemTopProps } from '../types' + +export const FormLayout = createVirtualBox( + 'layout', + props => { + const { inline } = useFormItem() + const isInline = props.inline || inline + const children = + isInline || props.className || props.style ? ( +
+ {props.children} +
+ ) : ( + props.children + ) + return {children} + } +) diff --git a/packages/next/src/components/FormStep.tsx b/packages/next/src/components/FormStep.tsx new file mode 100644 index 00000000000..31958ddbe3b --- /dev/null +++ b/packages/next/src/components/FormStep.tsx @@ -0,0 +1,100 @@ +import React, { useRef, Fragment } from 'react' +import { + createControllerBox, + ISchemaVirtualFieldComponentProps, + createEffectHook, + useFormEffects, + useFieldState +} from '@uform/react-schema-renderer' +import { toArr } from '@uform/shared' +import { Step } from '@alifd/next' +import { IFormStep } from '../types' + +enum StateMap { + ON_FORM_STEP_NEXT = 'onFormStepNext', + ON_FORM_STEP_PREVIOUS = 'onFormStepPrevious', + ON_FORM_STEP_GO_TO = 'onFormStepGoto', + ON_FORM_STEP_CURRENT_CHANGE = 'onFormStepCurrentChange' +} +const EffectHooks = { + onStepNext$: createEffectHook(StateMap.ON_FORM_STEP_NEXT), + onStepPrevious$: createEffectHook(StateMap.ON_FORM_STEP_PREVIOUS), + onStepGoto$: createEffectHook(StateMap.ON_FORM_STEP_GO_TO), + onStepCurrentChange$: createEffectHook<{ + value: number + preValue: number + }>(StateMap.ON_FORM_STEP_CURRENT_CHANGE) +} + +type StepComponentExtendsProps = StateMap + +export const FormStep: React.FC & + StepComponentExtendsProps = createControllerBox( + 'step', + ({ form, schema, children }: ISchemaVirtualFieldComponentProps) => { + const [{ current }, setFieldState] = useFieldState({ + current: 0 + }) + const ref = useRef(current) + const { dataSource, ...stepProps } = schema.getExtendsComponentProps() + const items = toArr(dataSource) + const update = (cur: number) => { + form.notify(StateMap.ON_FORM_STEP_CURRENT_CHANGE, { + value: cur, + preValue: current + }) + setFieldState({ + current: cur + }) + } + useFormEffects(($, { setFieldState }) => { + items.forEach(({ name }, index) => { + setFieldState(name, (state: any) => { + state.display = index === current + }) + }) + $(StateMap.ON_FORM_STEP_CURRENT_CHANGE).subscribe(({ value }) => { + items.forEach(({ name }, index) => { + if (!name) + throw new Error('FormStep dataSource must include `name` property') + setFieldState(name, (state: any) => { + state.display = index === value + }) + }) + }) + + $(StateMap.ON_FORM_STEP_NEXT).subscribe(() => { + form.validate().then(({ errors }) => { + if (errors.length === 0) { + update( + ref.current + 1 > items.length - 1 ? ref.current : ref.current + 1 + ) + } + }) + }) + + $(StateMap.ON_FORM_STEP_PREVIOUS).subscribe(() => { + update(ref.current - 1 < 0 ? ref.current : ref.current - 1) + }) + + $(StateMap.ON_FORM_STEP_GO_TO).subscribe(payload => { + if (!(payload < 0 || payload > items.length)) { + update(payload) + } + }) + }) + ref.current = current + return ( + + + {items.map((props, key) => { + return + })} + + {children} + + ) + } +) as any + +Object.assign(FormStep, StateMap, EffectHooks) diff --git a/packages/next/src/components/FormTextBox.tsx b/packages/next/src/components/FormTextBox.tsx new file mode 100644 index 00000000000..84ced3ff485 --- /dev/null +++ b/packages/next/src/components/FormTextBox.tsx @@ -0,0 +1,124 @@ +import React, { useRef, useLayoutEffect } from 'react' +import { createControllerBox } from '@uform/react-schema-renderer' +import { IFormTextBox } from '../types' +import { toArr } from '@uform/shared' +import { CompatNextFormItem } from '../compat/FormItem' +import styled from 'styled-components' + +export const FormTextBox = createControllerBox( + 'text-box', + styled(({ props, className, children }) => { + const { + title, + help, + text, + name, + extra, + gutter, + style, + ...componentProps + } = Object.assign( + { + gutter: 5 + }, + props['x-component-props'] + ) + const ref: React.RefObject = useRef() + const arrChildren = toArr(children) + const split = text.split('%s') + let index = 0 + useLayoutEffect(() => { + if (ref.current) { + const elements = ref.current.querySelectorAll('.text-box-field') + const syncLayouts = Array.prototype.map.call( + elements, + (el: HTMLElement) => { + return [ + el, + () => { + const ctrl = el.querySelector( + '.next-form-item-control:first-child' + ) + if (ctrl) { + el.style.width = ctrl.getBoundingClientRect().width + 'px' + } + } + ] + } + ) + syncLayouts.forEach(([el, handler]) => { + el.addEventListener('DOMSubtreeModified', handler) + }) + + return () => { + syncLayouts.forEach(([el, handler]) => { + el.removeEventListener('DOMSubtreeModified', handler) + }) + } + } + }, []) + const newChildren = split.reduce((buf, item, key) => { + return buf.concat( + item ? ( +

+ {item} +

+ ) : null, + arrChildren[key] ? ( +
+ {arrChildren[key]} +
+ ) : null + ) + }, []) + + const textChildren = ( +
+ {newChildren} +
+ ) + + if (!title) return textChildren + + return ( + + {textChildren} + + ) + })` + display: flex; + .text-box-words:nth-child(1) { + margin-left: 0; + } + .text-box-field { + display: inline-block; + } + .next-form-item { + margin-bottom: 0 !important; + } + .preview-text { + text-align: center !important; + white-space:nowrap; + } + ` +) diff --git a/packages/next/src/components/button.tsx b/packages/next/src/components/button.tsx deleted file mode 100644 index 7cbc4eb1d69..00000000000 --- a/packages/next/src/components/button.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react' -import { FormConsumer } from '@uform/react' -import { Button } from '@alifd/next' -import { ISubmitProps } from '../type' - -export const Submit = ({ showLoading, ...props }: ISubmitProps) => { - return ( - - {({ status }) => { - return ( - - ) - }} - - ) -} - -Submit.defaultProps = { - showLoading: true -} - -export const Reset: React.FC> = props => { - return ( - - {({ reset }) => { - return ( - - ) - }} - - ) -} diff --git a/packages/next/src/components/index.ts b/packages/next/src/components/index.ts new file mode 100644 index 00000000000..1015143d9c7 --- /dev/null +++ b/packages/next/src/components/index.ts @@ -0,0 +1,8 @@ +export * from './Button' +export * from './FormButtonGroup' +export * from './FormLayout' +export * from './FormItemGrid' +export * from './FormCard' +export * from './FormBlock' +export * from './FormTextBox' +export * from './FormStep' diff --git a/packages/next/src/components/layout.tsx b/packages/next/src/components/layout.tsx deleted file mode 100644 index 0b229d3bba1..00000000000 --- a/packages/next/src/components/layout.tsx +++ /dev/null @@ -1,329 +0,0 @@ -import React, { Component, useLayoutEffect, useRef } from 'react' -import { createVirtualBox, createControllerBox } from '@uform/react' -import { toArr } from '@uform/utils' -import { Grid } from '@alifd/next' -import Card from '@alifd/next/lib/card' -import styled from 'styled-components' -import cls from 'classnames' -import { IFormItemGridProps, IFormItemProps } from '@uform/types' - -import { FormLayoutConsumer, FormItem, FormLayoutProvider } from '../form' -import { - IFormTextBox, - IFormCardProps, - IFormBlockProps, - IFormLayoutProps, - TFormCardOrFormBlockProps, - IFormItemGridProps as IFormItemGridPropsAlias -} from '../type' - -const { Row, Col } = Grid - -const normalizeCol = ( - col: { span: number; offset?: number } | number, - defaultValue: { span: number } = { span: 0 } -): { span: number; offset?: number } => { - if (!col) { - return defaultValue - } else { - return typeof col === 'object' ? col : { span: col } - } -} - -export const FormLayout = createVirtualBox( - 'layout', - ({ children, ...props }) => { - return ( - - {value => { - let newValue = { ...value, ...props } - let child = - newValue.inline || newValue.className || newValue.style ? ( -
- {children} -
- ) : ( - children - ) - return ( - {child} - ) - }} -
- ) - } -) - -export const FormLayoutItem: React.FC = props => - React.createElement( - FormLayoutConsumer, - {}, - ({ - labelAlign, - labelTextAlign, - labelCol, - wrapperCol, - size, - autoAddColon - }) => { - return React.createElement( - FormItem, - { - labelAlign, - labelTextAlign, - labelCol, - wrapperCol, - autoAddColon, - size, - ...props - }, - props.children - ) - } - ) - -export const FormItemGrid = createVirtualBox( - 'grid', - class extends Component { - renderFormItem(children) { - const { title, help, name, extra, ...props } = this.props - return React.createElement( - FormLayoutItem, - { - label: title, - noMinHeight: true, - id: name, - extra, - help, - ...props - } as IFormItemGridProps, - children - ) - } - - renderGrid() { - const { - children: rawChildren, - cols: rawCols, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - title, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - description, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - help, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - extra, - ...props - } = this.props - - const children = toArr(rawChildren) - const cols = toArr(rawCols).map(col => normalizeCol(col)) - const childNum = children.length - - if (cols.length < childNum) { - let offset: number = childNum - cols.length - let lastSpan: number = - 24 - - cols.reduce((buf, col) => { - return ( - buf + - Number(col.span ? col.span : 0) + - Number(col.offset ? col.offset : 0) - ) - }, 0) - for (let i = 0; i < offset; i++) { - cols.push({ span: Math.floor(lastSpan / offset) }) - } - } - - return ( - - {children.reduce((buf, child, key) => { - return child - ? buf.concat( - - {child} - - ) - : buf - }, [])} - - ) - } - - render() { - const { title } = this.props - if (title) { - return this.renderFormItem(this.renderGrid()) - } else { - return this.renderGrid() - } - } - } -) - -export const FormCard = createVirtualBox( - 'card', - styled( - class extends Component { - static defaultProps = { - contentHeight: 'auto' - } - render() { - const { children, className, ...props } = this.props - return ( - - {children} - - ) - } - } - )` - margin-bottom: 30px; - .next-card-body { - padding-top: 30px; - padding-bottom: 0 !important; - } - ` -) - -export const FormBlock = createVirtualBox( - 'block', - styled( - class extends Component { - static defaultProps = { - contentHeight: 'auto' - } - render() { - const { children, className, ...props } = this.props - return ( - - {children} - - ) - } - } - )` - margin-bottom: 0px; - .next-card-body { - padding-top: 20px; - padding-bottom: 0 !important; - } - &.next-card { - border: none; - padding: 0 15px; - padding-bottom: 15px; - } - ` -) - -export const FormTextBox = createControllerBox( - 'text-box', - styled(({ children, schema, className }) => { - const { title, help, text, name, extra, ...props } = schema['x-props'] - const ref: React.RefObject = useRef() - const arrChildren = toArr(children) - const split = String(text).split('%s') - let index = 0 - useLayoutEffect(() => { - if (ref.current) { - const elements = ref.current.querySelectorAll('.text-box-field') - const syncLayouts = Array.prototype.map.call( - elements, - (el: HTMLElement) => { - return [ - el, - () => { - const ctrl = el.querySelector( - '.next-form-item-control:first-child' - ) - if (ctrl) { - el.style.width = ctrl.getBoundingClientRect().width + 'px' - } - } - ] - } - ) - syncLayouts.forEach(([el, handler]) => { - el.addEventListener('DOMSubtreeModified', handler) - }) - - return () => { - syncLayouts.forEach(([el, handler]) => { - el.removeEventListener('DOMSubtreeModified', handler) - }) - } - } - }, []) - const newChildren = split.reduce((buf, item, key) => { - return buf.concat( - item ? ( - - {item} - - ) : ( - undefined - ), - arrChildren[key] ? ( -
- {arrChildren[key]} -
- ) : ( - undefined - ) - ) - }, []) - - if (!title) - return ( -
- {newChildren} -
- ) - - return React.createElement( - FormLayoutItem, - { - label: title, - noMinHeight: true, - id: name, - extra, - help, - ...props - }, -
- {newChildren} -
- ) - })` - display: flex; - .text-box-words { - font-size: 12px; - line-height: 28px; - color: #333; - ${props => { - const { editable, schema } = props - const { gutter } = schema['x-props'] - if (!editable) { - return { - margin: 0 - } - } - return { - margin: `0 ${gutter === 0 || gutter ? gutter : 10}px` - } - }} - } - .text-box-words:nth-child(1) { - margin-left: 0; - } - .text-box-field { - display: inline-block; - } - ` -) diff --git a/packages/next/src/fields/array.tsx b/packages/next/src/fields/array.tsx deleted file mode 100644 index 3221814b335..00000000000 --- a/packages/next/src/fields/array.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import React from 'react' -import { registerFormField, createArrayField } from '@uform/react' -import { Button, Icon } from '@alifd/next' -import styled from 'styled-components' - -export const CircleButton = styled['div'].attrs({ className: 'cricle-btn' })` - ${props => - !props.hasText - ? `width:30px; - height:30px;` - : ''} - margin-right:10px; - border-radius: ${props => (!props.hasText ? '100px' : 'none')}; - border: ${props => (!props.hasText ? '1px solid #eee' : 'none')}; - margin-bottom:20px; - cursor:pointer; - display: flex; - align-items: center; - justify-content: center; - line-height: 1.3; - ${props => - !props.hasText - ? `&:hover{ - background:#f7f4f4; - }` - : ''} - .next-icon{ - display:flex; - align-items:'center' - } - .op-name{ - margin-left:3px; - } -} -` - -export const ArrayField = createArrayField({ - CircleButton, - TextButton: props => ( - - ), - AddIcon: () => , - RemoveIcon: () => ( - - ), - MoveDownIcon: () => ( - - ), - MoveUpIcon: () => ( - - ) -}) - -registerFormField( - 'array', - styled( - class extends ArrayField { - render() { - const { className, name, value, renderField } = this.props - const cls = this.getProps('className') - const style = this.getProps('style') - return ( -
- {value.map((item, index) => { - return ( -
-
- {index + 1} -
-
{renderField(index)}
-
- {this.renderRemove(index, item)} - {this.renderMoveDown(index, item)} - {this.renderMoveUp(index)} -
-
- ) - })} - {this.renderEmpty()} - {value.length > 0 && this.renderAddition()} -
- ) - } - } - )` - border: 1px solid #eee; - min-width: 400px; - .array-item { - padding: 20px; - padding-bottom: 0; - padding-top: 30px; - border-bottom: 1px solid #eee; - position: relative; - &:nth-child(even) { - background: #fafafa; - } - .array-index { - position: absolute; - top: 0; - left: 0; - display: block; - span { - position: absolute; - color: #fff; - z-index: 1; - font-size: 12px; - top: 3px; - left: 3px; - } - &::after { - content: ''; - display: block; - border-top: 20px solid transparent; - border-left: 20px solid transparent; - border-bottom: 20px solid transparent; - border-right: 20px solid #888; - transform: rotate(45deg); - position: absolute; - z-index: 0; - top: -20px; - left: -20px; - } - } - .array-item-operator { - display: flex; - border-top: 1px solid #eee; - padding-top: 20px; - } - } - .array-empty-wrapper { - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - &.disabled { - cursor: default; - } - .array-empty { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - margin: 20px; - img { - display: block; - height: 80px; - } - .next-btn-text { - color: #999; - .next-icon:before { - width: 16px !important; - font-size: 16px !important; - margin-right: 3px; - } - } - } - } - .array-item-wrapper { - margin: 0 -20px; - } - .array-item-addition { - padding: 10px 20px; - background: #fbfbfb; - .next-btn-text { - color: #888; - .next-icon:before { - width: 16px !important; - font-size: 16px !important; - margin-right: 3px; - } - } - } - ` -) diff --git a/packages/next/src/fields/boolean.ts b/packages/next/src/fields/boolean.ts index c742488cbbb..db32fcda84d 100644 --- a/packages/next/src/fields/boolean.ts +++ b/packages/next/src/fields/boolean.ts @@ -1,5 +1,5 @@ -import { connect, registerFormField } from '@uform/react' -import { acceptEnum, mapStyledProps } from '../utils' +import { connect, registerFormField } from '@uform/react-schema-renderer' +import { acceptEnum, mapStyledProps } from '../shared' import { Switch } from '@alifd/next' registerFormField( diff --git a/packages/next/src/fields/cards.tsx b/packages/next/src/fields/cards.tsx index 00b886f03e4..dc62ae1d27d 100644 --- a/packages/next/src/fields/cards.tsx +++ b/packages/next/src/fields/cards.tsx @@ -1,86 +1,131 @@ import React, { Fragment } from 'react' -import { registerFormField } from '@uform/react' -import { toArr } from '@uform/utils' -import { ArrayField } from './array' +import { Icon } from '@alifd/next' +import { + registerFormField, + ISchemaFieldComponentProps, + SchemaField +} from '@uform/react-schema-renderer' +import { toArr, isFn, FormPath } from '@uform/shared' +import { ArrayList } from '@uform/react-shared-components' +import { CircleButton, TextButton } from '../components/Button' import { Card } from '@alifd/next' import styled from 'styled-components' -const FormCardsField = styled( - class extends ArrayField { - renderOperations(item, index) { - return ( - - {this.renderRemove(index, item)} - {this.renderMoveDown(index, item)} - {this.renderMoveUp(index)} - {this.renderExtraOperations(index)} - - ) - } +const ArrayComponents = { + CircleButton, + TextButton, + AdditionIcon: () => , + RemoveIcon: () => ( + + ), + MoveDownIcon: () => ( + + ), + MoveUpIcon: () => ( + + ) +} - renderCardEmpty(title) { - return ( - - {this.renderEmpty()} - - ) +const FormCardsField = styled( + (props: ISchemaFieldComponentProps & { className: string }) => { + const { value, schema, className, editable, path, mutators } = props + const { + renderAddition, + renderRemove, + renderMoveDown, + renderMoveUp, + renderEmpty, + renderExtraOperations, + ...componentProps + } = schema.getExtendsComponentProps() || {} + const onAdd = () => { + const items = Array.isArray(schema.items) + ? schema.items[schema.items.length - 1] + : schema.items + mutators.push(items.getEmptyValue()) } - - render() { - const { value, className, schema, renderField } = this.props - /* eslint-disable @typescript-eslint/no-unused-vars */ - const { - title, - style, - className: cls, - renderAddition, - renderRemove, - renderEmpty, - renderMoveDown, - renderMoveUp, - renderOperations, - ...others - } = this.getProps() || ({} as any) - - /* eslint-enable @typescript-eslint/no-unused-vars */ - return ( -
+ {toArr(value).map((item, index) => { return ( - {index + 1}. {title || schema.title} + {index + 1}. {componentProps.title || schema.title} } - className="card-list" - key={index} - contentHeight="auto" - extra={this.renderOperations(item, index)} + extra={ + + mutators.remove(index)} + /> + mutators.moveDown(index)} + /> + mutators.moveUp(index)} + /> + {isFn(renderExtraOperations) + ? renderExtraOperations(index) + : renderExtraOperations} + + } > - {renderField(index)} + ) })} - {value.length === 0 && this.renderCardEmpty(title)} -
- {value.length > 0 && this.renderAddition()} -
-
- ) - } + + {({ children }) => { + return ( + +
{children}
+
+ ) + }} +
+ + {({ children, isEmpty }) => { + if (!isEmpty) { + return ( +
+ {children} +
+ ) + } + }} +
+ +
+ ) } -)` +)` .next-card-body { padding-top: 30px; padding-bottom: 0 !important; @@ -94,42 +139,15 @@ const FormCardsField = styled( display: block; margin-bottom: 0px; background: #fff; - .array-empty-wrapper { - display: flex; - justify-content: center; - cursor: pointer; - margin-bottom: 0px; - &.disabled { - cursor: default; - } - .array-empty { - display: flex; - flex-direction: column; - margin-bottom: 20px; - img { - margin-bottom: 16px; - height: 85px; - } - .next-btn-text { - color: #888; - } - .next-icon:before { - width: 16px !important; - font-size: 16px !important; - margin-right: 5px; - } - } - } .next-card { box-shadow: none; } - .card-list { + .card-list-item { box-shadow: none; border: 1px solid #eee; } - - .array-item-addition { + .array-cards-addition { box-shadow: none; border: 1px solid #eee; transition: all 0.35s ease-in-out; @@ -137,22 +155,41 @@ const FormCardsField = styled( border: 1px solid #ccc; } } + .empty-wrapper { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 20px; + img { + margin-bottom: 16px; + height: 85px; + } + .next-btn-text { + color: #888; + } + .next-icon:before { + width: 16px !important; + font-size: 16px !important; + margin-right: 5px; + } + } } - .next-card.card-list { - margin-top: 20px; + .card-list-empty.card-list-item { + cursor: pointer; } - - .addition-wrapper .array-item-addition { + .next-card.card-list-item { margin-top: 20px; - margin-bottom: 3px; - } - .cricle-btn { - margin-bottom: 0; } + .next-card-extra { display: flex; + button { + margin-right: 8px; + } } - .array-item-addition { + .array-cards-addition { + margin-top: 20px; + margin-bottom: 3px; background: #fff; display: flex; cursor: pointer; @@ -168,9 +205,10 @@ const FormCardsField = styled( margin-right: 5px; } } - .card-list:first-child { + .card-list-item:first-child { margin-top: 0 !important; } ` registerFormField('cards', FormCardsField) +registerFormField('array', FormCardsField) diff --git a/packages/next/src/fields/checkbox.ts b/packages/next/src/fields/checkbox.ts index bdacbe002d6..0a449eca07c 100644 --- a/packages/next/src/fields/checkbox.ts +++ b/packages/next/src/fields/checkbox.ts @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Checkbox } from '@alifd/next' -import { mapStyledProps, mapTextComponent } from '../utils' +import { mapStyledProps, mapTextComponent } from '../shared' const { Group: CheckboxGroup } = Checkbox diff --git a/packages/next/src/fields/date.tsx b/packages/next/src/fields/date.ts similarity index 90% rename from packages/next/src/fields/date.tsx rename to packages/next/src/fields/date.ts index 823ea9796ac..b0f75d94cf8 100644 --- a/packages/next/src/fields/date.tsx +++ b/packages/next/src/fields/date.ts @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { DatePicker } from '@alifd/next' -import { mapStyledProps, mapTextComponent } from '../utils' +import { mapStyledProps, mapTextComponent } from '../shared' const { RangePicker, MonthPicker, YearPicker } = DatePicker diff --git a/packages/next/src/fields/index.ts b/packages/next/src/fields/index.ts new file mode 100644 index 00000000000..59c23266c82 --- /dev/null +++ b/packages/next/src/fields/index.ts @@ -0,0 +1,15 @@ +import './string' +import './number' +import './boolean' +import './date' +import './time' +import './range' +import './upload' +import './checkbox' +import './radio' +import './rating' +import './transfer' +import './cards' +import './table' +import './textarea' +import './password' \ No newline at end of file diff --git a/packages/next/src/fields/number.ts b/packages/next/src/fields/number.ts index a346a1cf4cc..ab126e4e095 100644 --- a/packages/next/src/fields/number.ts +++ b/packages/next/src/fields/number.ts @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { NumberPicker } from '@alifd/next' -import { acceptEnum, mapStyledProps, mapTextComponent } from '../utils' +import { acceptEnum, mapStyledProps, mapTextComponent } from '../shared' registerFormField( 'number', diff --git a/packages/next/src/fields/password.tsx b/packages/next/src/fields/password.tsx index 5664e29b93d..496e08c7e5b 100644 --- a/packages/next/src/fields/password.tsx +++ b/packages/next/src/fields/password.tsx @@ -1,154 +1,10 @@ import React from 'react' -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Input } from '@alifd/next' import { InputProps } from '@alifd/next/types/input' +import { PasswordStrength } from '@uform/react-shared-components' import styled from 'styled-components' -import { mapStyledProps } from '../utils' - -var isNum = function(c) { - return c >= 48 && c <= 57 -} -var isLower = function(c) { - return c >= 97 && c <= 122 -} -var isUpper = function(c) { - return c >= 65 && c <= 90 -} -var isSymbol = function(c) { - return !(isLower(c) || isUpper(c) || isNum(c)) -} -var isLetter = function(c) { - return isLower(c) || isUpper(c) -} - -const getStrength = val => { - if (!val) return 0 - let num = 0 - let lower = 0 - let upper = 0 - let symbol = 0 - let MNS = 0 - let rep = 0 - let repC = 0 - let consecutive = 0 - let sequential = 0 - const len = () => num + lower + upper + symbol - const callme = () => { - var re = num > 0 ? 1 : 0 - re += lower > 0 ? 1 : 0 - re += upper > 0 ? 1 : 0 - re += symbol > 0 ? 1 : 0 - if (re > 2 && len() >= 8) { - return re + 1 - } else { - return 0 - } - } - for (var i = 0; i < val.length; i++) { - var c = val.charCodeAt(i) - if (isNum(c)) { - num++ - if (i !== 0 && i !== val.length - 1) { - MNS++ - } - if (i > 0 && isNum(val.charCodeAt(i - 1))) { - consecutive++ - } - } else if (isLower(c)) { - lower++ - if (i > 0 && isLower(val.charCodeAt(i - 1))) { - consecutive++ - } - } else if (isUpper(c)) { - upper++ - if (i > 0 && isUpper(val.charCodeAt(i - 1))) { - consecutive++ - } - } else { - symbol++ - if (i !== 0 && i !== val.length - 1) { - MNS++ - } - } - var exists = false - for (var j = 0; j < val.length; j++) { - if (val[i] === val[j] && i !== j) { - exists = true - repC += Math.abs(val.length / (j - i)) - } - } - if (exists) { - rep++ - var unique = val.length - rep - repC = unique ? Math.ceil(repC / unique) : Math.ceil(repC) - } - if (i > 1) { - var last1 = val.charCodeAt(i - 1) - var last2 = val.charCodeAt(i - 2) - if (isLetter(c)) { - if (isLetter(last1) && isLetter(last2)) { - var v = val.toLowerCase() - var vi = v.charCodeAt(i) - var vi1 = v.charCodeAt(i - 1) - var vi2 = v.charCodeAt(i - 2) - if (vi - vi1 === vi1 - vi2 && Math.abs(vi - vi1) === 1) { - sequential++ - } - } - } else if (isNum(c)) { - if (isNum(last1) && isNum(last2)) { - if (c - last1 === last1 - last2 && Math.abs(c - last1) === 1) { - sequential++ - } - } - } else { - if (isSymbol(last1) && isSymbol(last2)) { - if (c - last1 === last1 - last2 && Math.abs(c - last1) === 1) { - sequential++ - } - } - } - } - } - let sum = 0 - let length = len() - sum += 4 * length - if (lower > 0) { - sum += 2 * (length - lower) - } - if (upper > 0) { - sum += 2 * (length - upper) - } - if (num !== length) { - sum += 4 * num - } - sum += 6 * symbol - sum += 2 * MNS - sum += 2 * callme() - if (length === lower + upper) { - sum -= length - } - if (length === num) { - sum -= num - } - sum -= repC - sum -= 2 * consecutive - sum -= 3 * sequential - sum = sum < 0 ? 0 : sum - sum = sum > 100 ? 100 : sum - - if (sum >= 80) { - return 100 - } else if (sum >= 60) { - return 80 - } else if (sum >= 40) { - return 60 - } else if (sum >= 20) { - return 40 - } else { - return 20 - } -} +import { mapStyledProps } from '../shared' export interface IPasswordProps extends InputProps { checkStrength: boolean @@ -158,7 +14,6 @@ const Password = styled( class Password extends React.Component { state = { value: this.props.value || this.props.defaultValue, - strength: 0, eye: false } @@ -168,41 +23,44 @@ const Password = styled( this.props.value !== this.state.value ) { this.setState({ - value: this.props.value, - strength: getStrength(this.props.value) + value: this.props.value }) } } - onChangeHandler = (value, e) => { + onChangeHandler = (value) => { this.setState( { - value, - strength: getStrength(value) + value }, () => { if (this.props.onChange) { - this.props.onChange(value, e) + this.props.onChange(value,value) } } ) } renderStrength() { - const { strength } = this.state return ( -
-
-
-
-
-
-
+ + {score => { + return ( +
+
+
+
+
+
+
+ ) + }} + ) } diff --git a/packages/next/src/fields/radio.ts b/packages/next/src/fields/radio.ts index 5057f842427..319a718f1f7 100644 --- a/packages/next/src/fields/radio.ts +++ b/packages/next/src/fields/radio.ts @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Radio } from '@alifd/next' -import { mapStyledProps, mapTextComponent } from '../utils' +import { mapStyledProps, mapTextComponent } from '../shared' const { Group: RadioGroup } = Radio diff --git a/packages/next/src/fields/range.ts b/packages/next/src/fields/range.ts index e22a7780209..5398fa152ba 100644 --- a/packages/next/src/fields/range.ts +++ b/packages/next/src/fields/range.ts @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Range } from '@alifd/next' -import { mapStyledProps } from '../utils' +import { mapStyledProps } from '../shared' registerFormField( 'range', diff --git a/packages/next/src/fields/rating.ts b/packages/next/src/fields/rating.ts index bfc8be1771d..6b0473a63a3 100644 --- a/packages/next/src/fields/rating.ts +++ b/packages/next/src/fields/rating.ts @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Rating } from '@alifd/next' -import { mapStyledProps } from '../utils' +import { mapStyledProps } from '../shared' registerFormField( 'rating', diff --git a/packages/next/src/fields/string.ts b/packages/next/src/fields/string.ts index ab3e0bde5f6..e0139740ed1 100644 --- a/packages/next/src/fields/string.ts +++ b/packages/next/src/fields/string.ts @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Input } from '@alifd/next' -import { acceptEnum, mapStyledProps, mapTextComponent } from '../utils' +import { acceptEnum, mapStyledProps, mapTextComponent } from '../shared' registerFormField( 'string', diff --git a/packages/next/src/fields/table.tsx b/packages/next/src/fields/table.tsx index d090ce7adc4..131a09bfaa8 100644 --- a/packages/next/src/fields/table.tsx +++ b/packages/next/src/fields/table.tsx @@ -1,358 +1,181 @@ -import React, { Component } from 'react' -import { registerFormField } from '@uform/react' -import { isFn, toArr } from '@uform/utils' -import { ArrayField } from './array' +import React from 'react' +import { Icon } from '@alifd/next' +import { + registerFormField, + ISchemaFieldComponentProps, + SchemaField, + Schema +} from '@uform/react-schema-renderer' +import { toArr, isFn, isArr, FormPath } from '@uform/shared' +import { ArrayList } from '@uform/react-shared-components' +import { CircleButton, TextButton } from '../components/Button' +import { Table, Form } from '@alifd/next' import styled from 'styled-components' - -/** - * 轻量级Table,用next table实在太重了 - **/ - -export interface IColumnProps { - title?: string - dataIndex?: string - width?: string | number - cell: (item?: any, index?: number) => React.ReactElement +import { FormItemProps } from '../compat/FormItem' +const ArrayComponents = { + CircleButton, + TextButton, + AdditionIcon: () => , + RemoveIcon: () => ( + + ), + MoveDownIcon: () => ( + + ), + MoveUpIcon: () => ( + + ) } -export interface ITableProps { - className?: string - dataSource: any[] -} - -class Column extends Component { - static displayName = '@schema-table-column' - render() { - return this.props.children - } -} - -const Table = styled( - class Table extends Component { - renderCell({ record, col, rowIndex }) { - return ( -
- {isFn(col.cell) - ? col.cell( - record ? record[col.dataIndex] : undefined, - rowIndex, - record +const FormTableField = styled( + (props: ISchemaFieldComponentProps & { className: string }) => { + const { value, schema, className, editable, path, mutators } = props + const { + renderAddition, + renderRemove, + renderMoveDown, + renderMoveUp, + renderEmpty, + renderExtraOperations, + operations, + ...componentProps + } = schema.getExtendsComponentProps() || {} + const onAdd = () => { + const items = Array.isArray(schema.items) + ? schema.items[schema.items.length - 1] + : schema.items + mutators.push(items.getEmptyValue()) + } + const renderColumns = (items: Schema) => { + return items.mapProperties((props, key) => { + const itemProps = { + ...props.getExtendsItemProps(), + ...props.getExtendsProps() + } + return ( + { + return ( + + + ) - : record - ? record[col.dataIndex] - : undefined} -
- ) + }} + /> + ) + }) + return [] } - - renderTable(columns, dataSource) { - return ( -
- - - - {columns.map((col, index) => { - return ( - - ) - })} - - - - {dataSource.map((record, rowIndex) => { + return ( +
+ +
-
{col.title}
-
+ {isArr(schema.items) + ? schema.items.reduce((buf, items) => { + return buf.concat(renderColumns(items)) + }, []) + : renderColumns(schema.items)} + { return ( - - {columns.map((col, colIndex) => { - return ( - - ) - })} - + +
+ mutators.remove(index)} + /> + mutators.moveDown(index)} + /> + mutators.moveUp(index)} + /> + {isFn(renderExtraOperations) + ? renderExtraOperations(index) + : renderExtraOperations} +
+
) - })} - {this.renderPlacehodler(dataSource, columns)} - -
- {this.renderCell({ - record, - col, - rowIndex - })} -
-
- ) - } - - renderPlacehodler(dataSource, columns) { - if (dataSource.length === 0) { - return ( - - -
- -
- - - ) + }} + /> + + + {({ children }) => { + return ( +
+ {children} +
+ ) + }} +
+ +
+ ) + } +)` + display: inline-block; + min-width: 600px; + max-width: 100%; + overflow: scroll; + table { + margin-bottom: 0 !important; + th, + td { + padding: 0 !important; + vertical-align: top; + .next-form-item { + margin-bottom: 0 !important; } } - - getColumns(children) { - const columns: IColumnProps[] = [] - React.Children.forEach>( - children, - child => { - if (React.isValidElement(child)) { - if ( - child.type === Column || - child.type.displayName === '@schema-table-column' - ) { - columns.push(child.props) - } - } - } - ) - - return columns + } + .array-table-addition { + padding: 10px; + background: #fbfbfb; + border-left: 1px solid #dcdee3; + border-right: 1px solid #dcdee3; + border-bottom: 1px solid #dcdee3; + .next-btn-text { + color: #888; } - - render() { - const columns = this.getColumns(this.props.children) - const dataSource = toArr(this.props.dataSource) - return ( -
-
-
- {this.renderTable(columns, dataSource)} -
-
-
- ) + .next-icon:before { + width: 16px !important; + font-size: 16px !important; + margin-right: 5px; } - } -)` - .next-table { - position: relative; + margin-bottom: 10px; } - .next-table, - .next-table *, - .next-table :after, - .next-table :before { - -webkit-box-sizing: border-box; - box-sizing: border-box; - } - - .next-table table { - border-collapse: collapse; - border-spacing: 0; - width: 100%; - background: #fff; - display: table !important; - margin: 0 !important; - } - - .next-table table tr:first-child td { - border-top-width: 0; - } - - .next-table th { - padding: 0; - background: #ebecf0; - color: #333; - text-align: left; - font-weight: 400; - min-width: 200px; - border: 1px solid #dcdee3; - } - - .next-table th .next-table-cell-wrapper { - padding: 12px 16px; - overflow: hidden; - text-overflow: ellipsis; - word-break: break-all; - } - - .next-table td { - padding: 0; - border: 1px solid #dcdee3; - } - - .next-table td .next-table-cell-wrapper { - padding: 12px 16px; - overflow: hidden; - text-overflow: ellipsis; - word-break: break-all; + .array-item-operator { display: flex; - } - - .next-table.zebra tr:nth-child(odd) td { - background: #fff; - } - - .next-table.zebra tr:nth-child(2n) td { - background: #f7f8fa; - } - - .next-table-empty { - color: #a0a2ad; - padding: 32px 0; - text-align: center; - } - - .next-table-row { - -webkit-transition: all 0.3s ease; - transition: all 0.3s ease; - background: #fff; - color: #333; - border: none !important; - } - - .next-table-row.hidden { - display: none; - } - - .next-table-row.hovered, - .next-table-row.selected { - background: #f2f3f7; - color: #333; - } - - .next-table-body, - .next-table-header { - overflow: auto; - font-size: 12px; - } - - .next-table-body { - font-size: 12px; + align-items: center; + button { + margin-right: 8px; + } } ` -registerFormField( - 'table', - styled( - class extends ArrayField { - createFilter(key, payload) { - const { schema } = this.props - const columnFilter: (key: string, payload: any) => boolean = - schema['x-props'] && schema['x-props'].columnFilter - - return (render, otherwise) => { - if (isFn(columnFilter)) { - return columnFilter(key, payload) - ? isFn(render) - ? render() - : render - : isFn(otherwise) - ? otherwise() - : otherwise - } else { - return render() - } - } - } - - render() { - const { - value, - schema, - locale, - className, - renderField, - getOrderProperties - } = this.props - const cls = this.getProps('className') - const style = this.getProps('style') - const operationsWidth = this.getProps('operationsWidth') - return ( -
-
- - {getOrderProperties(schema.items).reduce( - (buf, { key, schema }) => { - const filter = this.createFilter(key, schema) - const res = filter( - () => { - return buf.concat( - { - return renderField([index, key]) - }} - /> - ) - }, - () => { - return buf - } - ) - return res - }, - [] - )} - { - return ( -
- {this.renderRemove(index, item)} - {this.renderMoveDown(index, item)} - {this.renderMoveUp(index)} - {this.renderExtraOperations(index)} -
- ) - }} - /> -
- {this.renderAddition()} -
-
- ) - } - } - )` - display: inline-block; - .array-item-addition { - padding: 10px; - background: #fbfbfb; - border-left: 1px solid #dcdee3; - border-right: 1px solid #dcdee3; - border-bottom: 1px solid #dcdee3; - .next-btn-text { - color: #888; - } - .next-icon:before { - width: 16px !important; - font-size: 16px !important; - margin-right: 5px; - } - } - - .next-table-cell-wrapper > .next-form-item { - margin-bottom: 0; - } - .array-item-operator { - display: flex; - } - ` -) +registerFormField('table', FormTableField) diff --git a/packages/next/src/fields/textarea.ts b/packages/next/src/fields/textarea.ts index c65ce5616f2..ed33de7a648 100644 --- a/packages/next/src/fields/textarea.ts +++ b/packages/next/src/fields/textarea.ts @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Input } from '@alifd/next' -import { acceptEnum, mapStyledProps, mapTextComponent } from '../utils' +import { acceptEnum, mapStyledProps, mapTextComponent } from '../shared' const { TextArea } = Input diff --git a/packages/next/src/fields/time.ts b/packages/next/src/fields/time.ts index 6a046c54a39..dd15c25b647 100644 --- a/packages/next/src/fields/time.ts +++ b/packages/next/src/fields/time.ts @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { TimePicker } from '@alifd/next' -import { mapStyledProps, mapTextComponent } from '../utils' +import { mapStyledProps, mapTextComponent } from '../shared' const transformMoment = (value, format = 'YYYY-MM-DD HH:mm:ss') => { return value && value.format ? value.format(format) : value diff --git a/packages/next/src/fields/transfer.ts b/packages/next/src/fields/transfer.ts index 006f5473cab..b38c0a9842e 100644 --- a/packages/next/src/fields/transfer.ts +++ b/packages/next/src/fields/transfer.ts @@ -1,6 +1,6 @@ -import { connect, registerFormField } from '@uform/react' +import { connect, registerFormField } from '@uform/react-schema-renderer' import { Transfer } from '@alifd/next' -import { mapStyledProps } from '../utils' +import { mapStyledProps } from '../shared' registerFormField( 'transfer', diff --git a/packages/next/src/fields/upload.tsx b/packages/next/src/fields/upload.tsx index 887c816a759..cb9f9905cb9 100644 --- a/packages/next/src/fields/upload.tsx +++ b/packages/next/src/fields/upload.tsx @@ -1,6 +1,6 @@ import React from 'react' -import { connect, registerFormField } from '@uform/react' -import { toArr, isArr, isEqual, mapStyledProps } from '../utils' +import { connect, registerFormField } from '@uform/react-schema-renderer' +import { toArr, isArr, isEqual, mapStyledProps } from '../shared' import { Button, Upload } from '@alifd/next' import { UploadProps, CardProps } from '@alifd/next/types/upload' const { Card: UploadCard, Dragger: UploadDragger } = Upload diff --git a/packages/next/src/form.tsx b/packages/next/src/form.tsx deleted file mode 100644 index 50cc06be6dc..00000000000 --- a/packages/next/src/form.tsx +++ /dev/null @@ -1,434 +0,0 @@ -import React from 'react' -import classNames from 'classnames' -import styled from 'styled-components' -import { ConfigProvider, Balloon, Icon, Grid } from '@alifd/next' -import { registerFormWrapper, registerFieldMiddleware } from '@uform/react' -import { IFormItemProps, IFormProps } from '@uform/types' - -import LOCALE from './locale' -import { isFn, moveTo, isStr, stringLength } from './utils' - -/** - * 轻量级Next Form,不包含任何数据管理能力 - */ - -const { Row, Col } = Grid - -export const { - Provider: FormLayoutProvider, - Consumer: FormLayoutConsumer -} = React.createContext(undefined) - -const normalizeCol = col => { - return typeof col === 'object' ? col : { span: col } -} - -const getParentNode = (node, selector) => { - if (!node || (node && !node.matches)) return - if (node.matches(selector)) return node - else { - return getParentNode(node.parentNode || node.parentElement, selector) - } -} - -const isPopDescription = (description, maxTipsNum = 30) => { - if (isStr(description)) { - return stringLength(description) > maxTipsNum - } else { - return React.isValidElement(description) - } -} - -export const FormItem = styled( - class FormItem extends React.Component { - static defaultProps = { - prefix: 'next-' - } - - private getItemLabel() { - const { - id, - required, - label, - labelCol, - wrapperCol, - prefix, - extra, - labelAlign, - labelTextAlign, - autoAddColon, - isTableColItem, - maxTipsNum - } = this.props - - if (!label || isTableColItem) { - return null - } - - const ele = ( - // @ts-ignore - - ) - - const cls = classNames({ - [`${prefix}form-item-label`]: true, - [`${prefix}left`]: labelTextAlign === 'left' - }) - - if ((wrapperCol || labelCol) && labelAlign !== 'top') { - return ( - - {ele} - {isPopDescription(extra, maxTipsNum) && this.renderHelper()} - - ) - } - - return ( -
- {ele} - {isPopDescription(extra, maxTipsNum) && this.renderHelper()} -
- ) - } - - private getItemWrapper() { - const { - labelCol, - wrapperCol, - children, - extra, - label, - labelAlign, - help, - size, - prefix, - noMinHeight, - isTableColItem, - maxTipsNum - } = this.props - - const message = ( -
- {help &&
{help}
} - {!help && !isPopDescription(extra, maxTipsNum) && ( -
{extra}
- )} -
- ) - if ( - (wrapperCol || labelCol) && - labelAlign !== 'top' && - !isTableColItem && - label - ) { - return ( - - {React.cloneElement(children, { size })} - {message} - - ) - } - - return ( -
- {React.cloneElement(children, { size })} - {message} -
- ) - } - - private renderHelper() { - return ( - } - > - {this.props.extra} - - ) - } - - public render() { - /* eslint-disable @typescript-eslint/no-unused-vars */ - const { - className, - labelAlign, - labelTextAlign, - style, - prefix, - wrapperCol, - labelCol, - size, - help, - extra, - noMinHeight, - isTableColItem, - validateState, - autoAddColon, - required, - maxTipsNum, - type, - schema, - ...others - } = this.props - - /* eslint-enable @typescript-eslint/no-unused-vars */ - const itemClassName = classNames({ - [`${prefix}form-item`]: true, - [`${prefix}${labelAlign}`]: labelAlign, - [`has-${validateState}`]: !!validateState, - [`${prefix}${size}`]: !!size, - [`${className}`]: !!className, - [`field-${type}`]: !!type - }) - - // 垂直模式并且左对齐才用到 - const Tag = (wrapperCol || labelCol) && labelAlign !== 'top' ? Row : 'div' - const label = labelAlign === 'inset' ? null : this.getItemLabel() - - return ( - - {label} - {this.getItemWrapper()} - - ) - } - } -)` - margin-bottom: 4px !important; - &.field-table { - .next-form-item-control { - overflow: auto; - } - } - .next-form-item-msg { - &.next-form-item-space { - min-height: 20px; - .next-form-item-help, - .next-form-item-extra { - margin-top: 0; - } - } - } - .next-form-item-extra { - color: #888; - font-size: 12px; - line-height: 1.7; - } -` - -const toArr = val => (Array.isArray(val) ? val : val ? [val] : []) - -registerFormWrapper(OriginForm => { - OriginForm = styled(OriginForm)` - &.next-inline { - display: flex; - .rs-uform-content { - margin-right: 15px; - } - } - .next-radio-group, - .next-checkbox-group { - line-height: 28px; - & > label { - margin-right: 8px; - } - } - .next-small { - .next-radio-group, - .next-checkbox-group { - line-height: 20px; - } - } - .next-small { - .next-radio-group, - .next-checkbox-group { - line-height: 40px; - } - } - .next-card-head { - background: none; - } - .next-rating-medium { - min-height: 28px; - line-height: 28px; - } - .next-rating-small { - min-height: 20px; - line-height: 20px; - } - .next-rating-large { - min-height: 40px; - line-height: 40px; - } - ` - - return ConfigProvider.config( - class Form extends React.Component { - static defaultProps = { - component: 'form', - prefix: 'next-', - size: 'medium', - labelAlign: 'left', - locale: LOCALE, - autoAddColon: true - } - - static displayName = 'SchemaForm' - - FormRef = React.createRef() - - validateFailedHandler(onValidateFailed) { - return (...args) => { - if (isFn(onValidateFailed)) { - onValidateFailed(...args) - } - const container = this.FormRef.current as HTMLElement - if (container) { - const errors = container.querySelectorAll('.next-form-item-help') - if (errors && errors.length) { - const node = getParentNode(errors[0], '.next-form-item') - if (node) { - moveTo(node) - } - } - } - } - } - - render() { - const { - className, - inline, - size, - labelAlign, - labelTextAlign, - autoAddColon, - children, - labelCol, - wrapperCol, - style, - prefix, - maxTipsNum, - ...others - } = this.props - - const formClassName = classNames({ - [`${prefix}form`]: true, - [`${prefix}inline`]: inline, // 内联 - [`${prefix}${size}`]: size, - [className]: !!className - }) - return ( - - - {children} - - - ) - } - }, - {} - ) -}) - -const isTableColItem = (path, getSchema) => { - const schema = getSchema(path) - return schema && schema.type === 'array' && schema['x-component'] === 'table' -} - -registerFieldMiddleware(Field => { - return props => { - const { - name, - editable, - errors, - path, - schemaPath, - schema, - getSchema, - required - } = props - - if (path.length === 0) { - // 根节点是不需要包FormItem的 - return React.createElement(Field, props) - } - - return React.createElement( - FormLayoutConsumer, - {}, - ({ - labelAlign, - labelTextAlign, - labelCol, - wrapperCol, - maxTipsNum, - size, - autoAddColon - }) => { - return React.createElement( - FormItem, - { - labelAlign, - labelTextAlign, - labelCol, - wrapperCol, - autoAddColon, - maxTipsNum, - size, - ...schema['x-item-props'], - label: schema.title, - noMinHeight: schema.type === 'object' && !schema['x-component'], - isTableColItem: isTableColItem( - schemaPath.slice(0, schemaPath.length - 2), - getSchema - ), - type: schema['x-component'] || schema['type'], - id: name, - validateState: toArr(errors).length ? 'error' : undefined, - required: editable === false ? false : required, - extra: schema.description, - help: - toArr(errors).join(' , ') || - (schema['x-item-props'] && schema['x-item-props'].help) - }, - React.createElement(Field, props) - ) - } - ) - } -}) diff --git a/packages/next/src/index.tsx b/packages/next/src/index.tsx index b47f2e0a123..27c4fac3fc1 100644 --- a/packages/next/src/index.tsx +++ b/packages/next/src/index.tsx @@ -1,45 +1,47 @@ -import './form' -import './fields/string' -import './fields/number' -import './fields/boolean' -import './fields/date' -import './fields/time' -import './fields/range' -import './fields/upload' -import './fields/checkbox' -import './fields/radio' -import './fields/rating' -import './fields/transfer' -import './fields/array' -import './fields/cards' -import './fields/table' -import './fields/textarea' -import './fields/password' - -export * from '@uform/react' -export * from './components/formButtonGroup' -export * from './components/button' -export * from './components/layout' - -import React from 'react' +import React, { useRef } from 'react' import { - SchemaForm as InternalSchemaForm, - Field as InternalField -} from '@uform/react' -import { SchemaFormProps, FieldProps } from './type' - -export { mapStyledProps, mapTextComponent } from './utils' - -export default class SchemaForm extends React.Component> { - render() { - return - } -} + SchemaMarkupForm, + SchemaMarkupField +} from '@uform/react-schema-renderer' +import { INextSchemaFormProps, INextSchemaFieldProps } from './types' +import './fields' +import './compat' +export * from '@uform/react-schema-renderer' +export * from './components' +export * from './types' +export { mapStyledProps, mapTextComponent } from './shared' +export const SchemaForm: React.FC = props => { + const formRef = useRef() -export class Field extends React.Component< - FieldProps -> { - render() { - return - } + return ( +
+ { + if (props.onValidateFailed) { + props.onValidateFailed(result) + } + if (formRef.current) { + setTimeout(() => { + const elements = formRef.current.querySelectorAll( + '.next-form-item.has-error' + ) + if (elements && elements.length) { + if (!elements[0].scrollIntoView) return + elements[0].scrollIntoView({ + behavior: 'smooth', + inline: 'center', + block: 'center' + }) + } + }, 30) + } + }} + > + {props.children} + +
+ ) } +export const Field: React.FC = SchemaMarkupField +export default SchemaForm diff --git a/packages/next/src/locale.ts b/packages/next/src/locale.ts deleted file mode 100644 index aa28ff56c6e..00000000000 --- a/packages/next/src/locale.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable @typescript-eslint/camelcase */ -export default { - addItem: '添加', - array_invalid_minItems: '条目数不允许小于%s条', - array_invalid_maxItems: '条目数不允许大于%s条', - operations: '操作' -} diff --git a/packages/next/src/shared.ts b/packages/next/src/shared.ts new file mode 100644 index 00000000000..fee454143d7 --- /dev/null +++ b/packages/next/src/shared.ts @@ -0,0 +1,68 @@ +import React from 'react' +import { Select } from '@alifd/next' +import { PreviewText } from '@uform/react-shared-components' +import { + MergedFieldComponentProps, + IConnectProps +} from '@uform/react-schema-renderer' +export * from '@uform/shared' + +export const mapTextComponent = ( + Target: React.JSXElementConstructor, + props: any = {}, + fieldProps: any = {} +): React.JSXElementConstructor => { + const { editable } = fieldProps + if (editable !== undefined) { + if (editable === false) { + return PreviewText + } + } + if (Array.isArray(props.dataSource)) { + return Select + } + return Target +} + +export const acceptEnum = (component: React.JSXElementConstructor) => { + return ({ dataSource, ...others }) => { + if (dataSource) { + return React.createElement(Select, { dataSource, ...others }) + } else { + return React.createElement(component, others) + } + } +} + +export const normalizeCol = ( + col: { span: number; offset?: number } | number, + defaultValue?: { span: number } +): { span: number; offset?: number } => { + if (!col) { + return defaultValue + } else { + return typeof col === 'object' ? col : { span: Number(col) } + } +} + +export const mapStyledProps = ( + props: IConnectProps, + fieldProps: MergedFieldComponentProps +) => { + const { loading, errors, warnings } = fieldProps + if (loading) { + props.state = props.state || 'loading' + } else if (errors && errors.length) { + props.state = 'error' + } else if (warnings && warnings.length) { + props.state = 'warning' + } +} + +export const compose = (...args: any[]) => { + return (payload: any, ...extra: any[]) => { + return args.reduce((buf, fn) => { + return buf !== undefined ? fn(buf, ...extra) : fn(payload, ...extra) + }, payload) + } +} diff --git a/packages/next/src/type.tsx b/packages/next/src/type.tsx deleted file mode 100644 index 24bae70f004..00000000000 --- a/packages/next/src/type.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { ButtonProps } from '@alifd/next/types/button' -import { CardProps } from '@alifd/next/types/card' -import { RowProps, ColProps } from '@alifd/next/types/grid' -import { - IFormActions, - ISchema, - IEffects, - IFieldError, - Size, - TextAlign, - Layout, - TextEl, - LabelAlign -} from '@uform/types' -import { SwitchProps } from '@alifd/next/types/switch' -import { GroupProps as CheckboxGroupProps } from '@alifd/next/types/checkbox' -import { GroupProps as RadioGroupProps } from '@alifd/next/types/radio' -import { - DatePickerProps, - RangePickerProps, - MonthPickerProps, - YearPickerProps -} from '@alifd/next/types/date-picker' -import { NumberPickerProps } from '@alifd/next/types/number-picker' -import { IPasswordProps } from './fields/password' -import { RangeProps } from '@alifd/next/types/range' -import { RatingProps } from '@alifd/next/types/rating' -import { InputProps, TextAreaProps } from '@alifd/next/types/input' -import { TimePickerProps } from '@alifd/next/types/time-picker' -import { TransferProps } from '@alifd/next/types/transfer' -import { IUploaderProps } from './fields/upload' -import { SelectProps } from '@alifd/next/types/select' - -type ColSpanType = number | string - -export interface ColSize { - span?: ColSpanType - offset?: ColSpanType -} - -export interface ILocaleMessages { - [key: string]: string | ILocaleMessages -} - -export interface IFormLayoutProps { - className?: string - inline?: boolean - labelAlign?: LabelAlign - wrapperCol?: IColProps | number - labelCol?: IColProps | number - labelTextAlign?: TextAlign - size?: Size - style?: React.CSSProperties -} - -export interface IFormItemGridProps { - cols?: Array - description?: TextEl - gutter?: number - title?: TextEl -} - -export type TFormCardOrFormBlockProps = Omit - -export interface IFormTextBox { - text?: string - title?: TextEl - description?: TextEl - gutter?: number -} - -export interface IRowProps extends RowProps { - prefix?: string - pure?: boolean - className?: string - style?: object -} - -export interface IColProps extends ColProps { - prefix?: string - pure?: boolean - className?: string -} - -export interface IFormCardProps extends CardProps { - className?: string -} - -export interface IFormBlockProps extends CardProps { - className?: string -} - -export interface ISubmitProps extends Omit { - showLoading?: boolean -} - -export interface IFormButtonGroupProps { - sticky?: boolean - style?: React.CSSProperties - itemStyle?: React.CSSProperties - className?: string - align?: 'left' | 'right' | 'start' | 'end' | 'top' | 'bottom' | 'center' - triggerDistance?: number - zIndex?: number - span?: ColSpanType - offset?: ColSpanType -} - -export interface SchemaFormProps { - actions?: IFormActions - initialValues?: V - defaultValue?: V - value?: V - editable?: boolean | ((name: string) => boolean) - effects?: IEffects - locale?: ILocaleMessages - schema?: ISchema - onChange?: (values: V) => void - onReset?: (values: V) => void - onSubmit?: (values: V) => void - onValidateFailed?: (fieldErrors: IFieldError[]) => void - autoAddColon?: boolean - className?: string - inline?: boolean - layout?: Layout - maxTipsNum?: number - labelAlign?: LabelAlign - labelTextAlign?: TextAlign - labelCol?: ColSize | number - wrapperCol?: ColSize | number - size?: Size - style?: React.CSSProperties - prefix?: string -} - -interface InternalFieldTypes { - boolean: SwitchProps | SelectProps - checkbox: CheckboxGroupProps - date: DatePickerProps - daterange: RangePickerProps - month: MonthPickerProps - // week: WeekPickerProps - year: YearPickerProps - number: NumberPickerProps | SelectProps - password: IPasswordProps - radio: RadioGroupProps - range: RangeProps - rating: RatingProps - string: InputProps | SelectProps - textarea: TextAreaProps | SelectProps - time: TimePickerProps - transfer: TransferProps - upload: IUploaderProps -} -export interface FieldProps extends ISchema { - type?: T - name?: string - editable?: boolean - ['x-props']?: T extends keyof InternalFieldTypes ? InternalFieldTypes[T] : any -} diff --git a/packages/next/src/types.ts b/packages/next/src/types.ts new file mode 100644 index 00000000000..0a0866a88c2 --- /dev/null +++ b/packages/next/src/types.ts @@ -0,0 +1,97 @@ +import { ButtonProps } from '@alifd/next/types/button' +import { FormProps, ItemProps } from '@alifd/next/types/form' +import { StepProps, ItemProps as StepItemProps } from '@alifd/next/types/step' +import { + ISchemaFormProps, + IMarkupSchemaFieldProps, + ISchemaFieldComponentProps +} from '@uform/react-schema-renderer' +import { PreviewTextConfigProps } from '@uform/react-shared-components' +import { StyledComponent } from 'styled-components' + +type ColSpanType = number | string + +export type INextSchemaFormProps = FormProps & + IFormItemTopProps & + PreviewTextConfigProps & + ISchemaFormProps + +export type INextSchemaFieldProps = IMarkupSchemaFieldProps + +export interface ISubmitProps extends ButtonProps { + onSubmit?: ISchemaFormProps['onSubmit'] + showLoading?: boolean +} + +export interface IResetProps extends ButtonProps { + forceClear?: boolean + validate?: boolean +} + +export type IFormItemTopProps = React.PropsWithChildren< + Exclude< + Pick< + ItemProps, + | 'prefix' + | 'labelCol' + | 'wrapperCol' + | 'labelAlign' + | 'labelTextAlign' + | 'size' + >, + 'labelCol' | 'wrapperCol' + > & { + inline?: boolean + className?: string + style?: React.CSSProperties + labelCol?: number | { span: number; offset?: number } + wrapperCol?: number | { span: number; offset?: number } + } +> + +export interface ICompatItemProps + extends Omit, + Partial { + labelCol?: number | { span: number; offset?: number } + wrapperCol?: number | { span: number; offset?: number } +} + +export type StyledCP

= StyledComponent< + (props: React.PropsWithChildren

) => React.ReactElement, + any, + {}, + never +> + +export type StyledCC = StyledCP & Statics + +export interface IFormButtonGroupProps { + sticky?: boolean + style?: React.CSSProperties + itemStyle?: React.CSSProperties + className?: string + align?: 'left' | 'right' | 'start' | 'end' | 'top' | 'bottom' | 'center' + triggerDistance?: number + zIndex?: number + span?: ColSpanType + offset?: ColSpanType +} + +export interface IItemProps { + title?: React.ReactText + description?: React.ReactText +} + +export interface IFormItemGridProps extends IItemProps { + cols?: Array + gutter?: number +} + +export interface IFormTextBox extends IItemProps { + text?: string + gutter?: number +} + +export interface IFormStep extends StepProps { + dataSource: StepItemProps[] +} diff --git a/packages/next/src/utils.tsx b/packages/next/src/utils.tsx deleted file mode 100644 index b3e0e1e8b65..00000000000 --- a/packages/next/src/utils.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import React from 'react' -import { Select } from '@alifd/next' -import styled from 'styled-components' -import { isFn } from '@uform/utils' -import { IConnectProps, IFieldProps } from '@uform/react' - -export * from '@uform/utils' - -const MoveTo = typeof window !== 'undefined' ? require('moveto') : null -const Text = styled(props => { - let value - if (props.dataSource && props.dataSource.length) { - let find = props.dataSource.filter(({ value }) => - Array.isArray(props.value) - ? props.value.some(val => val == value) - : props.value == value - ) - value = find.reduce((buf, item, index) => { - return buf.concat(item.label, index < find.length - 1 ? ', ' : '') - }, []) - } else { - value = Array.isArray(props.value) - ? props.value.join(' ~ ') - : String( - props.value === undefined || props.value === null ? '' : props.value - ) - } - return ( -

- {!value ? 'N/A' : value} - {props.innerAfter ? ' ' + props.innerAfter : ''} - {props.addonTextAfter ? ' ' + props.addonTextAfter : ''} - {props.addonAfter ? ' ' + props.addonAfter : ''} -
- ) -})` - height: 28px; - line-height: 28px; - vertical-align: middle; - font-size: 13px; - color: #333; - &.small { - height: 20px; - line-height: 20px; - } - &.large { - height: 40px; - line-height: 40px; - } -` - -export const acceptEnum = component => { - return ({ dataSource, ...others }) => { - if (dataSource) { - return React.createElement(Select, { dataSource, ...others }) - } else { - return React.createElement(component, others) - } - } -} - -export const mapStyledProps = ( - props: IConnectProps, - { loading, size, errors }: IFieldProps -) => { - if (loading) { - props.state = props.state || 'loading' - } else if (errors && errors.length) { - props.state = 'error' - } - if (size) { - props.size = size - } -} - -export const mapTextComponent = ( - Target: React.ComponentClass, - props, - { - editable, - name - }: { editable: boolean | ((name: string) => boolean); name: string } -): React.ComponentClass => { - if (editable !== undefined) { - if (isFn(editable)) { - if (!editable(name)) { - return Text - } - } else if (editable === false) { - return Text - } - } - return Target -} - -export const compose = (...args) => { - return (payload, ...extra) => { - return args.reduce((buf, fn) => { - return buf !== undefined ? fn(buf, ...extra) : fn(payload, ...extra) - }, payload) - } -} - -export const moveTo = element => { - if (!element || !MoveTo) return - if (element.scrollIntoView) { - element.scrollIntoView({ - behavior: 'smooth', - inline: 'start', - block: 'start' - }) - } else { - new MoveTo().move(element.getBoundingClientRect().top) - } -} diff --git a/packages/next/tsconfig.json b/packages/next/tsconfig.json index 1d669c29c46..7d101da7c66 100644 --- a/packages/next/tsconfig.json +++ b/packages/next/tsconfig.json @@ -3,6 +3,6 @@ "compilerOptions": { "outDir": "./lib" }, - "include": ["./src/**/*.ts", "./src/**/*.tsx"], + "include": ["./src/**/*.js", "./src/**/*.ts", "./src/**/*.tsx"], "exclude": ["./src/__tests__/*"] } diff --git a/packages/printer/package.json b/packages/printer/package.json index c52017673f7..7d489220131 100644 --- a/packages/printer/package.json +++ b/packages/printer/package.json @@ -1,6 +1,6 @@ { "name": "@uform/printer", - "version": "0.4.4", + "version": "1.0.0-alpha.5", "license": "MIT", "main": "lib", "repository": { @@ -26,7 +26,7 @@ "typescript": "^3.5.2" }, "dependencies": { - "@uform/react": "^0.4.4", + "@uform/react-schema-renderer": "^1.0.0-alpha.5", "react-modal": "^3.8.1", "styled-components": "^4.1.1" }, diff --git a/packages/printer/src/index.js b/packages/printer/src/index.js index 577a44ab55d..a0b9a71ce6a 100644 --- a/packages/printer/src/index.js +++ b/packages/printer/src/index.js @@ -1,6 +1,6 @@ import React, { useState } from 'react' import ReactDOM from 'react-dom' -import { createFormActions } from '@uform/react' +import { createFormActions } from '@uform/react-schema-renderer' import styled from 'styled-components' import Modal from 'react-modal' diff --git a/packages/react-schema-editor/README.md b/packages/react-schema-editor/README.md new file mode 100644 index 00000000000..219cee06929 --- /dev/null +++ b/packages/react-schema-editor/README.md @@ -0,0 +1,54 @@ +# schema-editor + +## Demo + +```jsx +import React from 'react' +import { SchemaEditor } from './src' + +function SchemaEditorDemo() { + const [schema, setSchema] = React.useState({ + type: 'object', + title: '我是表单标题', + description: '我是表单描述', + properties: { + fieldA: { + type: 'object', + title: '我是层级嵌套标题', + description: '我是层级嵌套描述', + properties: { + input: { + type: 'string', + 'x-component': 'Input', + 'x-component-props': { + value: 'abc', + onChange: '{{function(){console.log("abcd");}}}' + }, + 'x-props': { + help: 'test' + }, + 'x-rules': [ + { + required: true, + message: '此项必填' + } + ] + } + } + } + } + }) + + return ( + + ) +} + +ReactDOM.render( + , + document.getElementById('root') +) +``` diff --git a/packages/react-schema-editor/src/__tests__/index.spec.ts b/packages/react-schema-editor/src/__tests__/index.spec.ts new file mode 100644 index 00000000000..82d7b82e76a --- /dev/null +++ b/packages/react-schema-editor/src/__tests__/index.spec.ts @@ -0,0 +1,156 @@ +import { json2schema } from '../utils/json2schema' + +// mock datasource +const testValues = { + string: 'input test', + boolean: true, + checkbox: ['1'], + date: '2019-12-12', + daterange: ['2019-12-12', '2019-12-13'], + number: 1, + radio: '2', + rating: 4, + select: '1', + textarea: 'test text', + time: '00:00:04', + transfer: [1, 2], + year: '2013-01-01 00:00:00' +} + +describe('json object transform', () => { + test('values', () => { + const result = json2schema(testValues) + const validResult = { + title: '', + type: 'object', + properties: [ + { + title: '', + type: 'string', + example: 'input test', + enum: [], + description: '', + 'x-component': 'Input' + }, + { + title: '', + type: 'boolean', + example: true, + enum: [], + description: '', + 'x-component': 'Input' + }, + { + title: '', + type: 'array', + items: { + title: '', + type: 'string', + example: '1', + enum: [], + description: '', + 'x-component': 'Input' + }, + description: '', + 'x-component': 'Input' + }, + { + title: '', + type: 'string', + example: '2019-12-12', + enum: [], + description: '', + 'x-component': 'Input' + }, + { + title: '', + type: 'array', + items: { + title: '', + type: 'string', + example: '2019-12-12', + enum: [], + description: '', + 'x-component': 'Input' + }, + description: '', + 'x-component': 'Input' + }, + { + title: '', + type: 'number', + example: 1, + enum: [], + description: '', + 'x-component': 'Input' + }, + { + title: '', + type: 'string', + example: '2', + enum: [], + description: '', + 'x-component': 'Input' + }, + { + title: '', + type: 'number', + example: 4, + enum: [], + description: '', + 'x-component': 'Input' + }, + { + title: '', + type: 'string', + example: '1', + enum: [], + description: '', + 'x-component': 'Input' + }, + { + title: '', + type: 'string', + example: 'test text', + enum: [], + description: '', + 'x-component': 'Input' + }, + { + title: '', + type: 'string', + example: '00:00:04', + enum: [], + description: '', + 'x-component': 'Input' + }, + { + title: '', + type: 'array', + items: { + title: '', + type: 'number', + example: 1, + enum: [], + description: '', + 'x-component': 'Input' + }, + description: '', + 'x-component': 'Input' + }, + { + title: '', + type: 'string', + example: '2013-01-01 00:00:00', + enum: [], + description: '', + 'x-component': 'Input' + } + ], + description: '' + } + + // console.log(JSON.stringify(result, null, 2)) + expect(result).toEqual(validResult) + }) +}) diff --git a/packages/react-schema-editor/src/components/FieldEditor.css b/packages/react-schema-editor/src/components/FieldEditor.css new file mode 100644 index 00000000000..b14cf695fde --- /dev/null +++ b/packages/react-schema-editor/src/components/FieldEditor.css @@ -0,0 +1,13 @@ +.field-group{ + +} +.field-group-title{ + margin-top: 15px; + font-size: 16px; +} +.field-group-content{ + display: flex; +} +.field-group-form-item{ + flex: 1; +} diff --git a/packages/react-schema-editor/src/components/FieldEditor.tsx b/packages/react-schema-editor/src/components/FieldEditor.tsx new file mode 100644 index 00000000000..4eb881658ad --- /dev/null +++ b/packages/react-schema-editor/src/components/FieldEditor.tsx @@ -0,0 +1,485 @@ +import React from 'react' +import _ from 'lodash' +import { Form, Button, Checkbox, Input, InputNumber, Select } from 'antd' +import { + getFieldTypeData, + getInputTypeData, + getXComponentData, + getComponentPropsData, + getComponentPropsValue, + getInputType, + getPropertyValue, + getExpressionValue, + getRuleMessage +} from '../utils/fieldEditorHelpers' +import { InputTypes, ComponentPropsTypes } from '../utils/types' +import './FieldEditor.css' + +const FormItem = Form.Item +const SelectOption = Select.Option + +const formItemLayout = { + labelCol: { span: 24 }, + wrapperCol: { span: 24 } +} + +const BLANK_PROPERTY_VALUE = '' + +interface IFieldEditorProps { + schema: any + components: any + xProps: any + xRules: any + onChange: (schema: any) => void +} + +interface IFormItemGroupProps extends Partial { + title: string + propsKey: string +} + +const FormItemGroup: React.FC = ({ + title, + schema, + xProps, + xRules, + components, + propsKey, + onChange +}) => { + const componentName = schema[ComponentPropsTypes.X_COMPONENT] + const inputTypeData = getInputTypeData() + const componentPropsData = getComponentPropsData({ + schema, + xProps, + xRules, + components, + componentName, + propsKey + }) + + const componentPropsValue = getComponentPropsValue({ schema, propsKey }) + + const handleXComponentPropsValueChange = (value, property) => { + let newSchema + if (propsKey === ComponentPropsTypes.X_RULES) { + const newRules = _.map(schema[propsKey], rule => { + if (_.has(rule, property)) { + return { + ...rule, + [property]: value + } + } + return rule + }) + newSchema = { + ...schema, + [propsKey]: newRules + } + } else { + newSchema = { + ...schema, + [propsKey]: { + ...schema[propsKey], + [property]: value + } + } + } + onChange(newSchema) + } + + const handleInputTypeChange = (value, property) => { + let newSchema + let defaultValue + switch (value) { + case InputTypes.INPUT: { + defaultValue = '' + break + } + case InputTypes.NUMBER_PICKER: { + defaultValue = 0 + break + } + case InputTypes.CHECKBOX: { + defaultValue = false + break + } + case InputTypes.TEXT_AREA: { + defaultValue = null + break + } + } + if (propsKey === ComponentPropsTypes.X_RULES) { + const newRules = _.map(schema[propsKey], rule => { + if (_.has(rule, property)) { + return { + ...rule, + [property]: defaultValue + } + } + return rule + }) + newSchema = { + ...schema, + [propsKey]: newRules + } + } else { + newSchema = { + ...schema, + [propsKey]: { + ...schema[propsKey], + [property]: defaultValue + } + } + } + onChange(newSchema) + } + + const handlePropertyChange = (value, property) => { + let newComponentProps + if (propsKey === ComponentPropsTypes.X_RULES) { + newComponentProps = _.map(schema[propsKey], rule => { + if (_.has(rule, property)) { + return { + ..._.omit(rule, property), + [value]: rule[property] + } + } + return rule + }) + } else { + newComponentProps = {} + _.map(schema[propsKey], (v, k) => { + if (k === property) { + newComponentProps[value] = v + } else if (k !== value) { + newComponentProps[k] = v + } + }) + } + + onChange({ + ...schema, + [propsKey]: newComponentProps + }) + } + + const handleRuleMessageChange = (value, property) => { + const newRules = _.map(schema[propsKey], rule => { + if (_.has(rule, property)) { + return { + ...rule, + message: value + } + } + return rule + }) + + onChange({ + ...schema, + [ComponentPropsTypes.X_RULES]: newRules + }) + } + + const handleMinusClick = property => { + if (propsKey === ComponentPropsTypes.X_RULES) { + const newRules = _.reduce( + schema[propsKey], + (result, rule) => { + if (_.has(rule, property)) { + return result + } + return _.concat(result, rule) + }, + [] + ) + onChange({ + ...schema, + [ComponentPropsTypes.X_RULES]: newRules + }) + } else { + onChange({ + ..._.omit(schema, `${propsKey}.${property}`) + }) + } + } + + const handlePlusClick = () => { + if (propsKey === ComponentPropsTypes.X_RULES) { + onChange({ + ...schema, + [propsKey]: _.concat(schema[propsKey] || [], { + [componentPropsData.defaultValue]: BLANK_PROPERTY_VALUE + }) + }) + } else { + onChange({ + ...schema, + [propsKey]: { + ...schema[propsKey], + [componentPropsData.defaultValue]: BLANK_PROPERTY_VALUE + } + }) + } + } + + return ( +
+
{title}
+ {_.map(componentPropsValue, (property, index) => { + const value = getPropertyValue({ schema, propsKey, property }) + const inputType = getInputType(value) + return ( +
+ + + + + + + {inputType === InputTypes.INPUT && ( + + { + handleXComponentPropsValueChange( + event.target.value, + property + ) + }} + /> + + )} + {inputType === InputTypes.NUMBER_PICKER && ( + + { + handleXComponentPropsValueChange(value, property) + }} + /> + + )} + {inputType === InputTypes.CHECKBOX && ( + + { + handleXComponentPropsValueChange( + event.target.checked, + property + ) + }} + /> + + )} + {inputType === InputTypes.TEXT_AREA && ( + + { + let value = event.target.value + try { + value = JSON.parse(value) + } catch (error) {} + handleXComponentPropsValueChange(value, property) + }} + /> + + )} + {propsKey === ComponentPropsTypes.X_RULES && ( + + { + handleRuleMessageChange(event.target.value, property) + }} + /> + + )} + +
+ ) + })} +
+ ) +} + +const FieldEditor: React.FC = ({ + schema, + components, + xProps, + xRules, + onChange +}) => { + const fieldTypeData = getFieldTypeData() + + const xComponentData = getXComponentData(components) + + return ( + +
+
字段
+
+ + + + + + + + { + onChange({ + ...schema, + description: event.target.value + }) + }} + /> + +
+
+ + + + + ) +} + +export default Form.create()(FieldEditor) diff --git a/packages/react-schema-editor/src/components/JsonDialog.tsx b/packages/react-schema-editor/src/components/JsonDialog.tsx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/react-schema-editor/src/components/SchemaCode.tsx b/packages/react-schema-editor/src/components/SchemaCode.tsx new file mode 100644 index 00000000000..e817715480c --- /dev/null +++ b/packages/react-schema-editor/src/components/SchemaCode.tsx @@ -0,0 +1,24 @@ +import React from 'react' +// import MonacoEditor from 'react-monaco-editor' +import { ISchemaCodeProps } from '../utils/types' + +export const SchemaCode: React.FC = ({ + schema, + onChange +}) => { + if (typeof schema === 'object') { + schema = JSON.stringify(schema, null, '\t') + } + + return ( +
+ {/* */} +
+ ) +} diff --git a/packages/react-schema-editor/src/components/SchemaPreview.tsx b/packages/react-schema-editor/src/components/SchemaPreview.tsx new file mode 100644 index 00000000000..9b639a41287 --- /dev/null +++ b/packages/react-schema-editor/src/components/SchemaPreview.tsx @@ -0,0 +1,6 @@ +import * as React from 'react' +import { ISchemaPreviewProps } from '../utils/types' + +export const SchemaPreview: React.FC = ({ schema }) => { + return
SchemaPreview
+} diff --git a/packages/react-schema-editor/src/components/SchemaTree.tsx b/packages/react-schema-editor/src/components/SchemaTree.tsx new file mode 100644 index 00000000000..743821de549 --- /dev/null +++ b/packages/react-schema-editor/src/components/SchemaTree.tsx @@ -0,0 +1,165 @@ +import React, { useCallback } from 'react' +import { Tree, Row, Col } from 'antd' +import { ISchemaTreeProps } from '../utils/types' +import * as fp from 'lodash/fp' +import _ from 'lodash' +import FieldEditor from './FieldEditor' + +const TreeNode = Tree.TreeNode + +export const SchemaTree: React.FC = ({ + schema, + onChange +}) => { + const [selectedPath, setSelectedPath] = React.useState(null) + + const handleSelect = React.useCallback((path: string[]) => { + setSelectedPath(path[0]) + }, []) + + const handleDrop = React.useCallback( + (info: any) => { + const sourcePath = info.dragNode.props.eventKey + const sourceKeys = sourcePath.split('.') + const sourceKey = sourceKeys[sourceKeys.length - 1] + const targetPath = info.node.props.eventKey + const sourceValue = fp.get(sourcePath, schema) + const targetValue = + targetPath === 'root' ? schema : fp.get(targetPath, schema) + + if (!targetValue) { + return + } + + if (info.dropToGap) { + // 拖拽到这个元素的同级 + // info.dropPosition -1 表示上方同级,1 表示下方同级 + } else { + // 拖拽到这个元素内部 + if (targetValue.type !== 'object') { + // 只有 object 才能被拖入 + return + } + + if ( + (targetPath === 'root' && sourcePath.split('.').length === 2) || + (targetPath !== 'root' && + fp.dropRight(2, sourcePath.split('.')).join('.') === targetPath) + ) { + // 拖拽到直接父节点,等于不起作用 + return + } + + let newSchema = schema + + // 增加新的 key + const newTargetValue = fp.set( + ['properties', sourceKey], + sourceValue, + targetValue + ) + + newSchema = + targetPath === 'root' + ? newTargetValue + : fp.set(targetPath, newTargetValue, newSchema) + + // 删除旧的 key + newSchema = fp.unset(sourcePath, newSchema) + + onChange(newSchema) + } + }, + [schema, onChange] + ) + + const selectedSchema = + selectedPath && + (selectedPath === 'root' ? schema : fp.get(selectedPath, schema)) + console.log('selectedPath====', selectedPath) + console.log('selectedSchema====', selectedSchema) + return ( + + + + {TreeNodeBySchema({ schema, path: [] })} + + + + {selectedSchema && ( + { + const newSchema = _.clone(schema) + _.set(newSchema, selectedPath, value) + onChange(newSchema) + }} + /> + )} + + + ) +} + +const TreeNodeBySchema: React.FC<{ + schema: any + path: string[] +}> = ({ schema, path }) => { + if (!schema) { + return null + } + + const currentTreeLevelProps = { + title: path.length === 0 ? 'root' : path[path.length - 1], + key: path.length === 0 ? 'root' : path.join('.') + } + + switch (schema.type) { + case 'object': + return ( + + {schema.properties && + Object.keys(schema.properties).map(key => + TreeNodeBySchema({ + schema: schema.properties[key], + path: path.concat('properties', key) + }) + )} + + ) + case 'array': + default: + } + + return +} diff --git a/packages/react-schema-editor/src/index.tsx b/packages/react-schema-editor/src/index.tsx new file mode 100644 index 00000000000..07376adbfab --- /dev/null +++ b/packages/react-schema-editor/src/index.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import { Button, Icon, Row, Col } from 'antd' +import { SchemaTree } from './components/SchemaTree' +import { SchemaCode } from './components/SchemaCode' + +import 'antd/dist/antd.css' + +export const SchemaEditor: React.FC<{ + schema: any + onChange: (schema: any) => void +}> = ({ schema, onChange }) => { + return ( +
+
+ + +
+ + + + + + + + +
+ ) +} diff --git a/packages/react-schema-editor/src/main.scss b/packages/react-schema-editor/src/main.scss new file mode 100644 index 00000000000..d5ed7753ca1 --- /dev/null +++ b/packages/react-schema-editor/src/main.scss @@ -0,0 +1,70 @@ +/* write style here */ +.schema-editor { + padding: 20px; + + /* 操作区 */ + .schema-menus { + padding: 0 20px 20px 20px; + border-bottom: 1px solid #eee; + + .schema-preview-btn { + float: right; + } + } + + .splitter { + border-right: 1px dashed #aaa; + } + + /* 编辑区 */ + .schema-editor-main { + margin-top: 20px; + + .schema-col { + padding: 20px; + min-height: 400px; + } + + /* 树形编辑 */ + .schema-tree {} + + /* 代码编辑 */ + .schema-code {} + } + + .field-editor { + margin-top: 10px; + padding: 5px; + border: 1px solid #eee; + + .field-operate { + padding: 5px; + + .op-btn { + margin-left: 5px; + } + + .op-btn-expand { + float: right !important; + } + } + + .field-group { + display: flex; + background-color: #F2F3F7; + padding: 5px; + + .field-input { + margin-left: 5px; + width: 100px; + } + } + + .field-children { + padding: 10px 0 0 20px; + } + + + } + +} diff --git a/packages/react-schema-editor/src/utils/components.ts b/packages/react-schema-editor/src/utils/components.ts new file mode 100644 index 00000000000..be1f66e005b --- /dev/null +++ b/packages/react-schema-editor/src/utils/components.ts @@ -0,0 +1,22748 @@ +export default { + next: [ + { + "name": "Affix", + "title": "固钉", + "typeId": 6, + "props": { + "container": { + "type": { + "name": "func" + }, + "required": false, + "description": "设置 Affix 需要监听滚动事件的容器元素", + "defaultValue": { + "value": "() => window", + "computed": false + }, + "docblock": "设置 Affix 需要监听滚动事件的容器元素\n@return {ReactElement} 目标容器元素的实例", + "params": [], + "returns": { + "description": "目标容器元素的实例", + "type": { + "name": "ReactElement" + } + } + }, + "offsetTop": { + "type": { + "name": "number" + }, + "required": false, + "description": "距离窗口顶部达到指定偏移量后触发", + "docblock": "距离窗口顶部达到指定偏移量后触发" + }, + "offsetBottom": { + "type": { + "name": "number" + }, + "required": false, + "description": "距离窗口底部达到制定偏移量后触发", + "docblock": "距离窗口底部达到制定偏移量后触发" + }, + "onAffix": { + "type": { + "name": "func" + }, + "required": false, + "description": "当元素的样式发生固钉样式变化时触发的回调函数", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "当元素的样式发生固钉样式变化时触发的回调函数\n@param {Boolean} 元素是否被固钉", + "params": [{ + "name": "元素是否被固钉", + "description": null, + "type": { + "name": "Boolean" + } + }], + "returns": null + }, + "useAbsolute": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否启用绝对布局实现 affix", + "docblock": "是否启用绝对布局实现 affix\n@param {Boolean} 是否启用绝对布局" + } + }, + "methods": [], + "subComponents": [] + }, + { + "name": "Animate", + "title": "动画", + "typeId": 6, + "props": { + "animation": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "object" + } + ] + }, + "required": false, + "description": "动画 className", + "docblock": "动画 className" + }, + "animationAppear": { + "type": { + "name": "bool" + }, + "required": false, + "description": "子元素第一次挂载时是否执行动画", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "子元素第一次挂载时是否执行动画" + }, + "component": { + "type": { + "name": "any" + }, + "required": false, + "description": "包裹子元素的标签", + "defaultValue": { + "value": "'div'", + "computed": false + }, + "docblock": "包裹子元素的标签" + }, + "singleMode": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否只有单个子元素,如果有多个子元素,请设置为 false", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否只有单个子元素,如果有多个子元素,请设置为 false" + }, + "children": { + "type": { + "name": "union", + "value": [{ + "name": "element" + }, + { + "name": "arrayOf", + "value": { + "name": "element" + } + } + ] + }, + "required": false, + "description": "子元素", + "docblock": "子元素" + }, + "beforeAppear": { + "type": { + "name": "func" + }, + "required": false, + "description": "执行第一次挂载动画前触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "执行第一次挂载动画前触发的回调函数", + "params": [], + "returns": null + }, + "onAppear": { + "type": { + "name": "func" + }, + "required": false, + "description": "执行第一次挂载动画,添加 xxx-appear-active 类名后触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "执行第一次挂载动画,添加 xxx-appear-active 类名后触发的回调函数\n @param {HTMLElement} node \b执行动画的 dom 元素", + "params": [{ + "name": "node", + "description": "\b执行动画的 dom 元素", + "type": { + "name": "HTMLElement" + } + }], + "returns": null + }, + "afterAppear": { + "type": { + "name": "func" + }, + "required": false, + "description": "执行完第一次挂载动画后触发的函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "执行完第一次挂载动画后触发的函数\n@param {HTMLElement} node \b执行动画的 dom 元素", + "params": [{ + "name": "node", + "description": "\b执行动画的 dom 元素", + "type": { + "name": "HTMLElement" + } + }], + "returns": null + }, + "beforeEnter": { + "type": { + "name": "func" + }, + "required": false, + "description": "执行进场动画前触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "执行进场动画前触发的回调函数\n@param {HTMLElement} node \b执行动画的 dom 元素", + "params": [{ + "name": "node", + "description": "\b执行动画的 dom 元素", + "type": { + "name": "HTMLElement" + } + }], + "returns": null + }, + "onEnter": { + "type": { + "name": "func" + }, + "required": false, + "description": "执行进场动画,添加 xxx-enter-active 类名后触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "执行进场动画,添加 xxx-enter-active 类名后触发的回调函数\n@param {HTMLElement} node \b执行动画的 dom 元素", + "params": [{ + "name": "node", + "description": "\b执行动画的 dom 元素", + "type": { + "name": "HTMLElement" + } + }], + "returns": null + }, + "afterEnter": { + "type": { + "name": "func" + }, + "required": false, + "description": "执行完进场动画后触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "执行完进场动画后触发的回调函数\n@param {HTMLElement} node \b执行动画的 dom 元素", + "params": [{ + "name": "node", + "description": "\b执行动画的 dom 元素", + "type": { + "name": "HTMLElement" + } + }], + "returns": null + }, + "beforeLeave": { + "type": { + "name": "func" + }, + "required": false, + "description": "执行离场动画前触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "执行离场动画前触发的回调函数\n@param {HTMLElement} node \b执行动画的 dom 元素", + "params": [{ + "name": "node", + "description": "\b执行动画的 dom 元素", + "type": { + "name": "HTMLElement" + } + }], + "returns": null + }, + "onLeave": { + "type": { + "name": "func" + }, + "required": false, + "description": "执行离场动画,添加 xxx-leave-active 类名后触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "执行离场动画,添加 xxx-leave-active 类名后触发的回调函数\n@param {HTMLElement} node \b执行动画的 dom 元素", + "params": [{ + "name": "node", + "description": "\b执行动画的 dom 元素", + "type": { + "name": "HTMLElement" + } + }], + "returns": null + }, + "afterLeave": { + "type": { + "name": "func" + }, + "required": false, + "description": "执行完离场动画后触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "执行完离场动画后触发的回调函数\n@param {HTMLElement} node \b执行动画的 dom 元素", + "params": [{ + "name": "node", + "description": "\b执行动画的 dom 元素", + "type": { + "name": "HTMLElement" + } + }], + "returns": null + } + }, + "methods": [], + "subComponents": [] + }, + { + "name": "Badge", + "title": "徽标", + "typeId": 4, + "props": { + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "徽章依托的内容", + "docblock": "徽章依托的内容" + }, + "count": { + "type": { + "name": "union", + "value": [{ + "name": "number" + }, + { + "name": "string" + } + ] + }, + "required": false, + "description": "展示的数字,大于 overflowCount 时显示为 ${overflowCount}+,为 0 时默认隐藏", + "defaultValue": { + "value": "0", + "computed": false + }, + "docblock": "展示的数字,大于 overflowCount 时显示为 ${overflowCount}+,为 0 时默认隐藏" + }, + "showZero": { + "type": { + "name": "bool" + }, + "required": false, + "description": "当count为0时,是否显示count", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "当count为0时,是否显示count" + }, + "content": { + "type": { + "name": "node" + }, + "required": false, + "description": "自定义节点内容", + "docblock": "自定义节点内容" + }, + "overflowCount": { + "type": { + "name": "union", + "value": [{ + "name": "number" + }, + { + "name": "string" + } + ] + }, + "required": false, + "description": "展示的封顶的数字", + "defaultValue": { + "value": "99", + "computed": false + }, + "docblock": "展示的封顶的数字" + }, + "dot": { + "type": { + "name": "bool" + }, + "required": false, + "description": "不展示数字,只展示一个小红点", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "不展示数字,只展示一个小红点" + } + }, + "methods": [], + "subComponents": [] + }, + { + "name": "Balloon", + "title": "气泡", + "typeId": 5, + "props": { + "className": { + "type": { + "name": "string" + }, + "required": false, + "description": "自定义类名", + "docblock": "自定义类名" + }, + "style": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义内敛样式", + "docblock": "自定义内敛样式", + "properties": [] + }, + "children": { + "type": { + "name": "any" + }, + "required": false, + "description": "浮层的内容", + "docblock": "浮层的内容" + }, + "type": { + "type": { + "name": "enum", + "value": [{ + "value": "'normal'", + "computed": false + }, + { + "value": "'primary'", + "computed": false + } + ] + }, + "required": false, + "description": "样式类型", + "defaultValue": { + "value": "'normal'", + "computed": false + }, + "docblock": "样式类型" + }, + "visible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层当前显示的状态", + "docblock": "弹层当前显示的状态" + }, + "defaultVisible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层默认显示的状态", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "弹层默认显示的状态" + }, + "onVisibleChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层在显示和隐藏触发的事件", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层在显示和隐藏触发的事件\n@param {Boolean} visible 弹层是否隐藏和显示\n@param {String} type 触发弹层显示或隐藏的来源, closeClick 表示由自带的关闭按钮触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "params": [{ + "name": "visible", + "description": "弹层是否隐藏和显示", + "type": { + "name": "Boolean" + } + }, + { + "name": "type", + "description": "触发弹层显示或隐藏的来源, closeClick 表示由自带的关闭按钮触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "type": { + "name": "String" + } + } + ], + "returns": null + }, + "alignEdge": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹出层对齐方式", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "弹出层对齐方式" + }, + "closable": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否显示关闭按钮", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否显示关闭按钮" + }, + "align": { + "type": { + "name": "enum", + "value": [{ + "value": "'t'", + "computed": false, + "description": "上" + }, + { + "value": "'r'", + "computed": false, + "description": "右" + }, + { + "value": "'b'", + "computed": false, + "description": "下" + }, + { + "value": "'l'", + "computed": false, + "description": "左" + }, + { + "value": "'tl'", + "computed": false, + "description": "上左" + }, + { + "value": "'tr'", + "computed": false, + "description": "上右" + }, + { + "value": "'bl'", + "computed": false, + "description": "下左" + }, + { + "value": "'br'", + "computed": false, + "description": "下右" + }, + { + "value": "'lt'", + "computed": false, + "description": "左上" + }, + { + "value": "'lb'", + "computed": false, + "description": "左下" + }, + { + "value": "'rt'", + "computed": false, + "description": "右上" + }, + { + "value": "'rb'", + "computed": false, + "description": "右下 及其 两两组合" + } + ] + }, + "required": false, + "description": "弹出层位置", + "defaultValue": { + "value": "'b'", + "computed": false + }, + "docblock": "弹出层位置\n@enumdesc 上, 右, 下, 左, 上左, 上右, 下左, 下右, 左上, 左下, 右上, 右下 及其 两两组合", + "value": [{ + "value": "'t'", + "computed": false, + "description": "上" + }, + { + "value": "'r'", + "computed": false, + "description": "右" + }, + { + "value": "'b'", + "computed": false, + "description": "下" + }, + { + "value": "'l'", + "computed": false, + "description": "左" + }, + { + "value": "'tl'", + "computed": false, + "description": "上左" + }, + { + "value": "'tr'", + "computed": false, + "description": "上右" + }, + { + "value": "'bl'", + "computed": false, + "description": "下左" + }, + { + "value": "'br'", + "computed": false, + "description": "下右" + }, + { + "value": "'lt'", + "computed": false, + "description": "左上" + }, + { + "value": "'lb'", + "computed": false, + "description": "左下" + }, + { + "value": "'rt'", + "computed": false, + "description": "右上" + }, + { + "value": "'rb'", + "computed": false, + "description": "右下 及其 两两组合" + } + ] + }, + "offset": { + "type": { + "name": "array" + }, + "required": false, + "description": "弹层相对于trigger的定位的微调, 接收数组[hoz, ver], 表示弹层在 left / top 上的增量\ne.g. [100, 100] 表示往右(RTL 模式下是往左) 、下分布偏移100px", + "defaultValue": { + "value": "[0, 0]", + "computed": false + }, + "docblock": "弹层相对于trigger的定位的微调, 接收数组[hoz, ver], 表示弹层在 left / top 上的增量\ne.g. [100, 100] 表示往右(RTL 模式下是往左) 、下分布偏移100px" + }, + "trigger": { + "type": { + "name": "any" + }, + "required": false, + "description": "触发元素", + "defaultValue": { + "value": "", + "computed": false + }, + "docblock": "触发元素" + }, + "triggerType": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "触发行为\n鼠标悬浮, 鼠标点击('hover','click')或者它们组成的数组,如 ['hover', 'click'], 强烈不建议使用'focus',若弹窗内容有复杂交互请使用click", + "defaultValue": { + "value": "'hover'", + "computed": false + }, + "docblock": "触发行为\n鼠标悬浮, 鼠标点击('hover','click')或者它们组成的数组,如 ['hover', 'click'], 强烈不建议使用'focus',若弹窗内容有复杂交互请使用click" + }, + "onClose": { + "type": { + "name": "func" + }, + "required": false, + "description": "任何visible为false时会触发的事件", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "任何visible为false时会触发的事件", + "params": [], + "returns": null + }, + "needAdjust": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否进行自动位置调整", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否进行自动位置调整" + }, + "delay": { + "type": { + "name": "number" + }, + "required": false, + "description": "弹层在触发以后的延时显示, 单位毫秒 ms", + "docblock": "弹层在触发以后的延时显示, 单位毫秒 ms" + }, + "afterClose": { + "type": { + "name": "func" + }, + "required": false, + "description": "浮层关闭后触发的事件, 如果有动画,则在动画结束后触发", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "浮层关闭后触发的事件, 如果有动画,则在动画结束后触发", + "params": [], + "returns": null + }, + "shouldUpdatePosition": { + "type": { + "name": "bool" + }, + "required": false, + "description": "强制更新定位信息", + "docblock": "强制更新定位信息" + }, + "autoFocus": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层出现后是否自动focus到内部第一个元素", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "弹层出现后是否自动focus到内部第一个元素" + }, + "safeNode": { + "type": { + "name": "string" + }, + "required": false, + "description": "安全节点:对于triggetType为click的浮层,会在点击除了浮层外的其它区域时关闭浮层.safeNode用于添加不触发关闭的节点, 值可以是dom节点的id或者是节点的dom对象", + "defaultValue": { + "value": "undefined", + "computed": true + }, + "docblock": "安全节点:对于triggetType为click的浮层,会在点击除了浮层外的其它区域时关闭浮层.safeNode用于添加不触发关闭的节点, 值可以是dom节点的id或者是节点的dom对象" + }, + "safeId": { + "type": { + "name": "string" + }, + "required": false, + "description": "用来指定safeNode节点的id,和safeNode配合使用", + "defaultValue": { + "value": "null", + "computed": false + }, + "docblock": "用来指定safeNode节点的id,和safeNode配合使用" + }, + "animation": { + "type": { + "name": "union", + "value": [{ + "name": "object" + }, + { + "name": "bool" + } + ] + }, + "required": false, + "description": "配置动画的播放方式", + "defaultValue": { + "value": "{\n in: 'zoomIn',\n out: 'zoomOut',\n}", + "computed": false + }, + "docblock": "配置动画的播放方式\n@param {String} in 进场动画\n@param {String} out 出场动画" + }, + "cache": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层的dom节点关闭时是否删除", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "弹层的dom节点关闭时是否删除" + }, + "popupContainer": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "func" + } + ] + }, + "required": false, + "description": "指定浮层渲染的父节点, 可以为节点id的字符串,也可以返回节点的函数。", + "docblock": "指定浮层渲染的父节点, 可以为节点id的字符串,也可以返回节点的函数。" + }, + "popupStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层组件style,透传给Popup", + "defaultValue": { + "value": "{}", + "computed": false + }, + "docblock": "弹层组件style,透传给Popup", + "properties": [] + }, + "popupClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层组件className,透传给Popup", + "defaultValue": { + "value": "''", + "computed": false + }, + "docblock": "弹层组件className,透传给Popup" + }, + "popupProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层组件属性,透传给Popup", + "defaultValue": { + "value": "{}", + "computed": false + }, + "docblock": "弹层组件属性,透传给Popup", + "properties": [] + }, + "followTrigger": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否跟随滚动", + "docblock": "是否跟随滚动" + }, + "id": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层id, 传入值才会支持无障碍", + "docblock": "弹层id, 传入值才会支持无障碍" + } + }, + "methods": [], + "subComponents": [{ + "name": "Tooltip", + "title": "文字提示", + "typeId": 5, + "props": { + "prefix": { + "type": { + "name": "string" + }, + "required": false, + "description": "样式类名的品牌前缀", + "defaultValue": { + "value": "'next-'", + "computed": false + }, + "docblock": "样式类名的品牌前缀" + }, + "className": { + "type": { + "name": "string" + }, + "required": false, + "description": "自定义类名", + "docblock": "自定义类名" + }, + "style": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义内联样式", + "docblock": "自定义内联样式", + "properties": [] + }, + "children": { + "type": { + "name": "any" + }, + "required": false, + "description": "tooltip的内容", + "docblock": "tooltip的内容" + }, + "align": { + "type": { + "name": "enum", + "value": [{ + "value": "'t'", + "computed": false, + "description": "上" + }, + { + "value": "'r'", + "computed": false, + "description": "右" + }, + { + "value": "'b'", + "computed": false, + "description": "下" + }, + { + "value": "'l'", + "computed": false, + "description": "左" + }, + { + "value": "'tl'", + "computed": false, + "description": "上左" + }, + { + "value": "'tr'", + "computed": false, + "description": "上右" + }, + { + "value": "'bl'", + "computed": false, + "description": "下左" + }, + { + "value": "'br'", + "computed": false, + "description": "下右" + }, + { + "value": "'lt'", + "computed": false, + "description": "左上" + }, + { + "value": "'lb'", + "computed": false, + "description": "左下" + }, + { + "value": "'rt'", + "computed": false, + "description": "右上" + }, + { + "value": "'rb'", + "computed": false, + "description": "右下 及其 两两组合" + } + ] + }, + "required": false, + "description": "弹出层位置", + "defaultValue": { + "value": "'b'", + "computed": false + }, + "docblock": "弹出层位置\n@enumdesc 上, 右, 下, 左, 上左, 上右, 下左, 下右, 左上, 左下, 右上, 右下 及其 两两组合", + "value": [{ + "value": "'t'", + "computed": false, + "description": "上" + }, + { + "value": "'r'", + "computed": false, + "description": "右" + }, + { + "value": "'b'", + "computed": false, + "description": "下" + }, + { + "value": "'l'", + "computed": false, + "description": "左" + }, + { + "value": "'tl'", + "computed": false, + "description": "上左" + }, + { + "value": "'tr'", + "computed": false, + "description": "上右" + }, + { + "value": "'bl'", + "computed": false, + "description": "下左" + }, + { + "value": "'br'", + "computed": false, + "description": "下右" + }, + { + "value": "'lt'", + "computed": false, + "description": "左上" + }, + { + "value": "'lb'", + "computed": false, + "description": "左下" + }, + { + "value": "'rt'", + "computed": false, + "description": "右上" + }, + { + "value": "'rb'", + "computed": false, + "description": "右下 及其 两两组合" + } + ] + }, + "trigger": { + "type": { + "name": "any" + }, + "required": false, + "description": "触发元素", + "defaultValue": { + "value": "", + "computed": false + }, + "docblock": "触发元素" + }, + "triggerType": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "触发行为\n鼠标悬浮, 鼠标点击('hover', 'click')或者它们组成的数组,如 ['hover', 'click'], 强烈不建议使用'focus',若有复杂交互,推荐使用triggerType为click的Balloon组件", + "defaultValue": { + "value": "'hover'", + "computed": false + }, + "docblock": "触发行为\n鼠标悬浮, 鼠标点击('hover', 'click')或者它们组成的数组,如 ['hover', 'click'], 强烈不建议使用'focus',若有复杂交互,推荐使用triggerType为click的Balloon组件" + }, + "popupStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层组件style,透传给Popup", + "docblock": "弹层组件style,透传给Popup", + "properties": [] + }, + "popupClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层组件className,透传给Popup", + "docblock": "弹层组件className,透传给Popup" + }, + "popupProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层组件属性,透传给Popup", + "docblock": "弹层组件属性,透传给Popup", + "properties": [] + }, + "pure": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否pure render", + "docblock": "是否pure render" + }, + "popupContainer": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "func" + } + ] + }, + "required": false, + "description": "指定浮层渲染的父节点, 可以为节点id的字符串,也可以返回节点的函数。", + "docblock": "指定浮层渲染的父节点, 可以为节点id的字符串,也可以返回节点的函数。" + }, + "followTrigger": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否跟随滚动", + "docblock": "是否跟随滚动" + }, + "id": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层id, 传入值才会支持无障碍", + "docblock": "弹层id, 传入值才会支持无障碍" + } + }, + "methods": [] + }] + }, + { + "name": "Breadcrumb", + "title": "面包屑", + "typeId": 2, + "props": { + "prefix": { + "type": { + "name": "string" + }, + "required": false, + "description": "样式类名的品牌前缀", + "defaultValue": { + "value": "'next-'", + "computed": false + }, + "docblock": "样式类名的品牌前缀" + }, + "children": { + "type": { + "name": "custom", + "raw": "(props, propName) => {\n Children.forEach(props[propName], child => {\n if (\n !(\n child &&\n typeof child.type === 'function' &&\n child.type._typeMark === 'breadcrumb_item'\n )\n ) {\n throw new Error(\n \"Breadcrumb's children must be Breadcrumb.Item!\"\n );\n }\n });\n}" + }, + "required": false, + "description": "面包屑子节点,需传入 Breadcrumb.Item", + "docblock": "面包屑子节点,需传入 Breadcrumb.Item" + }, + "maxNode": { + "type": { + "name": "number" + }, + "required": false, + "description": "面包屑最多显示个数,超出部分会被隐藏", + "defaultValue": { + "value": "100", + "computed": false + }, + "docblock": "面包屑最多显示个数,超出部分会被隐藏" + }, + "separator": { + "type": { + "name": "node" + }, + "required": false, + "description": "分隔符,可以是文本或 Icon", + "defaultValue": { + "value": "", + "computed": false + }, + "docblock": "分隔符,可以是文本或 Icon" + }, + "component": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "func" + } + ] + }, + "required": false, + "description": "设置标签类型", + "defaultValue": { + "value": "'nav'", + "computed": false + }, + "docblock": "设置标签类型" + } + }, + "methods": [], + "subComponents": [{ + "name": "Item", + "title": "面包屑项", + "typeId": 2, + "props": { + "link": { + "type": { + "name": "string" + }, + "required": false, + "description": "面包屑节点链接,如果设置这个属性,则该节点为`` ,否则是``", + "docblock": "面包屑节点链接,如果设置这个属性,则该节点为`` ,否则是``" + } + }, + "methods": [] + }] + }, + { + "name": "Button", + "title": "按钮", + "typeId": 1, + "props": { + "type": { + "type": { + "name": "enum", + "value": [{ + "value": "'primary'", + "computed": false + }, + { + "value": "'secondary'", + "computed": false + }, + { + "value": "'normal'", + "computed": false + } + ] + }, + "required": false, + "description": "按钮的类型", + "defaultValue": { + "value": "'normal'", + "computed": false + }, + "docblock": "按钮的类型" + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + } + ] + }, + "required": false, + "description": "按钮的尺寸", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "按钮的尺寸" + }, + "iconSize": { + "type": { + "name": "enum", + "value": [{ + "value": "'xxs'", + "computed": false + }, + { + "value": "'xs'", + "computed": false + }, + { + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + }, + { + "value": "'xl'", + "computed": false + }, + { + "value": "'xxl'", + "computed": false + }, + { + "value": "'xxxl'", + "computed": false + } + ] + }, + "required": false, + "description": "按钮中 Icon 的尺寸,用于替代 Icon 的默认大小", + "docblock": "按钮中 Icon 的尺寸,用于替代 Icon 的默认大小" + }, + "htmlType": { + "type": { + "name": "enum", + "value": [{ + "value": "'submit'", + "computed": false + }, + { + "value": "'reset'", + "computed": false + }, + { + "value": "'button'", + "computed": false + } + ] + }, + "required": false, + "description": "当 component = 'button' 时,设置 button 标签的 type 值", + "defaultValue": { + "value": "'button'", + "computed": false + }, + "docblock": "当 component = 'button' 时,设置 button 标签的 type 值" + }, + "component": { + "type": { + "name": "enum", + "value": [{ + "value": "'button'", + "computed": false + }, + { + "value": "'a'", + "computed": false + }, + { + "value": "'div'", + "computed": false + }, + { + "value": "'span'", + "computed": false + } + ] + }, + "required": false, + "description": "设置标签类型", + "defaultValue": { + "value": "'button'", + "computed": false + }, + "docblock": "设置标签类型" + }, + "loading": { + "type": { + "name": "bool" + }, + "required": false, + "description": "设置按钮的载入状态", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "设置按钮的载入状态" + }, + "ghost": { + "type": { + "name": "enum", + "value": [{ + "value": "true", + "computed": false + }, + { + "value": "false", + "computed": false + }, + { + "value": "'light'", + "computed": false + }, + { + "value": "'dark'", + "computed": false + } + ] + }, + "required": false, + "description": "是否为幽灵按钮", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否为幽灵按钮" + }, + "text": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否为文本按钮", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否为文本按钮" + }, + "warning": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否为警告按钮", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否为警告按钮" + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁用", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否禁用" + }, + "onClick": { + "type": { + "name": "func" + }, + "required": false, + "description": "点击按钮的回调", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "点击按钮的回调\n@param {Object} e Event Object", + "params": [{ + "name": "e", + "description": "Event Object", + "type": { + "name": "Object" + } + }], + "returns": null + } + }, + "methods": [], + "subComponents": [{ + "name": "Group", + "title": "按钮组", + "typeId": 1, + "props": { + "size": { + "type": { + "name": "string" + }, + "required": false, + "description": "统一设置 Button 组件的按钮大小", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "统一设置 Button 组件的按钮大小" + } + }, + "methods": [] + }] + }, + { + "name": "Calendar", + "title": "日历", + "typeId": 4, + "props": { + "defaultValue": { + "type": { + "name": "custom", + "raw": "checkMomentObj" + }, + "required": false, + "description": "默认选中的日期(moment 对象)", + "docblock": "默认选中的日期(moment 对象)" + }, + "value": { + "type": { + "name": "custom", + "raw": "checkMomentObj" + }, + "required": false, + "description": "选中的日期值 (moment 对象)", + "docblock": "选中的日期值 (moment 对象)" + }, + "mode": { + "type": { + "name": "enum", + "computed": true, + "value": "CALENDAR_MODES" + }, + "required": false, + "description": "面板模式", + "docblock": "面板模式" + }, + "showOtherMonth": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否展示非本月的日期", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否展示非本月的日期" + }, + "defaultVisibleMonth": { + "type": { + "name": "func" + }, + "required": false, + "description": "默认展示的月份", + "docblock": "默认展示的月份", + "params": [], + "returns": null + }, + "shape": { + "type": { + "name": "enum", + "value": [{ + "value": "'card'", + "computed": false + }, + { + "value": "'fullscreen'", + "computed": false + }, + { + "value": "'panel'", + "computed": false + } + ] + }, + "required": false, + "description": "展现形态", + "defaultValue": { + "value": "'fullscreen'", + "computed": false + }, + "docblock": "展现形态" + }, + "onSelect": { + "type": { + "name": "func" + }, + "required": false, + "description": "选择日期单元格时的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "选择日期单元格时的回调\n@param {Object} value 对应的日期值 (moment 对象)", + "params": [{ + "name": "value", + "description": "对应的日期值 (moment 对象)", + "type": { + "name": "Object" + } + }], + "returns": null + }, + "onModeChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "面板模式变化时的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "面板模式变化时的回调\n@param {String} mode 对应面板模式 date month year", + "params": [{ + "name": "mode", + "description": "对应面板模式 date month year", + "type": { + "name": "String" + } + }], + "returns": null + }, + "onVisibleMonthChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "展现的月份变化时的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "展现的月份变化时的回调\n@param {Object} value 显示的月份 (moment 对象)\n@param {String} reason 触发月份改变原因", + "params": [{ + "name": "value", + "description": "显示的月份 (moment 对象)", + "type": { + "name": "Object" + } + }, + { + "name": "reason", + "description": "触发月份改变原因", + "type": { + "name": "String" + } + } + ], + "returns": null + }, + "className": { + "type": { + "name": "string" + }, + "required": false, + "description": "自定义样式类", + "docblock": "自定义样式类" + }, + "dateCellRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义日期渲染函数", + "defaultValue": { + "value": "value => value.date()", + "computed": false + }, + "docblock": "自定义日期渲染函数\n@param {Object} value 日期值(moment对象)\n@returns {ReactNode}", + "params": [{ + "name": "value", + "description": "日期值(moment对象)", + "type": { + "name": "Object" + } + }], + "returns": { + "description": null, + "type": { + "name": "ReactNode" + } + } + }, + "monthCellRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义月份渲染函数", + "docblock": "自定义月份渲染函数\n@param {Object} calendarDate 对应 Calendar 返回的自定义日期对象\n@returns {ReactNode}", + "params": [{ + "name": "calendarDate", + "description": "对应 Calendar 返回的自定义日期对象", + "type": { + "name": "Object" + } + }], + "returns": { + "description": null, + "type": { + "name": "ReactNode" + } + } + }, + "yearRange": { + "type": { + "name": "arrayOf", + "value": { + "name": "number" + } + }, + "required": false, + "description": "年份范围,[START_YEAR, END_YEAR] (只在shape 为 ‘card’, 'fullscreen' 下生效)", + "docblock": "年份范围,[START_YEAR, END_YEAR] (只在shape 为 ‘card’, 'fullscreen' 下生效)" + }, + "disabledDate": { + "type": { + "name": "func" + }, + "required": false, + "description": "不可选择的日期", + "docblock": "不可选择的日期\n@param {Object} calendarDate 对应 Calendar 返回的自定义日期对象\n@param {String} view 当前视图类型,year: 年, month: 月, date: 日\n@returns {Boolean}", + "params": [{ + "name": "calendarDate", + "description": "对应 Calendar 返回的自定义日期对象", + "type": { + "name": "Object" + } + }, + { + "name": "view", + "description": "当前视图类型,year: 年, month: 月, date: 日", + "type": { + "name": "String" + } + } + ], + "returns": { + "description": null, + "type": { + "name": "Boolean" + } + } + }, + "locale": { + "type": { + "name": "object" + }, + "required": false, + "description": "国际化配置", + "defaultValue": { + "value": "nextLocale.Calendar", + "computed": true + }, + "docblock": "国际化配置", + "properties": [] + } + }, + "methods": [{ + "name": "changeVisibleMonthByOffset", + "docblock": "根据日期偏移量设置当前展示的月份\n@param {Number} offset 日期偏移的数量\n@param {String} type 日期偏移的类型 days, months, years", + "modifiers": [], + "params": [{ + "name": "offset", + "description": "日期偏移的数量", + "type": { + "name": "Number" + } + }, + { + "name": "type", + "description": "日期偏移的类型 days, months, years", + "type": { + "name": "String" + } + } + ], + "returns": null, + "description": "根据日期偏移量设置当前展示的月份" + }], + "subComponents": [] + }, + { + "name": "Card", + "title": "卡片", + "typeId": 4, + "props": { + "title": { + "type": { + "name": "node" + }, + "required": false, + "description": "卡片的标题", + "docblock": "卡片的标题" + }, + "subTitle": { + "type": { + "name": "node" + }, + "required": false, + "description": "卡片的副标题", + "docblock": "卡片的副标题" + }, + "showTitleBullet": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否显示标题的项目符号", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否显示标题的项目符号" + }, + "showHeadDivider": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否展示头部的分隔线", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否展示头部的分隔线" + }, + "contentHeight": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + } + ] + }, + "required": false, + "description": "内容区域的固定高度", + "defaultValue": { + "value": "120", + "computed": false + }, + "docblock": "内容区域的固定高度" + }, + "extra": { + "type": { + "name": "node" + }, + "required": false, + "description": "标题区域的用户自定义内容", + "docblock": "标题区域的用户自定义内容" + } + }, + "methods": [], + "subComponents": [] + }, + { + "name": "Cascader", + "title": "级联", + "typeId": 4, + "props": { + "dataSource": { + "type": { + "name": "arrayOf", + "value": { + "name": "object" + } + }, + "required": false, + "description": "数据源,结构可参考下方说明", + "defaultValue": { + "value": "[]", + "computed": false + }, + "docblock": "数据源,结构可参考下方说明" + }, + "defaultValue": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "arrayOf", + "value": { + "name": "string" + } + } + ] + }, + "required": false, + "description": "(非受控)默认值", + "defaultValue": { + "value": "null", + "computed": false + }, + "docblock": "(非受控)默认值" + }, + "value": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "arrayOf", + "value": { + "name": "string" + } + } + ] + }, + "required": false, + "description": "(受控)当前值", + "docblock": "(受控)当前值" + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "选中值改变时触发的回调函数", + "docblock": "选中值改变时触发的回调函数\n@param {String|Array} value 选中的值,单选时返回单个值,多选时返回数组\n@param {Object|Array} data 选中的数据,包括 value 和 label,单选时返回单个值,多选时返回数组,父子节点选中关联时,同时选中,只返回父节点\n@param {Object} extra 额外参数\n@param {Array} extra.selectedPath 单选时选中的数据的路径\n@param {Boolean} extra.checked 多选时当前的操作是选中还是取消选中\n@param {Object} extra.currentData 多选时当前操作的数据\n@param {Array} extra.checkedData 多选时所有被选中的数据\n@param {Array} extra.indeterminateData 多选时半选的数据", + "params": [{ + "name": "value", + "description": "选中的值,单选时返回单个值,多选时返回数组", + "type": { + "name": "union", + "value": [ + "String", + "Array" + ] + } + }, + { + "name": "data", + "description": "选中的数据,包括 value 和 label,单选时返回单个值,多选时返回数组,父子节点选中关联时,同时选中,只返回父节点", + "type": { + "name": "union", + "value": [ + "Object", + "Array" + ] + } + }, + { + "name": "extra", + "description": "额外参数", + "type": { + "name": "Object" + } + }, + { + "name": "extra.selectedPath", + "description": "单选时选中的数据的路径", + "type": { + "name": "Array" + } + }, + { + "name": "extra.checked", + "description": "多选时当前的操作是选中还是取消选中", + "type": { + "name": "Boolean" + } + }, + { + "name": "extra.currentData", + "description": "多选时当前操作的数据", + "type": { + "name": "Object" + } + }, + { + "name": "extra.checkedData", + "description": "多选时所有被选中的数据", + "type": { + "name": "Array" + } + }, + { + "name": "extra.indeterminateData", + "description": "多选时半选的数据", + "type": { + "name": "Array" + } + } + ], + "returns": null + }, + "defaultExpandedValue": { + "type": { + "name": "arrayOf", + "value": { + "name": "string" + } + }, + "required": false, + "description": "(非受控)默认展开值,如果不设置,组件内部会根据 defaultValue/value 进行自动设置", + "docblock": "(非受控)默认展开值,如果不设置,组件内部会根据 defaultValue/value 进行自动设置" + }, + "expandedValue": { + "type": { + "name": "arrayOf", + "value": { + "name": "string" + } + }, + "required": false, + "description": "(受控)当前展开值", + "docblock": "(受控)当前展开值" + }, + "expandTriggerType": { + "type": { + "name": "enum", + "value": [{ + "value": "'click'", + "computed": false + }, + { + "value": "'hover'", + "computed": false + } + ] + }, + "required": false, + "description": "展开触发的方式", + "defaultValue": { + "value": "'click'", + "computed": false + }, + "docblock": "展开触发的方式" + }, + "onExpand": { + "type": { + "name": "func" + }, + "required": false, + "description": "展开时触发的回调函数", + "docblock": "展开时触发的回调函数\n@param {Array} expandedValue 各列展开值的数组", + "params": [{ + "name": "expandedValue", + "description": "各列展开值的数组", + "type": { + "name": "Array" + } + }], + "returns": null + }, + "useVirtual": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否开启虚拟滚动", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否开启虚拟滚动" + }, + "multiple": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否多选", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否多选" + }, + "canOnlySelectLeaf": { + "type": { + "name": "bool" + }, + "required": false, + "description": "单选时是否只能选中叶子节点", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "单选时是否只能选中叶子节点" + }, + "canOnlyCheckLeaf": { + "type": { + "name": "bool" + }, + "required": false, + "description": "多选时是否只能选中叶子节点", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "多选时是否只能选中叶子节点" + }, + "checkStrictly": { + "type": { + "name": "bool" + }, + "required": false, + "description": "父子节点是否选中不关联", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "父子节点是否选中不关联" + }, + "listStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "每列列表样式对象", + "docblock": "每列列表样式对象", + "properties": [] + }, + "listClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "每列列表类名", + "docblock": "每列列表类名" + }, + "itemRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "每列列表项渲染函数", + "defaultValue": { + "value": "item => item.label", + "computed": false + }, + "docblock": "每列列表项渲染函数\n@param {Object} data 数据\n@return {ReactNode} 列表项内容", + "params": [{ + "name": "data", + "description": "数据", + "type": { + "name": "Object" + } + }], + "returns": { + "description": "列表项内容", + "type": { + "name": "ReactNode" + } + } + }, + "loadData": { + "type": { + "name": "func" + }, + "required": false, + "description": "异步加载数据函数", + "docblock": "异步加载数据函数\n@param {Object} data 当前点击异步加载的数据\n@param {Object} source 当前点击数据", + "params": [{ + "name": "data", + "description": "当前点击异步加载的数据", + "type": { + "name": "Object" + } + }, + { + "name": "source", + "description": "当前点击数据", + "type": { + "name": "Object" + } + } + ], + "returns": null + } + }, + "methods": [], + "subComponents": [] + }, + { + "name": "CascaderSelect", + "title": "级联选择", + "typeId": 3, + "props": { + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + } + ] + }, + "required": false, + "description": "选择框大小", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "选择框大小" + }, + "placeholder": { + "type": { + "name": "string" + }, + "required": false, + "description": "选择框占位符", + "docblock": "选择框占位符" + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁用", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否禁用" + }, + "hasArrow": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否有下拉箭头", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否有下拉箭头" + }, + "hasBorder": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否有边框", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否有边框" + }, + "hasClear": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否有清除按钮", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否有清除按钮" + }, + "label": { + "type": { + "name": "node" + }, + "required": false, + "description": "自定义内联 label", + "docblock": "自定义内联 label" + }, + "readOnly": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否只读,只读模式下可以展开弹层但不能选", + "docblock": "是否只读,只读模式下可以展开弹层但不能选" + }, + "dataSource": { + "type": { + "name": "arrayOf", + "value": { + "name": "object" + } + }, + "required": false, + "description": "数据源,结构可参考下方说明", + "defaultValue": { + "value": "[]", + "computed": false + }, + "docblock": "数据源,结构可参考下方说明" + }, + "defaultValue": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "arrayOf", + "value": { + "name": "string" + } + } + ] + }, + "required": false, + "description": "(非受控)默认值", + "defaultValue": { + "value": "null", + "computed": false + }, + "docblock": "(非受控)默认值" + }, + "value": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "arrayOf", + "value": { + "name": "string" + } + } + ] + }, + "required": false, + "description": "(受控)当前值", + "docblock": "(受控)当前值" + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "选中值改变时触发的回调函数", + "docblock": "选中值改变时触发的回调函数\n@param {String|Array} value 选中的值,单选时返回单个值,多选时返回数组\n@param {Object|Array} data 选中的数据,包括 value 和 label,单选时返回单个值,多选时返回数组,父子节点选中关联时,同时选中,只返回父节点\n@param {Object} extra 额外参数\n@param {Array} extra.selectedPath 单选时选中的数据的路径\n@param {Boolean} extra.checked 多选时当前的操作是选中还是取消选中\n@param {Object} extra.currentData 多选时当前操作的数据\n@param {Array} extra.checkedData 多选时所有被选中的数据\n@param {Array} extra.indeterminateData 多选时半选的数据", + "params": [{ + "name": "value", + "description": "选中的值,单选时返回单个值,多选时返回数组", + "type": { + "name": "union", + "value": [ + "String", + "Array" + ] + } + }, + { + "name": "data", + "description": "选中的数据,包括 value 和 label,单选时返回单个值,多选时返回数组,父子节点选中关联时,同时选中,只返回父节点", + "type": { + "name": "union", + "value": [ + "Object", + "Array" + ] + } + }, + { + "name": "extra", + "description": "额外参数", + "type": { + "name": "Object" + } + }, + { + "name": "extra.selectedPath", + "description": "单选时选中的数据的路径", + "type": { + "name": "Array" + } + }, + { + "name": "extra.checked", + "description": "多选时当前的操作是选中还是取消选中", + "type": { + "name": "Boolean" + } + }, + { + "name": "extra.currentData", + "description": "多选时当前操作的数据", + "type": { + "name": "Object" + } + }, + { + "name": "extra.checkedData", + "description": "多选时所有被选中的数据", + "type": { + "name": "Array" + } + }, + { + "name": "extra.indeterminateData", + "description": "多选时半选的数据", + "type": { + "name": "Array" + } + } + ], + "returns": null + }, + "defaultExpandedValue": { + "type": { + "name": "arrayOf", + "value": { + "name": "string" + } + }, + "required": false, + "description": "默认展开值,如果不设置,组件内部会根据 defaultValue/value 进行自动设置", + "docblock": "默认展开值,如果不设置,组件内部会根据 defaultValue/value 进行自动设置" + }, + "expandTriggerType": { + "type": { + "name": "enum", + "value": [{ + "value": "'click'", + "computed": false + }, + { + "value": "'hover'", + "computed": false + } + ] + }, + "required": false, + "description": "展开触发的方式", + "defaultValue": { + "value": "'click'", + "computed": false + }, + "docblock": "展开触发的方式" + }, + "useVirtual": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否开启虚拟滚动", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否开启虚拟滚动" + }, + "multiple": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否多选", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否多选" + }, + "changeOnSelect": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否选中即发生改变, 该属性仅在单选模式下有效", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否选中即发生改变, 该属性仅在单选模式下有效" + }, + "canOnlyCheckLeaf": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否只能勾选叶子项的checkbox,该属性仅在多选模式下有效", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否只能勾选叶子项的checkbox,该属性仅在多选模式下有效" + }, + "checkStrictly": { + "type": { + "name": "bool" + }, + "required": false, + "description": "父子节点是否选中不关联", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "父子节点是否选中不关联" + }, + "listStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "每列列表样式对象", + "docblock": "每列列表样式对象", + "properties": [] + }, + "listClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "每列列表类名", + "docblock": "每列列表类名" + }, + "displayRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "选择框单选时展示结果的自定义渲染函数", + "docblock": "选择框单选时展示结果的自定义渲染函数\n@param {Array} label 选中路径的文本数组\n@return {ReactNode} 渲染在选择框中的内容\n@default 单选时:labelPath => labelPath.join(' / ');多选时:labelPath => labelPath[labelPath.length - 1]", + "params": [{ + "name": "label", + "description": "选中路径的文本数组", + "type": { + "name": "Array" + } + }], + "returns": { + "description": "渲染在选择框中的内容", + "type": { + "name": "ReactNode" + } + }, + "defaultValue": { + "value": "单选时:labelPath => labelPath.join(' / ');多选时:labelPath => labelPath[labelPath.length - 1]", + "computed": false + } + }, + "itemRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "渲染 item 内容的方法", + "docblock": "渲染 item 内容的方法\n@param {Object} item 渲染节点的item\n@return {ReactNode} item node", + "params": [{ + "name": "item", + "description": "渲染节点的item", + "type": { + "name": "Object" + } + }], + "returns": { + "description": "item node", + "type": { + "name": "ReactNode" + } + } + }, + "showSearch": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否显示搜索框", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否显示搜索框" + }, + "filter": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义搜索函数", + "defaultValue": { + "value": "根据路径所有节点的文本值模糊匹配", + "computed": false + }, + "docblock": "自定义搜索函数\n@param {String} searchValue 搜索的关键字\n@param {Array} path 节点路径\n@return {Boolean} 是否匹配\n@default 根据路径所有节点的文本值模糊匹配", + "params": [{ + "name": "searchValue", + "description": "搜索的关键字", + "type": { + "name": "String" + } + }, + { + "name": "path", + "description": "节点路径", + "type": { + "name": "Array" + } + } + ], + "returns": { + "description": "是否匹配", + "type": { + "name": "Boolean" + } + } + }, + "resultRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "搜索结果自定义渲染函数", + "defaultValue": { + "value": "按照节点文本 a / b / c 的模式渲染", + "computed": false + }, + "docblock": "搜索结果自定义渲染函数\n@param {String} searchValue 搜索的关键字\n@param {Array} path 匹配到的节点路径\n@return {ReactNode} 渲染的内容\n@default 按照节点文本 a / b / c 的模式渲染", + "params": [{ + "name": "searchValue", + "description": "搜索的关键字", + "type": { + "name": "String" + } + }, + { + "name": "path", + "description": "匹配到的节点路径", + "type": { + "name": "Array" + } + } + ], + "returns": { + "description": "渲染的内容", + "type": { + "name": "ReactNode" + } + } + }, + "resultAutoWidth": { + "type": { + "name": "bool" + }, + "required": false, + "description": "搜索结果列表是否和选择框等宽", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "搜索结果列表是否和选择框等宽" + }, + "notFoundContent": { + "type": { + "name": "node" + }, + "required": false, + "description": "无数据时显示内容", + "defaultValue": { + "value": "'Not Found'", + "computed": false + }, + "docblock": "无数据时显示内容" + }, + "loadData": { + "type": { + "name": "func" + }, + "required": false, + "description": "异步加载数据函数", + "docblock": "异步加载数据函数\n@param {Object} data 当前点击异步加载的数据", + "params": [{ + "name": "data", + "description": "当前点击异步加载的数据", + "type": { + "name": "Object" + } + }], + "returns": null + }, + "header": { + "type": { + "name": "node" + }, + "required": false, + "description": "自定义下拉框头部", + "docblock": "自定义下拉框头部" + }, + "footer": { + "type": { + "name": "node" + }, + "required": false, + "description": "自定义下拉框底部", + "docblock": "自定义下拉框底部" + }, + "defaultVisible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "初始下拉框是否显示", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "初始下拉框是否显示" + }, + "visible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "当前下拉框是否显示", + "docblock": "当前下拉框是否显示" + }, + "onVisibleChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "下拉框显示或关闭时触发事件的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "下拉框显示或关闭时触发事件的回调函数\n@param {Boolean} visible 是否显示\n@param {String} type 触发显示关闭的操作类型, fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "params": [{ + "name": "visible", + "description": "是否显示", + "type": { + "name": "Boolean" + } + }, + { + "name": "type", + "description": "触发显示关闭的操作类型, fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "type": { + "name": "String" + } + } + ], + "returns": null + }, + "popupStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "下拉框自定义样式对象", + "docblock": "下拉框自定义样式对象", + "properties": [] + }, + "popupClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "下拉框样式自定义类名", + "docblock": "下拉框样式自定义类名" + }, + "popupContainer": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "func" + } + ] + }, + "required": false, + "description": "下拉框挂载的容器节点", + "docblock": "下拉框挂载的容器节点" + }, + "popupProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "透传到 Popup 的属性对象", + "defaultValue": { + "value": "{}", + "computed": false + }, + "docblock": "透传到 Popup 的属性对象", + "properties": [] + }, + "followTrigger": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否跟随滚动", + "docblock": "是否跟随滚动" + } + }, + "methods": [], + "subComponents": [] + }, + { + "name": "Checkbox", + "title": "复选按钮", + "typeId": 3, + "props": { + "className": { + "type": { + "name": "string" + }, + "required": false, + "description": "自定义类名", + "docblock": "自定义类名" + }, + "id": { + "type": { + "name": "string" + }, + "required": false, + "description": "checkbox id, 挂载在input上", + "docblock": "checkbox id, 挂载在input上" + }, + "style": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义内敛样式", + "docblock": "自定义内敛样式", + "properties": [] + }, + "checked": { + "type": { + "name": "bool" + }, + "required": false, + "description": "选中状态", + "docblock": "选中状态" + }, + "defaultChecked": { + "type": { + "name": "bool" + }, + "required": false, + "description": "默认选中状态", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "默认选中状态" + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "禁用", + "docblock": "禁用" + }, + "label": { + "type": { + "name": "node" + }, + "required": false, + "description": "通过属性配置label,", + "docblock": "通过属性配置label," + }, + "indeterminate": { + "type": { + "name": "bool" + }, + "required": false, + "description": "Checkbox 的中间状态,只会影响到 Checkbox 的样式,并不影响其 checked 属性", + "docblock": "Checkbox 的中间状态,只会影响到 Checkbox 的样式,并不影响其 checked 属性" + }, + "defaultIndeterminate": { + "type": { + "name": "bool" + }, + "required": false, + "description": "Checkbox 的默认中间态,只会影响到 Checkbox 的样式,并不影响其 checked 属性", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "Checkbox 的默认中间态,只会影响到 Checkbox 的样式,并不影响其 checked 属性" + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "状态变化时触发的事件", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "状态变化时触发的事件\n@param {Boolean} checked 是否选中\n@param {Event} e Dom 事件对象", + "params": [{ + "name": "checked", + "description": "是否选中", + "type": { + "name": "Boolean" + } + }, + { + "name": "e", + "description": "Dom 事件对象", + "type": { + "name": "Event" + } + } + ], + "returns": null + }, + "onMouseEnter": { + "type": { + "name": "func" + }, + "required": false, + "description": "鼠标进入enter事件", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "鼠标进入enter事件\n@param {Event} e Dom 事件对象", + "params": [{ + "name": "e", + "description": "Dom 事件对象", + "type": { + "name": "Event" + } + }], + "returns": null + }, + "onMouseLeave": { + "type": { + "name": "func" + }, + "required": false, + "description": "鼠标离开Leave事件", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "鼠标离开Leave事件\n@param {Event} e Dom 事件对象", + "params": [{ + "name": "e", + "description": "Dom 事件对象", + "type": { + "name": "Event" + } + }], + "returns": null + } + }, + "methods": [], + "order": 1, + "subComponents": [{ + "name": "Group", + "title": "复选按钮组", + "typeId": 3, + "props": { + "className": { + "type": { + "name": "string" + }, + "required": false, + "description": "自定义类名", + "docblock": "自定义类名" + }, + "style": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义内敛样式", + "docblock": "自定义内敛样式", + "properties": [] + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "整体禁用", + "docblock": "整体禁用" + }, + "dataSource": { + "type": { + "name": "arrayOf", + "value": { + "name": "any" + } + }, + "required": false, + "description": "可选项列表, 数据项可为 String 或者 Object, 如 `['apple', 'pear', 'orange']` 或者 `[{value: 'apple', label: '苹果',}, {value: 'pear', label: '梨'}, {value: 'orange', label: '橙子'}]`", + "defaultValue": { + "value": "[]", + "computed": false + }, + "docblock": "可选项列表, 数据项可为 String 或者 Object, 如 `['apple', 'pear', 'orange']` 或者 `[{value: 'apple', label: '苹果',}, {value: 'pear', label: '梨'}, {value: 'orange', label: '橙子'}]`" + }, + "value": { + "type": { + "name": "union", + "value": [{ + "name": "array" + }, + { + "name": "string" + }, + { + "name": "number" + } + ] + }, + "required": false, + "description": "被选中的值列表", + "docblock": "被选中的值列表" + }, + "defaultValue": { + "type": { + "name": "union", + "value": [{ + "name": "array" + }, + { + "name": "string" + }, + { + "name": "number" + } + ] + }, + "required": false, + "description": "默认被选中的值列表", + "docblock": "默认被选中的值列表" + }, + "children": { + "type": { + "name": "arrayOf", + "value": { + "name": "element" + } + }, + "required": false, + "description": "通过子元素方式设置内部 checkbox", + "docblock": "通过子元素方式设置内部 checkbox" + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "选中值改变时的事件", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "选中值改变时的事件\n@param {Array} value 选中项列表\n@param {Event} e Dom 事件对象", + "params": [{ + "name": "value", + "description": "选中项列表", + "type": { + "name": "Array" + } + }, + { + "name": "e", + "description": "Dom 事件对象", + "type": { + "name": "Event" + } + } + ], + "returns": null + }, + "itemDirection": { + "type": { + "name": "enum", + "value": [{ + "value": "'hoz'", + "computed": false + }, + { + "value": "'ver'", + "computed": false + } + ] + }, + "required": false, + "description": "子项目的排列方式\n- hoz: 水平排列 (default)\n- ver: 垂直排列", + "defaultValue": { + "value": "'hoz'", + "computed": false + }, + "docblock": "子项目的排列方式\n- hoz: 水平排列 (default)\n- ver: 垂直排列" + } + }, + "methods": [] + }] + }, + { + "name": "Collapse", + "title": "折叠面板", + "typeId": 4, + "props": { + "prefix": { + "type": { + "name": "string" + }, + "required": false, + "description": "样式前缀", + "defaultValue": { + "value": "'next-'", + "computed": false + }, + "docblock": "样式前缀" + }, + "style": { + "type": { + "name": "object" + }, + "required": false, + "description": "组件接受行内样式", + "docblock": "组件接受行内样式", + "properties": [] + }, + "dataSource": { + "type": { + "name": "array" + }, + "required": false, + "description": "使用数据模型构建", + "docblock": "使用数据模型构建" + }, + "defaultExpandedKeys": { + "type": { + "name": "array" + }, + "required": false, + "description": "默认展开keys", + "docblock": "默认展开keys" + }, + "expandedKeys": { + "type": { + "name": "array" + }, + "required": false, + "description": "受控展开keys", + "docblock": "受控展开keys" + }, + "onExpand": { + "type": { + "name": "func" + }, + "required": false, + "description": "展开状态发升变化时候的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "展开状态发升变化时候的回调", + "params": [], + "returns": null + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "所有禁用", + "docblock": "所有禁用" + }, + "className": { + "type": { + "name": "string" + }, + "required": false, + "description": "扩展class", + "docblock": "扩展class" + }, + "accordion": { + "type": { + "name": "bool" + }, + "required": false, + "description": "手风琴模式,一次只能打开一个", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "手风琴模式,一次只能打开一个" + } + }, + "methods": [], + "subComponents": [{ + "name": "Panel", + "title": "单个折叠面板", + "props": { + "prefix": { + "type": { + "name": "string" + }, + "required": false, + "description": "样式类名的品牌前缀", + "defaultValue": { + "value": "'next-'", + "computed": false + }, + "docblock": "样式类名的品牌前缀" + }, + "style": { + "type": { + "name": "object" + }, + "required": false, + "description": "子组件接受行内样式", + "docblock": "子组件接受行内样式", + "properties": [] + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁止用户操作", + "docblock": "是否禁止用户操作" + }, + "title": { + "type": { + "name": "node" + }, + "required": false, + "description": "标题", + "docblock": "标题" + }, + "className": { + "type": { + "name": "string" + }, + "required": false, + "description": "扩展class", + "docblock": "扩展class" + } + }, + "methods": [] + }] + }, + { + "name": "ConfigProvider", + "title": "全局配置", + "typeId": 6, + "props": { + "prefix": { + "type": { + "name": "string" + }, + "required": false, + "description": "样式类名的品牌前缀", + "docblock": "样式类名的品牌前缀" + }, + "locale": { + "type": { + "name": "object" + }, + "required": false, + "description": "国际化文案对象,属性为组件的 displayName", + "docblock": "国际化文案对象,属性为组件的 displayName", + "properties": [] + }, + "errorBoundary": { + "type": { + "name": "union", + "value": [{ + "name": "bool" + }, + { + "name": "object" + } + ] + }, + "required": false, + "description": "是否开启错误捕捉 errorBoundary\n如需自定义参数,请传入对象 对象接受参数列表如下:\n\nfallbackUI `Function(error?: {}, errorInfo?: {}) => Element` 捕获错误后的展示\nafterCatch `Function(error?: {}, errorInfo?: {})` 捕获错误后的行为, 比如埋点上传", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否开启错误捕捉 errorBoundary\n如需自定义参数,请传入对象 对象接受参数列表如下:\n\nfallbackUI `Function(error?: {}, errorInfo?: {}) => Element` 捕获错误后的展示\nafterCatch `Function(error?: {}, errorInfo?: {})` 捕获错误后的行为, 比如埋点上传" + }, + "pure": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否开启 Pure Render 模式,会提高性能,但是也会带来副作用", + "docblock": "是否开启 Pure Render 模式,会提高性能,但是也会带来副作用" + }, + "warning": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否在开发模式下显示组件属性被废弃的 warning 提示", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否在开发模式下显示组件属性被废弃的 warning 提示" + }, + "rtl": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否开启 rtl 模式", + "docblock": "是否开启 rtl 模式" + }, + "children": { + "type": { + "name": "element" + }, + "required": false, + "description": "组件树", + "docblock": "组件树" + } + }, + "methods": [{ + "name": "config", + "docblock": "传入组件,生成受 ConfigProvider 控制的 HOC 组件\n@param {Component} Component 组件类\n@param {Object} options 可选项\n@returns {Component} HOC", + "modifiers": [ + "static" + ], + "params": [{ + "name": "Component", + "description": "组件类", + "type": { + "name": "Component" + } + }, + { + "name": "options", + "description": "可选项", + "type": { + "name": "Object" + } + } + ], + "returns": { + "description": "HOC", + "type": { + "name": "Component" + } + }, + "description": "传入组件,生成受 ConfigProvider 控制的 HOC 组件" + }, + { + "name": "getContextProps", + "docblock": "传入组件的 props 和 displayName,得到和 childContext 计算过的包含有 preifx/locale/pure 的对象,一般用于通过静态方法生成脱离组件树的组件\n@param {Object} props 组件的 props\n@param {String} displayName 组件的 displayName\n@returns {Object} 新的 context props", + "modifiers": [ + "static" + ], + "params": [{ + "name": "props", + "description": "组件的 props", + "type": { + "name": "Object" + } + }, + { + "name": "displayName", + "description": "组件的 displayName", + "type": { + "name": "String" + } + } + ], + "returns": { + "description": "新的 context props", + "type": { + "name": "Object" + } + }, + "description": "传入组件的 props 和 displayName,得到和 childContext 计算过的包含有 preifx/locale/pure 的对象,一般用于通过静态方法生成脱离组件树的组件" + } + ], + "propsExtends": false, + "subComponents": [] + }, + { + "name": "DatePicker", + "title": "日期选择框", + "typeId": 3, + "props": { + "label": { + "type": { + "name": "node" + }, + "required": false, + "description": "输入框内置标签", + "docblock": "输入框内置标签" + }, + "state": { + "type": { + "name": "enum", + "value": [{ + "value": "'success'", + "computed": false + }, + { + "value": "'loading'", + "computed": false + }, + { + "value": "'error'", + "computed": false + } + ] + }, + "required": false, + "description": "输入框状态", + "docblock": "输入框状态" + }, + "placeholder": { + "type": { + "name": "string" + }, + "required": false, + "description": "输入提示", + "docblock": "输入提示" + }, + "defaultVisibleMonth": { + "type": { + "name": "func" + }, + "required": false, + "description": "默认展现的月", + "docblock": "默认展现的月\n@return {MomentObject} 返回包含指定月份的 moment 对象实例", + "params": [], + "returns": { + "description": "返回包含指定月份的 moment 对象实例", + "type": { + "name": "MomentObject" + } + } + }, + "value": { + "type": { + "name": "custom", + "raw": "checkDateValue" + }, + "required": false, + "description": "日期值(受控)moment 对象", + "docblock": "日期值(受控)moment 对象" + }, + "defaultValue": { + "type": { + "name": "custom", + "raw": "checkDateValue" + }, + "required": false, + "description": "初始日期值,moment 对象", + "docblock": "初始日期值,moment 对象" + }, + "format": { + "type": { + "name": "string" + }, + "required": false, + "description": "日期值的格式(用于限定用户输入和展示)", + "defaultValue": { + "value": "'YYYY-MM-DD'", + "computed": false + }, + "docblock": "日期值的格式(用于限定用户输入和展示)" + }, + "showTime": { + "type": { + "name": "union", + "value": [{ + "name": "object" + }, + { + "name": "bool" + } + ] + }, + "required": false, + "description": "是否使用时间控件,传入 TimePicker 的属性 { defaultValue, format, ... }", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否使用时间控件,传入 TimePicker 的属性 { defaultValue, format, ... }" + }, + "resetTime": { + "type": { + "name": "bool" + }, + "required": false, + "description": "每次选择日期时是否重置时间(仅在 showTime 开启时有效)", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "每次选择日期时是否重置时间(仅在 showTime 开启时有效)" + }, + "disabledDate": { + "type": { + "name": "func" + }, + "required": false, + "description": "禁用日期函数", + "defaultValue": { + "value": "() => false", + "computed": false + }, + "docblock": "禁用日期函数\n@param {MomentObject} 日期值\n@param {String} view 当前视图类型,year: 年, month: 月, date: 日\n@return {Boolean} 是否禁用", + "params": [{ + "name": "日期值", + "description": null, + "type": { + "name": "MomentObject" + } + }, + { + "name": "view", + "description": "当前视图类型,year: 年, month: 月, date: 日", + "type": { + "name": "String" + } + } + ], + "returns": { + "description": "是否禁用", + "type": { + "name": "Boolean" + } + } + }, + "footerRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义面板页脚", + "defaultValue": { + "value": "() => null", + "computed": false + }, + "docblock": "自定义面板页脚\n@return {Node} 自定义的面板页脚组件", + "params": [], + "returns": { + "description": "自定义的面板页脚组件", + "type": { + "name": "Node" + } + } + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "日期值改变时的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "日期值改变时的回调\n@param {MomentObject|String} value 日期值", + "params": [{ + "name": "value", + "description": "日期值", + "type": { + "name": "union", + "value": [ + "MomentObject", + "String" + ] + } + }], + "returns": null + }, + "onOk": { + "type": { + "name": "func" + }, + "required": false, + "description": "点击确认按钮时的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "点击确认按钮时的回调\n@return {MomentObject|String} 日期值", + "params": [], + "returns": { + "description": "日期值", + "type": { + "name": "union", + "value": [ + "MomentObject", + "String" + ] + } + } + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + } + ] + }, + "required": false, + "description": "输入框尺寸", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "输入框尺寸" + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁用", + "docblock": "是否禁用" + }, + "hasClear": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否显示清空按钮", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否显示清空按钮" + }, + "visible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层显示状态", + "docblock": "弹层显示状态" + }, + "defaultVisible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层默认是否显示", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "弹层默认是否显示" + }, + "onVisibleChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层展示状态变化时的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层展示状态变化时的回调\n@param {Boolean} visible 弹层是否显示\n@param {String} type 触发弹层显示和隐藏的来源 calendarSelect 表示由日期表盘的选择触发; okBtnClick 表示由确认按钮触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "params": [{ + "name": "visible", + "description": "弹层是否显示", + "type": { + "name": "Boolean" + } + }, + { + "name": "type", + "description": "触发弹层显示和隐藏的来源 calendarSelect 表示由日期表盘的选择触发; okBtnClick 表示由确认按钮触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "type": { + "name": "String" + } + } + ], + "returns": null + }, + "popupTriggerType": { + "type": { + "name": "enum", + "value": [{ + "value": "'click'", + "computed": false + }, + { + "value": "'hover'", + "computed": false + } + ] + }, + "required": false, + "description": "弹层触发方式", + "defaultValue": { + "value": "'click'", + "computed": false + }, + "docblock": "弹层触发方式" + }, + "popupAlign": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层对齐方式,具体含义见 OverLay文档", + "defaultValue": { + "value": "'tl tl'", + "computed": false + }, + "docblock": "弹层对齐方式,具体含义见 OverLay文档" + }, + "popupContainer": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层容器", + "docblock": "弹层容器\n@param {Element} target 目标元素\n@return {Element} 弹层的容器元素", + "params": [{ + "name": "target", + "description": "目标元素", + "type": { + "name": "Element" + } + }], + "returns": { + "description": "弹层的容器元素", + "type": { + "name": "Element" + } + } + }, + "popupStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层自定义样式", + "docblock": "弹层自定义样式", + "properties": [] + }, + "popupClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层自定义样式类", + "docblock": "弹层自定义样式类" + }, + "popupProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层其他属性", + "docblock": "弹层其他属性", + "properties": [] + }, + "followTrigger": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否跟随滚动", + "docblock": "是否跟随滚动" + }, + "inputProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "输入框其他属性", + "docblock": "输入框其他属性", + "properties": [] + }, + "dateCellRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义日期渲染函数", + "docblock": "自定义日期渲染函数\n@param {Object} value 日期值(moment对象)\n@returns {ReactNode}", + "params": [{ + "name": "value", + "description": "日期值(moment对象)", + "type": { + "name": "Object" + } + }], + "returns": { + "description": null, + "type": { + "name": "ReactNode" + } + } + }, + "monthCellRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义月份渲染函数", + "docblock": "自定义月份渲染函数\n@param {Object} calendarDate 对应 Calendar 返回的自定义日期对象\n@returns {ReactNode}", + "params": [{ + "name": "calendarDate", + "description": "对应 Calendar 返回的自定义日期对象", + "type": { + "name": "Object" + } + }], + "returns": { + "description": null, + "type": { + "name": "ReactNode" + } + } + }, + "dateInputAriaLabel": { + "type": { + "name": "string" + }, + "required": false, + "description": "日期输入框的 aria-label 属性", + "docblock": "日期输入框的 aria-label 属性" + }, + "timeInputAriaLabel": { + "type": { + "name": "string" + }, + "required": false, + "description": "时间输入框的 aria-label 属性", + "docblock": "时间输入框的 aria-label 属性" + } + }, + "methods": [], + "subComponents": [{ + "name": "MonthPicker", + "title": "月份选择框", + "props": { + "label": { + "type": { + "name": "node" + }, + "required": false, + "description": "输入框内置标签", + "docblock": "输入框内置标签" + }, + "state": { + "type": { + "name": "enum", + "value": [{ + "value": "'success'", + "computed": false + }, + { + "value": "'loading'", + "computed": false + }, + { + "value": "'error'", + "computed": false + } + ] + }, + "required": false, + "description": "输入框状态", + "docblock": "输入框状态" + }, + "placeholder": { + "type": { + "name": "string" + }, + "required": false, + "description": "输入提示", + "docblock": "输入提示" + }, + "defaultVisibleYear": { + "type": { + "name": "func" + }, + "required": false, + "description": "默认展现的年", + "docblock": "默认展现的年\n@return {MomentObject} 返回包含指定年份的 moment 对象实例", + "params": [], + "returns": { + "description": "返回包含指定年份的 moment 对象实例", + "type": { + "name": "MomentObject" + } + } + }, + "value": { + "type": { + "name": "custom", + "raw": "checkDateValue" + }, + "required": false, + "description": "日期值(受控)moment 对象", + "docblock": "日期值(受控)moment 对象" + }, + "defaultValue": { + "type": { + "name": "custom", + "raw": "checkDateValue" + }, + "required": false, + "description": "初始日期值,moment 对象", + "docblock": "初始日期值,moment 对象" + }, + "format": { + "type": { + "name": "string" + }, + "required": false, + "description": "日期值的格式(用于限定用户输入和展示)", + "defaultValue": { + "value": "'YYYY-MM'", + "computed": false + }, + "docblock": "日期值的格式(用于限定用户输入和展示)" + }, + "disabledDate": { + "type": { + "name": "func" + }, + "required": false, + "description": "禁用日期函数", + "defaultValue": { + "value": "() => false", + "computed": false + }, + "docblock": "禁用日期函数\n@param {MomentObject} 日期值\n@param {String} view 当前视图类型,year: 年, month: 月, date: 日\n@return {Boolean} 是否禁用", + "params": [{ + "name": "日期值", + "description": null, + "type": { + "name": "MomentObject" + } + }, + { + "name": "view", + "description": "当前视图类型,year: 年, month: 月, date: 日", + "type": { + "name": "String" + } + } + ], + "returns": { + "description": "是否禁用", + "type": { + "name": "Boolean" + } + } + }, + "footerRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义面板页脚", + "defaultValue": { + "value": "() => null", + "computed": false + }, + "docblock": "自定义面板页脚\n@return {Node} 自定义的面板页脚组件", + "params": [], + "returns": { + "description": "自定义的面板页脚组件", + "type": { + "name": "Node" + } + } + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "日期值改变时的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "日期值改变时的回调\n@param {MomentObject|String} value 日期值", + "params": [{ + "name": "value", + "description": "日期值", + "type": { + "name": "union", + "value": [ + "MomentObject", + "String" + ] + } + }], + "returns": null + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + } + ] + }, + "required": false, + "description": "输入框尺寸", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "输入框尺寸" + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁用", + "docblock": "是否禁用" + }, + "hasClear": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否显示清空按钮", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否显示清空按钮" + }, + "visible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层显示状态", + "docblock": "弹层显示状态" + }, + "defaultVisible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层默认是否显示", + "docblock": "弹层默认是否显示" + }, + "onVisibleChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层展示状态变化时的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层展示状态变化时的回调\n@param {Boolean} visible 弹层是否显示\n@param {String} type 触发弹层显示和隐藏的来源 calendarSelect 表示由日期表盘的选择触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "params": [{ + "name": "visible", + "description": "弹层是否显示", + "type": { + "name": "Boolean" + } + }, + { + "name": "type", + "description": "触发弹层显示和隐藏的来源 calendarSelect 表示由日期表盘的选择触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "type": { + "name": "String" + } + } + ], + "returns": null + }, + "popupTriggerType": { + "type": { + "name": "enum", + "value": [{ + "value": "'click'", + "computed": false + }, + { + "value": "'hover'", + "computed": false + } + ] + }, + "required": false, + "description": "弹层触发方式", + "defaultValue": { + "value": "'click'", + "computed": false + }, + "docblock": "弹层触发方式" + }, + "popupAlign": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层对齐方式, 具体含义见 OverLay文档", + "defaultValue": { + "value": "'tl tl'", + "computed": false + }, + "docblock": "弹层对齐方式, 具体含义见 OverLay文档" + }, + "popupContainer": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层容器", + "docblock": "弹层容器\n@param {Element} target 目标元素\n@return {Element} 弹层的容器元素", + "params": [{ + "name": "target", + "description": "目标元素", + "type": { + "name": "Element" + } + }], + "returns": { + "description": "弹层的容器元素", + "type": { + "name": "Element" + } + } + }, + "popupStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层自定义样式", + "docblock": "弹层自定义样式", + "properties": [] + }, + "popupClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层自定义样式类", + "docblock": "弹层自定义样式类" + }, + "popupProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层其他属性", + "docblock": "弹层其他属性", + "properties": [] + }, + "followTrigger": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否跟随滚动", + "docblock": "是否跟随滚动" + }, + "inputProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "输入框其他属性", + "docblock": "输入框其他属性", + "properties": [] + }, + "monthCellRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义月份渲染函数", + "docblock": "自定义月份渲染函数\n@param {Object} calendarDate 对应 Calendar 返回的自定义日期对象\n@returns {ReactNode}", + "params": [{ + "name": "calendarDate", + "description": "对应 Calendar 返回的自定义日期对象", + "type": { + "name": "Object" + } + }], + "returns": { + "description": null, + "type": { + "name": "ReactNode" + } + } + }, + "dateInputAriaLabel": { + "type": { + "name": "string" + }, + "required": false, + "description": "日期输入框的 aria-label 属性", + "docblock": "日期输入框的 aria-label 属性" + } + }, + "methods": [] + }, + { + "name": "RangePicker", + "title": "区间选择框", + "props": { + "defaultVisibleMonth": { + "type": { + "name": "func" + }, + "required": false, + "description": "默认展示的起始月份", + "docblock": "默认展示的起始月份\n@return {MomentObject} 返回包含指定月份的 moment 对象实例", + "params": [], + "returns": { + "description": "返回包含指定月份的 moment 对象实例", + "type": { + "name": "MomentObject" + } + } + }, + "value": { + "type": { + "name": "array" + }, + "required": false, + "description": "日期范围值数组 [moment, moment]", + "docblock": "日期范围值数组 [moment, moment]" + }, + "defaultValue": { + "type": { + "name": "array" + }, + "required": false, + "description": "初始的日期范围值数组 [moment, moment]", + "docblock": "初始的日期范围值数组 [moment, moment]" + }, + "format": { + "type": { + "name": "string" + }, + "required": false, + "description": "日期格式", + "defaultValue": { + "value": "'YYYY-MM-DD'", + "computed": false + }, + "docblock": "日期格式" + }, + "showTime": { + "type": { + "name": "union", + "value": [{ + "name": "object" + }, + { + "name": "bool" + } + ] + }, + "required": false, + "description": "是否使用时间控件,支持传入 TimePicker 的属性", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否使用时间控件,支持传入 TimePicker 的属性" + }, + "resetTime": { + "type": { + "name": "bool" + }, + "required": false, + "description": "每次选择是否重置时间(仅在 showTime 开启时有效)", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "每次选择是否重置时间(仅在 showTime 开启时有效)" + }, + "disabledDate": { + "type": { + "name": "func" + }, + "required": false, + "description": "禁用日期函数", + "defaultValue": { + "value": "() => false", + "computed": false + }, + "docblock": "禁用日期函数\n@param {MomentObject} 日期值\n@param {String} view 当前视图类型,year: 年, month: 月, date: 日\n@return {Boolean} 是否禁用", + "params": [{ + "name": "日期值", + "description": null, + "type": { + "name": "MomentObject" + } + }, + { + "name": "view", + "description": "当前视图类型,year: 年, month: 月, date: 日", + "type": { + "name": "String" + } + } + ], + "returns": { + "description": "是否禁用", + "type": { + "name": "Boolean" + } + } + }, + "footerRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义面板页脚", + "defaultValue": { + "value": "() => null", + "computed": false + }, + "docblock": "自定义面板页脚\n@return {Node} 自定义的面板页脚组件", + "params": [], + "returns": { + "description": "自定义的面板页脚组件", + "type": { + "name": "Node" + } + } + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "日期范围值改变时的回调 [ MomentObject|String, MomentObject|String ]", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "日期范围值改变时的回调 [ MomentObject|String, MomentObject|String ]\n@param {Array} value 日期值", + "params": [{ + "name": "value", + "description": "日期值", + "type": { + "name": "Array" + } + }], + "returns": null + }, + "onOk": { + "type": { + "name": "func" + }, + "required": false, + "description": "点击确认按钮时的回调 返回开始时间和结束时间`[ MomentObject|String, MomentObject|String ]`", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "点击确认按钮时的回调 返回开始时间和结束时间`[ MomentObject|String, MomentObject|String ]`\n@return {Array} 日期范围", + "params": [], + "returns": { + "description": "日期范围", + "type": { + "name": "Array" + } + } + }, + "label": { + "type": { + "name": "node" + }, + "required": false, + "description": "输入框内置标签", + "docblock": "输入框内置标签" + }, + "state": { + "type": { + "name": "enum", + "value": [{ + "value": "'error'", + "computed": false + }, + { + "value": "'loading'", + "computed": false + }, + { + "value": "'success'", + "computed": false + } + ] + }, + "required": false, + "description": "输入框状态", + "docblock": "输入框状态" + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + } + ] + }, + "required": false, + "description": "输入框尺寸", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "输入框尺寸" + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁用", + "docblock": "是否禁用" + }, + "hasClear": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否显示清空按钮", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否显示清空按钮" + }, + "visible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层显示状态", + "docblock": "弹层显示状态" + }, + "defaultVisible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层默认是否显示", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "弹层默认是否显示" + }, + "onVisibleChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层展示状态变化时的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层展示状态变化时的回调\n@param {Boolean} visible 弹层是否显示\n@param {String} type 触发弹层显示和隐藏的来源 okBtnClick 表示由确认按钮触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "params": [{ + "name": "visible", + "description": "弹层是否显示", + "type": { + "name": "Boolean" + } + }, + { + "name": "type", + "description": "触发弹层显示和隐藏的来源 okBtnClick 表示由确认按钮触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "type": { + "name": "String" + } + } + ], + "returns": null + }, + "popupTriggerType": { + "type": { + "name": "enum", + "value": [{ + "value": "'click'", + "computed": false + }, + { + "value": "'hover'", + "computed": false + } + ] + }, + "required": false, + "description": "弹层触发方式", + "defaultValue": { + "value": "'click'", + "computed": false + }, + "docblock": "弹层触发方式" + }, + "popupAlign": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层对齐方式, 具体含义见 OverLay文档", + "defaultValue": { + "value": "'tl tl'", + "computed": false + }, + "docblock": "弹层对齐方式, 具体含义见 OverLay文档" + }, + "popupContainer": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层容器", + "docblock": "弹层容器\n@param {Element} target 目标元素\n@return {Element} 弹层的容器元素", + "params": [{ + "name": "target", + "description": "目标元素", + "type": { + "name": "Element" + } + }], + "returns": { + "description": "弹层的容器元素", + "type": { + "name": "Element" + } + } + }, + "popupStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层自定义样式", + "docblock": "弹层自定义样式", + "properties": [] + }, + "popupClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层自定义样式类", + "docblock": "弹层自定义样式类" + }, + "popupProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层其他属性", + "docblock": "弹层其他属性", + "properties": [] + }, + "followTrigger": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否跟随滚动", + "docblock": "是否跟随滚动" + }, + "inputProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "输入框其他属性", + "docblock": "输入框其他属性", + "properties": [] + }, + "dateCellRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义日期单元格渲染", + "docblock": "自定义日期单元格渲染", + "params": [], + "returns": null + }, + "monthCellRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义月份渲染函数", + "docblock": "自定义月份渲染函数\n@param {Object} calendarDate 对应 Calendar 返回的自定义日期对象\n@returns {ReactNode}", + "params": [{ + "name": "calendarDate", + "description": "对应 Calendar 返回的自定义日期对象", + "type": { + "name": "Object" + } + }], + "returns": { + "description": null, + "type": { + "name": "ReactNode" + } + } + }, + "startDateInputAriaLabel": { + "type": { + "name": "string" + }, + "required": false, + "description": "开始日期输入框的 aria-label 属性", + "docblock": "开始日期输入框的 aria-label 属性" + }, + "startTimeInputAriaLabel": { + "type": { + "name": "string" + }, + "required": false, + "description": "开始时间输入框的 aria-label 属性", + "docblock": "开始时间输入框的 aria-label 属性" + }, + "endDateInputAriaLabel": { + "type": { + "name": "string" + }, + "required": false, + "description": "结束日期输入框的 aria-label 属性", + "docblock": "结束日期输入框的 aria-label 属性" + }, + "endTimeInputAriaLabel": { + "type": { + "name": "string" + }, + "required": false, + "description": "结束时间输入框的 aria-label 属性", + "docblock": "结束时间输入框的 aria-label 属性" + } + }, + "methods": [] + }, + { + "name": "YearPicker", + "title": "年份选择框", + "props": { + "label": { + "type": { + "name": "node" + }, + "required": false, + "description": "输入框内置标签", + "docblock": "输入框内置标签" + }, + "state": { + "type": { + "name": "enum", + "value": [{ + "value": "'success'", + "computed": false + }, + { + "value": "'loading'", + "computed": false + }, + { + "value": "'error'", + "computed": false + } + ] + }, + "required": false, + "description": "输入框状态", + "docblock": "输入框状态" + }, + "placeholder": { + "type": { + "name": "string" + }, + "required": false, + "description": "输入提示", + "docblock": "输入提示" + }, + "value": { + "type": { + "name": "custom", + "raw": "checkDateValue" + }, + "required": false, + "description": "日期值(受控)moment 对象", + "docblock": "日期值(受控)moment 对象" + }, + "defaultValue": { + "type": { + "name": "custom", + "raw": "checkDateValue" + }, + "required": false, + "description": "初始日期值,moment 对象", + "docblock": "初始日期值,moment 对象" + }, + "format": { + "type": { + "name": "string" + }, + "required": false, + "description": "日期值的格式(用于限定用户输入和展示)", + "defaultValue": { + "value": "'YYYY'", + "computed": false + }, + "docblock": "日期值的格式(用于限定用户输入和展示)" + }, + "disabledDate": { + "type": { + "name": "func" + }, + "required": false, + "description": "禁用日期函数", + "defaultValue": { + "value": "() => false", + "computed": false + }, + "docblock": "禁用日期函数\n@param {MomentObject} 日期值\n@param {String} view 当前视图类型,year: 年, month: 月, date: 日\n@return {Boolean} 是否禁用", + "params": [{ + "name": "日期值", + "description": null, + "type": { + "name": "MomentObject" + } + }, + { + "name": "view", + "description": "当前视图类型,year: 年, month: 月, date: 日", + "type": { + "name": "String" + } + } + ], + "returns": { + "description": "是否禁用", + "type": { + "name": "Boolean" + } + } + }, + "footerRender": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义面板页脚", + "defaultValue": { + "value": "() => null", + "computed": false + }, + "docblock": "自定义面板页脚\n@return {Node} 自定义的面板页脚组件", + "params": [], + "returns": { + "description": "自定义的面板页脚组件", + "type": { + "name": "Node" + } + } + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "日期值改变时的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "日期值改变时的回调\n@param {MomentObject|String} value 日期值", + "params": [{ + "name": "value", + "description": "日期值", + "type": { + "name": "union", + "value": [ + "MomentObject", + "String" + ] + } + }], + "returns": null + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + } + ] + }, + "required": false, + "description": "输入框尺寸", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "输入框尺寸" + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁用", + "docblock": "是否禁用" + }, + "hasClear": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否显示清空按钮", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否显示清空按钮" + }, + "visible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层显示状态", + "docblock": "弹层显示状态" + }, + "defaultVisible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层默认是否显示", + "docblock": "弹层默认是否显示" + }, + "onVisibleChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层展示状态变化时的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层展示状态变化时的回调\n@param {Boolean} visible 弹层是否显示\n@param {String} reason 触发弹层显示和隐藏的来源 calendarSelect 表示由日期表盘的选择触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "params": [{ + "name": "visible", + "description": "弹层是否显示", + "type": { + "name": "Boolean" + } + }, + { + "name": "reason", + "description": "触发弹层显示和隐藏的来源 calendarSelect 表示由日期表盘的选择触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "type": { + "name": "String" + } + } + ], + "returns": null + }, + "popupTriggerType": { + "type": { + "name": "enum", + "value": [{ + "value": "'click'", + "computed": false + }, + { + "value": "'hover'", + "computed": false + } + ] + }, + "required": false, + "description": "弹层触发方式", + "defaultValue": { + "value": "'click'", + "computed": false + }, + "docblock": "弹层触发方式" + }, + "popupAlign": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层对齐方式, 具体含义见 OverLay文档", + "defaultValue": { + "value": "'tl tl'", + "computed": false + }, + "docblock": "弹层对齐方式, 具体含义见 OverLay文档" + }, + "popupContainer": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层容器", + "docblock": "弹层容器\n@param {Element} target 目标元素\n@return {Element} 弹层的容器元素", + "params": [{ + "name": "target", + "description": "目标元素", + "type": { + "name": "Element" + } + }], + "returns": { + "description": "弹层的容器元素", + "type": { + "name": "Element" + } + } + }, + "popupStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层自定义样式", + "docblock": "弹层自定义样式", + "properties": [] + }, + "popupClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层自定义样式类", + "docblock": "弹层自定义样式类" + }, + "popupProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层其他属性", + "docblock": "弹层其他属性", + "properties": [] + }, + "followTrigger": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否跟随滚动", + "docblock": "是否跟随滚动" + }, + "inputProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "输入框其他属性", + "docblock": "输入框其他属性", + "properties": [] + }, + "dateInputAriaLabel": { + "type": { + "name": "string" + }, + "required": false, + "description": "日期输入框的 aria-label 属性", + "docblock": "日期输入框的 aria-label 属性" + } + }, + "methods": [] + } + ] + }, + { + "name": "Dialog", + "title": "弹窗", + "typeId": 5, + "props": { + "visible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否显示", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否显示" + }, + "title": { + "type": { + "name": "node" + }, + "required": false, + "description": "标题", + "docblock": "标题" + }, + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "内容", + "docblock": "内容" + }, + "footer": { + "type": { + "name": "union", + "value": [{ + "name": "bool" + }, + { + "name": "node" + } + ] + }, + "required": false, + "description": "底部内容,设置为 false,则不进行显示", + "docblock": "底部内容,设置为 false,则不进行显示\n@default [, ]", + "defaultValue": { + "value": "[, ]", + "computed": false + } + }, + "footerAlign": { + "type": { + "name": "enum", + "value": [{ + "value": "'left'", + "computed": false + }, + { + "value": "'center'", + "computed": false + }, + { + "value": "'right'", + "computed": false + } + ] + }, + "required": false, + "description": "底部按钮的对齐方式", + "defaultValue": { + "value": "'right'", + "computed": false + }, + "docblock": "底部按钮的对齐方式" + }, + "footerActions": { + "type": { + "name": "array" + }, + "required": false, + "description": "指定确定按钮和取消按钮是否存在以及如何排列,

**可选值**:\n['ok', 'cancel'](确认取消按钮同时存在,确认按钮在左)\n['cancel', 'ok'](确认取消按钮同时存在,确认按钮在右)\n['ok'](只存在确认按钮)\n['cancel'](只存在取消按钮)", + "defaultValue": { + "value": "['ok', 'cancel']", + "computed": false + }, + "docblock": "指定确定按钮和取消按钮是否存在以及如何排列,

**可选值**:\n['ok', 'cancel'](确认取消按钮同时存在,确认按钮在左)\n['cancel', 'ok'](确认取消按钮同时存在,确认按钮在右)\n['ok'](只存在确认按钮)\n['cancel'](只存在取消按钮)" + }, + "onOk": { + "type": { + "name": "func" + }, + "required": false, + "description": "在点击确定按钮时触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "在点击确定按钮时触发的回调函数\n@param {Object} event 点击事件对象", + "params": [{ + "name": "event", + "description": "点击事件对象", + "type": { + "name": "Object" + } + }], + "returns": null + }, + "onCancel": { + "type": { + "name": "func" + }, + "required": false, + "description": "在点击取消按钮时触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "在点击取消按钮时触发的回调函数\n@param {Object} event 点击事件对象", + "params": [{ + "name": "event", + "description": "点击事件对象", + "type": { + "name": "Object" + } + }], + "returns": null + }, + "okProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "应用于确定按钮的属性对象", + "defaultValue": { + "value": "{}", + "computed": false + }, + "docblock": "应用于确定按钮的属性对象", + "properties": [] + }, + "cancelProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "应用于取消按钮的属性对象", + "defaultValue": { + "value": "{}", + "computed": false + }, + "docblock": "应用于取消按钮的属性对象", + "properties": [] + }, + "closeable": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "bool" + } + ] + }, + "required": false, + "description": "控制对话框关闭的方式,值可以为字符串或者布尔值,其中字符串是由以下值组成:\n**close** 表示点击关闭按钮可以关闭对话框\n**mask** 表示点击遮罩区域可以关闭对话框\n**esc** 表示按下 esc 键可以关闭对话框\n如 'close' 或 'close,esc,mask'\n如果设置为 true,则以上关闭方式全部生效\n如果设置为 false,则以上关闭方式全部失效", + "defaultValue": { + "value": "'esc,close'", + "computed": false + }, + "docblock": "控制对话框关闭的方式,值可以为字符串或者布尔值,其中字符串是由以下值组成:\n**close** 表示点击关闭按钮可以关闭对话框\n**mask** 表示点击遮罩区域可以关闭对话框\n**esc** 表示按下 esc 键可以关闭对话框\n如 'close' 或 'close,esc,mask'\n如果设置为 true,则以上关闭方式全部生效\n如果设置为 false,则以上关闭方式全部失效" + }, + "onClose": { + "type": { + "name": "func" + }, + "required": false, + "description": "对话框关闭时触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "对话框关闭时触发的回调函数\n@param {String} trigger 关闭触发行为的描述字符串\n@param {Object} event 关闭时事件对象", + "params": [{ + "name": "trigger", + "description": "关闭触发行为的描述字符串", + "type": { + "name": "String" + } + }, + { + "name": "event", + "description": "关闭时事件对象", + "type": { + "name": "Object" + } + } + ], + "returns": null + }, + "afterClose": { + "type": { + "name": "func" + }, + "required": false, + "description": "对话框关闭后触发的回调函数, 如果有动画,则在动画结束后触发", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "对话框关闭后触发的回调函数, 如果有动画,则在动画结束后触发", + "params": [], + "returns": null + }, + "hasMask": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否显示遮罩", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否显示遮罩" + }, + "animation": { + "type": { + "name": "union", + "value": [{ + "name": "object" + }, + { + "name": "bool" + } + ] + }, + "required": false, + "description": "显示隐藏时动画的播放方式", + "defaultValue": { + "value": "{\n in: 'fadeInDown',\n out: 'fadeOutUp',\n}", + "computed": false + }, + "docblock": "显示隐藏时动画的播放方式\n@property {String} in 进场动画\n@property {String} out 出场动画" + }, + "autoFocus": { + "type": { + "name": "bool" + }, + "required": false, + "description": "对话框弹出时是否自动获得焦点", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "对话框弹出时是否自动获得焦点" + }, + "align": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "bool" + } + ] + }, + "required": false, + "description": "对话框对齐方式, 具体见Overlay文档", + "defaultValue": { + "value": "'cc cc'", + "computed": false + }, + "docblock": "对话框对齐方式, 具体见Overlay文档" + }, + "isFullScreen": { + "type": { + "name": "bool" + }, + "required": false, + "description": "当对话框高度超过浏览器视口高度时,是否显示所有内容而不是出现滚动条以保证对话框完整显示在浏览器视口内,该属性仅在对话框垂直水平居中时生效,即 align 被设置为 'cc cc' 时", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "当对话框高度超过浏览器视口高度时,是否显示所有内容而不是出现滚动条以保证对话框完整显示在浏览器视口内,该属性仅在对话框垂直水平居中时生效,即 align 被设置为 'cc cc' 时" + }, + "shouldUpdatePosition": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否在对话框重新渲染时及时更新对话框位置,一般用于对话框高度变化后依然能保证原来的对齐方式", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否在对话框重新渲染时及时更新对话框位置,一般用于对话框高度变化后依然能保证原来的对齐方式" + }, + "minMargin": { + "type": { + "name": "number" + }, + "required": false, + "description": "对话框距离浏览器顶部和底部的最小间距,align 被设置为 'cc cc' 并且 isFullScreen 被设置为 true 时不生效", + "defaultValue": { + "value": "40", + "computed": false + }, + "docblock": "对话框距离浏览器顶部和底部的最小间距,align 被设置为 'cc cc' 并且 isFullScreen 被设置为 true 时不生效" + }, + "overlayProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "透传到弹层组件的属性对象", + "defaultValue": { + "value": "{}", + "computed": false + }, + "docblock": "透传到弹层组件的属性对象", + "properties": [] + }, + "locale": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义国际化文案对象", + "defaultValue": { + "value": "zhCN.Dialog", + "computed": true + }, + "docblock": "自定义国际化文案对象\n@property {String} ok 确认按钮文案\n@property {String} cancel 取消按钮文案", + "properties": [{ + "name": "ok", + "description": "确认按钮文案", + "type": { + "name": "String" + } + }, + { + "name": "cancel", + "description": "取消按钮文案", + "type": { + "name": "String" + } + } + ] + }, + "height": { + "type": { + "name": "string" + }, + "required": false, + "description": "对话框的高度样式属性", + "docblock": "对话框的高度样式属性" + } + }, + "methods": [{ + "name": "show", + "docblock": "\n 创建对话框\n @exportName show\n @param {Object} config 配置项\n @returns {Object} \b包含有 hide 方法,可用来关闭对话框\n ", + "description": "创建对话框", + "modifiers": [ + "static" + ], + "params": [{ + "name": "config", + "description": "配置项", + "type": { + "type": "NameExpression", + "name": "Object" + } + }], + "returns": { + "description": "\b包含有 hide 方法,可用来关闭对话框", + "type": { + "type": "NameExpression", + "name": "Object" + } + } + }, + { + "name": "alert", + "docblock": "\n 创建警示对话框\n @exportName alert\n @param {Object} config 配置项\n @returns {Object} \b包含有 hide 方法,可用来关闭对话框\n ", + "description": "创建警示对话框", + "modifiers": [ + "static" + ], + "params": [{ + "name": "config", + "description": "配置项", + "type": { + "type": "NameExpression", + "name": "Object" + } + }], + "returns": { + "description": "\b包含有 hide 方法,可用来关闭对话框", + "type": { + "type": "NameExpression", + "name": "Object" + } + } + }, + { + "name": "confirm", + "docblock": "\n 创建确认对话框\n @exportName confirm\n @param {Object} config 配置项\n @returns {Object} \b包含有 hide 方法,可用来关闭对话框\n ", + "description": "创建确认对话框", + "modifiers": [ + "static" + ], + "params": [{ + "name": "config", + "description": "配置项", + "type": { + "type": "NameExpression", + "name": "Object" + } + }], + "returns": { + "description": "\b包含有 hide 方法,可用来关闭对话框", + "type": { + "type": "NameExpression", + "name": "Object" + } + } + } + ], + "subComponents": [] + }, + { + "name": "Dropdown", + "title": "下拉菜单", + "typeId": 6, + "props": { + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "弹层内容", + "docblock": "弹层内容" + }, + "visible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层当前是否显示", + "docblock": "弹层当前是否显示" + }, + "onRequestClose": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层请求关闭时触发事件的回调函数", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层请求关闭时触发事件的回调函数\n@param {String} type 弹层关闭的来源\n@param {Object} e DOM 事件", + "params": [{ + "name": "type", + "description": "弹层关闭的来源", + "type": { + "name": "String" + } + }, + { + "name": "e", + "description": "DOM 事件", + "type": { + "name": "Object" + } + } + ], + "returns": null + }, + "target": { + "type": { + "name": "any" + }, + "required": false, + "description": "弹层定位的参照元素", + "docblock": "弹层定位的参照元素\n@default target 属性,即触发元素", + "defaultValue": { + "value": "target 属性,即触发元素", + "computed": false + } + }, + "align": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层相对于触发元素的定位, 详见 Overlay 的定位部分", + "defaultValue": { + "value": "'tl bl'", + "computed": false + }, + "docblock": "弹层相对于触发元素的定位, 详见 Overlay 的定位部分" + }, + "offset": { + "type": { + "name": "array" + }, + "required": false, + "description": "弹层相对于trigger的定位的微调, 接收数组[hoz, ver], 表示弹层在 left / top 上的增量\ne.g. [100, 100] 表示往右(RTL 模式下是往左) 、下分布偏移100px", + "defaultValue": { + "value": "[0, 0]", + "computed": false + }, + "docblock": "弹层相对于trigger的定位的微调, 接收数组[hoz, ver], 表示弹层在 left / top 上的增量\ne.g. [100, 100] 表示往右(RTL 模式下是往左) 、下分布偏移100px" + }, + "container": { + "type": { + "name": "any" + }, + "required": false, + "description": "渲染组件的容器,如果是函数需要返回 ref,如果是字符串则是该 DOM 的 id,也可以直接传入 DOM 节点", + "docblock": "渲染组件的容器,如果是函数需要返回 ref,如果是字符串则是该 DOM 的 id,也可以直接传入 DOM 节点" + }, + "hasMask": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否显示遮罩", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否显示遮罩" + }, + "canCloseByEsc": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否支持 esc 按键关闭弹层", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否支持 esc 按键关闭弹层" + }, + "canCloseByOutSideClick": { + "type": { + "name": "bool" + }, + "required": false, + "description": "点击弹层外的区域是否关闭弹层,不显示遮罩时生效", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "点击弹层外的区域是否关闭弹层,不显示遮罩时生效" + }, + "canCloseByMask": { + "type": { + "name": "bool" + }, + "required": false, + "description": "点击遮罩区域是否关闭弹层,显示遮罩时生效", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "点击遮罩区域是否关闭弹层,显示遮罩时生效" + }, + "beforeOpen": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层打开前触发事件的回调函数", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层打开前触发事件的回调函数", + "params": [], + "returns": null + }, + "onOpen": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层打开时触发事件的回调函数", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层打开时触发事件的回调函数", + "params": [], + "returns": null + }, + "afterOpen": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层打开后触发事件的回调函数, 如果有动画,则在动画结束后触发", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层打开后触发事件的回调函数, 如果有动画,则在动画结束后触发", + "params": [], + "returns": null + }, + "beforeClose": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层关闭前触发事件的回调函数", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层关闭前触发事件的回调函数", + "params": [], + "returns": null + }, + "onClose": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层关闭时触发事件的回调函数", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层关闭时触发事件的回调函数", + "params": [], + "returns": null + }, + "afterClose": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层关闭后触发事件的回调函数, 如果有动画,则在动画结束后触发", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层关闭后触发事件的回调函数, 如果有动画,则在动画结束后触发", + "params": [], + "returns": null + }, + "beforePosition": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层定位完成前触发的事件", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层定位完成前触发的事件", + "params": [], + "returns": null + }, + "onPosition": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层定位完成时触发的事件", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层定位完成时触发的事件\n@param {Object} config 定位的参数\n@param {Array} config.align 对齐方式,如 ['cc', 'cc'](如果开启 needAdjust,可能和预先设置的 align 不同)\n@param {Number} config.top 距离视口顶部距离\n@param {Number} config.left 距离视口左侧距离\n@param {Object} node 定位参照的容器节点", + "params": [{ + "name": "config", + "description": "定位的参数", + "type": { + "name": "Object" + } + }, + { + "name": "config.align", + "description": "对齐方式,如 ['cc', 'cc'](如果开启 needAdjust,可能和预先设置的 align 不同)", + "type": { + "name": "Array" + } + }, + { + "name": "config.top", + "description": "距离视口顶部距离", + "type": { + "name": "Number" + } + }, + { + "name": "config.left", + "description": "距离视口左侧距离", + "type": { + "name": "Number" + } + }, + { + "name": "node", + "description": "定位参照的容器节点", + "type": { + "name": "Object" + } + } + ], + "returns": null + }, + "shouldUpdatePosition": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否在每次弹层重新渲染后强制更新定位信息,一般用于弹层内容区域大小发生变化时,仍需保持原来的定位方式", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否在每次弹层重新渲染后强制更新定位信息,一般用于弹层内容区域大小发生变化时,仍需保持原来的定位方式" + }, + "autoFocus": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层打开时是否让其中的元素自动获取焦点", + "docblock": "弹层打开时是否让其中的元素自动获取焦点" + }, + "needAdjust": { + "type": { + "name": "bool" + }, + "required": false, + "description": "当弹层由于页面滚动等情况不在可视区域时,是否自动调整定位以出现在可视区域", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "当弹层由于页面滚动等情况不在可视区域时,是否自动调整定位以出现在可视区域" + }, + "disableScroll": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁用页面滚动", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否禁用页面滚动" + }, + "cache": { + "type": { + "name": "bool" + }, + "required": false, + "description": "隐藏时是否保留子节点", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "隐藏时是否保留子节点" + }, + "safeNode": { + "type": { + "name": "any" + }, + "required": false, + "description": "安全节点,当点击 document 的时候,如果包含该节点则不会关闭弹层,如果是函数需要返回 ref,如果是字符串则是该 DOM 的 id,也可以直接传入 DOM 节点,或者以上值组成的数组", + "docblock": "安全节点,当点击 document 的时候,如果包含该节点则不会关闭弹层,如果是函数需要返回 ref,如果是字符串则是该 DOM 的 id,也可以直接传入 DOM 节点,或者以上值组成的数组" + }, + "wrapperClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层的根节点的样式类", + "docblock": "弹层的根节点的样式类" + }, + "wrapperStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层的根节点的内联样式", + "docblock": "弹层的根节点的内联样式", + "properties": [] + }, + "animation": { + "type": { + "name": "union", + "value": [{ + "name": "object" + }, + { + "name": "bool" + } + ] + }, + "required": false, + "description": "配置动画的播放方式,支持 { in: 'enter-class', out: 'leave-class' } 的对象参数,如果设置为 false,则不播放动画", + "docblock": "配置动画的播放方式,支持 { in: 'enter-class', out: 'leave-class' } 的对象参数,如果设置为 false,则不播放动画\n@default { in: 'expandInDown', out: 'expandOutUp' }", + "defaultValue": { + "value": "{ in: 'expandInDown', out: 'expandOutUp' }", + "computed": false + } + }, + "trigger": { + "type": { + "name": "node" + }, + "required": false, + "description": "触发弹层显示或者隐藏的元素", + "docblock": "触发弹层显示或者隐藏的元素" + }, + "triggerType": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "触发弹层显示或隐藏的操作类型,可以是 'click','hover',或者它们组成的数组,如 ['hover', 'click']", + "defaultValue": { + "value": "'hover'", + "computed": false + }, + "docblock": "触发弹层显示或隐藏的操作类型,可以是 'click','hover',或者它们组成的数组,如 ['hover', 'click']" + }, + "triggerClickKeycode": { + "type": { + "name": "union", + "value": [{ + "name": "number" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "当 triggerType 为 click 时才生效,可自定义触发弹层显示的键盘码", + "defaultValue": { + "value": "[KEYCODE.SPACE, KEYCODE.ENTER]", + "computed": false + }, + "docblock": "当 triggerType 为 click 时才生效,可自定义触发弹层显示的键盘码" + }, + "defaultVisible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层默认是否显示", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "弹层默认是否显示" + }, + "onVisibleChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层显示或隐藏时触发的回调函数", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层显示或隐藏时触发的回调函数\n@param {Boolean} visible 弹层是否显示\n@param {String} type 触发弹层显示或隐藏的来源 fromContent 表示由Dropdown内容触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "params": [{ + "name": "visible", + "description": "弹层是否显示", + "type": { + "name": "Boolean" + } + }, + { + "name": "type", + "description": "触发弹层显示或隐藏的来源 fromContent 表示由Dropdown内容触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发", + "type": { + "name": "String" + } + } + ], + "returns": null + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "设置此属性,弹层无法显示或隐藏", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "设置此属性,弹层无法显示或隐藏" + }, + "delay": { + "type": { + "name": "number" + }, + "required": false, + "description": "弹层显示或隐藏的延时时间(以毫秒为单位),在 triggerType 被设置为 hover 时生效", + "defaultValue": { + "value": "200", + "computed": false + }, + "docblock": "弹层显示或隐藏的延时时间(以毫秒为单位),在 triggerType 被设置为 hover 时生效" + }, + "canCloseByTrigger": { + "type": { + "name": "bool" + }, + "required": false, + "description": "trigger 是否可以关闭弹层", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "trigger 是否可以关闭弹层" + }, + "followTrigger": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否跟随trigger滚动", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否跟随trigger滚动" + } + }, + "methods": [], + "description": "继承 Popup 的 API,除非特别说明", + "subComponents": [] + }, + { + "name": "Form", + "title": "表单", + "typeId": 3, + "props": { + "prefix": { + "type": { + "name": "string" + }, + "required": false, + "description": "样式前缀", + "defaultValue": { + "value": "'next-'", + "computed": false + }, + "docblock": "样式前缀" + }, + "inline": { + "type": { + "name": "bool" + }, + "required": false, + "description": "内联表单", + "docblock": "内联表单" + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'large'", + "computed": false, + "description": "大" + }, + { + "value": "'medium'", + "computed": false, + "description": "中" + }, + { + "value": "'small'", + "computed": false, + "description": "小" + } + ] + }, + "required": false, + "description": "单个 Item 的 size 自定义,优先级高于 Form 的 size, 并且当组件与 Item 一起使用时,组件自身设置 size 属性无效。", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "单个 Item 的 size 自定义,优先级高于 Form 的 size, 并且当组件与 Item 一起使用时,组件自身设置 size 属性无效。\n@enumdesc 大, 中, 小", + "value": [{ + "value": "'large'", + "computed": false, + "description": "大" + }, + { + "value": "'medium'", + "computed": false, + "description": "中" + }, + { + "value": "'small'", + "computed": false, + "description": "小" + } + ] + }, + "labelAlign": { + "type": { + "name": "enum", + "value": [{ + "value": "'top'", + "computed": false, + "description": "上" + }, + { + "value": "'left'", + "computed": false, + "description": "左" + }, + { + "value": "'inset'", + "computed": false, + "description": "内" + } + ] + }, + "required": false, + "description": "标签的位置", + "defaultValue": { + "value": "'left'", + "computed": false + }, + "docblock": "标签的位置\n@enumdesc 上, 左, 内", + "value": [{ + "value": "'top'", + "computed": false, + "description": "上" + }, + { + "value": "'left'", + "computed": false, + "description": "左" + }, + { + "value": "'inset'", + "computed": false, + "description": "内" + } + ] + }, + "labelTextAlign": { + "type": { + "name": "enum", + "value": [{ + "value": "'left'", + "computed": false, + "description": "左" + }, + { + "value": "'right'", + "computed": false, + "description": "右" + } + ] + }, + "required": false, + "description": "标签的左右对齐方式", + "docblock": "标签的左右对齐方式\n@enumdesc 左, 右", + "value": [{ + "value": "'left'", + "computed": false, + "description": "左" + }, + { + "value": "'right'", + "computed": false, + "description": "右" + } + ] + }, + "field": { + "type": { + "name": "any" + }, + "required": false, + "description": "经 `new Field(this)` 初始化后,直接传给 Form 即可 用到表单校验则不可忽略此项", + "docblock": "经 `new Field(this)` 初始化后,直接传给 Form 即可 用到表单校验则不可忽略此项" + }, + "saveField": { + "type": { + "name": "func" + }, + "required": false, + "description": "保存 Form 自动生成的 field 对象", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "保存 Form 自动生成的 field 对象", + "params": [], + "returns": null + }, + "labelCol": { + "type": { + "name": "object" + }, + "required": false, + "description": "控制第一级 Item 的 labelCol", + "docblock": "控制第一级 Item 的 labelCol", + "properties": [] + }, + "wrapperCol": { + "type": { + "name": "object" + }, + "required": false, + "description": "控制第一级 Item 的 wrapperCol", + "docblock": "控制第一级 Item 的 wrapperCol", + "properties": [] + }, + "onSubmit": { + "type": { + "name": "func" + }, + "required": false, + "description": "form内有 `htmlType=\"submit\"` 的元素的时候会触发", + "defaultValue": { + "value": "function preventDefault(e) {\n e.preventDefault();\n}", + "computed": false + }, + "docblock": "form内有 `htmlType=\"submit\"` 的元素的时候会触发", + "params": [], + "returns": null + }, + "children": { + "type": { + "name": "any" + }, + "required": false, + "description": "子元素", + "docblock": "子元素" + }, + "className": { + "type": { + "name": "string" + }, + "required": false, + "description": "扩展class", + "docblock": "扩展class" + }, + "style": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义内联样式", + "docblock": "自定义内联样式", + "properties": [] + }, + "value": { + "type": { + "name": "object" + }, + "required": false, + "description": "表单数值", + "docblock": "表单数值", + "properties": [] + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "表单变化回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "表单变化回调\n@param {Object} values 表单数据\n@param {Object} item 详细\n@param {String} item.name 变化的组件名\n@param {String} item.value 变化的数据\n@param {Object} item.field field 实例", + "params": [{ + "name": "values", + "description": "表单数据", + "type": { + "name": "Object" + } + }, + { + "name": "item", + "description": "详细", + "type": { + "name": "Object" + } + }, + { + "name": "item.name", + "description": "变化的组件名", + "type": { + "name": "String" + } + }, + { + "name": "item.value", + "description": "变化的数据", + "type": { + "name": "String" + } + }, + { + "name": "item.field", + "description": "field 实例", + "type": { + "name": "Object" + } + } + ], + "returns": null + }, + "component": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "func" + } + ] + }, + "required": false, + "description": "设置标签类型", + "defaultValue": { + "value": "'form'", + "computed": false + }, + "docblock": "设置标签类型" + } + }, + "methods": [], + "subComponents": [{ + "name": "Item", + "title": "表单项", + "props": { + "prefix": { + "type": { + "name": "string" + }, + "required": false, + "description": "样式前缀", + "defaultValue": { + "value": "'next-'", + "computed": false + }, + "docblock": "样式前缀" + }, + "label": { + "type": { + "name": "node" + }, + "required": false, + "description": "label 标签的文本", + "docblock": "label 标签的文本" + }, + "labelCol": { + "type": { + "name": "object" + }, + "required": false, + "description": "label 标签布局,通 `` 组件,设置 span offset 值,如 {span: 8, offset: 16},该项仅在垂直表单有效", + "docblock": "label 标签布局,通 `` 组件,设置 span offset 值,如 {span: 8, offset: 16},该项仅在垂直表单有效", + "properties": [] + }, + "wrapperCol": { + "type": { + "name": "object" + }, + "required": false, + "description": "需要为输入控件设置布局样式时,使用该属性,用法同 labelCol", + "docblock": "需要为输入控件设置布局样式时,使用该属性,用法同 labelCol", + "properties": [] + }, + "help": { + "type": { + "name": "node" + }, + "required": false, + "description": "自定义提示信息,如不设置,则会根据校验规则自动生成.", + "docblock": "自定义提示信息,如不设置,则会根据校验规则自动生成." + }, + "extra": { + "type": { + "name": "node" + }, + "required": false, + "description": "额外的提示信息,和 help 类似,当需要错误信息和提示文案同时出现时,可以使用这个。 位于错误信息后面", + "docblock": "额外的提示信息,和 help 类似,当需要错误信息和提示文案同时出现时,可以使用这个。 位于错误信息后面" + }, + "validateState": { + "type": { + "name": "enum", + "value": [{ + "value": "'error'", + "computed": false, + "description": "失败" + }, + { + "value": "'success'", + "computed": false, + "description": "成功" + }, + { + "value": "'loading'", + "computed": false, + "description": "校验中" + } + ] + }, + "required": false, + "description": "校验状态,如不设置,则会根据校验规则自动生成", + "docblock": "校验状态,如不设置,则会根据校验规则自动生成\n@enumdesc 失败, 成功, 校验中", + "value": [{ + "value": "'error'", + "computed": false, + "description": "失败" + }, + { + "value": "'success'", + "computed": false, + "description": "成功" + }, + { + "value": "'loading'", + "computed": false, + "description": "校验中" + } + ] + }, + "hasFeedback": { + "type": { + "name": "bool" + }, + "required": false, + "description": "配合 validateState 属性使用,是否展示 success/loading 的校验状态图标, 目前只有Input支持", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "配合 validateState 属性使用,是否展示 success/loading 的校验状态图标, 目前只有Input支持" + }, + "style": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义内联样式", + "docblock": "自定义内联样式", + "properties": [] + }, + "children": { + "type": { + "name": "union", + "value": [{ + "name": "node" + }, + { + "name": "func" + } + ] + }, + "required": false, + "description": "node 或者 function(values)", + "docblock": "node 或者 function(values)" + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'large'", + "computed": false + }, + { + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + } + ] + }, + "required": false, + "description": "单个 Item 的 size 自定义,优先级高于 Form 的 size, 并且当组件与 Item 一起使用时,组件自身设置 size 属性无效。", + "docblock": "单个 Item 的 size 自定义,优先级高于 Form 的 size, 并且当组件与 Item 一起使用时,组件自身设置 size 属性无效。" + }, + "labelAlign": { + "type": { + "name": "enum", + "value": [{ + "value": "'top'", + "computed": false, + "description": "上" + }, + { + "value": "'left'", + "computed": false, + "description": "左" + }, + { + "value": "'inset'", + "computed": false, + "description": "内" + } + ] + }, + "required": false, + "description": "标签的位置", + "docblock": "标签的位置\n@enumdesc 上, 左, 内", + "value": [{ + "value": "'top'", + "computed": false, + "description": "上" + }, + { + "value": "'left'", + "computed": false, + "description": "左" + }, + { + "value": "'inset'", + "computed": false, + "description": "内" + } + ] + }, + "labelTextAlign": { + "type": { + "name": "enum", + "value": [{ + "value": "'left'", + "computed": false, + "description": "左" + }, + { + "value": "'right'", + "computed": false, + "description": "右" + } + ] + }, + "required": false, + "description": "标签的左右对齐方式", + "docblock": "标签的左右对齐方式\n@enumdesc 左, 右", + "value": [{ + "value": "'left'", + "computed": false, + "description": "左" + }, + { + "value": "'right'", + "computed": false, + "description": "右" + } + ] + }, + "className": { + "type": { + "name": "string" + }, + "required": false, + "description": "扩展class", + "docblock": "扩展class" + }, + "required": { + "type": { + "name": "bool" + }, + "required": false, + "description": "[表单校验] 不能为空", + "docblock": "[表单校验] 不能为空" + }, + "asterisk": { + "type": { + "name": "bool" + }, + "required": false, + "description": "required 的星号是否显示", + "docblock": "required 的星号是否显示" + }, + "requiredMessage": { + "type": { + "name": "string" + }, + "required": false, + "description": "required 自定义错误信息", + "docblock": "required 自定义错误信息" + }, + "requiredTrigger": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "required 自定义触发方式", + "docblock": "required 自定义触发方式" + }, + "min": { + "type": { + "name": "number" + }, + "required": false, + "description": "[表单校验] 最小值", + "docblock": "[表单校验] 最小值" + }, + "max": { + "type": { + "name": "number" + }, + "required": false, + "description": "[表单校验] 最大值", + "docblock": "[表单校验] 最大值" + }, + "minmaxMessage": { + "type": { + "name": "string" + }, + "required": false, + "description": "min/max 自定义错误信息", + "docblock": "min/max 自定义错误信息" + }, + "minmaxTrigger": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "min/max 自定义触发方式", + "docblock": "min/max 自定义触发方式" + }, + "minLength": { + "type": { + "name": "number" + }, + "required": false, + "description": "[表单校验] 字符串最小长度 / 数组最小个数", + "docblock": "[表单校验] 字符串最小长度 / 数组最小个数" + }, + "maxLength": { + "type": { + "name": "number" + }, + "required": false, + "description": "[表单校验] 字符串最大长度 / 数组最大个数", + "docblock": "[表单校验] 字符串最大长度 / 数组最大个数" + }, + "minmaxLengthMessage": { + "type": { + "name": "string" + }, + "required": false, + "description": "minLength/maxLength 自定义错误信息", + "docblock": "minLength/maxLength 自定义错误信息" + }, + "minmaxLengthTrigger": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "minLength/maxLength 自定义触发方式", + "docblock": "minLength/maxLength 自定义触发方式" + }, + "length": { + "type": { + "name": "number" + }, + "required": false, + "description": "[表单校验] 字符串精确长度 / 数组精确个数", + "docblock": "[表单校验] 字符串精确长度 / 数组精确个数" + }, + "lengthMessage": { + "type": { + "name": "string" + }, + "required": false, + "description": "length 自定义错误信息", + "docblock": "length 自定义错误信息" + }, + "lengthTrigger": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "length 自定义触发方式", + "docblock": "length 自定义触发方式" + }, + "pattern": { + "type": { + "name": "any" + }, + "required": false, + "description": "正则校验", + "docblock": "正则校验" + }, + "patternMessage": { + "type": { + "name": "string" + }, + "required": false, + "description": "pattern 自定义错误信息", + "docblock": "pattern 自定义错误信息" + }, + "patternTrigger": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "pattern 自定义触发方式", + "docblock": "pattern 自定义触发方式" + }, + "format": { + "type": { + "name": "enum", + "value": [{ + "value": "'number'", + "computed": false + }, + { + "value": "'email'", + "computed": false + }, + { + "value": "'url'", + "computed": false + }, + { + "value": "'tel'", + "computed": false + } + ] + }, + "required": false, + "description": "[表单校验] 四种常用的 pattern", + "docblock": "[表单校验] 四种常用的 pattern" + }, + "formatMessage": { + "type": { + "name": "string" + }, + "required": false, + "description": "format 自定义错误信息", + "docblock": "format 自定义错误信息" + }, + "formatTrigger": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "format 自定义触发方式", + "docblock": "format 自定义触发方式" + }, + "validator": { + "type": { + "name": "func" + }, + "required": false, + "description": "[表单校验] 自定义校验函数", + "docblock": "[表单校验] 自定义校验函数", + "params": [], + "returns": null + }, + "validatorTrigger": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "validator 自定义触发方式", + "docblock": "validator 自定义触发方式" + }, + "autoValidate": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否修改数据时自动触发校验", + "docblock": "是否修改数据时自动触发校验" + } + }, + "methods": [{ + "name": "getNames", + "docblock": "从子元素里面提取表单组件", + "modifiers": [], + "params": [], + "returns": null, + "description": "从子元素里面提取表单组件" + }], + "description": "手动传递了 wrapCol labelCol 会使用 Grid 辅助布局; labelAlign='top' 会强制禁用 Grid", + "order": 1 + }, + { + "name": "Submit", + "title": "表单提交", + "props": { + "type": { + "type": { + "name": "enum", + "value": [{ + "value": "'primary'", + "computed": false + }, + { + "value": "'secondary'", + "computed": false + }, + { + "value": "'normal'", + "computed": false + } + ] + }, + "required": false, + "description": "按钮的类型", + "defaultValue": { + "value": "'normal'", + "computed": false + }, + "docblock": "按钮的类型" + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + } + ] + }, + "required": false, + "description": "按钮的尺寸", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "按钮的尺寸" + }, + "iconSize": { + "type": { + "name": "enum", + "value": [{ + "value": "'xxs'", + "computed": false + }, + { + "value": "'xs'", + "computed": false + }, + { + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + }, + { + "value": "'xl'", + "computed": false + }, + { + "value": "'xxl'", + "computed": false + }, + { + "value": "'xxxl'", + "computed": false + } + ] + }, + "required": false, + "description": "按钮中 Icon 的尺寸,用于替代 Icon 的默认大小", + "docblock": "按钮中 Icon 的尺寸,用于替代 Icon 的默认大小" + }, + "htmlType": { + "type": { + "name": "enum", + "value": [{ + "value": "'submit'", + "computed": false + }, + { + "value": "'reset'", + "computed": false + }, + { + "value": "'button'", + "computed": false + } + ] + }, + "required": false, + "description": "当 component = 'button' 时,设置 button 标签的 type 值", + "defaultValue": { + "value": "'button'", + "computed": false + }, + "docblock": "当 component = 'button' 时,设置 button 标签的 type 值" + }, + "component": { + "type": { + "name": "enum", + "value": [{ + "value": "'button'", + "computed": false + }, + { + "value": "'a'", + "computed": false + }, + { + "value": "'div'", + "computed": false + }, + { + "value": "'span'", + "computed": false + } + ] + }, + "required": false, + "description": "设置标签类型", + "defaultValue": { + "value": "'button'", + "computed": false + }, + "docblock": "设置标签类型" + }, + "loading": { + "type": { + "name": "bool" + }, + "required": false, + "description": "设置按钮的载入状态", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "设置按钮的载入状态" + }, + "ghost": { + "type": { + "name": "enum", + "value": [{ + "value": "true", + "computed": false + }, + { + "value": "false", + "computed": false + }, + { + "value": "'light'", + "computed": false + }, + { + "value": "'dark'", + "computed": false + } + ] + }, + "required": false, + "description": "是否为幽灵按钮", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否为幽灵按钮" + }, + "text": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否为文本按钮", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否为文本按钮" + }, + "warning": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否为警告按钮", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否为警告按钮" + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁用", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否禁用" + }, + "onClick": { + "type": { + "name": "func" + }, + "required": false, + "description": "点击提交后触发", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "点击提交后触发\n@param {Object} value 数据\n@param {Object} errors 错误数据\n@param {class} field 实例", + "params": [{ + "name": "value", + "description": "数据", + "type": { + "name": "Object" + } + }, + { + "name": "errors", + "description": "错误数据", + "type": { + "name": "Object" + } + }, + { + "name": "field", + "description": "实例", + "type": { + "name": "class" + } + } + ], + "returns": null + }, + "validate": { + "type": { + "name": "union", + "value": [{ + "name": "bool" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "是否校验/需要校验的 name 数组", + "docblock": "是否校验/需要校验的 name 数组" + }, + "field": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义 field (在 Form 内不需要设置)", + "docblock": "自定义 field (在 Form 内不需要设置)", + "properties": [] + } + }, + "methods": [], + "description": "继承 Button API", + "order": 2 + }, + { + "name": "Reset", + "title": "表单重置", + "props": { + "type": { + "type": { + "name": "enum", + "value": [{ + "value": "'primary'", + "computed": false + }, + { + "value": "'secondary'", + "computed": false + }, + { + "value": "'normal'", + "computed": false + } + ] + }, + "required": false, + "description": "按钮的类型", + "defaultValue": { + "value": "'normal'", + "computed": false + }, + "docblock": "按钮的类型" + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + } + ] + }, + "required": false, + "description": "按钮的尺寸", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "按钮的尺寸" + }, + "iconSize": { + "type": { + "name": "enum", + "value": [{ + "value": "'xxs'", + "computed": false + }, + { + "value": "'xs'", + "computed": false + }, + { + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + }, + { + "value": "'xl'", + "computed": false + }, + { + "value": "'xxl'", + "computed": false + }, + { + "value": "'xxxl'", + "computed": false + } + ] + }, + "required": false, + "description": "按钮中 Icon 的尺寸,用于替代 Icon 的默认大小", + "docblock": "按钮中 Icon 的尺寸,用于替代 Icon 的默认大小" + }, + "htmlType": { + "type": { + "name": "enum", + "value": [{ + "value": "'submit'", + "computed": false + }, + { + "value": "'reset'", + "computed": false + }, + { + "value": "'button'", + "computed": false + } + ] + }, + "required": false, + "description": "当 component = 'button' 时,设置 button 标签的 type 值", + "defaultValue": { + "value": "'button'", + "computed": false + }, + "docblock": "当 component = 'button' 时,设置 button 标签的 type 值" + }, + "component": { + "type": { + "name": "enum", + "value": [{ + "value": "'button'", + "computed": false + }, + { + "value": "'a'", + "computed": false + }, + { + "value": "'div'", + "computed": false + }, + { + "value": "'span'", + "computed": false + } + ] + }, + "required": false, + "description": "设置标签类型", + "defaultValue": { + "value": "'button'", + "computed": false + }, + "docblock": "设置标签类型" + }, + "loading": { + "type": { + "name": "bool" + }, + "required": false, + "description": "设置按钮的载入状态", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "设置按钮的载入状态" + }, + "ghost": { + "type": { + "name": "enum", + "value": [{ + "value": "true", + "computed": false + }, + { + "value": "false", + "computed": false + }, + { + "value": "'light'", + "computed": false + }, + { + "value": "'dark'", + "computed": false + } + ] + }, + "required": false, + "description": "是否为幽灵按钮", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否为幽灵按钮" + }, + "text": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否为文本按钮", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否为文本按钮" + }, + "warning": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否为警告按钮", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否为警告按钮" + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁用", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否禁用" + }, + "onClick": { + "type": { + "name": "func" + }, + "required": false, + "description": "点击提交后触发", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "点击提交后触发", + "params": [], + "returns": null + }, + "names": { + "type": { + "name": "array" + }, + "required": false, + "description": "自定义重置的字段", + "docblock": "自定义重置的字段" + }, + "toDefault": { + "type": { + "name": "bool" + }, + "required": false, + "description": "返回默认值", + "docblock": "返回默认值" + }, + "field": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义 field (在 Form 内不需要设置)", + "docblock": "自定义 field (在 Form 内不需要设置)", + "properties": [] + } + }, + "methods": [], + "description": "继承 Button API", + "order": 3 + }, + { + "name": "Error", + "title": "表单错误", + "props": { + "name": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "表单名", + "docblock": "表单名" + }, + "field": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义 field (在 Form 内不需要设置)", + "docblock": "自定义 field (在 Form 内不需要设置)", + "properties": [] + }, + "children": { + "type": { + "name": "union", + "value": [{ + "name": "node" + }, + { + "name": "func" + } + ] + }, + "required": false, + "description": "自定义错误渲染, 可以是 node 或者 function(errors, state)", + "docblock": "自定义错误渲染, 可以是 node 或者 function(errors, state)" + } + }, + "methods": [], + "description": "自定义错误展示", + "order": 4 + } + ] + }, + { + "name": "Grid", + "title": "栅格", + "typeId": 1, + "subComponents": [{ + "name": "Row", + "title": "行", + "props": { + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "行内容", + "docblock": "行内容" + }, + "gutter": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + } + ] + }, + "required": false, + "description": "列间隔", + "defaultValue": { + "value": "0", + "computed": false + }, + "docblock": "列间隔" + }, + "wrap": { + "type": { + "name": "bool" + }, + "required": false, + "description": "列在行中宽度溢出后是否换行", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "列在行中宽度溢出后是否换行" + }, + "fixed": { + "type": { + "name": "bool" + }, + "required": false, + "description": "行在某一断点下宽度是否保持不变(默认行宽度随视口变化而变化)", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "行在某一断点下宽度是否保持不变(默认行宽度随视口变化而变化)" + }, + "fixedWidth": { + "type": { + "name": "enum", + "value": [{ + "value": "'xxs'", + "computed": false, + "description": "320px" + }, + { + "value": "'xs'", + "computed": false, + "description": "480px" + }, + { + "value": "'s'", + "computed": false, + "description": "720px" + }, + { + "value": "'m'", + "computed": false, + "description": "990px" + }, + { + "value": "'l'", + "computed": false, + "description": "1200px" + }, + { + "value": "'xl'", + "computed": false, + "description": "1500px" + } + ] + }, + "required": false, + "description": "固定行的宽度为某一断点的宽度,不受视口影响而变动", + "docblock": "固定行的宽度为某一断点的宽度,不受视口影响而变动\n@enumdesc 320px, 480px, 720px, 990px, 1200px, 1500px", + "value": [{ + "value": "'xxs'", + "computed": false, + "description": "320px" + }, + { + "value": "'xs'", + "computed": false, + "description": "480px" + }, + { + "value": "'s'", + "computed": false, + "description": "720px" + }, + { + "value": "'m'", + "computed": false, + "description": "990px" + }, + { + "value": "'l'", + "computed": false, + "description": "1200px" + }, + { + "value": "'xl'", + "computed": false, + "description": "1500px" + } + ] + }, + "align": { + "type": { + "name": "enum", + "value": [{ + "value": "'top'", + "computed": false, + "description": "顶部对齐" + }, + { + "value": "'center'", + "computed": false, + "description": "居中对齐" + }, + { + "value": "'bottom'", + "computed": false, + "description": "底部对齐" + }, + { + "value": "'baseline'", + "computed": false, + "description": "按第一行文字基线对齐" + }, + { + "value": "'stretch'", + "computed": false, + "description": "未设置高度或设为 auto,将占满整个容器的高度" + } + ] + }, + "required": false, + "description": "(不支持IE9浏览器)多列垂直方向对齐方式", + "docblock": "(不支持IE9浏览器)多列垂直方向对齐方式\n@enumdesc 顶部对齐, 居中对齐, 底部对齐, 按第一行文字基线对齐, 未设置高度或设为 auto,将占满整个容器的高度", + "value": [{ + "value": "'top'", + "computed": false, + "description": "顶部对齐" + }, + { + "value": "'center'", + "computed": false, + "description": "居中对齐" + }, + { + "value": "'bottom'", + "computed": false, + "description": "底部对齐" + }, + { + "value": "'baseline'", + "computed": false, + "description": "按第一行文字基线对齐" + }, + { + "value": "'stretch'", + "computed": false, + "description": "未设置高度或设为 auto,将占满整个容器的高度" + } + ] + }, + "justify": { + "type": { + "name": "enum", + "value": [{ + "value": "'start'", + "computed": false, + "description": "左对齐" + }, + { + "value": "'center'", + "computed": false, + "description": "居中对齐" + }, + { + "value": "'end'", + "computed": false, + "description": "右对齐" + }, + { + "value": "'space-between'", + "computed": false, + "description": "两端对齐,列之间间距相等" + }, + { + "value": "'space-around'", + "computed": false, + "description": "每列具有相同的左右间距,行两端间距是列间距的二分之一" + } + ] + }, + "required": false, + "description": "(不支持IE9浏览器)行内具有多余空间时的布局方式", + "docblock": "(不支持IE9浏览器)行内具有多余空间时的布局方式\n@enumdesc 左对齐, 居中对齐, 右对齐, 两端对齐,列之间间距相等, 每列具有相同的左右间距,行两端间距是列间距的二分之一", + "value": [{ + "value": "'start'", + "computed": false, + "description": "左对齐" + }, + { + "value": "'center'", + "computed": false, + "description": "居中对齐" + }, + { + "value": "'end'", + "computed": false, + "description": "右对齐" + }, + { + "value": "'space-between'", + "computed": false, + "description": "两端对齐,列之间间距相等" + }, + { + "value": "'space-around'", + "computed": false, + "description": "每列具有相同的左右间距,行两端间距是列间距的二分之一" + } + ] + }, + "hidden": { + "type": { + "name": "union", + "value": [{ + "name": "bool" + }, + { + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "行在不同断点下的显示与隐藏

**可选值**:
true(在所有断点下隐藏)
false(在所有断点下显示)
'xs'(在 xs 断点下隐藏)
['xxs', 'xs', 's', 'm', 'l', 'xl'](在 xxs, xs, s, m, l, xl 断点下隐藏)", + "docblock": "行在不同断点下的显示与隐藏

**可选值**:
true(在所有断点下隐藏)
false(在所有断点下显示)
'xs'(在 xs 断点下隐藏)
['xxs', 'xs', 's', 'm', 'l', 'xl'](在 xxs, xs, s, m, l, xl 断点下隐藏)" + }, + "component": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "func" + } + ] + }, + "required": false, + "description": "指定以何种元素渲染该节点\n- 默认为 'div'", + "defaultValue": { + "value": "'div'", + "computed": false + }, + "docblock": "指定以何种元素渲染该节点\n- 默认为 'div'" + } + }, + "methods": [], + "order": 1 + }, + { + "name": "Col", + "title": "列", + "props": { + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "列内容", + "docblock": "列内容" + }, + "span": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + } + ] + }, + "required": false, + "description": "列宽度

**可选值**:
1, 2, 3, ..., 22, 23, 24", + "docblock": "列宽度

**可选值**:
1, 2, 3, ..., 22, 23, 24" + }, + "fixedSpan": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + } + ] + }, + "required": false, + "description": "固定列宽度,宽度值为20 * 栅格数

**可选值**:
1, 2, 3, ..., 28, 29, 30", + "docblock": "固定列宽度,宽度值为20 * 栅格数

**可选值**:
1, 2, 3, ..., 28, 29, 30" + }, + "offset": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + } + ] + }, + "required": false, + "description": "(不支持IE9浏览器)列偏移

**可选值**:
1, 2, 3, ..., 22, 23, 24", + "docblock": "(不支持IE9浏览器)列偏移

**可选值**:
1, 2, 3, ..., 22, 23, 24" + }, + "fixedOffset": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + } + ] + }, + "required": false, + "description": "(不支持IE9浏览器)固定列偏移,宽度值为20 * 栅格数

**可选值**:
1, 2, 3, ..., 28, 29, 30", + "docblock": "(不支持IE9浏览器)固定列偏移,宽度值为20 * 栅格数

**可选值**:
1, 2, 3, ..., 28, 29, 30" + }, + "align": { + "type": { + "name": "enum", + "value": [{ + "value": "'top'", + "computed": false + }, + { + "value": "'center'", + "computed": false + }, + { + "value": "'bottom'", + "computed": false + }, + { + "value": "'baseline'", + "computed": false + }, + { + "value": "'stretch'", + "computed": false + } + ] + }, + "required": false, + "description": "(不支持IE9浏览器)多列垂直方向对齐方式,可覆盖Row的align属性", + "docblock": "(不支持IE9浏览器)多列垂直方向对齐方式,可覆盖Row的align属性" + }, + "hidden": { + "type": { + "name": "union", + "value": [{ + "name": "bool" + }, + { + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "列在不同断点下的显示与隐藏

**可选值**:
true(在所有断点下隐藏)
false(在所有断点下显示)
'xs'(在 xs 断点下隐藏)
['xxs', 'xs', 's', 'm', 'l', 'xl'](在 xxs, xs, s, m, l, xl 断点下隐藏)", + "docblock": "列在不同断点下的显示与隐藏

**可选值**:
true(在所有断点下隐藏)
false(在所有断点下显示)
'xs'(在 xs 断点下隐藏)
['xxs', 'xs', 's', 'm', 'l', 'xl'](在 xxs, xs, s, m, l, xl 断点下隐藏)" + }, + "xxs": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + }, + { + "name": "object" + } + ] + }, + "required": false, + "description": ">=320px,响应式栅格,可为栅格数(span)或一个包含栅格数(span)和偏移栅格数(offset)对象", + "docblock": ">=320px,响应式栅格,可为栅格数(span)或一个包含栅格数(span)和偏移栅格数(offset)对象" + }, + "xs": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + }, + { + "name": "object" + } + ] + }, + "required": false, + "description": ">=480px,响应式栅格,可为栅格数(span)或一个包含栅格数(span)和偏移栅格数(offset)对象", + "docblock": ">=480px,响应式栅格,可为栅格数(span)或一个包含栅格数(span)和偏移栅格数(offset)对象" + }, + "s": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + }, + { + "name": "object" + } + ] + }, + "required": false, + "description": ">=720px,响应式栅格,可为栅格数(span)或一个包含栅格数(span)和偏移栅格数(offset)对象", + "docblock": ">=720px,响应式栅格,可为栅格数(span)或一个包含栅格数(span)和偏移栅格数(offset)对象" + }, + "m": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + }, + { + "name": "object" + } + ] + }, + "required": false, + "description": ">=990px,响应式栅格,可为栅格数(span)或一个包含栅格数(span)和偏移栅格数(offset)对象", + "docblock": ">=990px,响应式栅格,可为栅格数(span)或一个包含栅格数(span)和偏移栅格数(offset)对象" + }, + "l": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + }, + { + "name": "object" + } + ] + }, + "required": false, + "description": ">=1200px,响应式栅格,可为栅格数(span)或一个包含栅格数(span)和偏移栅格数(offset)对象", + "docblock": ">=1200px,响应式栅格,可为栅格数(span)或一个包含栅格数(span)和偏移栅格数(offset)对象" + }, + "xl": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + }, + { + "name": "object" + } + ] + }, + "required": false, + "description": ">=1500px,响应式栅格,可为栅格数(span)或一个包含栅格数(span)和偏移栅格数(offset)对象", + "docblock": ">=1500px,响应式栅格,可为栅格数(span)或一个包含栅格数(span)和偏移栅格数(offset)对象" + }, + "component": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "func" + } + ] + }, + "required": false, + "description": "指定以何种元素渲染该节点,默认为 'div'", + "defaultValue": { + "value": "'div'", + "computed": false + }, + "docblock": "指定以何种元素渲染该节点,默认为 'div'" + } + }, + "methods": [], + "order": 2 + } + ], + "methods": [] + }, + { + "name": "Icon", + "title": "图标", + "typeId": 1, + "props": { + "type": { + "type": { + "name": "string" + }, + "required": false, + "description": "指定显示哪种图标", + "docblock": "指定显示哪种图标" + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'xxs'", + "computed": false + }, + { + "value": "'xs'", + "computed": false + }, + { + "value": "'small'", + "computed": false + }, + { + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + }, + { + "value": "'xl'", + "computed": false + }, + { + "value": "'xxl'", + "computed": false + }, + { + "value": "'xxxl'", + "computed": false + }, + { + "value": "'inherit'", + "computed": false + } + ] + }, + "required": false, + "description": "指定图标大小", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "指定图标大小" + } + }, + "methods": [], + "subComponents": [] + }, + { + "name": "Input", + "title": "输入框", + "typeId": 3, + "props": { + "value": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + } + ] + }, + "required": false, + "description": "当前值", + "docblock": "当前值" + }, + "defaultValue": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + } + ] + }, + "required": false, + "description": "初始化值", + "docblock": "初始化值" + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "发生改变的时候触发的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "发生改变的时候触发的回调\n@param {String} value 数据\n@param {Event} e DOM事件对象", + "params": [{ + "name": "value", + "description": "数据", + "type": { + "name": "String" + } + }, + { + "name": "e", + "description": "DOM事件对象", + "type": { + "name": "Event" + } + } + ], + "returns": null + }, + "onKeyDown": { + "type": { + "name": "func" + }, + "required": false, + "description": "键盘按下的时候触发的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "键盘按下的时候触发的回调\n@param {Event} e DOM事件对象\n@param {Object} opts 可扩展的附加信息:
- opts.overMaxLength: {Boolean} 已超出最大长度
- opts.beTrimed: {Boolean} 输入的空格被清理", + "params": [{ + "name": "e", + "description": "DOM事件对象", + "type": { + "name": "Event" + } + }, + { + "name": "opts", + "description": "可扩展的附加信息:
- opts.overMaxLength: {Boolean} 已超出最大长度
- opts.beTrimed: {Boolean} 输入的空格被清理", + "type": { + "name": "Object" + } + } + ], + "returns": null + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "禁用状态", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "禁用状态" + }, + "maxLength": { + "type": { + "name": "number" + }, + "required": false, + "description": "最大长度", + "defaultValue": { + "value": "null", + "computed": false + }, + "docblock": "最大长度" + }, + "hasLimitHint": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否展现最大长度样式", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否展现最大长度样式" + }, + "cutString": { + "type": { + "name": "bool" + }, + "required": false, + "description": "当设置了maxLength时,是否截断超出字符串", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "当设置了maxLength时,是否截断超出字符串" + }, + "readOnly": { + "type": { + "name": "bool" + }, + "required": false, + "description": "只读", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "只读" + }, + "trim": { + "type": { + "name": "bool" + }, + "required": false, + "description": "onChange返回会自动去除头尾空字符", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "onChange返回会自动去除头尾空字符" + }, + "placeholder": { + "type": { + "name": "string" + }, + "required": false, + "description": "输入提示", + "docblock": "输入提示" + }, + "onFocus": { + "type": { + "name": "func" + }, + "required": false, + "description": "获取焦点时候触发的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "获取焦点时候触发的回调\n@param {Event} e DOM事件对象", + "params": [{ + "name": "e", + "description": "DOM事件对象", + "type": { + "name": "Event" + } + }], + "returns": null + }, + "onBlur": { + "type": { + "name": "func" + }, + "required": false, + "description": "失去焦点时候触发的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "失去焦点时候触发的回调\n@param {Event} e DOM事件对象", + "params": [{ + "name": "e", + "description": "DOM事件对象", + "type": { + "name": "Event" + } + }], + "returns": null + }, + "getValueLength": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义字符串计算长度方式", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "自定义字符串计算长度方式\n@param {String} value 数据\n@returns {Number} 自定义长度", + "params": [{ + "name": "value", + "description": "数据", + "type": { + "name": "String" + } + }], + "returns": { + "description": "自定义长度", + "type": { + "name": "Number" + } + } + }, + "className": { + "type": { + "name": "string" + }, + "required": false, + "description": "自定义class", + "docblock": "自定义class" + }, + "style": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义内联样式", + "docblock": "自定义内联样式", + "properties": [] + }, + "htmlType": { + "type": { + "name": "string" + }, + "required": false, + "description": "原生type", + "docblock": "原生type" + }, + "name": { + "type": { + "name": "string" + }, + "required": false, + "description": "name", + "docblock": "name" + }, + "state": { + "type": { + "name": "enum", + "value": [{ + "value": "'error'", + "computed": false, + "description": "错误" + }, + { + "value": "'loading'", + "computed": false, + "description": "校验中" + }, + { + "value": "'success'", + "computed": false, + "description": "成功" + } + ] + }, + "required": false, + "description": "状态", + "docblock": "状态\n@enumdesc 错误, 校验中, 成功", + "value": [{ + "value": "'error'", + "computed": false, + "description": "错误" + }, + { + "value": "'loading'", + "computed": false, + "description": "校验中" + }, + { + "value": "'success'", + "computed": false, + "description": "成功" + } + ] + }, + "label": { + "type": { + "name": "node" + }, + "required": false, + "description": "label", + "docblock": "label" + }, + "hasClear": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否出现clear按钮", + "docblock": "是否出现clear按钮" + }, + "hasBorder": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否有边框", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否有边框" + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'small'", + "computed": false, + "description": "小" + }, + { + "value": "'medium'", + "computed": false, + "description": "中" + }, + { + "value": "'large'", + "computed": false, + "description": "大" + } + ] + }, + "required": false, + "description": "尺寸", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "尺寸\n@enumdesc 小, 中, 大", + "value": [{ + "value": "'small'", + "computed": false, + "description": "小" + }, + { + "value": "'medium'", + "computed": false, + "description": "中" + }, + { + "value": "'large'", + "computed": false, + "description": "大" + } + ] + }, + "onPressEnter": { + "type": { + "name": "func" + }, + "required": false, + "description": "按下回车的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "按下回车的回调", + "params": [], + "returns": null + }, + "hint": { + "type": { + "name": "string" + }, + "required": false, + "description": "水印 (Icon的type类型,和hasClear占用一个地方)", + "docblock": "水印 (Icon的type类型,和hasClear占用一个地方)" + }, + "innerBefore": { + "type": { + "name": "node" + }, + "required": false, + "description": "文字前附加内容", + "docblock": "文字前附加内容" + }, + "innerAfter": { + "type": { + "name": "node" + }, + "required": false, + "description": "文字后附加内容", + "docblock": "文字后附加内容" + }, + "addonBefore": { + "type": { + "name": "node" + }, + "required": false, + "description": "输入框前附加内容", + "docblock": "输入框前附加内容" + }, + "addonAfter": { + "type": { + "name": "node" + }, + "required": false, + "description": "输入框后附加内容", + "docblock": "输入框后附加内容" + }, + "addonTextBefore": { + "type": { + "name": "node" + }, + "required": false, + "description": "输入框前附加文字", + "docblock": "输入框前附加文字" + }, + "addonTextAfter": { + "type": { + "name": "node" + }, + "required": false, + "description": "输入框后附加文字", + "docblock": "输入框后附加文字" + }, + "autoComplete": { + "type": { + "name": "string" + }, + "required": false, + "description": "(原生input支持)", + "defaultValue": { + "value": "'off'", + "computed": false + }, + "docblock": "(原生input支持)" + }, + "autoFocus": { + "type": { + "name": "bool" + }, + "required": false, + "description": "自动聚焦(原生input支持)", + "docblock": "自动聚焦(原生input支持)" + } + }, + "methods": [], + "subComponents": [{ + "name": "TextArea", + "title": "文本域", + "props": { + "value": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + } + ] + }, + "required": false, + "description": "当前值", + "docblock": "当前值" + }, + "defaultValue": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "number" + } + ] + }, + "required": false, + "description": "初始化值", + "docblock": "初始化值" + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "发生改变的时候触发的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "发生改变的时候触发的回调\n@param {String} value 数据\n@param {Event} e DOM事件对象", + "params": [{ + "name": "value", + "description": "数据", + "type": { + "name": "String" + } + }, + { + "name": "e", + "description": "DOM事件对象", + "type": { + "name": "Event" + } + } + ], + "returns": null + }, + "onKeyDown": { + "type": { + "name": "func" + }, + "required": false, + "description": "键盘按下的时候触发的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "键盘按下的时候触发的回调\n@param {Event} e DOM事件对象\n@param {Object} opts 可扩展的附加信息:
- opts.overMaxLength: {Boolean} 已超出最大长度
- opts.beTrimed: {Boolean} 输入的空格被清理", + "params": [{ + "name": "e", + "description": "DOM事件对象", + "type": { + "name": "Event" + } + }, + { + "name": "opts", + "description": "可扩展的附加信息:
- opts.overMaxLength: {Boolean} 已超出最大长度
- opts.beTrimed: {Boolean} 输入的空格被清理", + "type": { + "name": "Object" + } + } + ], + "returns": null + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "禁用状态", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "禁用状态" + }, + "maxLength": { + "type": { + "name": "number" + }, + "required": false, + "description": "最大长度", + "defaultValue": { + "value": "null", + "computed": false + }, + "docblock": "最大长度" + }, + "hasLimitHint": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否展现最大长度样式", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否展现最大长度样式" + }, + "cutString": { + "type": { + "name": "bool" + }, + "required": false, + "description": "当设置了maxLength时,是否截断超出字符串", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "当设置了maxLength时,是否截断超出字符串" + }, + "readOnly": { + "type": { + "name": "bool" + }, + "required": false, + "description": "只读", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "只读" + }, + "trim": { + "type": { + "name": "bool" + }, + "required": false, + "description": "onChange返回会自动去除头尾空字符", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "onChange返回会自动去除头尾空字符" + }, + "placeholder": { + "type": { + "name": "string" + }, + "required": false, + "description": "输入提示", + "docblock": "输入提示" + }, + "onFocus": { + "type": { + "name": "func" + }, + "required": false, + "description": "获取焦点时候触发的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "获取焦点时候触发的回调\n@param {Event} e DOM事件对象", + "params": [{ + "name": "e", + "description": "DOM事件对象", + "type": { + "name": "Event" + } + }], + "returns": null + }, + "onBlur": { + "type": { + "name": "func" + }, + "required": false, + "description": "失去焦点时候触发的回调", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "失去焦点时候触发的回调\n@param {Event} e DOM事件对象", + "params": [{ + "name": "e", + "description": "DOM事件对象", + "type": { + "name": "Event" + } + }], + "returns": null + }, + "getValueLength": { + "type": { + "name": "func" + }, + "required": false, + "description": "自定义字符串计算长度方式", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "自定义字符串计算长度方式\n@param {String} value 数据\n@returns {Number} 自定义长度", + "params": [{ + "name": "value", + "description": "数据", + "type": { + "name": "String" + } + }], + "returns": { + "description": "自定义长度", + "type": { + "name": "Number" + } + } + }, + "className": { + "type": { + "name": "string" + }, + "required": false, + "description": "自定义class", + "docblock": "自定义class" + }, + "style": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义内联样式", + "docblock": "自定义内联样式", + "properties": [] + }, + "htmlType": { + "type": { + "name": "string" + }, + "required": false, + "description": "原生type", + "docblock": "原生type" + }, + "name": { + "type": { + "name": "string" + }, + "required": false, + "description": "name", + "docblock": "name" + }, + "state": { + "type": { + "name": "enum", + "value": [{ + "value": "'error'", + "computed": false, + "description": "错误" + }] + }, + "required": false, + "description": "状态", + "docblock": "状态\n@enumdesc 错误", + "value": [{ + "value": "'error'", + "computed": false, + "description": "错误" + }] + }, + "hasBorder": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否有边框", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否有边框" + }, + "autoHeight": { + "type": { + "name": "union", + "value": [{ + "name": "bool" + }, + { + "name": "object" + } + ] + }, + "required": false, + "description": "自动高度 true / {minRows: 2, maxRows: 4}", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "自动高度 true / {minRows: 2, maxRows: 4}" + }, + "rows": { + "type": { + "name": "number" + }, + "required": false, + "description": "多行文本框高度
(不要直接用height设置多行文本框的高度, ie9 10会有兼容性问题)", + "defaultValue": { + "value": "4", + "computed": false + }, + "docblock": "多行文本框高度
(不要直接用height设置多行文本框的高度, ie9 10会有兼容性问题)" + } + }, + "methods": [{ + "name": "getValueLength", + "docblock": "value.length !== maxLength in ie/safari(mac) while value has `Enter`\nabout maxLength compute: `Enter` was considered to be one char(\\n) in chrome , but two chars(\\r\\n) in ie/safari(mac).\nso while value has `Enter`, we should let display length + 1", + "modifiers": [], + "params": [{ + "name": "value" + }], + "returns": null, + "description": "value.length !== maxLength in ie/safari(mac) while value has `Enter`\nabout maxLength compute: `Enter` was considered to be one char(\\n) in chrome , but two chars(\\r\\n) in ie/safari(mac).\nso while value has `Enter`, we should let display length + 1" + }], + "order": 2 + }, + { + "name": "Group", + "title": "输入框组", + "props": { + "prefix": { + "type": { + "name": "string" + }, + "required": false, + "description": "样式前缀", + "defaultValue": { + "value": "'next-'", + "computed": false + }, + "docblock": "样式前缀" + }, + "addonBefore": { + "type": { + "name": "node" + }, + "required": false, + "description": "输入框前附加内容", + "docblock": "输入框前附加内容" + }, + "addonBeforeClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "输入框前附加内容css", + "docblock": "输入框前附加内容css" + }, + "addonAfter": { + "type": { + "name": "node" + }, + "required": false, + "description": "输入框后附加内容", + "docblock": "输入框后附加内容" + }, + "addonAfterClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "输入框后额外css", + "docblock": "输入框后额外css" + }, + "rtl": { + "type": { + "name": "bool" + }, + "required": false, + "description": "rtl", + "docblock": "rtl" + } + }, + "methods": [] + } + ] + }, + { + "name": "Loading", + "title": "加载", + "typeId": 5, + "props": { + "prefix": { + "type": { + "name": "string" + }, + "required": false, + "description": "样式前缀", + "defaultValue": { + "value": "'next-'", + "computed": false + }, + "docblock": "样式前缀" + }, + "tip": { + "type": { + "name": "any" + }, + "required": false, + "description": "自定义内容", + "docblock": "自定义内容" + }, + "tipAlign": { + "type": { + "name": "enum", + "value": [{ + "value": "'right'", + "computed": false, + "description": "出现在动画右边" + }, + { + "value": "'bottom'", + "computed": false, + "description": "出现在动画下面" + } + ] + }, + "required": false, + "description": "自定义内容位置", + "defaultValue": { + "value": "'bottom'", + "computed": false + }, + "docblock": "自定义内容位置\n@enumdesc 出现在动画右边, 出现在动画下面", + "value": [{ + "value": "'right'", + "computed": false, + "description": "出现在动画右边" + }, + { + "value": "'bottom'", + "computed": false, + "description": "出现在动画下面" + } + ] + }, + "visible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "loading 状态, 默认 true", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "loading 状态, 默认 true" + }, + "className": { + "type": { + "name": "string" + }, + "required": false, + "description": "自定义class", + "docblock": "自定义class" + }, + "style": { + "type": { + "name": "object" + }, + "required": false, + "description": "自定义内联样式", + "docblock": "自定义内联样式", + "properties": [] + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'large'", + "computed": false, + "description": "大号" + }, + { + "value": "'medium'", + "computed": false, + "description": "中号" + } + ] + }, + "required": false, + "description": "设置动画尺寸", + "defaultValue": { + "value": "'large'", + "computed": false + }, + "docblock": "设置动画尺寸\n@description 仅仅对默认动画效果起作用\n@enumdesc 大号, 中号", + "value": [{ + "value": "'large'", + "computed": false, + "description": "大号" + }, + { + "value": "'medium'", + "computed": false, + "description": "中号" + } + ] + }, + "indicator": { + "type": { + "name": "any" + }, + "required": false, + "description": "自定义动画", + "docblock": "自定义动画" + }, + "color": { + "type": { + "name": "string" + }, + "required": false, + "description": "动画颜色", + "docblock": "动画颜色" + }, + "fullScreen": { + "type": { + "name": "bool" + }, + "required": false, + "description": "全屏展示", + "docblock": "全屏展示" + }, + "children": { + "type": { + "name": "any" + }, + "required": false, + "description": "子元素", + "docblock": "子元素" + }, + "inline": { + "type": { + "name": "bool" + }, + "required": false, + "description": "should loader be displayed inline", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "should loader be displayed inline" + } + }, + "methods": [], + "subComponents": [] + }, + { + "name": "Menu", + "title": "菜单", + "typeId": 4, + "props": { + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "菜单项和子菜单", + "docblock": "菜单项和子菜单" + }, + "onItemClick": { + "type": { + "name": "func" + }, + "required": false, + "description": "点击菜单项触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "点击菜单项触发的回调函数\n@param {String} key 点击的菜单项的 key 值\n@param {Object} item 点击的菜单项对象\n@param {Object} event 点击的事件对象", + "params": [{ + "name": "key", + "description": "点击的菜单项的 key 值", + "type": { + "name": "String" + } + }, + { + "name": "item", + "description": "点击的菜单项对象", + "type": { + "name": "Object" + } + }, + { + "name": "event", + "description": "点击的事件对象", + "type": { + "name": "Object" + } + } + ], + "returns": null + }, + "openKeys": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "当前打开的子菜单的 key 值", + "docblock": "当前打开的子菜单的 key 值" + }, + "defaultOpenKeys": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "初始打开的子菜单的 key 值", + "defaultValue": { + "value": "[]", + "computed": false + }, + "docblock": "初始打开的子菜单的 key 值" + }, + "defaultOpenAll": { + "type": { + "name": "bool" + }, + "required": false, + "description": "初始展开所有的子菜单,只在 mode 设置为 'inline' 以及 openMode 设置为 'multiple' 下生效,优先级高于 defaultOpenKeys", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "初始展开所有的子菜单,只在 mode 设置为 'inline' 以及 openMode 设置为 'multiple' 下生效,优先级高于 defaultOpenKeys" + }, + "onOpen": { + "type": { + "name": "func" + }, + "required": false, + "description": "打开或关闭子菜单触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "打开或关闭子菜单触发的回调函数\n@param {String} key 打开的所有子菜单的 key 值\n@param {Object} extra 额外参数\n@param {String} extra.key 当前操作子菜单的 key 值\n@param {Boolean} extra.open 是否是打开", + "params": [{ + "name": "key", + "description": "打开的所有子菜单的 key 值", + "type": { + "name": "String" + } + }, + { + "name": "extra", + "description": "额外参数", + "type": { + "name": "Object" + } + }, + { + "name": "extra.key", + "description": "当前操作子菜单的 key 值", + "type": { + "name": "String" + } + }, + { + "name": "extra.open", + "description": "是否是打开", + "type": { + "name": "Boolean" + } + } + ], + "returns": null + }, + "mode": { + "type": { + "name": "enum", + "value": [{ + "value": "'inline'", + "computed": false + }, + { + "value": "'popup'", + "computed": false + } + ] + }, + "required": false, + "description": "子菜单打开的模式", + "defaultValue": { + "value": "'inline'", + "computed": false + }, + "docblock": "子菜单打开的模式" + }, + "triggerType": { + "type": { + "name": "enum", + "value": [{ + "value": "'click'", + "computed": false + }, + { + "value": "'hover'", + "computed": false + } + ] + }, + "required": false, + "description": "子菜单打开的触发行为", + "defaultValue": { + "value": "'click'", + "computed": false + }, + "docblock": "子菜单打开的触发行为" + }, + "openMode": { + "type": { + "name": "enum", + "value": [{ + "value": "'single'", + "computed": false + }, + { + "value": "'multiple'", + "computed": false + } + ] + }, + "required": false, + "description": "展开内连子菜单的模式,同时可以展开一个子菜单还是多个子菜单,该属性仅在 mode 为 inline 时生效", + "defaultValue": { + "value": "'multiple'", + "computed": false + }, + "docblock": "展开内连子菜单的模式,同时可以展开一个子菜单还是多个子菜单,该属性仅在 mode 为 inline 时生效" + }, + "inlineIndent": { + "type": { + "name": "number" + }, + "required": false, + "description": "内连子菜单缩进距离", + "defaultValue": { + "value": "20", + "computed": false + }, + "docblock": "内连子菜单缩进距离" + }, + "popupAutoWidth": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否自动让弹层的宽度和菜单项保持一致,如果弹层的宽度比菜单项小则和菜单项保持一致,如果宽度大于菜单项则不做处理", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否自动让弹层的宽度和菜单项保持一致,如果弹层的宽度比菜单项小则和菜单项保持一致,如果宽度大于菜单项则不做处理" + }, + "popupAlign": { + "type": { + "name": "enum", + "value": [{ + "value": "'follow'", + "computed": false + }, + { + "value": "'outside'", + "computed": false + } + ] + }, + "required": false, + "description": "弹层的对齐方式", + "defaultValue": { + "value": "'follow'", + "computed": false + }, + "docblock": "弹层的对齐方式" + }, + "popupProps": { + "type": { + "name": "union", + "value": [{ + "name": "object" + }, + { + "name": "func" + } + ] + }, + "required": false, + "description": "弹层自定义 props", + "defaultValue": { + "value": "{}", + "computed": false + }, + "docblock": "弹层自定义 props" + }, + "popupClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹出子菜单自定义 className", + "docblock": "弹出子菜单自定义 className" + }, + "popupStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹出子菜单自定义 style", + "docblock": "弹出子菜单自定义 style", + "properties": [] + }, + "selectedKeys": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "当前选中菜单项的 key 值", + "docblock": "当前选中菜单项的 key 值" + }, + "defaultSelectedKeys": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "初始选中菜单项的 key 值", + "defaultValue": { + "value": "[]", + "computed": false + }, + "docblock": "初始选中菜单项的 key 值" + }, + "onSelect": { + "type": { + "name": "func" + }, + "required": false, + "description": "选中或取消选中菜单项触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "选中或取消选中菜单项触发的回调函数\n@param {Array} selectedKeys 选中的所有菜单项的值\n@param {Object} item 选中或取消选中的菜单项\n@param {Object} extra 额外参数\n@param {Boolean} extra.select 是否是选中\n@param {Array} extra.key 菜单项的 key\n@param {Object} extra.label 菜单项的文本\n@param {Array} extra.keyPath 菜单项 key 的路径", + "params": [{ + "name": "selectedKeys", + "description": "选中的所有菜单项的值", + "type": { + "name": "Array" + } + }, + { + "name": "item", + "description": "选中或取消选中的菜单项", + "type": { + "name": "Object" + } + }, + { + "name": "extra", + "description": "额外参数", + "type": { + "name": "Object" + } + }, + { + "name": "extra.select", + "description": "是否是选中", + "type": { + "name": "Boolean" + } + }, + { + "name": "extra.key", + "description": "菜单项的 key", + "type": { + "name": "Array" + } + }, + { + "name": "extra.label", + "description": "菜单项的文本", + "type": { + "name": "Object" + } + }, + { + "name": "extra.keyPath", + "description": "菜单项 key 的路径", + "type": { + "name": "Array" + } + } + ], + "returns": null + }, + "selectMode": { + "type": { + "name": "enum", + "value": [{ + "value": "'single'", + "computed": false + }, + { + "value": "'multiple'", + "computed": false + } + ] + }, + "required": false, + "description": "选中模式,单选还是多选,默认无值,不可选", + "docblock": "选中模式,单选还是多选,默认无值,不可选" + }, + "shallowSelect": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否只能选择第一层菜单项(不能选择子菜单中的菜单项)", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否只能选择第一层菜单项(不能选择子菜单中的菜单项)" + }, + "hasSelectedIcon": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否显示选中图标,如果设置为 false 需配合配置平台设置选中时的背景色以示区分", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否显示选中图标,如果设置为 false 需配合配置平台设置选中时的背景色以示区分" + }, + "isSelectIconRight": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否将选中图标居右,仅当 hasSelectedIcon 为true 时生效。\n注意:SubMenu 上的选中图标一直居左,不受此API控制", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否将选中图标居右,仅当 hasSelectedIcon 为true 时生效。\n注意:SubMenu 上的选中图标一直居左,不受此API控制" + }, + "direction": { + "type": { + "name": "enum", + "value": [{ + "value": "'ver'", + "computed": false + }, + { + "value": "'hoz'", + "computed": false + } + ] + }, + "required": false, + "description": "菜单第一层展示方向", + "defaultValue": { + "value": "'ver'", + "computed": false + }, + "docblock": "菜单第一层展示方向" + }, + "hozAlign": { + "type": { + "name": "enum", + "value": [{ + "value": "'left'", + "computed": false + }, + { + "value": "'right'", + "computed": false + } + ] + }, + "required": false, + "description": "横向菜单条 item 和 footer 的对齐方向,在 direction 设置为 'hoz' 并且 header 存在时生效", + "defaultValue": { + "value": "'left'", + "computed": false + }, + "docblock": "横向菜单条 item 和 footer 的对齐方向,在 direction 设置为 'hoz' 并且 header 存在时生效" + }, + "hozInLine": { + "type": { + "name": "bool" + }, + "required": false, + "description": "横向菜单模式下,是否维持在一行,即超出一行折叠成 SubMenu 显示, 仅在 direction='hoz' mode='popup' 时生效", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "横向菜单模式下,是否维持在一行,即超出一行折叠成 SubMenu 显示, 仅在 direction='hoz' mode='popup' 时生效" + }, + "header": { + "type": { + "name": "node" + }, + "required": false, + "description": "自定义菜单头部", + "docblock": "自定义菜单头部" + }, + "footer": { + "type": { + "name": "node" + }, + "required": false, + "description": "自定义菜单尾部", + "docblock": "自定义菜单尾部" + }, + "autoFocus": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否自动获得焦点", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否自动获得焦点" + }, + "focusedKey": { + "type": { + "name": "string" + }, + "required": false, + "description": "当前获得焦点的子菜单或菜单项 key 值", + "docblock": "当前获得焦点的子菜单或菜单项 key 值" + }, + "embeddable": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否开启嵌入式模式,一般用于Layout的布局中,开启后没有默认背景、外层border、box-shadow,可以配合`` 自定义高度", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否开启嵌入式模式,一般用于Layout的布局中,开启后没有默认背景、外层border、box-shadow,可以配合`` 自定义高度" + } + }, + "methods": [{ + "name": "create", + "docblock": "\n 创建上下文菜单\n @exportName create\n @param {Object} props 属性对象\n ", + "description": "创建上下文菜单", + "modifiers": [ + "static" + ], + "params": [{ + "name": "props", + "description": "属性对象", + "type": { + "type": "NameExpression", + "name": "Object" + } + }], + "returns": null + }], + "subComponents": [{ + "name": "Item", + "title": "菜单项", + "props": { + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁用", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否禁用" + }, + "helper": { + "type": { + "name": "node" + }, + "required": false, + "description": "帮助文本", + "docblock": "帮助文本" + }, + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "菜单项标签内容", + "docblock": "菜单项标签内容" + } + }, + "methods": [], + "order": 0 + }, + { + "name": "SubMenu", + "title": "子菜单", + "props": { + "label": { + "type": { + "name": "node" + }, + "required": false, + "description": "标签内容", + "docblock": "标签内容" + }, + "selectable": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否可选,该属性仅在设置 Menu 组件 selectMode 属性后生效", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否可选,该属性仅在设置 Menu 组件 selectMode 属性后生效" + }, + "mode": { + "type": { + "name": "enum", + "value": [{ + "value": "'inline'", + "computed": false + }, + { + "value": "'popup'", + "computed": false + } + ] + }, + "required": false, + "description": "子菜单打开方式,如果设置会覆盖 Menu 上的同名属性", + "docblock": "子菜单打开方式,如果设置会覆盖 Menu 上的同名属性\n@default Menu 的 mode 属性值", + "defaultValue": { + "value": "Menu 的 mode 属性值", + "computed": false + } + }, + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "菜单项或下一级子菜单", + "docblock": "菜单项或下一级子菜单" + } + }, + "methods": [], + "order": 1 + }, + { + "name": "PopupItem", + "title": "弹出菜单", + "props": { + "label": { + "type": { + "name": "node" + }, + "required": false, + "description": "标签内容", + "docblock": "标签内容" + }, + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "自定义弹层内容", + "docblock": "自定义弹层内容" + } + }, + "methods": [], + "order": 2 + }, + { + "name": "CheckboxItem", + "title": "复选菜单", + "props": { + "checked": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否选中", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否选中" + }, + "indeterminate": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否半选中", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否半选中" + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁用", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否禁用" + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "选中或取消选中触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "选中或取消选中触发的回调函数\n@param {Boolean} checked 是否选中\n@param {Object} event 选中事件对象", + "params": [{ + "name": "checked", + "description": "是否选中", + "type": { + "name": "Boolean" + } + }, + { + "name": "event", + "description": "选中事件对象", + "type": { + "name": "Object" + } + } + ], + "returns": null + }, + "helper": { + "type": { + "name": "node" + }, + "required": false, + "description": "帮助文本", + "docblock": "帮助文本" + }, + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "标签内容", + "docblock": "标签内容" + } + }, + "methods": [], + "description": "该子组件选中情况不受 defaultSelectedKeys/selectedKeys 控制,请自行控制选中逻辑", + "order": 3 + }, + { + "name": "RadioItem", + "title": "单选菜单", + "props": { + "checked": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否选中", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否选中" + }, + "disabled": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否禁用", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否禁用" + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "选中或取消选中触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "选中或取消选中触发的回调函数\n@param {Boolean} checked 是否选中\n@param {Object} event 选中事件对象", + "params": [{ + "name": "checked", + "description": "是否选中", + "type": { + "name": "Boolean" + } + }, + { + "name": "event", + "description": "选中事件对象", + "type": { + "name": "Object" + } + } + ], + "returns": null + }, + "helper": { + "type": { + "name": "node" + }, + "required": false, + "description": "帮助文本", + "docblock": "帮助文本" + }, + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "标签内容", + "docblock": "标签内容" + } + }, + "methods": [], + "description": "该子组件选中情况不受 defaultSelectedKeys/selectedKeys 控制,请自行控制选中逻辑", + "order": 4 + }, + { + "name": "Group", + "title": "菜单组", + "props": { + "label": { + "type": { + "name": "node" + }, + "required": false, + "description": "标签内容", + "docblock": "标签内容" + }, + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "菜单项", + "docblock": "菜单项" + } + }, + "methods": [], + "order": 5 + }, + { + "name": "Divider", + "title": "菜单分隔", + "props": {}, + "methods": [], + "order": 6 + } + ] + }, + { + "name": "MenuButton", + "title": "菜单按钮", + "typeId": 1, + "props": { + "label": { + "type": { + "name": "node" + }, + "required": false, + "description": "按钮上的文本内容", + "docblock": "按钮上的文本内容" + }, + "autoWidth": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层是否与按钮宽度相同", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "弹层是否与按钮宽度相同" + }, + "popupTriggerType": { + "type": { + "name": "enum", + "value": [{ + "value": "'click'", + "computed": false + }, + { + "value": "'hover'", + "computed": false + } + ] + }, + "required": false, + "description": "弹层触发方式", + "defaultValue": { + "value": "'click'", + "computed": false + }, + "docblock": "弹层触发方式" + }, + "popupContainer": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层容器", + "docblock": "弹层容器", + "params": [], + "returns": null + }, + "visible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层展开状态", + "docblock": "弹层展开状态" + }, + "defaultVisible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "弹层默认是否展开", + "docblock": "弹层默认是否展开" + }, + "onVisibleChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "弹层在显示和隐藏触发的事件", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "弹层在显示和隐藏触发的事件", + "params": [], + "returns": null + }, + "popupStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层自定义样式", + "docblock": "弹层自定义样式", + "properties": [] + }, + "popupClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹层自定义样式类", + "docblock": "弹层自定义样式类" + }, + "popupProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹层属性透传", + "docblock": "弹层属性透传", + "properties": [] + }, + "followTrigger": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否跟随滚动", + "docblock": "是否跟随滚动" + }, + "defaultSelectedKeys": { + "type": { + "name": "array" + }, + "required": false, + "description": "默认激活的菜单项(用法同 Menu 非受控)", + "defaultValue": { + "value": "[]", + "computed": false + }, + "docblock": "默认激活的菜单项(用法同 Menu 非受控)" + }, + "selectedKeys": { + "type": { + "name": "array" + }, + "required": false, + "description": "激活的菜单项(用法同 Menu 受控)", + "docblock": "激活的菜单项(用法同 Menu 受控)" + }, + "selectMode": { + "type": { + "name": "enum", + "value": [{ + "value": "'single'", + "computed": false + }, + { + "value": "'multiple'", + "computed": false + } + ] + }, + "required": false, + "description": "菜单的选择模式,同 Menu", + "docblock": "菜单的选择模式,同 Menu" + }, + "onItemClick": { + "type": { + "name": "func" + }, + "required": false, + "description": "点击菜单项后的回调,同 Menu", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "点击菜单项后的回调,同 Menu", + "params": [], + "returns": null + }, + "onSelect": { + "type": { + "name": "func" + }, + "required": false, + "description": "选择菜单后的回调,同 Menu", + "defaultValue": { + "value": "func.noop", + "computed": true + }, + "docblock": "选择菜单后的回调,同 Menu", + "params": [], + "returns": null + }, + "menuProps": { + "type": { + "name": "object" + }, + "required": false, + "description": "菜单属性透传", + "defaultValue": { + "value": "{}", + "computed": false + }, + "docblock": "菜单属性透传", + "properties": [] + } + }, + "methods": [], + "subComponents": [] + }, + { + "name": "Message", + "title": "信息", + "typeId": 5, + "props": { + "type": { + "type": { + "name": "enum", + "value": [{ + "value": "'success'", + "computed": false + }, + { + "value": "'warning'", + "computed": false + }, + { + "value": "'error'", + "computed": false + }, + { + "value": "'notice'", + "computed": false + }, + { + "value": "'help'", + "computed": false + }, + { + "value": "'loading'", + "computed": false + } + ] + }, + "required": false, + "description": "反馈类型", + "defaultValue": { + "value": "'success'", + "computed": false + }, + "docblock": "反馈类型" + }, + "shape": { + "type": { + "name": "enum", + "value": [{ + "value": "'inline'", + "computed": false + }, + { + "value": "'addon'", + "computed": false + }, + { + "value": "'toast'", + "computed": false + } + ] + }, + "required": false, + "description": "反馈外观", + "defaultValue": { + "value": "'inline'", + "computed": false + }, + "docblock": "反馈外观" + }, + "size": { + "type": { + "name": "enum", + "value": [{ + "value": "'medium'", + "computed": false + }, + { + "value": "'large'", + "computed": false + } + ] + }, + "required": false, + "description": "反馈大小", + "defaultValue": { + "value": "'medium'", + "computed": false + }, + "docblock": "反馈大小" + }, + "title": { + "type": { + "name": "node" + }, + "required": false, + "description": "标题", + "docblock": "标题" + }, + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "内容", + "docblock": "内容" + }, + "defaultVisible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "默认是否显示", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "默认是否显示" + }, + "visible": { + "type": { + "name": "bool" + }, + "required": false, + "description": "当前是否显示", + "docblock": "当前是否显示" + }, + "iconType": { + "type": { + "name": "string" + }, + "required": false, + "description": "显示的图标类型,会覆盖内部设置的IconType", + "docblock": "显示的图标类型,会覆盖内部设置的IconType" + }, + "closeable": { + "type": { + "name": "bool" + }, + "required": false, + "description": "显示关闭按钮", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "显示关闭按钮" + }, + "onClose": { + "type": { + "name": "func" + }, + "required": false, + "description": "关闭按钮的回调", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "关闭按钮的回调", + "params": [], + "returns": null + }, + "afterClose": { + "type": { + "name": "func" + }, + "required": false, + "description": "关闭之后调用的函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "关闭之后调用的函数", + "params": [], + "returns": null + }, + "animation": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否开启展开收起动画", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否开启展开收起动画" + } + }, + "methods": [{ + "name": "show", + "docblock": "\n 创建提示弹层\n @exportName show\n @param {Object} props 属性对象\n ", + "description": "创建提示弹层", + "modifiers": [ + "static" + ], + "params": [{ + "name": "props", + "description": "属性对象", + "type": { + "type": "NameExpression", + "name": "Object" + } + }], + "returns": null + }, + { + "name": "hide", + "docblock": "\n 关闭提示弹层\n @exportName hide\n ", + "description": "关闭提示弹层", + "modifiers": [ + "static" + ], + "params": [], + "returns": null + }, + { + "name": "success", + "docblock": "\n 创建成功提示弹层\n @exportName success\n @param {Object} props 属性对象\n ", + "description": "创建成功提示弹层", + "modifiers": [ + "static" + ], + "params": [{ + "name": "props", + "description": "属性对象", + "type": { + "type": "NameExpression", + "name": "Object" + } + }], + "returns": null + }, + { + "name": "warning", + "docblock": "\n 创建警告提示弹层\n @exportName warning\n @param {Object} props 属性对象\n ", + "description": "创建警告提示弹层", + "modifiers": [ + "static" + ], + "params": [{ + "name": "props", + "description": "属性对象", + "type": { + "type": "NameExpression", + "name": "Object" + } + }], + "returns": null + }, + { + "name": "error", + "docblock": "\n 创建错误提示弹层\n @exportName error\n @param {Object} props 属性对象\n ", + "description": "创建错误提示弹层", + "modifiers": [ + "static" + ], + "params": [{ + "name": "props", + "description": "属性对象", + "type": { + "type": "NameExpression", + "name": "Object" + } + }], + "returns": null + }, + { + "name": "help", + "docblock": "\n 创建帮助提示弹层\n @exportName help\n @param {Object} props 属性对象\n ", + "description": "创建帮助提示弹层", + "modifiers": [ + "static" + ], + "params": [{ + "name": "props", + "description": "属性对象", + "type": { + "type": "NameExpression", + "name": "Object" + } + }], + "returns": null + }, + { + "name": "loading", + "docblock": "\n 创建加载中提示弹层\n @exportName loading\n @param {Object} props 属性对象\n ", + "description": "创建加载中提示弹层", + "modifiers": [ + "static" + ], + "params": [{ + "name": "props", + "description": "属性对象", + "type": { + "type": "NameExpression", + "name": "Object" + } + }], + "returns": null + }, + { + "name": "notice", + "docblock": "\n 创建通知提示弹层\n @exportName notice\n @param {Object} props 属性对象\n ", + "description": "创建通知提示弹层", + "modifiers": [ + "static" + ], + "params": [{ + "name": "props", + "description": "属性对象", + "type": { + "type": "NameExpression", + "name": "Object" + } + }], + "returns": null + } + ], + "subComponents": [] + }, + { + "name": "Nav", + "title": "导航", + "typeId": 2, + "props": { + "children": { + "type": { + "name": "node" + }, + "required": false, + "description": "导航项和子导航", + "docblock": "导航项和子导航" + }, + "onItemClick": { + "type": { + "name": "func" + }, + "required": false, + "description": "点击菜单项触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "点击菜单项触发的回调函数\n@param {String} key 点击的菜单项的 key 值\n@param {Object} item 点击的菜单项对象\n@param {Object} event 点击的事件对象", + "params": [{ + "name": "key", + "description": "点击的菜单项的 key 值", + "type": { + "name": "String" + } + }, + { + "name": "item", + "description": "点击的菜单项对象", + "type": { + "name": "Object" + } + }, + { + "name": "event", + "description": "点击的事件对象", + "type": { + "name": "Object" + } + } + ], + "returns": null + }, + "openKeys": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "当前打开的子菜单的 key 值", + "docblock": "当前打开的子菜单的 key 值" + }, + "defaultOpenKeys": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "初始打开的子菜单的 key 值", + "defaultValue": { + "value": "[]", + "computed": false + }, + "docblock": "初始打开的子菜单的 key 值" + }, + "defaultOpenAll": { + "type": { + "name": "bool" + }, + "required": false, + "description": "初始展开所有的子导航,只在 mode 设置为 'inline' 以及 openMode 设置为 'multiple' 下生效", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "初始展开所有的子导航,只在 mode 设置为 'inline' 以及 openMode 设置为 'multiple' 下生效" + }, + "onOpen": { + "type": { + "name": "func" + }, + "required": false, + "description": "打开或关闭子菜单触发的回调函数", + "defaultValue": { + "value": "() => {}", + "computed": false + }, + "docblock": "打开或关闭子菜单触发的回调函数\n@param {String} key 打开的所有子菜单的 key 值\n@param {Object} extra 额外参数\n@param {String} extra.key 当前操作子菜单的 key 值\n@param {Boolean} extra.open 是否是打开", + "params": [{ + "name": "key", + "description": "打开的所有子菜单的 key 值", + "type": { + "name": "String" + } + }, + { + "name": "extra", + "description": "额外参数", + "type": { + "name": "Object" + } + }, + { + "name": "extra.key", + "description": "当前操作子菜单的 key 值", + "type": { + "name": "String" + } + }, + { + "name": "extra.open", + "description": "是否是打开", + "type": { + "name": "Boolean" + } + } + ], + "returns": null + }, + "mode": { + "type": { + "name": "enum", + "value": [{ + "value": "'inline'", + "computed": false + }, + { + "value": "'popup'", + "computed": false + } + ] + }, + "required": false, + "description": "子导航打开的模式(水平导航只支持弹出)", + "defaultValue": { + "value": "'inline'", + "computed": false + }, + "docblock": "子导航打开的模式(水平导航只支持弹出)\n@eumdesc 行内, 弹出" + }, + "triggerType": { + "type": { + "name": "enum", + "value": [{ + "value": "'click'", + "computed": false + }, + { + "value": "'hover'", + "computed": false + } + ] + }, + "required": false, + "description": "子导航打开的触发方式", + "defaultValue": { + "value": "'click'", + "computed": false + }, + "docblock": "子导航打开的触发方式" + }, + "openMode": { + "type": { + "name": "enum", + "value": [{ + "value": "'single'", + "computed": false + }, + { + "value": "'multiple'", + "computed": false + } + ] + }, + "required": false, + "description": "内联子导航的展开模式,同时可以展开一个同级子导航还是多个同级子导航,该属性仅在 mode 为 inline 时生效", + "defaultValue": { + "value": "'multiple'", + "computed": false + }, + "docblock": "内联子导航的展开模式,同时可以展开一个同级子导航还是多个同级子导航,该属性仅在 mode 为 inline 时生效\n@eumdesc 一个, 多个" + }, + "inlineIndent": { + "type": { + "name": "number" + }, + "required": false, + "description": "内联子导航缩进距离", + "defaultValue": { + "value": "20", + "computed": false + }, + "docblock": "内联子导航缩进距离" + }, + "popupAutoWidth": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否自动让弹层的宽度和菜单项保持一致,如果弹层的宽度比菜单项小则和菜单项保持一致,如果宽度大于菜单项则不做处理", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否自动让弹层的宽度和菜单项保持一致,如果弹层的宽度比菜单项小则和菜单项保持一致,如果宽度大于菜单项则不做处理" + }, + "popupAlign": { + "type": { + "name": "enum", + "value": [{ + "value": "'follow'", + "computed": false + }, + { + "value": "'outside'", + "computed": false + } + ] + }, + "required": false, + "description": "弹出子导航的对齐方式(水平导航只支持 follow )", + "defaultValue": { + "value": "'follow'", + "computed": false + }, + "docblock": "弹出子导航的对齐方式(水平导航只支持 follow )\n@eumdesc Item 顶端对齐, Nav 顶端对齐" + }, + "popupProps": { + "type": { + "name": "union", + "value": [{ + "name": "object" + }, + { + "name": "func" + } + ] + }, + "required": false, + "description": "弹层自定义 props", + "defaultValue": { + "value": "{}", + "computed": false + }, + "docblock": "弹层自定义 props" + }, + "popupClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "弹出子导航的自定义类名", + "docblock": "弹出子导航的自定义类名" + }, + "popupStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "弹出子菜单自定义 style", + "docblock": "弹出子菜单自定义 style", + "properties": [] + }, + "selectedKeys": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "当前选中导航项的 key 值", + "docblock": "当前选中导航项的 key 值" + }, + "defaultSelectedKeys": { + "type": { + "name": "union", + "value": [{ + "name": "string" + }, + { + "name": "array" + } + ] + }, + "required": false, + "description": "初始选中导航项的 key 值", + "defaultValue": { + "value": "[]", + "computed": false + }, + "docblock": "初始选中导航项的 key 值" + }, + "onSelect": { + "type": { + "name": "func" + }, + "required": false, + "description": "选中或取消选中导航项触发的回调函数", + "docblock": "选中或取消选中导航项触发的回调函数\n@param {Array} selectedKeys 选中的所有导航项的 key\n@param {Object} item 选中或取消选中的导航项\n@param {Object} extra 额外参数\n@param {Boolean} extra.select 是否是选中\n@param {Array} extra.key 导航项的 key\n@param {Object} extra.label 导航项的文本\n@param {Array} extra.keyPath 导航项 key 的路径", + "params": [{ + "name": "selectedKeys", + "description": "选中的所有导航项的 key", + "type": { + "name": "Array" + } + }, + { + "name": "item", + "description": "选中或取消选中的导航项", + "type": { + "name": "Object" + } + }, + { + "name": "extra", + "description": "额外参数", + "type": { + "name": "Object" + } + }, + { + "name": "extra.select", + "description": "是否是选中", + "type": { + "name": "Boolean" + } + }, + { + "name": "extra.key", + "description": "导航项的 key", + "type": { + "name": "Array" + } + }, + { + "name": "extra.label", + "description": "导航项的文本", + "type": { + "name": "Object" + } + }, + { + "name": "extra.keyPath", + "description": "导航项 key 的路径", + "type": { + "name": "Array" + } + } + ], + "returns": null + }, + "selectMode": { + "type": { + "name": "enum", + "value": [{ + "value": "'single'", + "computed": false + }, + { + "value": "'multiple'", + "computed": false + } + ] + }, + "required": false, + "description": "选中模式,单选还是多选,默认无值,不可选", + "docblock": "选中模式,单选还是多选,默认无值,不可选" + }, + "shallowSelect": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否只能选择第一层菜单项(不能选择子菜单中的菜单项)", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否只能选择第一层菜单项(不能选择子菜单中的菜单项)" + }, + "hasSelectedIcon": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否显示选中图标,如果设置为 false 需配合配置平台设置选中时的背景色以示区分", + "defaultValue": { + "value": "true", + "computed": false + }, + "docblock": "是否显示选中图标,如果设置为 false 需配合配置平台设置选中时的背景色以示区分" + }, + "isSelectIconRight": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否将选中图标居右,仅当 hasSelectedIcon 为true 时生效。\n注意:SubMenu 上的选中图标一直居左,不受此API控制", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否将选中图标居右,仅当 hasSelectedIcon 为true 时生效。\n注意:SubMenu 上的选中图标一直居左,不受此API控制" + }, + "direction": { + "type": { + "name": "enum", + "value": [{ + "value": "'hoz'", + "computed": false, + "description": "水平" + }, + { + "value": "'ver'", + "computed": false, + "description": "垂直" + } + ] + }, + "required": false, + "description": "导航布局", + "defaultValue": { + "value": "'ver'", + "computed": false + }, + "docblock": "导航布局\n@enumdesc 水平, 垂直", + "value": [{ + "value": "'hoz'", + "computed": false, + "description": "水平" + }, + { + "value": "'ver'", + "computed": false, + "description": "垂直" + } + ] + }, + "hozAlign": { + "type": { + "name": "enum", + "value": [{ + "value": "'left'", + "computed": false + }, + { + "value": "'right'", + "computed": false + } + ] + }, + "required": false, + "description": "横向导航条 items 和 footer 的对齐方向,在 direction 设置为 'hoz' 并且 header 存在时生效", + "defaultValue": { + "value": "'left'", + "computed": false + }, + "docblock": "横向导航条 items 和 footer 的对齐方向,在 direction 设置为 'hoz' 并且 header 存在时生效" + }, + "hozInLine": { + "type": { + "name": "bool" + }, + "required": false, + "description": "横向菜单模式下,是否维持在一行,即超出一行折叠成 SubMenu 显示, 仅在 direction='hoz' mode='popup' 时生效", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "横向菜单模式下,是否维持在一行,即超出一行折叠成 SubMenu 显示, 仅在 direction='hoz' mode='popup' 时生效" + }, + "header": { + "type": { + "name": "node" + }, + "required": false, + "description": "自定义导航头部", + "docblock": "自定义导航头部" + }, + "footer": { + "type": { + "name": "node" + }, + "required": false, + "description": "自定义导航尾部", + "docblock": "自定义导航尾部" + }, + "autoFocus": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否自动获得焦点", + "defaultValue": { + "value": "false", + "computed": false + }, + "docblock": "是否自动获得焦点" + }, + "focusedKey": { + "type": { + "name": "string" + }, + "required": false, + "description": "当前获得焦点的子菜单或菜单项 key 值", + "docblock": "当前获得焦点的子菜单或菜单项 key 值" + }, + "embeddable": { + "type": { + "name": "bool" + }, + "required": false, + "description": "是否开启嵌入式模式,一般用于Layout的布局中,开启后没有默认背景、外层border、box-shadow,可以配合`