This is page 30 of 52. Use http://codebase.md/alibaba/formily?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .all-contributorsrc ├── .codecov.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github │ ├── CONTRIBUTING.md │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ └── config.yml │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows │ ├── check-pr-title.yml │ ├── ci.yml │ ├── commitlint.yml │ ├── issue-open-check.yml │ ├── package-size.yml │ └── pr-welcome.yml ├── .gitignore ├── .prettierrc.js ├── .umirc.js ├── .vscode │ └── cspell.json ├── .yarnrc ├── CHANGELOG.md ├── commitlint.config.js ├── devtools │ ├── .eslintrc │ └── chrome-extension │ ├── .npmignore │ ├── assets │ │ └── img │ │ ├── loading.svg │ │ └── logo │ │ ├── 128x128.png │ │ ├── 16x16.png │ │ ├── 38x38.png │ │ ├── 48x48.png │ │ ├── error.png │ │ ├── gray.png │ │ └── scalable.png │ ├── config │ │ ├── webpack.base.ts │ │ ├── webpack.dev.ts │ │ └── webpack.prod.ts │ ├── LICENSE.md │ ├── package.json │ ├── src │ │ ├── app │ │ │ ├── components │ │ │ │ ├── FieldTree.tsx │ │ │ │ ├── filter.ts │ │ │ │ ├── LeftPanel.tsx │ │ │ │ ├── RightPanel.tsx │ │ │ │ ├── SearchBox.tsx │ │ │ │ └── Tabs.tsx │ │ │ ├── demo.tsx │ │ │ └── index.tsx │ │ └── extension │ │ ├── backend.ts │ │ ├── background.ts │ │ ├── content.ts │ │ ├── devpanel.tsx │ │ ├── devtools.tsx │ │ ├── inject.ts │ │ ├── manifest.json │ │ ├── popup.tsx │ │ └── views │ │ ├── devpanel.ejs │ │ ├── devtools.ejs │ │ └── popup.ejs │ ├── tsconfig.build.json │ └── tsconfig.json ├── docs │ ├── functions │ │ ├── contributors.ts │ │ └── npm-search.ts │ ├── guide │ │ ├── advanced │ │ │ ├── async.md │ │ │ ├── async.zh-CN.md │ │ │ ├── build.md │ │ │ ├── build.zh-CN.md │ │ │ ├── business-logic.md │ │ │ ├── business-logic.zh-CN.md │ │ │ ├── calculator.md │ │ │ ├── calculator.zh-CN.md │ │ │ ├── controlled.md │ │ │ ├── controlled.zh-CN.md │ │ │ ├── custom.md │ │ │ ├── custom.zh-CN.md │ │ │ ├── destructor.md │ │ │ ├── destructor.zh-CN.md │ │ │ ├── input.less │ │ │ ├── layout.md │ │ │ ├── layout.zh-CN.md │ │ │ ├── linkages.md │ │ │ ├── linkages.zh-CN.md │ │ │ ├── validate.md │ │ │ └── validate.zh-CN.md │ │ ├── contribution.md │ │ ├── contribution.zh-CN.md │ │ ├── form-builder.md │ │ ├── form-builder.zh-CN.md │ │ ├── index.md │ │ ├── index.zh-CN.md │ │ ├── issue-helper.md │ │ ├── issue-helper.zh-CN.md │ │ ├── learn-formily.md │ │ ├── learn-formily.zh-CN.md │ │ ├── quick-start.md │ │ ├── quick-start.zh-CN.md │ │ ├── scenes │ │ │ ├── dialog-drawer.md │ │ │ ├── dialog-drawer.zh-CN.md │ │ │ ├── edit-detail.md │ │ │ ├── edit-detail.zh-CN.md │ │ │ ├── index.less │ │ │ ├── login-register.md │ │ │ ├── login-register.zh-CN.md │ │ │ ├── more.md │ │ │ ├── more.zh-CN.md │ │ │ ├── query-list.md │ │ │ ├── query-list.zh-CN.md │ │ │ ├── step-form.md │ │ │ ├── step-form.zh-CN.md │ │ │ ├── tab-form.md │ │ │ ├── tab-form.zh-CN.md │ │ │ └── VerifyCode.tsx │ │ ├── upgrade.md │ │ └── upgrade.zh-CN.md │ ├── index.md │ ├── index.zh-CN.md │ └── site │ ├── Contributors.less │ ├── Contributors.tsx │ ├── QrCode.less │ ├── QrCode.tsx │ ├── Section.less │ ├── Section.tsx │ └── styles.less ├── global.config.ts ├── jest.config.js ├── lerna.json ├── LICENSE.md ├── package.json ├── packages │ ├── .eslintrc │ ├── antd │ │ ├── __tests__ │ │ │ ├── moment.spec.ts │ │ │ └── sideEffects.spec.ts │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── build-style.ts │ │ ├── create-style.ts │ │ ├── docs │ │ │ ├── components │ │ │ │ ├── ArrayCards.md │ │ │ │ ├── ArrayCards.zh-CN.md │ │ │ │ ├── ArrayCollapse.md │ │ │ │ ├── ArrayCollapse.zh-CN.md │ │ │ │ ├── ArrayItems.md │ │ │ │ ├── ArrayItems.zh-CN.md │ │ │ │ ├── ArrayTable.md │ │ │ │ ├── ArrayTable.zh-CN.md │ │ │ │ ├── ArrayTabs.md │ │ │ │ ├── ArrayTabs.zh-CN.md │ │ │ │ ├── Cascader.md │ │ │ │ ├── Cascader.zh-CN.md │ │ │ │ ├── Checkbox.md │ │ │ │ ├── Checkbox.zh-CN.md │ │ │ │ ├── DatePicker.md │ │ │ │ ├── DatePicker.zh-CN.md │ │ │ │ ├── Editable.md │ │ │ │ ├── Editable.zh-CN.md │ │ │ │ ├── Form.md │ │ │ │ ├── Form.zh-CN.md │ │ │ │ ├── FormButtonGroup.md │ │ │ │ ├── FormButtonGroup.zh-CN.md │ │ │ │ ├── FormCollapse.md │ │ │ │ ├── FormCollapse.zh-CN.md │ │ │ │ ├── FormDialog.md │ │ │ │ ├── FormDialog.zh-CN.md │ │ │ │ ├── FormDrawer.md │ │ │ │ ├── FormDrawer.zh-CN.md │ │ │ │ ├── FormGrid.md │ │ │ │ ├── FormGrid.zh-CN.md │ │ │ │ ├── FormItem.md │ │ │ │ ├── FormItem.zh-CN.md │ │ │ │ ├── FormLayout.md │ │ │ │ ├── FormLayout.zh-CN.md │ │ │ │ ├── FormStep.md │ │ │ │ ├── FormStep.zh-CN.md │ │ │ │ ├── FormTab.md │ │ │ │ ├── FormTab.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ ├── index.zh-CN.md │ │ │ │ ├── Input.md │ │ │ │ ├── Input.zh-CN.md │ │ │ │ ├── NumberPicker.md │ │ │ │ ├── NumberPicker.zh-CN.md │ │ │ │ ├── Password.md │ │ │ │ ├── Password.zh-CN.md │ │ │ │ ├── PreviewText.md │ │ │ │ ├── PreviewText.zh-CN.md │ │ │ │ ├── Radio.md │ │ │ │ ├── Radio.zh-CN.md │ │ │ │ ├── Reset.md │ │ │ │ ├── Reset.zh-CN.md │ │ │ │ ├── Select.md │ │ │ │ ├── Select.zh-CN.md │ │ │ │ ├── SelectTable.md │ │ │ │ ├── SelectTable.zh-CN.md │ │ │ │ ├── Space.md │ │ │ │ ├── Space.zh-CN.md │ │ │ │ ├── Submit.md │ │ │ │ ├── Submit.zh-CN.md │ │ │ │ ├── Switch.md │ │ │ │ ├── Switch.zh-CN.md │ │ │ │ ├── TimePicker.md │ │ │ │ ├── TimePicker.zh-CN.md │ │ │ │ ├── Transfer.md │ │ │ │ ├── Transfer.zh-CN.md │ │ │ │ ├── TreeSelect.md │ │ │ │ ├── TreeSelect.zh-CN.md │ │ │ │ ├── Upload.md │ │ │ │ └── Upload.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __builtins__ │ │ │ │ ├── hooks │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── useClickAway.ts │ │ │ │ │ └── usePrefixCls.ts │ │ │ │ ├── index.ts │ │ │ │ ├── loading.ts │ │ │ │ ├── moment.ts │ │ │ │ ├── pickDataProps.ts │ │ │ │ ├── portal.tsx │ │ │ │ ├── render.ts │ │ │ │ └── sort.tsx │ │ │ ├── array-base │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-cards │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-collapse │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-items │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-table │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-tabs │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── cascader │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── checkbox │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── date-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── editable │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form-button-group │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form-collapse │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-dialog │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-drawer │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-grid │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form-item │ │ │ │ ├── animation.less │ │ │ │ ├── grid.less │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form-layout │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ ├── style.ts │ │ │ │ └── useResponsiveFormLayout.ts │ │ │ ├── form-step │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-tab │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── index.ts │ │ │ ├── input │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── number-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── password │ │ │ │ ├── index.tsx │ │ │ │ ├── PasswordStrength.tsx │ │ │ │ └── style.ts │ │ │ ├── preview-text │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── radio │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── reset │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── select │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── select-table │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ ├── style.ts │ │ │ │ ├── useCheckSlackly.tsx │ │ │ │ ├── useFilterOptions.tsx │ │ │ │ ├── useFlatOptions.tsx │ │ │ │ ├── useSize.tsx │ │ │ │ ├── useTitleAddon.tsx │ │ │ │ └── utils.ts │ │ │ ├── space │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── style.less │ │ │ ├── style.ts │ │ │ ├── submit │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── switch │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── time-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── transfer │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── tree-select │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ └── upload │ │ │ ├── index.tsx │ │ │ ├── placeholder.ts │ │ │ └── style.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── benchmark │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ └── index.tsx │ │ ├── template.ejs │ │ ├── tsconfig.build.json │ │ ├── tsconfig.json │ │ ├── webpack.base.ts │ │ ├── webpack.dev.ts │ │ └── webpack.prod.ts │ ├── core │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── docs │ │ │ ├── api │ │ │ │ ├── entry │ │ │ │ │ ├── ActionResponse.less │ │ │ │ │ ├── ActionResponse.tsx │ │ │ │ │ ├── createForm.md │ │ │ │ │ ├── createForm.zh-CN.md │ │ │ │ │ ├── FieldEffectHooks.md │ │ │ │ │ ├── FieldEffectHooks.zh-CN.md │ │ │ │ │ ├── FormChecker.md │ │ │ │ │ ├── FormChecker.zh-CN.md │ │ │ │ │ ├── FormEffectHooks.md │ │ │ │ │ ├── FormEffectHooks.zh-CN.md │ │ │ │ │ ├── FormHooksAPI.md │ │ │ │ │ ├── FormHooksAPI.zh-CN.md │ │ │ │ │ ├── FormPath.md │ │ │ │ │ ├── FormPath.zh-CN.md │ │ │ │ │ ├── FormValidatorRegistry.md │ │ │ │ │ └── FormValidatorRegistry.zh-CN.md │ │ │ │ └── models │ │ │ │ ├── ArrayField.md │ │ │ │ ├── ArrayField.zh-CN.md │ │ │ │ ├── Field.md │ │ │ │ ├── Field.zh-CN.md │ │ │ │ ├── Form.md │ │ │ │ ├── Form.zh-CN.md │ │ │ │ ├── ObjectField.md │ │ │ │ ├── ObjectField.zh-CN.md │ │ │ │ ├── Query.md │ │ │ │ ├── Query.zh-CN.md │ │ │ │ ├── VoidField.md │ │ │ │ └── VoidField.zh-CN.md │ │ │ ├── guide │ │ │ │ ├── architecture.md │ │ │ │ ├── architecture.zh-CN.md │ │ │ │ ├── field.md │ │ │ │ ├── field.zh-CN.md │ │ │ │ ├── form.md │ │ │ │ ├── form.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ ├── index.zh-CN.md │ │ │ │ ├── mvvm.md │ │ │ │ ├── mvvm.zh-CN.md │ │ │ │ ├── values.md │ │ │ │ └── values.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── array.spec.ts │ │ │ │ ├── effects.spec.ts │ │ │ │ ├── externals.spec.ts │ │ │ │ ├── field.spec.ts │ │ │ │ ├── form.spec.ts │ │ │ │ ├── graph.spec.ts │ │ │ │ ├── heart.spec.ts │ │ │ │ ├── internals.spec.ts │ │ │ │ ├── lifecycle.spec.ts │ │ │ │ ├── object.spec.ts │ │ │ │ ├── shared.ts │ │ │ │ └── void.spec.ts │ │ │ ├── effects │ │ │ │ ├── index.ts │ │ │ │ ├── onFieldEffects.ts │ │ │ │ └── onFormEffects.ts │ │ │ ├── global.d.ts │ │ │ ├── index.ts │ │ │ ├── models │ │ │ │ ├── ArrayField.ts │ │ │ │ ├── BaseField.ts │ │ │ │ ├── Field.ts │ │ │ │ ├── Form.ts │ │ │ │ ├── Graph.ts │ │ │ │ ├── Heart.ts │ │ │ │ ├── index.ts │ │ │ │ ├── LifeCycle.ts │ │ │ │ ├── ObjectField.ts │ │ │ │ ├── Query.ts │ │ │ │ ├── types.ts │ │ │ │ └── VoidField.ts │ │ │ ├── shared │ │ │ │ ├── checkers.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── effective.ts │ │ │ │ ├── externals.ts │ │ │ │ └── internals.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── element │ │ ├── .npmignore │ │ ├── build-style.ts │ │ ├── create-style.ts │ │ ├── docs │ │ │ ├── .vuepress │ │ │ │ ├── components │ │ │ │ │ ├── createCodeSandBox.js │ │ │ │ │ ├── dumi-previewer.vue │ │ │ │ │ └── highlight.js │ │ │ │ ├── config.js │ │ │ │ ├── enhanceApp.js │ │ │ │ ├── styles │ │ │ │ │ └── index.styl │ │ │ │ └── util.js │ │ │ ├── demos │ │ │ │ ├── guide │ │ │ │ │ ├── array-cards │ │ │ │ │ │ ├── effects-json-schema.vue │ │ │ │ │ │ ├── effects-markup-schema.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── array-collapse │ │ │ │ │ │ ├── effects-json-schema.vue │ │ │ │ │ │ ├── effects-markup-schema.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── array-items │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── array-table │ │ │ │ │ │ ├── effects-json-schema.vue │ │ │ │ │ │ ├── effects-markup-schema.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── array-tabs │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── cascader │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── checkbox │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── date-picker │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── editable │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-button-group.vue │ │ │ │ │ ├── form-collapse │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── form-dialog │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-drawer │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-grid │ │ │ │ │ │ ├── form.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── native.vue │ │ │ │ │ ├── form-item │ │ │ │ │ │ ├── bordered-none.vue │ │ │ │ │ │ ├── common.vue │ │ │ │ │ │ ├── feedback.vue │ │ │ │ │ │ ├── inset.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ ├── size.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-layout │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-step │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── form-tab │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── form.vue │ │ │ │ │ ├── input │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── input-number │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── password │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── preview-text │ │ │ │ │ │ ├── base.vue │ │ │ │ │ │ └── extend.vue │ │ │ │ │ ├── radio │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── reset │ │ │ │ │ │ ├── base.vue │ │ │ │ │ │ ├── force.vue │ │ │ │ │ │ └── validate.vue │ │ │ │ │ ├── select │ │ │ │ │ │ ├── json-schema-async.vue │ │ │ │ │ │ ├── json-schema-sync.vue │ │ │ │ │ │ ├── markup-schema-async-search.vue │ │ │ │ │ │ ├── markup-schema-async.vue │ │ │ │ │ │ ├── markup-schema-sync.vue │ │ │ │ │ │ ├── template-async.vue │ │ │ │ │ │ └── template-sync.vue │ │ │ │ │ ├── space │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── submit │ │ │ │ │ │ ├── base.vue │ │ │ │ │ │ └── loading.vue │ │ │ │ │ ├── switch │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── time-picker │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── transfer │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ └── upload │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ └── template.vue │ │ │ │ └── index.vue │ │ │ ├── guide │ │ │ │ ├── array-cards.md │ │ │ │ ├── array-collapse.md │ │ │ │ ├── array-items.md │ │ │ │ ├── array-table.md │ │ │ │ ├── array-tabs.md │ │ │ │ ├── cascader.md │ │ │ │ ├── checkbox.md │ │ │ │ ├── date-picker.md │ │ │ │ ├── editable.md │ │ │ │ ├── form-button-group.md │ │ │ │ ├── form-collapse.md │ │ │ │ ├── form-dialog.md │ │ │ │ ├── form-drawer.md │ │ │ │ ├── form-grid.md │ │ │ │ ├── form-item.md │ │ │ │ ├── form-layout.md │ │ │ │ ├── form-step.md │ │ │ │ ├── form-tab.md │ │ │ │ ├── form.md │ │ │ │ ├── index.md │ │ │ │ ├── input-number.md │ │ │ │ ├── input.md │ │ │ │ ├── password.md │ │ │ │ ├── preview-text.md │ │ │ │ ├── radio.md │ │ │ │ ├── reset.md │ │ │ │ ├── select.md │ │ │ │ ├── space.md │ │ │ │ ├── submit.md │ │ │ │ ├── switch.md │ │ │ │ ├── time-picker.md │ │ │ │ ├── transfer.md │ │ │ │ └── upload.md │ │ │ └── README.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __builtins__ │ │ │ │ ├── configs │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── shared │ │ │ │ │ ├── create-context.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── loading.ts │ │ │ │ │ ├── portal.ts │ │ │ │ │ ├── resolve-component.ts │ │ │ │ │ ├── transform-component.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ └── styles │ │ │ │ └── common.scss │ │ │ ├── array-base │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-cards │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-collapse │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-items │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-table │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-tabs │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── cascader │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── checkbox │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── date-picker │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── editable │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── el-form │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── el-form-item │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── form │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-button-group │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-collapse │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-dialog │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── form-drawer │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-grid │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-item │ │ │ │ ├── animation.scss │ │ │ │ ├── grid.scss │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ ├── style.ts │ │ │ │ └── var.scss │ │ │ ├── form-layout │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ ├── style.ts │ │ │ │ └── useResponsiveFormLayout.ts │ │ │ ├── form-step │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── form-tab │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── index.ts │ │ │ ├── input │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── input-number │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── password │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── preview-text │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── radio │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── reset │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── select │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── space │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── style.ts │ │ │ ├── submit │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── switch │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── time-picker │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── transfer │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ └── upload │ │ │ ├── index.ts │ │ │ └── style.ts │ │ ├── transformer.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── grid │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── index.ts │ │ │ └── observer.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── json-schema │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── schema.spec.ts.snap │ │ │ │ ├── compiler.spec.ts │ │ │ │ ├── patches.spec.ts │ │ │ │ ├── schema.spec.ts │ │ │ │ ├── server-validate.spec.ts │ │ │ │ ├── shared.spec.ts │ │ │ │ ├── transformer.spec.ts │ │ │ │ └── traverse.spec.ts │ │ │ ├── compiler.ts │ │ │ ├── global.d.ts │ │ │ ├── index.ts │ │ │ ├── patches.ts │ │ │ ├── polyfills │ │ │ │ ├── index.ts │ │ │ │ └── SPECIFICATION_1_0.ts │ │ │ ├── schema.ts │ │ │ ├── shared.ts │ │ │ ├── transformer.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── next │ │ ├── __tests__ │ │ │ ├── moment.spec.ts │ │ │ └── sideEffects.spec.ts │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── build-style.ts │ │ ├── create-style.ts │ │ ├── docs │ │ │ ├── components │ │ │ │ ├── ArrayCards.md │ │ │ │ ├── ArrayCards.zh-CN.md │ │ │ │ ├── ArrayCollapse.md │ │ │ │ ├── ArrayCollapse.zh-CN.md │ │ │ │ ├── ArrayItems.md │ │ │ │ ├── ArrayItems.zh-CN.md │ │ │ │ ├── ArrayTable.md │ │ │ │ ├── ArrayTable.zh-CN.md │ │ │ │ ├── Cascader.md │ │ │ │ ├── Cascader.zh-CN.md │ │ │ │ ├── Checkbox.md │ │ │ │ ├── Checkbox.zh-CN.md │ │ │ │ ├── DatePicker.md │ │ │ │ ├── DatePicker.zh-CN.md │ │ │ │ ├── DatePicker2.md │ │ │ │ ├── DatePicker2.zh-CN.md │ │ │ │ ├── Editable.md │ │ │ │ ├── Editable.zh-CN.md │ │ │ │ ├── Form.md │ │ │ │ ├── Form.zh-CN.md │ │ │ │ ├── FormButtonGroup.md │ │ │ │ ├── FormButtonGroup.zh-CN.md │ │ │ │ ├── FormCollapse.md │ │ │ │ ├── FormCollapse.zh-CN.md │ │ │ │ ├── FormDialog.md │ │ │ │ ├── FormDialog.zh-CN.md │ │ │ │ ├── FormDrawer.md │ │ │ │ ├── FormDrawer.zh-CN.md │ │ │ │ ├── FormGrid.md │ │ │ │ ├── FormGrid.zh-CN.md │ │ │ │ ├── FormItem.md │ │ │ │ ├── FormItem.zh-CN.md │ │ │ │ ├── FormLayout.md │ │ │ │ ├── FormLayout.zh-CN.md │ │ │ │ ├── FormStep.md │ │ │ │ ├── FormStep.zh-CN.md │ │ │ │ ├── FormTab.md │ │ │ │ ├── FormTab.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ ├── index.zh-CN.md │ │ │ │ ├── Input.md │ │ │ │ ├── Input.zh-CN.md │ │ │ │ ├── NumberPicker.md │ │ │ │ ├── NumberPicker.zh-CN.md │ │ │ │ ├── Password.md │ │ │ │ ├── Password.zh-CN.md │ │ │ │ ├── PreviewText.md │ │ │ │ ├── PreviewText.zh-CN.md │ │ │ │ ├── Radio.md │ │ │ │ ├── Radio.zh-CN.md │ │ │ │ ├── Reset.md │ │ │ │ ├── Reset.zh-CN.md │ │ │ │ ├── Select.md │ │ │ │ ├── Select.zh-CN.md │ │ │ │ ├── SelectTable.md │ │ │ │ ├── SelectTable.zh-CN.md │ │ │ │ ├── Space.md │ │ │ │ ├── Space.zh-CN.md │ │ │ │ ├── Submit.md │ │ │ │ ├── Submit.zh-CN.md │ │ │ │ ├── Switch.md │ │ │ │ ├── Switch.zh-CN.md │ │ │ │ ├── TimePicker.md │ │ │ │ ├── TimePicker.zh-CN.md │ │ │ │ ├── TimePicker2.md │ │ │ │ ├── TimePicker2.zh-CN.md │ │ │ │ ├── Transfer.md │ │ │ │ ├── Transfer.zh-CN.md │ │ │ │ ├── TreeSelect.md │ │ │ │ ├── TreeSelect.zh-CN.md │ │ │ │ ├── Upload.md │ │ │ │ └── Upload.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LESENCE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __builtins__ │ │ │ │ ├── empty.tsx │ │ │ │ ├── hooks │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── useClickAway.ts │ │ │ │ │ └── usePrefixCls.ts │ │ │ │ ├── icons.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── loading.ts │ │ │ │ ├── mapSize.ts │ │ │ │ ├── mapStatus.ts │ │ │ │ ├── moment.ts │ │ │ │ ├── pickDataProps.ts │ │ │ │ ├── portal.tsx │ │ │ │ ├── render.ts │ │ │ │ └── toArray.ts │ │ │ ├── array-base │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── array-cards │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── array-collapse │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── array-items │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── array-table │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── cascader │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── checkbox │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── date-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── date-picker2 │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── editable │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── form │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── form-button-group │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── form-collapse │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-dialog │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-drawer │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-grid │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── form-item │ │ │ │ ├── animation.scss │ │ │ │ ├── grid.scss │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ ├── scss │ │ │ │ │ └── variable.scss │ │ │ │ └── style.ts │ │ │ ├── form-layout │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ ├── style.ts │ │ │ │ └── useResponsiveFormLayout.ts │ │ │ ├── form-step │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-tab │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── index.ts │ │ │ ├── input │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── main.scss │ │ │ ├── number-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── password │ │ │ │ ├── index.tsx │ │ │ │ ├── PasswordStrength.tsx │ │ │ │ └── style.ts │ │ │ ├── preview-text │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── radio │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── reset │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── select │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── select-table │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ ├── style.ts │ │ │ │ ├── useCheckSlackly.tsx │ │ │ │ ├── useFilterOptions.tsx │ │ │ │ ├── useFlatOptions.tsx │ │ │ │ ├── useSize.tsx │ │ │ │ ├── useTitleAddon.tsx │ │ │ │ └── utils.ts │ │ │ ├── space │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── style.ts │ │ │ ├── submit │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── switch │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── time-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── time-picker2 │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── transfer │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── tree-select │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ └── upload │ │ │ ├── index.tsx │ │ │ ├── main.scss │ │ │ ├── placeholder.ts │ │ │ └── style.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── path │ │ ├── .npmignore │ │ ├── benchmark.ts │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── accessor.spec.ts │ │ │ │ ├── basic.spec.ts │ │ │ │ ├── match.spec.ts │ │ │ │ ├── parser.spec.ts │ │ │ │ └── share.spec.ts │ │ │ ├── contexts.ts │ │ │ ├── destructor.ts │ │ │ ├── index.ts │ │ │ ├── matcher.ts │ │ │ ├── parser.ts │ │ │ ├── shared.ts │ │ │ ├── tokenizer.ts │ │ │ ├── tokens.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── react │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── docs │ │ │ ├── api │ │ │ │ ├── components │ │ │ │ │ ├── ArrayField.md │ │ │ │ │ ├── ArrayField.zh-CN.md │ │ │ │ │ ├── ExpressionScope.md │ │ │ │ │ ├── ExpressionScope.zh-CN.md │ │ │ │ │ ├── Field.md │ │ │ │ │ ├── Field.zh-CN.md │ │ │ │ │ ├── FormConsumer.md │ │ │ │ │ ├── FormConsumer.zh-CN.md │ │ │ │ │ ├── FormProvider.md │ │ │ │ │ ├── FormProvider.zh-CN.md │ │ │ │ │ ├── ObjectField.md │ │ │ │ │ ├── ObjectField.zh-CN.md │ │ │ │ │ ├── RecordScope.md │ │ │ │ │ ├── RecordScope.zh-CN.md │ │ │ │ │ ├── RecordsScope.md │ │ │ │ │ ├── RecordsScope.zh-CN.md │ │ │ │ │ ├── RecursionField.md │ │ │ │ │ ├── RecursionField.zh-CN.md │ │ │ │ │ ├── SchemaField.md │ │ │ │ │ ├── SchemaField.zh-CN.md │ │ │ │ │ ├── VoidField.md │ │ │ │ │ └── VoidField.zh-CN.md │ │ │ │ ├── hooks │ │ │ │ │ ├── useExpressionScope.md │ │ │ │ │ ├── useExpressionScope.zh-CN.md │ │ │ │ │ ├── useField.md │ │ │ │ │ ├── useField.zh-CN.md │ │ │ │ │ ├── useFieldSchema.md │ │ │ │ │ ├── useFieldSchema.zh-CN.md │ │ │ │ │ ├── useForm.md │ │ │ │ │ ├── useForm.zh-CN.md │ │ │ │ │ ├── useFormEffects.md │ │ │ │ │ ├── useFormEffects.zh-CN.md │ │ │ │ │ ├── useParentForm.md │ │ │ │ │ └── useParentForm.zh-CN.md │ │ │ │ └── shared │ │ │ │ ├── connect.md │ │ │ │ ├── connect.zh-CN.md │ │ │ │ ├── context.md │ │ │ │ ├── context.zh-CN.md │ │ │ │ ├── mapProps.md │ │ │ │ ├── mapProps.zh-CN.md │ │ │ │ ├── mapReadPretty.md │ │ │ │ ├── mapReadPretty.zh-CN.md │ │ │ │ ├── observer.md │ │ │ │ ├── observer.zh-CN.md │ │ │ │ ├── Schema.md │ │ │ │ └── Schema.zh-CN.md │ │ │ ├── guide │ │ │ │ ├── architecture.md │ │ │ │ ├── architecture.zh-CN.md │ │ │ │ ├── concept.md │ │ │ │ ├── concept.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ └── index.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── expression.spec.tsx │ │ │ │ ├── field.spec.tsx │ │ │ │ ├── form.spec.tsx │ │ │ │ ├── schema.json.spec.tsx │ │ │ │ ├── schema.markup.spec.tsx │ │ │ │ └── shared.tsx │ │ │ ├── components │ │ │ │ ├── ArrayField.tsx │ │ │ │ ├── ExpressionScope.tsx │ │ │ │ ├── Field.tsx │ │ │ │ ├── FormConsumer.tsx │ │ │ │ ├── FormProvider.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── ObjectField.tsx │ │ │ │ ├── ReactiveField.tsx │ │ │ │ ├── RecordScope.tsx │ │ │ │ ├── RecordsScope.tsx │ │ │ │ ├── RecursionField.tsx │ │ │ │ ├── SchemaField.tsx │ │ │ │ └── VoidField.tsx │ │ │ ├── global.d.ts │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ ├── useAttach.ts │ │ │ │ ├── useExpressionScope.ts │ │ │ │ ├── useField.ts │ │ │ │ ├── useFieldSchema.ts │ │ │ │ ├── useForm.ts │ │ │ │ ├── useFormEffects.ts │ │ │ │ └── useParentForm.ts │ │ │ ├── index.ts │ │ │ ├── shared │ │ │ │ ├── connect.ts │ │ │ │ ├── context.ts │ │ │ │ ├── index.ts │ │ │ │ └── render.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── reactive │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── benchmark.ts │ │ ├── docs │ │ │ ├── api │ │ │ │ ├── action.md │ │ │ │ ├── action.zh-CN.md │ │ │ │ ├── autorun.md │ │ │ │ ├── autorun.zh-CN.md │ │ │ │ ├── batch.md │ │ │ │ ├── batch.zh-CN.md │ │ │ │ ├── define.md │ │ │ │ ├── define.zh-CN.md │ │ │ │ ├── hasCollected.md │ │ │ │ ├── hasCollected.zh-CN.md │ │ │ │ ├── markObservable.md │ │ │ │ ├── markObservable.zh-CN.md │ │ │ │ ├── markRaw.md │ │ │ │ ├── markRaw.zh-CN.md │ │ │ │ ├── model.md │ │ │ │ ├── model.zh-CN.md │ │ │ │ ├── observable.md │ │ │ │ ├── observable.zh-CN.md │ │ │ │ ├── observe.md │ │ │ │ ├── observe.zh-CN.md │ │ │ │ ├── raw.md │ │ │ │ ├── raw.zh-CN.md │ │ │ │ ├── react │ │ │ │ │ ├── observer.md │ │ │ │ │ └── observer.zh-CN.md │ │ │ │ ├── reaction.md │ │ │ │ ├── reaction.zh-CN.md │ │ │ │ ├── toJS.md │ │ │ │ ├── toJS.zh-CN.md │ │ │ │ ├── tracker.md │ │ │ │ ├── tracker.zh-CN.md │ │ │ │ ├── typeChecker.md │ │ │ │ ├── typeChecker.zh-CN.md │ │ │ │ ├── untracked.md │ │ │ │ ├── untracked.zh-CN.md │ │ │ │ └── vue │ │ │ │ ├── observer.md │ │ │ │ └── observer.zh-CN.md │ │ │ ├── guide │ │ │ │ ├── best-practice.md │ │ │ │ ├── best-practice.zh-CN.md │ │ │ │ ├── concept.md │ │ │ │ ├── concept.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ └── index.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── action.spec.ts │ │ │ │ ├── annotations.spec.ts │ │ │ │ ├── array.spec.ts │ │ │ │ ├── autorun.spec.ts │ │ │ │ ├── batch.spec.ts │ │ │ │ ├── collections-map.spec.ts │ │ │ │ ├── collections-set.spec.ts │ │ │ │ ├── collections-weakmap.spec.ts │ │ │ │ ├── collections-weakset.spec.ts │ │ │ │ ├── define.spec.ts │ │ │ │ ├── externals.spec.ts │ │ │ │ ├── hasCollected.spec.ts │ │ │ │ ├── observable.spec.ts │ │ │ │ ├── observe.spec.ts │ │ │ │ ├── tracker.spec.ts │ │ │ │ └── untracked.spec.ts │ │ │ ├── action.ts │ │ │ ├── annotations │ │ │ │ ├── box.ts │ │ │ │ ├── computed.ts │ │ │ │ ├── index.ts │ │ │ │ ├── observable.ts │ │ │ │ ├── ref.ts │ │ │ │ └── shallow.ts │ │ │ ├── array.ts │ │ │ ├── autorun.ts │ │ │ ├── batch.ts │ │ │ ├── checkers.ts │ │ │ ├── environment.ts │ │ │ ├── externals.ts │ │ │ ├── global.d.ts │ │ │ ├── handlers.ts │ │ │ ├── index.ts │ │ │ ├── internals.ts │ │ │ ├── model.ts │ │ │ ├── observable.ts │ │ │ ├── observe.ts │ │ │ ├── reaction.ts │ │ │ ├── tracker.ts │ │ │ ├── tree.ts │ │ │ ├── types.ts │ │ │ └── untracked.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── reactive-react │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ ├── useCompatEffect.ts │ │ │ │ ├── useCompatFactory.ts │ │ │ │ ├── useDidUpdate.ts │ │ │ │ ├── useForceUpdate.ts │ │ │ │ ├── useLayoutEffect.ts │ │ │ │ └── useObserver.ts │ │ │ ├── index.ts │ │ │ ├── observer.ts │ │ │ ├── shared │ │ │ │ ├── gc.ts │ │ │ │ ├── global.ts │ │ │ │ ├── immediate.ts │ │ │ │ └── index.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── reactive-test-cases-for-react18 │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.js │ │ │ └── MySlowList.js │ │ ├── template.ejs │ │ ├── tsconfig.build.json │ │ ├── tsconfig.json │ │ ├── webpack.base.ts │ │ ├── webpack.dev.ts │ │ └── webpack.prod.ts │ ├── reactive-vue │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ └── observer.spec.ts │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ └── useObserver.ts │ │ │ ├── index.ts │ │ │ ├── observer │ │ │ │ ├── collectData.ts │ │ │ │ ├── index.ts │ │ │ │ ├── observerInVue2.ts │ │ │ │ └── observerInVue3.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── shared │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ └── index.spec.ts │ │ │ ├── array.ts │ │ │ ├── case.ts │ │ │ ├── checkers.ts │ │ │ ├── clone.ts │ │ │ ├── compare.ts │ │ │ ├── defaults.ts │ │ │ ├── deprecate.ts │ │ │ ├── global.ts │ │ │ ├── index.ts │ │ │ ├── instanceof.ts │ │ │ ├── isEmpty.ts │ │ │ ├── merge.ts │ │ │ ├── middleware.ts │ │ │ ├── path.ts │ │ │ ├── string.ts │ │ │ ├── subscribable.ts │ │ │ └── uid.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── validator │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── parser.spec.ts │ │ │ │ ├── registry.spec.ts │ │ │ │ └── validator.spec.ts │ │ │ ├── formats.ts │ │ │ ├── index.ts │ │ │ ├── locale.ts │ │ │ ├── parser.ts │ │ │ ├── registry.ts │ │ │ ├── rules.ts │ │ │ ├── template.ts │ │ │ ├── types.ts │ │ │ └── validator.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ └── vue │ ├── .npmignore │ ├── bin │ │ ├── formily-vue-fix.js │ │ └── formily-vue-switch.js │ ├── docs │ │ ├── .vuepress │ │ │ ├── components │ │ │ │ ├── createCodeSandBox.js │ │ │ │ ├── dumi-previewer.vue │ │ │ │ └── highlight.js │ │ │ ├── config.js │ │ │ ├── enhanceApp.js │ │ │ └── styles │ │ │ └── index.styl │ │ ├── api │ │ │ ├── components │ │ │ │ ├── array-field.md │ │ │ │ ├── expression-scope.md │ │ │ │ ├── field.md │ │ │ │ ├── form-consumer.md │ │ │ │ ├── form-provider.md │ │ │ │ ├── object-field.md │ │ │ │ ├── recursion-field-with-component.md │ │ │ │ ├── recursion-field.md │ │ │ │ ├── schema-field-with-schema.md │ │ │ │ ├── schema-field.md │ │ │ │ └── void-field.md │ │ │ ├── hooks │ │ │ │ ├── use-field-schema.md │ │ │ │ ├── use-field.md │ │ │ │ ├── use-form-effects.md │ │ │ │ ├── use-form.md │ │ │ │ └── use-parent-form.md │ │ │ └── shared │ │ │ ├── connect.md │ │ │ ├── injections.md │ │ │ ├── map-props.md │ │ │ ├── map-read-pretty.md │ │ │ ├── observer.md │ │ │ └── schema.md │ │ ├── demos │ │ │ ├── api │ │ │ │ ├── components │ │ │ │ │ ├── array-field.vue │ │ │ │ │ ├── expression-scope.vue │ │ │ │ │ ├── field.vue │ │ │ │ │ ├── form-consumer.vue │ │ │ │ │ ├── form-provider.vue │ │ │ │ │ ├── object-field.vue │ │ │ │ │ ├── recursion-field-with-component.vue │ │ │ │ │ ├── recursion-field.vue │ │ │ │ │ ├── schema-field-with-schema.vue │ │ │ │ │ ├── schema-field.vue │ │ │ │ │ └── void-field.vue │ │ │ │ ├── hooks │ │ │ │ │ ├── use-field-schema.vue │ │ │ │ │ ├── use-field.vue │ │ │ │ │ ├── use-form-effects.vue │ │ │ │ │ ├── use-form.vue │ │ │ │ │ └── use-parent-form.vue │ │ │ │ └── shared │ │ │ │ ├── connect.vue │ │ │ │ ├── map-props.vue │ │ │ │ ├── map-read-pretty.vue │ │ │ │ └── observer.vue │ │ │ ├── index.vue │ │ │ └── questions │ │ │ ├── default-slot.vue │ │ │ ├── events.vue │ │ │ ├── named-slot.vue │ │ │ └── scoped-slot.vue │ │ ├── guide │ │ │ ├── architecture.md │ │ │ ├── concept.md │ │ │ └── README.md │ │ ├── questions │ │ │ └── README.md │ │ └── README.md │ ├── package.json │ ├── README.md │ ├── rollup.config.js │ ├── scripts │ │ ├── postinstall.js │ │ ├── switch-cli.js │ │ └── utils.js │ ├── src │ │ ├── __tests__ │ │ │ ├── expression.scope.spec.ts │ │ │ ├── field.spec.ts │ │ │ ├── form.spec.ts │ │ │ ├── schema.json.spec.ts │ │ │ ├── schema.markup.spec.ts │ │ │ ├── shared.spec.ts │ │ │ └── utils.spec.ts │ │ ├── components │ │ │ ├── ArrayField.ts │ │ │ ├── ExpressionScope.ts │ │ │ ├── Field.ts │ │ │ ├── FormConsumer.ts │ │ │ ├── FormProvider.ts │ │ │ ├── index.ts │ │ │ ├── ObjectField.ts │ │ │ ├── ReactiveField.ts │ │ │ ├── RecursionField.ts │ │ │ ├── SchemaField.ts │ │ │ └── VoidField.ts │ │ ├── global.d.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useAttach.ts │ │ │ ├── useField.ts │ │ │ ├── useFieldSchema.ts │ │ │ ├── useForm.ts │ │ │ ├── useFormEffects.ts │ │ │ ├── useInjectionCleaner.ts │ │ │ └── useParentForm.ts │ │ ├── index.ts │ │ ├── shared │ │ │ ├── connect.ts │ │ │ ├── context.ts │ │ │ ├── createForm.ts │ │ │ ├── fragment.ts │ │ │ ├── h.ts │ │ │ └── index.ts │ │ ├── types │ │ │ └── index.ts │ │ ├── utils │ │ │ ├── formatVNodeData.ts │ │ │ ├── getFieldProps.ts │ │ │ ├── getRawComponent.ts │ │ │ └── resolveSchemaProps.ts │ │ └── vue2-components.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── tsconfig.types.json ├── README.md ├── README.zh-cn.md ├── scripts │ ├── build-style │ │ ├── buildAllStyles.ts │ │ ├── copy.ts │ │ ├── helper.ts │ │ └── index.ts │ └── rollup.base.js ├── tsconfig.build.json ├── tsconfig.jest.json ├── tsconfig.json └── yarn.lock ``` # Files -------------------------------------------------------------------------------- /packages/antd/src/form-item/index.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { useEffect, useRef, useContext, useState } from 'react' 2 | import cls from 'classnames' 3 | import { usePrefixCls, pickDataProps } from '../__builtins__' 4 | import { isVoidField } from '@formily/core' 5 | import { connect, mapProps } from '@formily/react' 6 | import { useFormLayout, FormLayoutShallowContext } from '../form-layout' 7 | import { isElement } from 'react-is' 8 | import { Tooltip, Popover, ConfigProvider } from 'antd' 9 | import { 10 | QuestionCircleOutlined, 11 | CloseCircleOutlined, 12 | CheckCircleOutlined, 13 | ExclamationCircleOutlined, 14 | } from '@ant-design/icons' 15 | 16 | export interface IFormItemProps { 17 | className?: string 18 | style?: React.CSSProperties 19 | prefixCls?: string 20 | label?: React.ReactNode 21 | colon?: boolean 22 | tooltip?: React.ReactNode | React.ComponentProps<typeof Tooltip> 23 | tooltipIcon?: React.ReactNode 24 | layout?: 'vertical' | 'horizontal' | 'inline' 25 | tooltipLayout?: 'icon' | 'text' 26 | labelStyle?: React.CSSProperties 27 | labelAlign?: 'left' | 'right' 28 | labelFor?: string 29 | labelWrap?: boolean 30 | labelWidth?: number | string 31 | wrapperWidth?: number | string 32 | labelCol?: number 33 | wrapperCol?: number 34 | wrapperAlign?: 'left' | 'right' 35 | wrapperWrap?: boolean 36 | wrapperStyle?: React.CSSProperties 37 | fullness?: boolean 38 | addonBefore?: React.ReactNode 39 | addonAfter?: React.ReactNode 40 | size?: 'small' | 'default' | 'large' 41 | inset?: boolean 42 | extra?: React.ReactNode 43 | feedbackText?: React.ReactNode 44 | feedbackLayout?: 'loose' | 'terse' | 'popover' | 'none' | (string & {}) 45 | feedbackStatus?: 'error' | 'warning' | 'success' | 'pending' | (string & {}) 46 | feedbackIcon?: React.ReactNode 47 | enableOutlineFeedback?: boolean 48 | getPopupContainer?: (node: HTMLElement) => HTMLElement 49 | asterisk?: boolean 50 | optionalMarkHidden?: boolean 51 | gridSpan?: number 52 | bordered?: boolean 53 | } 54 | 55 | type ComposeFormItem = React.FC<React.PropsWithChildren<IFormItemProps>> & { 56 | BaseItem?: React.FC<React.PropsWithChildren<IFormItemProps>> 57 | } 58 | 59 | const isTooltipProps = ( 60 | tooltip: React.ReactNode | React.ComponentProps<typeof Tooltip> 61 | ): tooltip is React.ComponentProps<typeof Tooltip> => { 62 | return !isElement(tooltip) 63 | } 64 | 65 | const useFormItemLayout = (props: IFormItemProps) => { 66 | const layout = useFormLayout() 67 | const layoutType = props.layout ?? layout.layout ?? 'horizontal' 68 | return { 69 | ...props, 70 | layout: layoutType, 71 | colon: props.colon ?? layout.colon, 72 | labelAlign: 73 | layoutType === 'vertical' 74 | ? props.labelAlign ?? 'left' 75 | : props.labelAlign ?? layout.labelAlign ?? 'right', 76 | labelWrap: props.labelWrap ?? layout.labelWrap, 77 | labelWidth: props.labelWidth ?? layout.labelWidth, 78 | wrapperWidth: props.wrapperWidth ?? layout.wrapperWidth, 79 | labelCol: props.labelCol ?? layout.labelCol, 80 | wrapperCol: props.wrapperCol ?? layout.wrapperCol, 81 | wrapperAlign: props.wrapperAlign ?? layout.wrapperAlign, 82 | wrapperWrap: props.wrapperWrap ?? layout.wrapperWrap, 83 | fullness: props.fullness ?? layout.fullness, 84 | size: props.size ?? layout.size, 85 | inset: props.inset ?? layout.inset, 86 | asterisk: props.asterisk, 87 | requiredMark: layout.requiredMark, 88 | optionalMarkHidden: props.optionalMarkHidden, 89 | bordered: props.bordered ?? layout.bordered, 90 | feedbackIcon: props.feedbackIcon, 91 | feedbackLayout: props.feedbackLayout ?? layout.feedbackLayout ?? 'loose', 92 | tooltipLayout: props.tooltipLayout ?? layout.tooltipLayout ?? 'icon', 93 | tooltipIcon: props.tooltipIcon ?? layout.tooltipIcon ?? ( 94 | <QuestionCircleOutlined /> 95 | ), 96 | } 97 | } 98 | 99 | function useOverflow< 100 | Container extends HTMLElement, 101 | Content extends HTMLElement 102 | >() { 103 | const [overflow, setOverflow] = useState(false) 104 | const containerRef = useRef<Container>() 105 | const contentRef = useRef<Content>() 106 | const layout = useFormLayout() 107 | const labelCol = JSON.stringify(layout.labelCol) 108 | 109 | useEffect(() => { 110 | requestAnimationFrame(() => { 111 | if (containerRef.current && contentRef.current) { 112 | const contentWidth = contentRef.current.getBoundingClientRect().width 113 | const containerWidth = 114 | containerRef.current.getBoundingClientRect().width 115 | if (contentWidth && containerWidth && containerWidth < contentWidth) { 116 | if (!overflow) setOverflow(true) 117 | } else { 118 | if (overflow) setOverflow(false) 119 | } 120 | } 121 | }) 122 | }, [labelCol]) 123 | 124 | return { 125 | overflow, 126 | containerRef, 127 | contentRef, 128 | } 129 | } 130 | 131 | const ICON_MAP = { 132 | error: <CloseCircleOutlined />, 133 | success: <CheckCircleOutlined />, 134 | warning: <ExclamationCircleOutlined />, 135 | } 136 | 137 | export const BaseItem: React.FC<React.PropsWithChildren<IFormItemProps>> = ({ 138 | children, 139 | ...props 140 | }) => { 141 | const [active, setActive] = useState(false) 142 | const formLayout = useFormItemLayout(props) 143 | const { locale } = useContext(ConfigProvider.ConfigContext) 144 | const { containerRef, contentRef, overflow } = useOverflow< 145 | HTMLDivElement, 146 | HTMLSpanElement 147 | >() 148 | const { 149 | label, 150 | style, 151 | layout, 152 | colon = true, 153 | addonBefore, 154 | addonAfter, 155 | asterisk, 156 | requiredMark = true, 157 | optionalMarkHidden = false, 158 | feedbackStatus, 159 | extra, 160 | feedbackText, 161 | fullness, 162 | feedbackLayout, 163 | feedbackIcon, 164 | enableOutlineFeedback = true, 165 | getPopupContainer, 166 | inset, 167 | bordered = true, 168 | labelWidth, 169 | wrapperWidth, 170 | labelCol, 171 | wrapperCol, 172 | labelAlign, 173 | wrapperAlign = 'left', 174 | size, 175 | labelWrap, 176 | wrapperWrap, 177 | tooltipLayout, 178 | tooltip, 179 | tooltipIcon, 180 | } = formLayout 181 | const labelStyle = { ...formLayout.labelStyle } 182 | const wrapperStyle = { ...formLayout.wrapperStyle } 183 | // 固定宽度 184 | let enableCol = false 185 | if (labelWidth || wrapperWidth) { 186 | if (labelWidth) { 187 | labelStyle.width = labelWidth === 'auto' ? undefined : labelWidth 188 | labelStyle.maxWidth = labelWidth === 'auto' ? undefined : labelWidth 189 | } 190 | if (wrapperWidth) { 191 | wrapperStyle.width = wrapperWidth === 'auto' ? undefined : wrapperWidth 192 | wrapperStyle.maxWidth = wrapperWidth === 'auto' ? undefined : wrapperWidth 193 | } 194 | // 栅格模式 195 | } 196 | if (labelCol || wrapperCol) { 197 | if (!labelStyle.width && !wrapperStyle.width && layout !== 'vertical') { 198 | enableCol = true 199 | } 200 | } 201 | 202 | const prefixCls = usePrefixCls('formily-item', props) 203 | const formatChildren = 204 | feedbackLayout === 'popover' ? ( 205 | <Popover 206 | autoAdjustOverflow 207 | placement="top" 208 | content={ 209 | <div 210 | className={cls({ 211 | [`${prefixCls}-${feedbackStatus}-help`]: !!feedbackStatus, 212 | [`${prefixCls}-help`]: true, 213 | })} 214 | > 215 | {ICON_MAP[feedbackStatus]} {feedbackText} 216 | </div> 217 | } 218 | visible={!!feedbackText} 219 | getPopupContainer={getPopupContainer} 220 | > 221 | {children} 222 | </Popover> 223 | ) : ( 224 | children 225 | ) 226 | 227 | const gridStyles: React.CSSProperties = {} 228 | 229 | const tooltipNode = isTooltipProps(tooltip) ? ( 230 | <Tooltip {...tooltip}></Tooltip> 231 | ) : ( 232 | tooltip 233 | ) 234 | 235 | const getOverflowTooltip = () => { 236 | if (overflow) { 237 | return ( 238 | <div> 239 | <div>{label}</div> 240 | <div>{tooltipNode}</div> 241 | </div> 242 | ) 243 | } 244 | return tooltipNode 245 | } 246 | 247 | const renderLabelText = () => { 248 | const labelChildren = ( 249 | <div className={`${prefixCls}-label-content`} ref={containerRef}> 250 | <span ref={contentRef}> 251 | {asterisk && requiredMark === true && ( 252 | <span className={`${prefixCls}-asterisk`}>{'*'}</span> 253 | )} 254 | <label htmlFor={props.labelFor}>{label}</label> 255 | {!asterisk && requiredMark === 'optional' && !optionalMarkHidden && ( 256 | <span className={`${prefixCls}-optional`}> 257 | {locale?.Form?.optional} 258 | </span> 259 | )} 260 | </span> 261 | </div> 262 | ) 263 | 264 | if ((tooltipLayout === 'text' && tooltip) || overflow) { 265 | return ( 266 | <Tooltip 267 | placement="top" 268 | align={{ offset: [0, 10] }} 269 | title={getOverflowTooltip()} 270 | > 271 | {labelChildren} 272 | </Tooltip> 273 | ) 274 | } 275 | return labelChildren 276 | } 277 | 278 | const renderTooltipIcon = () => { 279 | if (tooltip && tooltipLayout === 'icon' && !overflow) { 280 | return ( 281 | <span className={`${prefixCls}-label-tooltip-icon`}> 282 | <Tooltip 283 | placement="top" 284 | align={{ offset: [0, 2] }} 285 | title={tooltipNode} 286 | > 287 | {tooltipIcon} 288 | </Tooltip> 289 | </span> 290 | ) 291 | } 292 | } 293 | 294 | const renderLabel = () => { 295 | if (!label) return null 296 | return ( 297 | <div 298 | className={cls({ 299 | [`${prefixCls}-label`]: true, 300 | [`${prefixCls}-label-tooltip`]: 301 | (tooltip && tooltipLayout === 'text') || overflow, 302 | [`${prefixCls}-item-col-${labelCol}`]: enableCol && !!labelCol, 303 | })} 304 | style={labelStyle} 305 | > 306 | {renderLabelText()} 307 | {renderTooltipIcon()} 308 | {label !== ' ' && ( 309 | <span className={`${prefixCls}-colon`}>{colon ? ':' : ''}</span> 310 | )} 311 | </div> 312 | ) 313 | } 314 | 315 | return ( 316 | <div 317 | {...pickDataProps(props)} 318 | style={{ 319 | ...style, 320 | ...gridStyles, 321 | }} 322 | data-grid-span={props.gridSpan} 323 | className={cls({ 324 | [`${prefixCls}`]: true, 325 | [`${prefixCls}-layout-${layout}`]: true, 326 | [`${prefixCls}-${feedbackStatus}`]: 327 | enableOutlineFeedback && !!feedbackStatus, 328 | [`${prefixCls}-feedback-has-text`]: !!feedbackText, 329 | [`${prefixCls}-size-${size}`]: !!size, 330 | [`${prefixCls}-feedback-layout-${feedbackLayout}`]: !!feedbackLayout, 331 | [`${prefixCls}-fullness`]: !!fullness || !!inset || !!feedbackIcon, 332 | [`${prefixCls}-inset`]: !!inset, 333 | [`${prefixCls}-active`]: active, 334 | [`${prefixCls}-inset-active`]: !!inset && active, 335 | [`${prefixCls}-label-align-${labelAlign}`]: true, 336 | [`${prefixCls}-control-align-${wrapperAlign}`]: true, 337 | [`${prefixCls}-label-wrap`]: !!labelWrap, 338 | [`${prefixCls}-control-wrap`]: !!wrapperWrap, 339 | [`${prefixCls}-bordered-none`]: 340 | bordered === false || !!inset || !!feedbackIcon, 341 | [props.className]: !!props.className, 342 | })} 343 | onFocus={() => { 344 | if (feedbackIcon || inset) { 345 | setActive(true) 346 | } 347 | }} 348 | onBlur={() => { 349 | if (feedbackIcon || inset) { 350 | setActive(false) 351 | } 352 | }} 353 | > 354 | {renderLabel()} 355 | <div 356 | className={cls({ 357 | [`${prefixCls}-control`]: true, 358 | [`${prefixCls}-item-col-${wrapperCol}`]: 359 | enableCol && !!wrapperCol && label, 360 | })} 361 | > 362 | <div className={cls(`${prefixCls}-control-content`)}> 363 | {addonBefore && ( 364 | <div className={cls(`${prefixCls}-addon-before`)}> 365 | {addonBefore} 366 | </div> 367 | )} 368 | <div 369 | style={wrapperStyle} 370 | className={cls({ 371 | [`${prefixCls}-control-content-component`]: true, 372 | [`${prefixCls}-control-content-component-has-feedback-icon`]: 373 | !!feedbackIcon, 374 | })} 375 | > 376 | <FormLayoutShallowContext.Provider value={undefined}> 377 | {formatChildren} 378 | </FormLayoutShallowContext.Provider> 379 | {feedbackIcon && ( 380 | <div className={cls(`${prefixCls}-feedback-icon`)}> 381 | {feedbackIcon} 382 | </div> 383 | )} 384 | </div> 385 | {addonAfter && ( 386 | <div className={cls(`${prefixCls}-addon-after`)}>{addonAfter}</div> 387 | )} 388 | </div> 389 | {!!feedbackText && 390 | feedbackLayout !== 'popover' && 391 | feedbackLayout !== 'none' && ( 392 | <div 393 | className={cls({ 394 | [`${prefixCls}-${feedbackStatus}-help`]: !!feedbackStatus, 395 | [`${prefixCls}-help`]: true, 396 | [`${prefixCls}-help-enter`]: true, 397 | [`${prefixCls}-help-enter-active`]: true, 398 | })} 399 | > 400 | {feedbackText} 401 | </div> 402 | )} 403 | {extra && <div className={cls(`${prefixCls}-extra`)}>{extra}</div>} 404 | </div> 405 | </div> 406 | ) 407 | } 408 | 409 | // 适配 410 | export const FormItem: ComposeFormItem = connect( 411 | BaseItem, 412 | mapProps((props, field) => { 413 | if (isVoidField(field)) 414 | return { 415 | label: field.title || props.label, 416 | asterisk: props.asterisk, 417 | extra: props.extra || field.description, 418 | } 419 | if (!field) return props 420 | const takeFeedbackStatus = () => { 421 | if (field.validating) return 'pending' 422 | return field.decoratorProps.feedbackStatus || field.validateStatus 423 | } 424 | const takeMessage = () => { 425 | const split = (messages: any[]) => { 426 | return messages.reduce((buf, text, index) => { 427 | if (!text) return buf 428 | return index < messages.length - 1 429 | ? buf.concat([text, ', ']) 430 | : buf.concat([text]) 431 | }, []) 432 | } 433 | if (field.validating) return 434 | if (props.feedbackText) return props.feedbackText 435 | if (field.selfErrors.length) return split(field.selfErrors) 436 | if (field.selfWarnings.length) return split(field.selfWarnings) 437 | if (field.selfSuccesses.length) return split(field.selfSuccesses) 438 | } 439 | const takeAsterisk = () => { 440 | if (field.required && field.pattern !== 'readPretty') { 441 | return true 442 | } 443 | if ('asterisk' in props) { 444 | return props.asterisk 445 | } 446 | return false 447 | } 448 | return { 449 | label: props.label || field.title, 450 | feedbackStatus: takeFeedbackStatus(), 451 | feedbackText: takeMessage(), 452 | asterisk: takeAsterisk(), 453 | optionalMarkHidden: 454 | field.pattern === 'readPretty' && !('asterisk' in props), 455 | extra: props.extra || field.description, 456 | } 457 | }) 458 | ) 459 | 460 | FormItem.BaseItem = BaseItem 461 | 462 | export default FormItem 463 | ``` -------------------------------------------------------------------------------- /packages/next/docs/components/ArrayCards.md: -------------------------------------------------------------------------------- ```markdown 1 | # ArrayCards 2 | 3 | > Card list, it is more suitable to use ArrayCards for scenarios with a large number of fields in each row and more linkages 4 | > 5 | > Note: This component is only applicable to Schema scenarios 6 | 7 | ## Markup Schema example 8 | 9 | ```tsx 10 | import React from 'react' 11 | import { 12 | FormItem, 13 | Input, 14 | ArrayCards, 15 | FormButtonGroup, 16 | Submit, 17 | } from '@formily/next' 18 | import { createForm } from '@formily/core' 19 | import { FormProvider, createSchemaField } from '@formily/react' 20 | 21 | const SchemaField = createSchemaField({ 22 | components: { 23 | FormItem, 24 | Input, 25 | ArrayCards, 26 | }, 27 | }) 28 | 29 | const form = createForm() 30 | 31 | export default () => { 32 | return ( 33 | <FormProvider form={form}> 34 | <SchemaField> 35 | <SchemaField.Array 36 | name="string_array" 37 | maxItems={3} 38 | x-decorator="FormItem" 39 | x-component="ArrayCards" 40 | x-component-props={{ 41 | title: 'String array', 42 | }} 43 | > 44 | <SchemaField.Void> 45 | <SchemaField.Void x-component="ArrayCards.Index" /> 46 | <SchemaField.String 47 | name="input" 48 | x-decorator="FormItem" 49 | title="Input" 50 | required 51 | x-component="Input" 52 | /> 53 | <SchemaField.Void x-component="ArrayCards.Remove" /> 54 | <SchemaField.Void x-component="ArrayCards.Copy" /> 55 | <SchemaField.Void x-component="ArrayCards.MoveUp" /> 56 | <SchemaField.Void x-component="ArrayCards.MoveDown" /> 57 | </SchemaField.Void> 58 | <SchemaField.Void 59 | x-component="ArrayCards.Addition" 60 | title="Add entry" 61 | /> 62 | </SchemaField.Array> 63 | <SchemaField.Array 64 | name="array" 65 | maxItems={3} 66 | x-decorator="FormItem" 67 | x-component="ArrayCards" 68 | x-component-props={{ 69 | title: 'Object array', 70 | }} 71 | > 72 | <SchemaField.Object> 73 | <SchemaField.Void x-component="ArrayCards.Index" /> 74 | <SchemaField.String 75 | name="input" 76 | x-decorator="FormItem" 77 | title="Input" 78 | required 79 | x-component="Input" 80 | /> 81 | <SchemaField.Void x-component="ArrayCards.Remove" /> 82 | <SchemaField.Void x-component="ArrayCards.MoveUp" /> 83 | <SchemaField.Void x-component="ArrayCards.MoveDown" /> 84 | </SchemaField.Object> 85 | <SchemaField.Void 86 | x-component="ArrayCards.Addition" 87 | title="Add entry" 88 | /> 89 | </SchemaField.Array> 90 | </SchemaField> 91 | <FormButtonGroup> 92 | <Submit onSubmit={console.log}>Submit</Submit> 93 | </FormButtonGroup> 94 | </FormProvider> 95 | ) 96 | } 97 | ``` 98 | 99 | ## JSON Schema case 100 | 101 | ```tsx 102 | import React from 'react' 103 | import { 104 | FormItem, 105 | Input, 106 | ArrayCards, 107 | FormButtonGroup, 108 | Submit, 109 | } from '@formily/next' 110 | import { createForm } from '@formily/core' 111 | import { FormProvider, createSchemaField } from '@formily/react' 112 | 113 | const SchemaField = createSchemaField({ 114 | components: { 115 | FormItem, 116 | Input, 117 | ArrayCards, 118 | }, 119 | }) 120 | 121 | const form = createForm() 122 | 123 | const schema = { 124 | type: 'object', 125 | properties: { 126 | string_array: { 127 | type: 'array', 128 | 'x-component': 'ArrayCards', 129 | maxItems: 3, 130 | 'x-decorator': 'FormItem', 131 | 'x-component-props': { 132 | title: 'String array', 133 | }, 134 | items: { 135 | type: 'void', 136 | properties: { 137 | index: { 138 | type: 'void', 139 | 'x-component': 'ArrayCards.Index', 140 | }, 141 | input: { 142 | type: 'string', 143 | 'x-decorator': 'FormItem', 144 | title: 'Input', 145 | required: true, 146 | 'x-component': 'Input', 147 | }, 148 | remove: { 149 | type: 'void', 150 | 'x-component': 'ArrayCards.Remove', 151 | }, 152 | moveUp: { 153 | type: 'void', 154 | 'x-component': 'ArrayCards.MoveUp', 155 | }, 156 | moveDown: { 157 | type: 'void', 158 | 'x-component': 'ArrayCards.MoveDown', 159 | }, 160 | }, 161 | }, 162 | properties: { 163 | addition: { 164 | type: 'void', 165 | title: 'Add entry', 166 | 'x-component': 'ArrayCards.Addition', 167 | }, 168 | }, 169 | }, 170 | array: { 171 | type: 'array', 172 | 'x-component': 'ArrayCards', 173 | maxItems: 3, 174 | 'x-decorator': 'FormItem', 175 | 'x-component-props': { 176 | title: 'Object array', 177 | }, 178 | items: { 179 | type: 'object', 180 | properties: { 181 | index: { 182 | type: 'void', 183 | 'x-component': 'ArrayCards.Index', 184 | }, 185 | input: { 186 | type: 'string', 187 | 'x-decorator': 'FormItem', 188 | title: 'Input', 189 | required: true, 190 | 'x-component': 'Input', 191 | }, 192 | remove: { 193 | type: 'void', 194 | 'x-component': 'ArrayCards.Remove', 195 | }, 196 | moveUp: { 197 | type: 'void', 198 | 'x-component': 'ArrayCards.MoveUp', 199 | }, 200 | moveDown: { 201 | type: 'void', 202 | 'x-component': 'ArrayCards.MoveDown', 203 | }, 204 | }, 205 | }, 206 | properties: { 207 | addition: { 208 | type: 'void', 209 | title: 'Add entry', 210 | 'x-component': 'ArrayCards.Addition', 211 | }, 212 | }, 213 | }, 214 | }, 215 | } 216 | 217 | export default () => { 218 | return ( 219 | <FormProvider form={form}> 220 | <SchemaField schema={schema} /> 221 | <FormButtonGroup> 222 | <Submit onSubmit={console.log}>Submit</Submit> 223 | </FormButtonGroup> 224 | </FormProvider> 225 | ) 226 | } 227 | ``` 228 | 229 | ## Effects linkage case 230 | 231 | ```tsx 232 | import React from 'react' 233 | import { 234 | FormItem, 235 | Input, 236 | ArrayCards, 237 | FormButtonGroup, 238 | Submit, 239 | } from '@formily/next' 240 | import { createForm, onFieldChange, onFieldReact } from '@formily/core' 241 | import { FormProvider, createSchemaField } from '@formily/react' 242 | 243 | const SchemaField = createSchemaField({ 244 | components: { 245 | FormItem, 246 | Input, 247 | ArrayCards, 248 | }, 249 | }) 250 | 251 | const form = createForm({ 252 | effects: () => { 253 | //Active linkage mode 254 | onFieldChange('array.*.aa', ['value'], (field, form) => { 255 | form.setFieldState(field.query('.bb'), (state) => { 256 | state.visible = field.value != '123' 257 | }) 258 | }) 259 | //Passive linkage mode 260 | onFieldReact('array.*.dd', (field) => { 261 | field.visible = field.query('.cc').get('value') != '123' 262 | }) 263 | }, 264 | }) 265 | 266 | export default () => { 267 | return ( 268 | <FormProvider form={form}> 269 | <SchemaField> 270 | <SchemaField.Array 271 | name="array" 272 | maxItems={3} 273 | x-component="ArrayCards" 274 | x-decorator="FormItem" 275 | x-component-props={{ 276 | title: 'Object array', 277 | }} 278 | > 279 | <SchemaField.Object> 280 | <SchemaField.Void x-component="ArrayCards.Index" /> 281 | <SchemaField.String 282 | name="aa" 283 | x-decorator="FormItem" 284 | title="AA" 285 | required 286 | description="AA hide BB when entering 123" 287 | x-component="Input" 288 | /> 289 | <SchemaField.String 290 | name="bb" 291 | x-decorator="FormItem" 292 | title="BB" 293 | required 294 | x-component="Input" 295 | /> 296 | <SchemaField.String 297 | name="cc" 298 | x-decorator="FormItem" 299 | title="CC" 300 | required 301 | description="Hide DD when CC enters 123" 302 | x-component="Input" 303 | /> 304 | <SchemaField.String 305 | name="dd" 306 | x-decorator="FormItem" 307 | title="DD" 308 | required 309 | x-component="Input" 310 | /> 311 | <SchemaField.Void x-component="ArrayCards.Remove" /> 312 | <SchemaField.Void x-component="ArrayCards.MoveUp" /> 313 | <SchemaField.Void x-component="ArrayCards.MoveDown" /> 314 | </SchemaField.Object> 315 | <SchemaField.Void 316 | x-component="ArrayCards.Addition" 317 | title="Add entry" 318 | /> 319 | </SchemaField.Array> 320 | </SchemaField> 321 | <FormButtonGroup> 322 | <Submit onSubmit={console.log}>Submit</Submit> 323 | </FormButtonGroup> 324 | </FormProvider> 325 | ) 326 | } 327 | ``` 328 | 329 | ## JSON Schema linkage case 330 | 331 | ```tsx 332 | import React from 'react' 333 | import { 334 | FormItem, 335 | Input, 336 | ArrayCards, 337 | FormButtonGroup, 338 | Submit, 339 | } from '@formily/next' 340 | import { createForm } from '@formily/core' 341 | import { FormProvider, createSchemaField } from '@formily/react' 342 | 343 | const SchemaField = createSchemaField({ 344 | components: { 345 | FormItem, 346 | Input, 347 | ArrayCards, 348 | }, 349 | }) 350 | 351 | const form = createForm() 352 | 353 | const schema = { 354 | type: 'object', 355 | properties: { 356 | array: { 357 | type: 'array', 358 | 'x-component': 'ArrayCards', 359 | maxItems: 3, 360 | title: 'Object array', 361 | items: { 362 | type: 'object', 363 | properties: { 364 | index: { 365 | type: 'void', 366 | 'x-component': 'ArrayCards.Index', 367 | }, 368 | aa: { 369 | type: 'string', 370 | 'x-decorator': 'FormItem', 371 | title: 'AA', 372 | required: true, 373 | 'x-component': 'Input', 374 | description: 'Enter 123', 375 | }, 376 | bb: { 377 | type: 'string', 378 | title: 'BB', 379 | required: true, 380 | 'x-decorator': 'FormItem', 381 | 'x-component': 'Input', 382 | 'x-reactions': [ 383 | { 384 | dependencies: ['.aa'], 385 | when: "{{$deps[0] != '123'}}", 386 | fulfill: { 387 | schema: { 388 | title: 'BB', 389 | 'x-disabled': true, 390 | }, 391 | }, 392 | otherwise: { 393 | schema: { 394 | title: 'Changed', 395 | 'x-disabled': false, 396 | }, 397 | }, 398 | }, 399 | ], 400 | }, 401 | remove: { 402 | type: 'void', 403 | 'x-component': 'ArrayCards.Remove', 404 | }, 405 | moveUp: { 406 | type: 'void', 407 | 'x-component': 'ArrayCards.MoveUp', 408 | }, 409 | moveDown: { 410 | type: 'void', 411 | 'x-component': 'ArrayCards.MoveDown', 412 | }, 413 | }, 414 | }, 415 | properties: { 416 | addition: { 417 | type: 'void', 418 | title: 'Add entry', 419 | 'x-component': 'ArrayCards.Addition', 420 | }, 421 | }, 422 | }, 423 | }, 424 | } 425 | 426 | export default () => { 427 | return ( 428 | <FormProvider form={form}> 429 | <SchemaField schema={schema} /> 430 | <FormButtonGroup> 431 | <Submit onSubmit={console.log}>Submit</Submit> 432 | </FormButtonGroup> 433 | </FormProvider> 434 | ) 435 | } 436 | ``` 437 | 438 | ## API 439 | 440 | ### ArrayCards 441 | 442 | Extended attributes 443 | 444 | | Property name | Type | Description | Default value | 445 | | ------------- | ------------------------- | --------------- | ------------- | 446 | | onAdd | `(index: number) => void` | add method | | 447 | | onRemove | `(index: number) => void` | remove method | | 448 | | onCopy | `(index: number) => void` | copy method | | 449 | | onMoveUp | `(index: number) => void` | moveUp method | | 450 | | onMoveDown | `(index: number) => void` | moveDown method | | 451 | 452 | Other Reference https://fusion.design/pc/component/basic/card 453 | 454 | ### ArrayCards.Addition 455 | 456 | > Add button 457 | 458 | Extended attributes 459 | 460 | | Property name | Type | Description | Default value | 461 | | ------------- | -------------------- | ------------- | ------------- | 462 | | title | ReactText | Copywriting | | 463 | | method | `'push' \|'unshift'` | add method | `'push'` | 464 | | defaultValue | `any` | Default value | | 465 | 466 | Other references https://fusion.design/pc/component/basic/button 467 | 468 | Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective 469 | 470 | ### ArrayCards.Copy 471 | 472 | > Copy button 473 | 474 | Extended attributes 475 | 476 | | Property name | Type | Description | Default value | 477 | | ------------- | -------------------- | ----------- | ------------- | 478 | | title | ReactText | Copywriting | | 479 | | method | `'push' \|'unshift'` | add method | `'push'` | 480 | 481 | Other references https://fusion.design/pc/component/basic/button 482 | 483 | Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective 484 | 485 | ### ArrayCards.Remove 486 | 487 | > Delete button 488 | 489 | | Property name | Type | Description | Default value | 490 | | ------------- | --------- | ----------- | ------------- | 491 | | title | ReactText | Copywriting | | 492 | 493 | Other references https://ant.design/components/icon-cn/ 494 | 495 | Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective 496 | 497 | ### ArrayCards.MoveDown 498 | 499 | > Move down button 500 | 501 | | Property name | Type | Description | Default value | 502 | | ------------- | --------- | ----------- | ------------- | 503 | | title | ReactText | Copywriting | | 504 | 505 | Other references https://ant.design/components/icon-cn/ 506 | 507 | Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective 508 | 509 | ### ArrayCards.MoveUp 510 | 511 | > Move up button 512 | 513 | | Property name | Type | Description | Default value | 514 | | ------------- | --------- | ----------- | ------------- | 515 | | title | ReactText | Copywriting | | 516 | 517 | Other references https://ant.design/components/icon-cn/ 518 | 519 | Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective 520 | 521 | ### ArrayCards.Index 522 | 523 | > Index Renderer 524 | 525 | No attributes 526 | 527 | ### ArrayCards.useIndex 528 | 529 | > Read the React Hook of the current rendering row index 530 | 531 | ### ArrayCards.useRecord 532 | 533 | > Read the React Hook of the current rendering row 534 | ``` -------------------------------------------------------------------------------- /packages/validator/src/__tests__/validator.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | validate, 3 | registerValidateRules, 4 | registerValidateFormats, 5 | setValidateLanguage, 6 | registerValidateMessageTemplateEngine, 7 | } from '../index' 8 | 9 | registerValidateRules({ 10 | custom: (value) => (value === '123' ? 'custom error' : ''), 11 | customBool: () => false, 12 | customBool2: () => true, 13 | }) 14 | 15 | registerValidateFormats({ 16 | custom: /^[\u4e00-\u9fa5]+$/, 17 | }) 18 | 19 | const hasError = (results: any, message?: string) => { 20 | if (!message) { 21 | return expect(results?.error?.[0]).not.toBeUndefined() 22 | } 23 | return expect(results?.error?.[0]).toEqual(message) 24 | } 25 | 26 | const noError = (results: any) => { 27 | return expect(results?.error?.[0]).toBeUndefined() 28 | } 29 | 30 | test('empty string validate', async () => { 31 | const results = await validate('', { required: true }) 32 | expect(results).toEqual({ 33 | error: ['The field value is required'], 34 | success: [], 35 | warning: [], 36 | }) 37 | }) 38 | 39 | test('empty array validate', async () => { 40 | const results = await validate([], { required: true }) 41 | expect(results).toEqual({ 42 | error: ['The field value is required'], 43 | success: [], 44 | warning: [], 45 | }) 46 | noError(await validate([''], { required: true })) 47 | }) 48 | 49 | test('empty object validate', async () => { 50 | const results = await validate({}, { required: true }) 51 | expect(results).toEqual({ 52 | error: ['The field value is required'], 53 | success: [], 54 | warning: [], 55 | }) 56 | }) 57 | 58 | test('empty number validate', async () => { 59 | const results = await validate(0, { required: true }) 60 | expect(results).toEqual({ 61 | error: [], 62 | success: [], 63 | warning: [], 64 | }) 65 | }) 66 | 67 | test('multi validate', async () => { 68 | const results = await validate('', { 69 | required: true, 70 | validator() { 71 | return 'validate error' 72 | }, 73 | }) 74 | expect(results).toEqual({ 75 | error: ['The field value is required', 'validate error'], 76 | success: [], 77 | warning: [], 78 | }) 79 | }) 80 | 81 | test('message scope', async () => { 82 | const results = await validate( 83 | '', 84 | { 85 | required: true, 86 | validator() { 87 | return 'validate error {{name}}' 88 | }, 89 | }, 90 | { 91 | context: { 92 | name: 'scopeName', 93 | }, 94 | } 95 | ) 96 | expect(results).toEqual({ 97 | error: ['The field value is required', 'validate error scopeName'], 98 | success: [], 99 | warning: [], 100 | }) 101 | }) 102 | 103 | test('first validate', async () => { 104 | const results = await validate( 105 | '', 106 | { 107 | required: true, 108 | validator() { 109 | return 'validate error' 110 | }, 111 | }, 112 | { 113 | validateFirst: true, 114 | } 115 | ) 116 | expect(results).toEqual({ 117 | error: ['The field value is required'], 118 | success: [], 119 | warning: [], 120 | }) 121 | }) 122 | 123 | test('custom validate results', async () => { 124 | const results = await validate('', { 125 | validator() { 126 | return { type: 'error', message: 'validate error' } 127 | }, 128 | }) 129 | expect(results).toEqual({ 130 | error: ['validate error'], 131 | success: [], 132 | warning: [], 133 | }) 134 | }) 135 | 136 | test('exception validate', async () => { 137 | const results1 = await validate('', { 138 | validator() { 139 | throw new Error('validate error') 140 | }, 141 | }) 142 | expect(results1).toEqual({ 143 | error: ['validate error'], 144 | success: [], 145 | warning: [], 146 | }) 147 | 148 | const results2 = await validate('', { 149 | validator() { 150 | throw 'custom string' 151 | }, 152 | }) 153 | expect(results2).toEqual({ 154 | error: ['custom string'], 155 | success: [], 156 | warning: [], 157 | }) 158 | }) 159 | 160 | test('max/maxItems/maxLength/minItems/minLength/min/maximum/exclusiveMaximum/minimum/exclusiveMinimum/len', async () => { 161 | hasError(await validate(6, { max: 5 })) 162 | hasError(await validate(6, { maxLength: 5 })) 163 | hasError(await validate(6, { maxItems: 5 })) 164 | noError(await validate(5, { max: 5 })) 165 | noError(await validate(5, { maxLength: 5 })) 166 | noError(await validate(5, { maxItems: 5 })) 167 | hasError(await validate([1, 2, 3, 4, 5, 6], { max: 5 })) 168 | hasError(await validate([1, 2, 3, 4, 5, 6], { maxLength: 5 })) 169 | hasError(await validate([1, 2, 3, 4, 5, 6], { maxItems: 5 })) 170 | noError(await validate([1, 2, 3, 4, 5], { max: 5 })) 171 | noError(await validate([1, 2, 3, 4, 5], { maxLength: 5 })) 172 | noError(await validate([1, 2, 3, 4, 5], { maxItems: 5 })) 173 | hasError(await validate('123456', { max: 5 })) 174 | hasError(await validate('123456', { maxLength: 5 })) 175 | hasError(await validate('123456', { maxItems: 5 })) 176 | noError(await validate('12345', { max: 5 })) 177 | noError(await validate('12345', { maxLength: 5 })) 178 | noError(await validate('12345', { maxItems: 5 })) 179 | hasError(await validate(2, { min: 3 })) 180 | hasError(await validate(2, { minLength: 3 })) 181 | hasError(await validate(2, { minItems: 3 })) 182 | noError(await validate(3, { min: 3 })) 183 | noError(await validate(3, { minLength: 3 })) 184 | noError(await validate(3, { minItems: 3 })) 185 | hasError(await validate([1, 2], { min: 3 })) 186 | hasError(await validate([1, 2], { minLength: 3 })) 187 | hasError(await validate([1, 2], { minItems: 3 })) 188 | noError(await validate([1, 2, 3], { min: 3 })) 189 | noError(await validate([1, 2, 3], { minLength: 3 })) 190 | noError(await validate([1, 2, 3], { minItems: 3 })) 191 | hasError(await validate('12', { min: 3 })) 192 | hasError(await validate('12', { minLength: 3 })) 193 | hasError(await validate('12', { minItems: 3 })) 194 | noError(await validate('123', { min: 3 })) 195 | noError(await validate('123', { minLength: 3 })) 196 | noError(await validate('123', { minItems: 3 })) 197 | 198 | hasError(await validate(6, { maximum: 5 })) 199 | noError(await validate(5, { maximum: 5 })) 200 | hasError(await validate([1, 2, 3, 4, 5, 6], { maximum: 5 })) 201 | noError(await validate([1, 2, 3, 4, 5], { maximum: 5 })) 202 | hasError(await validate('123456', { maximum: 5 })) 203 | noError(await validate('12345', { maximum: 5 })) 204 | hasError(await validate(2, { minimum: 3 })) 205 | noError(await validate(3, { minimum: 3 })) 206 | hasError(await validate([1, 2], { minimum: 3 })) 207 | noError(await validate([1, 2, 3], { minimum: 3 })) 208 | hasError(await validate('12', { minimum: 3 })) 209 | noError(await validate('123', { minimum: 3 })) 210 | 211 | hasError(await validate(6, { exclusiveMaximum: 5 })) 212 | hasError(await validate(5, { exclusiveMaximum: 5 })) 213 | hasError(await validate([1, 2, 3, 4, 5, 6], { exclusiveMaximum: 5 })) 214 | hasError(await validate([1, 2, 3, 4, 5], { exclusiveMaximum: 5 })) 215 | hasError(await validate('123456', { exclusiveMaximum: 5 })) 216 | hasError(await validate('12345', { exclusiveMaximum: 5 })) 217 | hasError(await validate(2, { exclusiveMinimum: 3 })) 218 | hasError(await validate(3, { exclusiveMinimum: 3 })) 219 | hasError(await validate([1, 2], { exclusiveMinimum: 3 })) 220 | hasError(await validate([1, 2, 3], { exclusiveMinimum: 3 })) 221 | hasError(await validate('12', { exclusiveMinimum: 3 })) 222 | hasError(await validate('123', { exclusiveMinimum: 3 })) 223 | 224 | hasError(await validate('1234', { len: 3 })) 225 | hasError(await validate({ aa: 1, bb: 2, cc: 3 }, { maxProperties: 2 })) 226 | noError(await validate({ aa: 1, cc: 3 }, { maxProperties: 2 })) 227 | hasError(await validate({ aa: 1 }, { minProperties: 2 })) 228 | noError(await validate({ aa: 1, bb: 2, cc: 3 }, { minProperties: 2 })) 229 | noError(await validate({ aa: 1, cc: 3 }, { maxProperties: 2 })) 230 | }) 231 | 232 | test('const', async () => { 233 | noError(await validate('', { const: '123' })) 234 | noError(await validate('123', { const: '123' })) 235 | hasError(await validate('xxx', { const: '123' })) 236 | }) 237 | 238 | test('multipleOf', async () => { 239 | noError(await validate('', { multipleOf: 2 })) 240 | noError(await validate(4, { multipleOf: 2 })) 241 | hasError(await validate(3, { multipleOf: 2 })) 242 | }) 243 | 244 | test('uniqueItems', async () => { 245 | noError(await validate('', { uniqueItems: true })) 246 | noError(await validate(4, { uniqueItems: true })) 247 | hasError(await validate([1, 2], { uniqueItems: true })) 248 | hasError( 249 | await validate([{ label: '11', value: '11' }, { label: '11' }], { 250 | uniqueItems: true, 251 | }) 252 | ) 253 | noError(await validate([1, 1], { uniqueItems: true })) 254 | noError( 255 | await validate( 256 | [ 257 | { label: '11', value: '11' }, 258 | { label: '11', value: '11' }, 259 | ], 260 | { uniqueItems: true } 261 | ) 262 | ) 263 | }) 264 | 265 | test('pattern', async () => { 266 | hasError(await validate('aaa', { pattern: /^\d+$/ })) 267 | }) 268 | 269 | test('validator', async () => { 270 | hasError( 271 | await validate('aaa', { 272 | validator() { 273 | return false 274 | }, 275 | message: 'error', 276 | }), 277 | 'error' 278 | ) 279 | }) 280 | 281 | test('whitespace', async () => { 282 | hasError( 283 | await validate(' ', { 284 | whitespace: true, 285 | }) 286 | ) 287 | }) 288 | 289 | test('enum', async () => { 290 | hasError( 291 | await validate('11', { 292 | enum: ['22', '33'], 293 | }) 294 | ) 295 | noError( 296 | await validate('11', { 297 | enum: ['22', '33', '11'], 298 | }) 299 | ) 300 | }) 301 | 302 | test('filter trigger type(unmatch)', async () => { 303 | expect( 304 | await validate( 305 | '', 306 | { 307 | triggerType: 'onBlur', 308 | required: true, 309 | validator() { 310 | return 'validate error' 311 | }, 312 | }, 313 | { 314 | validateFirst: true, 315 | triggerType: 'onInput', 316 | } 317 | ) 318 | ).toEqual({ 319 | error: [], 320 | success: [], 321 | warning: [], 322 | }) 323 | }) 324 | 325 | test('filter trigger type(match first validate)', async () => { 326 | expect( 327 | await validate( 328 | '', 329 | { 330 | triggerType: 'onBlur', 331 | required: true, 332 | validator() { 333 | return 'validate error' 334 | }, 335 | }, 336 | { 337 | validateFirst: true, 338 | triggerType: 'onBlur', 339 | } 340 | ) 341 | ).toEqual({ 342 | error: ['The field value is required'], 343 | success: [], 344 | warning: [], 345 | }) 346 | }) 347 | 348 | test('filter trigger type(match multi validate)', async () => { 349 | expect( 350 | await validate( 351 | '', 352 | { 353 | triggerType: 'onBlur', 354 | required: true, 355 | validator() { 356 | return 'validate error' 357 | }, 358 | }, 359 | { 360 | triggerType: 'onBlur', 361 | } 362 | ) 363 | ).toEqual({ 364 | error: ['The field value is required', 'validate error'], 365 | success: [], 366 | warning: [], 367 | }) 368 | }) 369 | 370 | test('validate formats(date)', async () => { 371 | noError(await validate('', 'date')) 372 | hasError(await validate('2020-1', 'date')) 373 | hasError(await validate('2020-01- 11:23:33', 'date')) 374 | hasError(await validate('12/01/', 'date')) 375 | noError(await validate('2020-1-12', 'date')) 376 | noError(await validate('2020/1/12', 'date')) 377 | noError(await validate('2020-01-12', 'date')) 378 | noError(await validate('2020/01/12', 'date')) 379 | noError(await validate('12/01/2020', 'date')) 380 | noError(await validate('2020-01-12 11:23:33', 'date')) 381 | noError(await validate('2020/01/12 11:23:33', 'date')) 382 | noError(await validate('12/01/2020 11:23:33', 'date')) 383 | noError(await validate('12/1/2020 11:23:33', 'date')) 384 | }) 385 | 386 | test('validate formats(number)', async () => { 387 | noError(await validate('', 'number')) 388 | hasError(await validate('12323d', 'number')) 389 | noError(await validate('12323', 'number')) 390 | noError(await validate('12323.12', 'number')) 391 | noError(await validate('-12323.12', 'number')) 392 | noError(await validate('+12323.12', 'number')) 393 | }) 394 | 395 | test('validate formats(integer)', async () => { 396 | noError(await validate('', 'integer')) 397 | hasError(await validate('222.333', 'integer')) 398 | noError(await validate('12323', 'integer')) 399 | }) 400 | 401 | test('validate formats(phone)', async () => { 402 | noError(await validate('', 'phone')) 403 | hasError(await validate('222333', 'phone')) 404 | noError(await validate('15934567899', 'phone')) 405 | }) 406 | 407 | test('validate formats(money)', async () => { 408 | noError(await validate('$12', 'money')) 409 | hasError(await validate('$12.', 'money')) 410 | noError(await validate('$12.3', 'money')) 411 | }) 412 | 413 | test('validate custom validator', async () => { 414 | hasError(await validate('123', { custom: true })) 415 | noError(await validate('', { custom: true })) 416 | }) 417 | 418 | test('validate custom formats', async () => { 419 | hasError(await validate('aa asd', 'custom')) 420 | hasError(await validate('aa asd 中文', 'custom')) 421 | noError(await validate('中文', 'custom')) 422 | }) 423 | 424 | test('validate undefined format', async () => { 425 | expect( 426 | ( 427 | await validate('a', { 428 | required: false, 429 | pattern: '(\\d{3,4}-\\d{7,8}-\\d{4})|(4\\d{4,9})|(\\d{3,4}-\\d{7,8})', 430 | format: undefined, 431 | message: 'error', 432 | }) 433 | ).error 434 | ).toEqual(['error']) 435 | }) 436 | 437 | test('validator return boolean', async () => { 438 | hasError( 439 | await validate('123', { 440 | customBool: true, 441 | message: 'custom error', 442 | }), 443 | 'custom error' 444 | ) 445 | noError( 446 | await validate('123', { 447 | customBool2: true, 448 | message: 'custom error', 449 | }) 450 | ) 451 | }) 452 | 453 | test('language', async () => { 454 | setValidateLanguage('zh-CN') 455 | hasError( 456 | await validate('', { 457 | required: true, 458 | }), 459 | '该字段是必填字段' 460 | ) 461 | setValidateLanguage('en-US') 462 | hasError( 463 | await validate('', { 464 | required: true, 465 | }), 466 | 'The field value is required' 467 | ) 468 | }) 469 | 470 | test('validator template', async () => { 471 | registerValidateMessageTemplateEngine((message) => { 472 | if (typeof message !== 'string') return message 473 | return message.replace(/\<\<\s*([\w.]+)\s*\>\>/g, (_, $0) => { 474 | return { aa: 123 }[$0] 475 | }) 476 | }) 477 | hasError( 478 | await validate('', () => { 479 | return `<<aa>>=123` 480 | }), 481 | '123=123' 482 | ) 483 | }) 484 | 485 | test('validator template with format', async () => { 486 | registerValidateMessageTemplateEngine((message) => { 487 | if (typeof message !== 'string') return message 488 | return message.replace(/\<\<\s*([\w.]+)\s*\>\>/g, (_, $0) => { 489 | return { aa: 123 }[$0] 490 | }) 491 | }) 492 | hasError( 493 | await validate('', (value, rules, ctx, format) => { 494 | return `<<aa>>=123&${format('<<aa>>')}` 495 | }), 496 | '123=123&123' 497 | ) 498 | }) 499 | 500 | test('validator template with format and scope', async () => { 501 | registerValidateMessageTemplateEngine((message) => { 502 | if (typeof message !== 'string') return message 503 | return message.replace(/\<\<\s*([\w.]+)\s*\>\>/g, (_, $0) => { 504 | return { aa: 123 }[$0] 505 | }) 506 | }) 507 | 508 | const result = await validate( 509 | '', 510 | (value, rules, ctx, format) => { 511 | return `<<aa>>=123&${format('<<aa>>{{name}}')}` 512 | }, 513 | { 514 | context: { 515 | name: 'scopeName', 516 | }, 517 | } 518 | ) 519 | 520 | expect(result.error[0]).toEqual('123=123&123scopeName') 521 | }) 522 | 523 | test('validator order with format', async () => { 524 | hasError( 525 | await validate('', [ 526 | { required: true }, 527 | { 528 | format: 'url', 529 | }, 530 | ]), 531 | 'The field value is required' 532 | ) 533 | }) 534 | ``` -------------------------------------------------------------------------------- /docs/guide/advanced/controlled.md: -------------------------------------------------------------------------------- ```markdown 1 | # Form Controlled 2 | 3 | Formily 2.x has given up supporting controlled mode for form components and field components. Because the internal management state mode of the form itself is not a controlled mode, there will be many boundary problems in the process of changing the controlled mode to the uncontrolled mode. At the same time, the controlled mode will have a large number of dirty inspection processes, and the performance is very poor. Instead, the controlled mode itself can solve most of the problems. 4 | 5 | So Formily no longer supports the controlled mode, but if we insist on implementing ordinary React controlled, we can still support it. It can only achieve value control, not field-level control, which is the Field component we use. The properties will only take effect during the first rendering. Any changes to the properties in the future will not be automatically updated. If you want to update automatically, unless you recreate the Form instance (obviously this will lose all the previously maintained state). 6 | 7 | Therefore, we more recommend using [@formily/reactive](https://reactive.formilyjs.org) to achieve responsive control, which can achieve both value control and field-level control. 8 | 9 | ## Value Controlled 10 | 11 | Ordinary controlled mode, which will rely heavily on dirty checking to achieve data synchronization, and the number of component renderings will be very high. 12 | 13 | ```tsx 14 | import React, { useMemo, useState, useEffect, useRef } from 'react' 15 | import { createForm, onFormValuesChange } from '@formily/core' 16 | import { createSchemaField } from '@formily/react' 17 | import { Form, FormItem, Input } from '@formily/antd' 18 | 19 | const SchemaField = createSchemaField({ 20 | components: { 21 | Input, 22 | FormItem, 23 | }, 24 | }) 25 | 26 | const MyForm = (props) => { 27 | const form = useMemo( 28 | () => 29 | createForm({ 30 | values: props.values, 31 | effects: () => { 32 | onFormValuesChange((form) => { 33 | props.onChange(form.values) 34 | }) 35 | }, 36 | }), 37 | [] 38 | ) 39 | const count = useRef(1) 40 | 41 | useEffect(() => { 42 | form.setValues(props.values, 'overwrite') 43 | }, [JSON.stringify(props.values)]) 44 | 45 | return ( 46 | <Form form={form}> 47 | <SchemaField> 48 | <SchemaField.String 49 | name="input" 50 | x-decorator="FormItem" 51 | x-component="Input" 52 | x-component-props={{ placeholder: 'controlled target' }} 53 | /> 54 | </SchemaField> 55 | Form component rendering times:{count.current++} 56 | </Form> 57 | ) 58 | } 59 | 60 | export default () => { 61 | const [values, setValues] = useState({ input: '' }) 62 | const count = useRef(1) 63 | return ( 64 | <> 65 | <FormItem> 66 | <Input 67 | value={values.input} 68 | placeholder="controller" 69 | onChange={(event) => { 70 | setValues({ ...values, input: event.target.value }) 71 | }} 72 | /> 73 | </FormItem> 74 | <MyForm 75 | values={values} 76 | onChange={(values) => { 77 | setValues({ ...values }) 78 | }} 79 | /> 80 | root component rendering times: {count.current++} 81 | </> 82 | ) 83 | } 84 | ``` 85 | 86 | ## Responsive Value Controlled 87 | 88 | Responsive control is mainly to use [@formily/reactive](https://reactive.formilyjs.org) to achieve responsive updates, we can easily achieve two-way binding, while the performance is full of normal controlled updates. 89 | 90 | ```tsx 91 | import React, { useMemo, useRef } from 'react' 92 | import { createForm } from '@formily/core' 93 | import { createSchemaField } from '@formily/react' 94 | import { Form, FormItem, Input } from '@formily/antd' 95 | import { observable } from '@formily/reactive' 96 | import { observer } from '@formily/reactive-react' 97 | 98 | const SchemaField = createSchemaField({ 99 | components: { 100 | Input, 101 | FormItem, 102 | }, 103 | }) 104 | 105 | const MyForm = (props) => { 106 | const count = useRef(1) 107 | const form = useMemo( 108 | () => 109 | createForm({ 110 | values: props.values, 111 | }), 112 | [] 113 | ) 114 | 115 | return ( 116 | <Form form={form}> 117 | <SchemaField> 118 | <SchemaField.String 119 | name="input" 120 | x-decorator="FormItem" 121 | x-component="Input" 122 | x-component-props={{ placeholder: 'controlled target' }} 123 | /> 124 | </SchemaField> 125 | Form component rendering times:{count.current++} 126 | </Form> 127 | ) 128 | } 129 | 130 | const Controller = observer((props) => { 131 | const count = useRef(1) 132 | return ( 133 | <FormItem> 134 | <Input 135 | value={props.values.input} 136 | placeholder="controller" 137 | onChange={(event) => { 138 | props.values.input = event.target.value 139 | }} 140 | /> 141 | Controller component rendering times:{count.current++} 142 | </FormItem> 143 | ) 144 | }) 145 | 146 | export default () => { 147 | const count = useRef(1) 148 | const values = useMemo(() => 149 | observable({ 150 | input: '', 151 | }) 152 | ) 153 | return ( 154 | <> 155 | <Controller values={values} /> 156 | <MyForm values={values} /> 157 | root component rendering times:{count.current++} 158 | </> 159 | ) 160 | } 161 | ``` 162 | 163 | ## Schema Controlled 164 | 165 | There will be a requirement for the form configuration scenario. The Schema of the form will change frequently. In fact, it is equivalent to frequently creating new forms. The state of the previous operation should be discarded. 166 | 167 | ```tsx 168 | import React, { useMemo, useState } from 'react' 169 | import { createForm } from '@formily/core' 170 | import { createSchemaField } from '@formily/react' 171 | import { Form, FormItem, Input, Select } from '@formily/antd' 172 | import { Button, Space } from 'antd' 173 | 174 | const SchemaField = createSchemaField({ 175 | components: { 176 | Input, 177 | FormItem, 178 | Select, 179 | }, 180 | }) 181 | 182 | export default () => { 183 | const [current, setCurrent] = useState({}) 184 | const form = useMemo(() => createForm(), [current]) 185 | return ( 186 | <Form form={form} layout="vertical"> 187 | <Space style={{ marginBottom: 20 }}> 188 | <Button 189 | onClick={() => { 190 | setCurrent({ 191 | type: 'object', 192 | properties: { 193 | aa: { 194 | type: 'string', 195 | title: 'AA', 196 | 'x-decorator': 'FormItem', 197 | 'x-component': 'Input', 198 | 'x-component-props': { 199 | placeholder: 'Input', 200 | }, 201 | }, 202 | }, 203 | }) 204 | }} 205 | > 206 | Schema1 207 | </Button> 208 | <Button 209 | onClick={() => { 210 | setCurrent({ 211 | type: 'object', 212 | properties: { 213 | aa: { 214 | type: 'string', 215 | title: 'AA', 216 | 'x-decorator': 'FormItem', 217 | enum: [ 218 | { 219 | label: '111', 220 | value: '111', 221 | }, 222 | { label: '222', value: '222' }, 223 | ], 224 | 'x-component': 'Select', 225 | 'x-component-props': { 226 | placeholder: 'Select', 227 | }, 228 | }, 229 | bb: { 230 | type: 'string', 231 | title: 'BB', 232 | 'x-decorator': 'FormItem', 233 | 'x-component': 'Input', 234 | }, 235 | }, 236 | }) 237 | }} 238 | > 239 | Schema2 240 | </Button> 241 | </Space> 242 | <SchemaField schema={current} /> 243 | </Form> 244 | ) 245 | } 246 | ``` 247 | 248 | ## Schema fragment linkage (top level control) 249 | 250 | The most important thing for fragment linkage is to manually clean up the field model, otherwise the UI cannot be synchronized 251 | 252 | ```tsx 253 | import React, { useMemo, useRef } from 'react' 254 | import { createForm } from '@formily/core' 255 | import { createSchemaField, observer } from '@formily/react' 256 | import { Form, FormItem, Input, Select } from '@formily/antd' 257 | 258 | const SchemaField = createSchemaField({ 259 | components: { 260 | Input, 261 | FormItem, 262 | Select, 263 | }, 264 | }) 265 | 266 | const DYNAMIC_INJECT_SCHEMA = { 267 | type_1: { 268 | type: 'void', 269 | properties: { 270 | aa: { 271 | type: 'string', 272 | title: 'AA', 273 | 'x-decorator': 'FormItem', 274 | 'x-component': 'Input', 275 | 'x-component-props': { 276 | placeholder: 'Input', 277 | }, 278 | }, 279 | }, 280 | }, 281 | type_2: { 282 | type: 'void', 283 | properties: { 284 | aa: { 285 | type: 'string', 286 | title: 'AA', 287 | 'x-decorator': 'FormItem', 288 | enum: [ 289 | { 290 | label: '111', 291 | value: '111', 292 | }, 293 | { label: '222', value: '222' }, 294 | ], 295 | 'x-component': 'Select', 296 | 'x-component-props': { 297 | placeholder: 'Select', 298 | }, 299 | }, 300 | bb: { 301 | type: 'string', 302 | title: 'BB', 303 | 'x-decorator': 'FormItem', 304 | 'x-component': 'Input', 305 | }, 306 | }, 307 | }, 308 | } 309 | 310 | const App = observer(() => { 311 | const oldTypeRef = useRef() 312 | const form = useMemo(() => createForm(), []) 313 | const currentType = form.values.type 314 | const schema = { 315 | type: 'object', 316 | properties: { 317 | type: { 318 | type: 'string', 319 | title: 'Type', 320 | enum: [ 321 | { label: 'type 1', value: 'type_1' }, 322 | { label: 'type 2', value: 'type_2' }, 323 | ], 324 | 'x-decorator': 'FormItem', 325 | 'x-component': 'Select', 326 | }, 327 | container: DYNAMIC_INJECT_SCHEMA[currentType], 328 | }, 329 | } 330 | 331 | if (oldTypeRef.current !== currentType) { 332 | form.clearFormGraph('container.*') //Recycle field model 333 | } 334 | 335 | oldTypeRef.current = currentType 336 | 337 | return ( 338 | <Form form={form} layout="vertical"> 339 | <SchemaField schema={schema} /> 340 | </Form> 341 | ) 342 | }) 343 | 344 | export default App 345 | ``` 346 | 347 | ## Schema fragment linkage (custom component) 348 | 349 | ```tsx 350 | import React, { useMemo, useState, useEffect } from 'react' 351 | import { createForm } from '@formily/core' 352 | import { 353 | createSchemaField, 354 | RecursionField, 355 | useForm, 356 | useField, 357 | observer, 358 | } from '@formily/react' 359 | import { Form, FormItem, Input, Select } from '@formily/antd' 360 | 361 | const Custom = observer(() => { 362 | const field = useField() 363 | const form = useForm() 364 | const [schema, setSchema] = useState({}) 365 | 366 | useEffect(() => { 367 | form.clearFormGraph(`${field.address}.*`) //Recycle field model 368 | //Can be obtained asynchronously 369 | setSchema(DYNAMIC_INJECT_SCHEMA[form.values.type]) 370 | }, [form.values.type]) 371 | 372 | return ( 373 | <RecursionField 374 | basePath={field.address} 375 | schema={schema} 376 | onlyRenderProperties 377 | /> 378 | ) 379 | }) 380 | 381 | const SchemaField = createSchemaField({ 382 | components: { 383 | Input, 384 | FormItem, 385 | Select, 386 | Custom, 387 | }, 388 | }) 389 | 390 | const DYNAMIC_INJECT_SCHEMA = { 391 | type_1: { 392 | type: 'void', 393 | properties: { 394 | aa: { 395 | type: 'string', 396 | title: 'AA', 397 | 'x-decorator': 'FormItem', 398 | 'x-component': 'Input', 399 | 'x-component-props': { 400 | placeholder: 'Input', 401 | }, 402 | }, 403 | }, 404 | }, 405 | type_2: { 406 | type: 'void', 407 | properties: { 408 | aa: { 409 | type: 'string', 410 | title: 'AA', 411 | 'x-decorator': 'FormItem', 412 | enum: [ 413 | { 414 | label: '111', 415 | value: '111', 416 | }, 417 | { label: '222', value: '222' }, 418 | ], 419 | 'x-component': 'Select', 420 | 'x-component-props': { 421 | placeholder: 'Select', 422 | }, 423 | }, 424 | bb: { 425 | type: 'string', 426 | title: 'BB', 427 | 'x-decorator': 'FormItem', 428 | 'x-component': 'Input', 429 | }, 430 | }, 431 | }, 432 | } 433 | 434 | const App = observer(() => { 435 | const form = useMemo(() => createForm(), []) 436 | const schema = { 437 | type: 'object', 438 | properties: { 439 | type: { 440 | type: 'string', 441 | title: 'Type', 442 | enum: [ 443 | { label: 'type 1', value: 'type_1' }, 444 | { label: 'type 2', value: 'type_2' }, 445 | ], 446 | 'x-decorator': 'FormItem', 447 | 'x-component': 'Select', 448 | }, 449 | container: { 450 | type: 'object', 451 | 'x-component': 'Custom', 452 | }, 453 | }, 454 | } 455 | 456 | return ( 457 | <Form form={form} layout="vertical"> 458 | <SchemaField schema={schema} /> 459 | </Form> 460 | ) 461 | }) 462 | 463 | export default App 464 | ``` 465 | 466 | ## Field Level Control 467 | 468 | ### Best Practices 469 | 470 | It is recommended to use [@formily/reactive](https://reactive.formilyjs.org) to achieve responsive control. 471 | 472 | ```tsx 473 | import React from 'react' 474 | import { createForm } from '@formily/core' 475 | import { createSchemaField } from '@formily/react' 476 | import { Form, FormItem, Input } from '@formily/antd' 477 | import { observable } from '@formily/reactive' 478 | import { observer } from '@formily/reactive-react' 479 | 480 | const SchemaField = createSchemaField({ 481 | components: { 482 | Input, 483 | FormItem, 484 | }, 485 | }) 486 | 487 | const form = createForm() 488 | 489 | const obs = observable({ 490 | input: '', 491 | }) 492 | 493 | const Controller = observer(() => { 494 | return ( 495 | <FormItem> 496 | <Input 497 | value={obs.input} 498 | placeholder="controller" 499 | onChange={(event) => { 500 | obs.input = event.target.value 501 | }} 502 | /> 503 | </FormItem> 504 | ) 505 | }) 506 | 507 | export default () => { 508 | return ( 509 | <> 510 | <Controller /> 511 | <Form form={form}> 512 | <SchemaField> 513 | <SchemaField.String 514 | name="input" 515 | x-decorator="FormItem" 516 | x-component="Input" 517 | x-component-props={{ placeholder: 'controlled target' }} 518 | x-reactions={(field) => { 519 | field.component[1].placeholder = obs.input || 'controlled target' 520 | }} 521 | /> 522 | </SchemaField> 523 | </Form> 524 | </> 525 | ) 526 | } 527 | ``` 528 | 529 | ### Anti-pattern 530 | 531 | It is not possible to update automatically when using traditional controlled mode. 532 | 533 | ```tsx 534 | import React, { useState } from 'react' 535 | import { createForm } from '@formily/core' 536 | import { createSchemaField } from '@formily/react' 537 | import { Form, FormItem, Input } from '@formily/antd' 538 | 539 | const SchemaField = createSchemaField({ 540 | components: { 541 | Input, 542 | FormItem, 543 | }, 544 | }) 545 | 546 | const form = createForm() 547 | 548 | export default () => { 549 | const [value, setValue] = useState('') 550 | return ( 551 | <> 552 | <FormItem> 553 | <Input 554 | value={value} 555 | placeholder="controller" 556 | onChange={(event) => { 557 | setValue(event.target.value) 558 | }} 559 | /> 560 | </FormItem> 561 | <Form form={form}> 562 | <SchemaField> 563 | <SchemaField.String 564 | name="input" 565 | x-decorator="FormItem" 566 | x-component="Input" 567 | x-component-props={{ placeholder: value || 'controlled target' }} 568 | /> 569 | </SchemaField> 570 | </Form> 571 | </> 572 | ) 573 | } 574 | ``` 575 | ``` -------------------------------------------------------------------------------- /packages/antd/docs/components/ArrayCards.md: -------------------------------------------------------------------------------- ```markdown 1 | # ArrayCards 2 | 3 | > Card list, it is more suitable to use ArrayCards for scenarios with a large number of fields in each row and more linkages 4 | > 5 | > Note: This component is only applicable to Schema scenarios 6 | 7 | ## Markup Schema example 8 | 9 | ```tsx 10 | import React from 'react' 11 | import { 12 | FormItem, 13 | Input, 14 | ArrayCards, 15 | FormButtonGroup, 16 | Submit, 17 | } from '@formily/antd' 18 | import { createForm } from '@formily/core' 19 | import { FormProvider, createSchemaField } from '@formily/react' 20 | 21 | const SchemaField = createSchemaField({ 22 | components: { 23 | FormItem, 24 | Input, 25 | ArrayCards, 26 | }, 27 | }) 28 | 29 | const form = createForm() 30 | 31 | export default () => { 32 | return ( 33 | <FormProvider form={form}> 34 | <SchemaField> 35 | <SchemaField.Array 36 | name="string_array" 37 | maxItems={3} 38 | x-decorator="FormItem" 39 | x-component="ArrayCards" 40 | x-component-props={{ 41 | title: 'String array', 42 | }} 43 | > 44 | <SchemaField.Void> 45 | <SchemaField.Void x-component="ArrayCards.Index" /> 46 | <SchemaField.String 47 | name="input" 48 | x-decorator="FormItem" 49 | title="Input" 50 | required 51 | x-component="Input" 52 | /> 53 | <SchemaField.Void x-component="ArrayCards.Remove" /> 54 | <SchemaField.Void x-component="ArrayCards.Copy" /> 55 | <SchemaField.Void x-component="ArrayCards.MoveUp" /> 56 | <SchemaField.Void x-component="ArrayCards.MoveDown" /> 57 | </SchemaField.Void> 58 | <SchemaField.Void 59 | x-component="ArrayCards.Addition" 60 | title="Add entry" 61 | /> 62 | </SchemaField.Array> 63 | <SchemaField.Array 64 | name="array" 65 | maxItems={3} 66 | x-decorator="FormItem" 67 | x-component="ArrayCards" 68 | x-component-props={{ 69 | title: 'Object array', 70 | }} 71 | > 72 | <SchemaField.Object> 73 | <SchemaField.Void x-component="ArrayCards.Index" /> 74 | <SchemaField.String 75 | name="input" 76 | x-decorator="FormItem" 77 | title="Input" 78 | required 79 | x-component="Input" 80 | /> 81 | <SchemaField.Void x-component="ArrayCards.Remove" /> 82 | <SchemaField.Void x-component="ArrayCards.MoveUp" /> 83 | <SchemaField.Void x-component="ArrayCards.MoveDown" /> 84 | </SchemaField.Object> 85 | <SchemaField.Void 86 | x-component="ArrayCards.Addition" 87 | title="Add entry" 88 | /> 89 | </SchemaField.Array> 90 | </SchemaField> 91 | <FormButtonGroup> 92 | <Submit onSubmit={console.log}>Submit</Submit> 93 | </FormButtonGroup> 94 | </FormProvider> 95 | ) 96 | } 97 | ``` 98 | 99 | ## JSON Schema case 100 | 101 | ```tsx 102 | import React from 'react' 103 | import { 104 | FormItem, 105 | Input, 106 | ArrayCards, 107 | FormButtonGroup, 108 | Submit, 109 | } from '@formily/antd' 110 | import { createForm } from '@formily/core' 111 | import { FormProvider, createSchemaField } from '@formily/react' 112 | 113 | const SchemaField = createSchemaField({ 114 | components: { 115 | FormItem, 116 | Input, 117 | ArrayCards, 118 | }, 119 | }) 120 | 121 | const form = createForm() 122 | 123 | const schema = { 124 | type: 'object', 125 | properties: { 126 | string_array: { 127 | type: 'array', 128 | 'x-component': 'ArrayCards', 129 | maxItems: 3, 130 | 'x-decorator': 'FormItem', 131 | 'x-component-props': { 132 | title: 'String array', 133 | }, 134 | items: { 135 | type: 'void', 136 | properties: { 137 | index: { 138 | type: 'void', 139 | 'x-component': 'ArrayCards.Index', 140 | }, 141 | input: { 142 | type: 'string', 143 | 'x-decorator': 'FormItem', 144 | title: 'Input', 145 | required: true, 146 | 'x-component': 'Input', 147 | }, 148 | remove: { 149 | type: 'void', 150 | 'x-component': 'ArrayCards.Remove', 151 | }, 152 | moveUp: { 153 | type: 'void', 154 | 'x-component': 'ArrayCards.MoveUp', 155 | }, 156 | moveDown: { 157 | type: 'void', 158 | 'x-component': 'ArrayCards.MoveDown', 159 | }, 160 | }, 161 | }, 162 | properties: { 163 | addition: { 164 | type: 'void', 165 | title: 'Add entry', 166 | 'x-component': 'ArrayCards.Addition', 167 | }, 168 | }, 169 | }, 170 | array: { 171 | type: 'array', 172 | 'x-component': 'ArrayCards', 173 | maxItems: 3, 174 | 'x-decorator': 'FormItem', 175 | 'x-component-props': { 176 | title: 'Object array', 177 | }, 178 | items: { 179 | type: 'object', 180 | properties: { 181 | index: { 182 | type: 'void', 183 | 'x-component': 'ArrayCards.Index', 184 | }, 185 | input: { 186 | type: 'string', 187 | 'x-decorator': 'FormItem', 188 | title: 'Input', 189 | required: true, 190 | 'x-component': 'Input', 191 | }, 192 | remove: { 193 | type: 'void', 194 | 'x-component': 'ArrayCards.Remove', 195 | }, 196 | moveUp: { 197 | type: 'void', 198 | 'x-component': 'ArrayCards.MoveUp', 199 | }, 200 | moveDown: { 201 | type: 'void', 202 | 'x-component': 'ArrayCards.MoveDown', 203 | }, 204 | }, 205 | }, 206 | properties: { 207 | addition: { 208 | type: 'void', 209 | title: 'Add entry', 210 | 'x-component': 'ArrayCards.Addition', 211 | }, 212 | }, 213 | }, 214 | }, 215 | } 216 | 217 | export default () => { 218 | return ( 219 | <FormProvider form={form}> 220 | <SchemaField schema={schema} /> 221 | <FormButtonGroup> 222 | <Submit onSubmit={console.log}>Submit</Submit> 223 | </FormButtonGroup> 224 | </FormProvider> 225 | ) 226 | } 227 | ``` 228 | 229 | ## Effects linkage case 230 | 231 | ```tsx 232 | import React from 'react' 233 | import { 234 | FormItem, 235 | Input, 236 | ArrayCards, 237 | FormButtonGroup, 238 | Submit, 239 | } from '@formily/antd' 240 | import { createForm, onFieldChange, onFieldReact } from '@formily/core' 241 | import { FormProvider, createSchemaField } from '@formily/react' 242 | 243 | const SchemaField = createSchemaField({ 244 | components: { 245 | FormItem, 246 | Input, 247 | ArrayCards, 248 | }, 249 | }) 250 | 251 | const form = createForm({ 252 | effects: () => { 253 | //Active linkage mode 254 | onFieldChange('array.*.aa', ['value'], (field, form) => { 255 | form.setFieldState(field.query('.bb'), (state) => { 256 | state.visible = field.value != '123' 257 | }) 258 | }) 259 | //Passive linkage mode 260 | onFieldReact('array.*.dd', (field) => { 261 | field.visible = field.query('.cc').get('value') != '123' 262 | }) 263 | }, 264 | }) 265 | 266 | export default () => { 267 | return ( 268 | <FormProvider form={form}> 269 | <SchemaField> 270 | <SchemaField.Array 271 | name="array" 272 | maxItems={3} 273 | x-component="ArrayCards" 274 | x-decorator="FormItem" 275 | x-component-props={{ 276 | title: 'Object array', 277 | }} 278 | > 279 | <SchemaField.Object> 280 | <SchemaField.Void x-component="ArrayCards.Index" /> 281 | <SchemaField.String 282 | name="aa" 283 | x-decorator="FormItem" 284 | title="AA" 285 | required 286 | description="AA hide BB when entering 123" 287 | x-component="Input" 288 | /> 289 | <SchemaField.String 290 | name="bb" 291 | x-decorator="FormItem" 292 | title="BB" 293 | required 294 | x-component="Input" 295 | /> 296 | <SchemaField.String 297 | name="cc" 298 | x-decorator="FormItem" 299 | title="CC" 300 | required 301 | description="Hide DD when CC enters 123" 302 | x-component="Input" 303 | /> 304 | <SchemaField.String 305 | name="dd" 306 | x-decorator="FormItem" 307 | title="DD" 308 | required 309 | x-component="Input" 310 | /> 311 | <SchemaField.Void x-component="ArrayCards.Remove" /> 312 | <SchemaField.Void x-component="ArrayCards.MoveUp" /> 313 | <SchemaField.Void x-component="ArrayCards.MoveDown" /> 314 | </SchemaField.Object> 315 | <SchemaField.Void 316 | x-component="ArrayCards.Addition" 317 | title="Add entry" 318 | /> 319 | </SchemaField.Array> 320 | </SchemaField> 321 | <FormButtonGroup> 322 | <Submit onSubmit={console.log}>Submit</Submit> 323 | </FormButtonGroup> 324 | </FormProvider> 325 | ) 326 | } 327 | ``` 328 | 329 | ## JSON Schema linkage case 330 | 331 | ```tsx 332 | import React from 'react' 333 | import { 334 | FormItem, 335 | Input, 336 | ArrayCards, 337 | FormButtonGroup, 338 | Submit, 339 | } from '@formily/antd' 340 | import { createForm } from '@formily/core' 341 | import { FormProvider, createSchemaField } from '@formily/react' 342 | 343 | const SchemaField = createSchemaField({ 344 | components: { 345 | FormItem, 346 | Input, 347 | ArrayCards, 348 | }, 349 | }) 350 | 351 | const form = createForm() 352 | 353 | const schema = { 354 | type: 'object', 355 | properties: { 356 | array: { 357 | type: 'array', 358 | 'x-component': 'ArrayCards', 359 | maxItems: 3, 360 | title: 'Object array', 361 | items: { 362 | type: 'object', 363 | properties: { 364 | index: { 365 | type: 'void', 366 | 'x-component': 'ArrayCards.Index', 367 | }, 368 | aa: { 369 | type: 'string', 370 | 'x-decorator': 'FormItem', 371 | title: 'AA', 372 | required: true, 373 | 'x-component': 'Input', 374 | description: 'Enter 123', 375 | }, 376 | bb: { 377 | type: 'string', 378 | title: 'BB', 379 | required: true, 380 | 'x-decorator': 'FormItem', 381 | 'x-component': 'Input', 382 | 'x-reactions': [ 383 | { 384 | dependencies: ['.aa'], 385 | when: "{{$deps[0] != '123'}}", 386 | fulfill: { 387 | schema: { 388 | title: 'BB', 389 | 'x-disabled': true, 390 | }, 391 | }, 392 | otherwise: { 393 | schema: { 394 | title: 'Changed', 395 | 'x-disabled': false, 396 | }, 397 | }, 398 | }, 399 | ], 400 | }, 401 | remove: { 402 | type: 'void', 403 | 'x-component': 'ArrayCards.Remove', 404 | }, 405 | moveUp: { 406 | type: 'void', 407 | 'x-component': 'ArrayCards.MoveUp', 408 | }, 409 | moveDown: { 410 | type: 'void', 411 | 'x-component': 'ArrayCards.MoveDown', 412 | }, 413 | }, 414 | }, 415 | properties: { 416 | addition: { 417 | type: 'void', 418 | title: 'Add entry', 419 | 'x-component': 'ArrayCards.Addition', 420 | }, 421 | }, 422 | }, 423 | }, 424 | } 425 | 426 | export default () => { 427 | return ( 428 | <FormProvider form={form}> 429 | <SchemaField schema={schema} /> 430 | <FormButtonGroup> 431 | <Submit onSubmit={console.log}>Submit</Submit> 432 | </FormButtonGroup> 433 | </FormProvider> 434 | ) 435 | } 436 | ``` 437 | 438 | ## API 439 | 440 | ### ArrayCards 441 | 442 | Extended attributes 443 | 444 | | Property name | Type | Description | Default value | 445 | | ------------- | ------------------------- | --------------- | ------------- | 446 | | onAdd | `(index: number) => void` | add method | | 447 | | onRemove | `(index: number) => void` | remove method | | 448 | | onCopy | `(index: number) => void` | copy method | | 449 | | onMoveUp | `(index: number) => void` | moveUp method | | 450 | | onMoveDown | `(index: number) => void` | moveDown method | | 451 | 452 | Other Reference https://ant.design/components/card-cn/ 453 | 454 | ### ArrayCards.Addition 455 | 456 | > Add button 457 | 458 | Extended attributes 459 | 460 | | Property name | Type | Description | Default value | 461 | | ------------- | -------------------- | ------------- | ------------- | 462 | | title | ReactText | Copywriting | | 463 | | method | `'push' \|'unshift'` | add method | `'push'` | 464 | | defaultValue | `any` | Default value | | 465 | 466 | Other references https://ant.design/components/button-cn/ 467 | 468 | Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective 469 | 470 | Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. 471 | 472 | ### ArrayCards.Copy 473 | 474 | > Copy button 475 | 476 | Extended attributes 477 | 478 | | Property name | Type | Description | Default value | 479 | | ------------- | -------------------- | ----------- | ------------- | 480 | | title | ReactText | Copywriting | | 481 | | method | `'push' \|'unshift'` | Copy method | `'push'` | 482 | 483 | Other references https://ant.design/components/button-cn/ 484 | 485 | Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective 486 | 487 | Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. 488 | 489 | ### ArrayCards.Remove 490 | 491 | > Delete button 492 | 493 | | Property name | Type | Description | Default value | 494 | | ------------- | --------- | ----------- | ------------- | 495 | | title | ReactText | Copywriting | | 496 | 497 | Other references https://ant.design/components/icon-cn/ 498 | 499 | Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective 500 | 501 | Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. 502 | 503 | ### ArrayCards.MoveDown 504 | 505 | > Move down button 506 | 507 | | Property name | Type | Description | Default value | 508 | | ------------- | --------- | ----------- | ------------- | 509 | | title | ReactText | Copywriting | | 510 | 511 | Other references https://ant.design/components/icon-cn/ 512 | 513 | Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective 514 | 515 | Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. 516 | 517 | ### ArrayCards.MoveUp 518 | 519 | > Move up button 520 | 521 | | Property name | Type | Description | Default value | 522 | | ------------- | --------- | ----------- | ------------- | 523 | | title | ReactText | Copywriting | | 524 | 525 | Other references https://ant.design/components/icon-cn/ 526 | 527 | Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective 528 | 529 | Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. 530 | 531 | ### ArrayCards.Index 532 | 533 | > Index Renderer 534 | 535 | No attributes 536 | 537 | ### ArrayCards.useIndex 538 | 539 | > Read the React Hook of the current rendering row index 540 | 541 | ### ArrayCards.useRecord 542 | 543 | > Read the React Hook of the current rendering row 544 | ``` -------------------------------------------------------------------------------- /packages/core/docs/api/entry/FieldEffectHooks.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | order: 2 3 | --- 4 | 5 | # Field Effect Hooks 6 | 7 | ## onFieldInit 8 | 9 | #### Description 10 | 11 | Used to monitor the side effect hook of a field initialization, we will trigger the field initialization event when we call createField 12 | 13 | #### Signature 14 | 15 | ```ts 16 | interface onFieldInit { 17 | (pattern: FormPathPattern, callback: (field: Field, form: Form) => void) 18 | } 19 | ``` 20 | 21 | <Alert> 22 | For the syntax format of FormPathPattern, please refer to <a href="/api/entry/form-path">FormPath</a> 23 | </Alert> 24 | 25 | #### Example 26 | 27 | ```tsx 28 | import React, { useMemo, useState } from 'react' 29 | import { createForm, onFieldInit } from '@formily/core' 30 | import { ActionResponse } from './ActionResponse' 31 | 32 | export default () => { 33 | const [response, setResponse] = useState('') 34 | const form = useMemo( 35 | () => 36 | createForm({ 37 | effects() { 38 | onFieldInit('target', () => { 39 | setResponse('target has been initialized') 40 | }) 41 | }, 42 | }), 43 | [] 44 | ) 45 | return ( 46 | <ActionResponse response={response}> 47 | <button 48 | onClick={() => { 49 | form.createField({ name: 'target' }) 50 | }} 51 | > 52 | Create field 53 | </button> 54 | </ActionResponse> 55 | ) 56 | } 57 | ``` 58 | 59 | ## onFieldMount 60 | 61 | #### Description 62 | 63 | Used to monitor the side-effect hook of a field that has been mounted, we will trigger the field mount event when we call onMount 64 | 65 | #### Signature 66 | 67 | ```ts 68 | interface onFieldMount { 69 | (pattern: FormPathPattern, callback: (field: Field, form: Form) => void) 70 | } 71 | ``` 72 | 73 | #### Example 74 | 75 | ```tsx 76 | import React, { useMemo, useState } from 'react' 77 | import { createForm, onFieldMount } from '@formily/core' 78 | import { ActionResponse } from './ActionResponse' 79 | 80 | export default () => { 81 | const [response, setResponse] = useState('') 82 | const form = useMemo( 83 | () => 84 | createForm({ 85 | effects() { 86 | onFieldMount('target', () => { 87 | setResponse('target is mounted') 88 | }) 89 | }, 90 | }), 91 | [] 92 | ) 93 | return ( 94 | <ActionResponse response={response}> 95 | <button 96 | onClick={() => { 97 | form.createField({ name: 'target' }).onMount() 98 | }} 99 | > 100 | Create and mount fields 101 | </button> 102 | </ActionResponse> 103 | ) 104 | } 105 | ``` 106 | 107 | ## onFieldUnmount 108 | 109 | #### Description 110 | 111 | It is used to monitor the side effect hook that a field has been unloaded. When we call onUnmount, the unmount event will be triggered 112 | 113 | #### Signature 114 | 115 | ```ts 116 | interface onFieldUnmount { 117 | (pattern: FormPathPattern, callback: (field: Field, form: Form) => void) 118 | } 119 | ``` 120 | 121 | #### Example 122 | 123 | ```tsx 124 | import React, { useMemo, useState } from 'react' 125 | import { createForm, onFieldMount, onFieldUnmount } from '@formily/core' 126 | import { ActionResponse } from './ActionResponse' 127 | 128 | export default () => { 129 | const [response, setResponse] = useState('') 130 | const form = useMemo( 131 | () => 132 | createForm({ 133 | effects() { 134 | onFieldMount('target', () => { 135 | setResponse('target is mounted') 136 | }) 137 | onFieldUnmount('target', () => { 138 | setResponse('target has been uninstalled') 139 | }) 140 | }, 141 | }), 142 | [] 143 | ) 144 | return ( 145 | <ActionResponse response={response}> 146 | <button 147 | onClick={() => { 148 | form.createField({ name: 'target' }).onMount() 149 | }} 150 | > 151 | Create and mount fields 152 | </button> 153 | <button 154 | onClick={() => { 155 | form.createField({ name: 'target' }).onUnmount() 156 | }} 157 | > 158 | Unload field 159 | </button> 160 | </ActionResponse> 161 | ) 162 | } 163 | ``` 164 | 165 | ## onFieldReact 166 | 167 | A side-effect hook used to implement field reactive logic. Its core principle is that the callback function will be executed when the field is initialized, and the dependency will be automatically tracked at the same time. The callback function will be executed repeatedly when the dependent data changes. 168 | 169 | #### Signature 170 | 171 | ```ts 172 | interface onFieldReact { 173 | (pattern: FormPathPattern, callback: (field: Field, form: Form) => void) 174 | } 175 | ``` 176 | 177 | #### Example 178 | 179 | ```tsx 180 | import React, { useMemo, useState } from 'react' 181 | import { createForm, onFieldReact } from '@formily/core' 182 | import { ActionResponse } from './ActionResponse' 183 | 184 | export default () => { 185 | const [response, setResponse] = useState('') 186 | const form = useMemo( 187 | () => 188 | createForm({ 189 | effects(form) { 190 | onFieldReact('target', () => { 191 | setResponse( 192 | 'target ' + (form.values.other === 123 ? 'display' : 'hide') 193 | ) 194 | }) 195 | }, 196 | }), 197 | [] 198 | ) 199 | return ( 200 | <ActionResponse response={response}> 201 | <button 202 | onClick={() => { 203 | form.createField({ name: 'target' }) 204 | }} 205 | > 206 | Initialize target 207 | </button> 208 | <button 209 | onClick={() => { 210 | const field = form.createField({ name: 'other' }) 211 | field.setValue(123) 212 | }} 213 | > 214 | Assign other = 123 215 | </button> 216 | <button 217 | onClick={() => { 218 | const field = form.createField({ name: 'other' }) 219 | field.setValue(null) 220 | }} 221 | > 222 | Assign other = null 223 | </button> 224 | </ActionResponse> 225 | ) 226 | } 227 | ``` 228 | 229 | > This example will track the changes of values.other, if it is equal to 123, it will control the display of the target, otherwise it will be hidden 230 | 231 | ## onFieldChange 232 | 233 | #### Description 234 | 235 | Side effect hook used to monitor the property changes of a field 236 | 237 | #### Signature 238 | 239 | ```ts 240 | interface onFieldChange { 241 | ( 242 | pattern: FormPathPattern, 243 | watches?: string[], 244 | callback: (field: Field, form: Form) => void 245 | ) 246 | (pattern: FormPathPattern, callback: (field: Field, form: Form) => void) 247 | } 248 | ``` 249 | 250 | You can pass in the specific set of attributes you want to monitor, or you can leave it alone, the default is to monitor value changes 251 | 252 | #### Example 253 | 254 | ```tsx 255 | import React, { useMemo, useState } from 'react' 256 | import { createForm, onFieldChange } from '@formily/core' 257 | import { ActionResponse } from './ActionResponse' 258 | 259 | export default () => { 260 | const [response, setResponse] = useState('') 261 | const form = useMemo( 262 | () => 263 | createForm({ 264 | effects() { 265 | onFieldChange('target', (field) => { 266 | setResponse('target value change:' + field.value) 267 | }) 268 | onFieldChange('target', ['component'], () => { 269 | setResponse('target component change') 270 | }) 271 | }, 272 | }), 273 | [] 274 | ) 275 | return ( 276 | <ActionResponse response={response}> 277 | <button 278 | onClick={() => { 279 | const field = form.createField({ name: 'target' }) 280 | field.setValue(field.value ? field.value + 1 : 1) 281 | }} 282 | > 283 | Settings 284 | </button> 285 | <button 286 | onClick={() => { 287 | const field = form.createField({ name: 'target' }) 288 | field.setComponent('Input') 289 | }} 290 | > 291 | Set up components 292 | </button> 293 | </ActionResponse> 294 | ) 295 | } 296 | ``` 297 | 298 | ## onFieldValueChange 299 | 300 | Side effect hooks used to monitor changes in a field value 301 | 302 | #### Signature 303 | 304 | ```ts 305 | interface onFieldValueChange { 306 | (pattern: FormPathPattern, callback: (field: Field, form: Form) => void) 307 | } 308 | ``` 309 | 310 | #### Example 311 | 312 | ```tsx 313 | import React, { useMemo, useState } from 'react' 314 | import { createForm, onFieldValueChange } from '@formily/core' 315 | import { ActionResponse } from './ActionResponse' 316 | 317 | export default () => { 318 | const [response, setResponse] = useState('') 319 | const form = useMemo( 320 | () => 321 | createForm({ 322 | effects() { 323 | onFieldValueChange('target', (field) => { 324 | setResponse('target value change:' + field.value) 325 | }) 326 | }, 327 | }), 328 | [] 329 | ) 330 | return ( 331 | <ActionResponse response={response}> 332 | <button 333 | onClick={() => { 334 | const field = form.createField({ name: 'target' }) 335 | field.setValue(field.value ? field.value + 1 : 1) 336 | }} 337 | > 338 | Settings 339 | </button> 340 | </ActionResponse> 341 | ) 342 | } 343 | ``` 344 | 345 | ## onFieldInitialValueChange 346 | 347 | Side-effect hooks used to monitor changes in the default value of a field 348 | 349 | #### Signature 350 | 351 | ```ts 352 | interface onFieldInitialValueChange { 353 | (pattern: FormPathPattern, callback: (field: Field, form: Form) => void) 354 | } 355 | ``` 356 | 357 | #### Example 358 | 359 | ```tsx 360 | import React, { useMemo, useState } from 'react' 361 | import { createForm, onFieldInitialValueChange } from '@formily/core' 362 | import { ActionResponse } from './ActionResponse' 363 | 364 | export default () => { 365 | const [response, setResponse] = useState('') 366 | const form = useMemo( 367 | () => 368 | createForm({ 369 | effects() { 370 | onFieldInitialValueChange('target', (field) => { 371 | setResponse('target default value change:' + field.value) 372 | }) 373 | }, 374 | }), 375 | [] 376 | ) 377 | return ( 378 | <ActionResponse response={response}> 379 | <button 380 | onClick={() => { 381 | const field = form.createField({ name: 'target' }) 382 | field.setInitialValue(field.value ? field.value + 1 : 1) 383 | }} 384 | > 385 | Settings 386 | </button> 387 | </ActionResponse> 388 | ) 389 | } 390 | ``` 391 | 392 | ## onFieldInputValueChange 393 | 394 | Used to monitor the side effect hook triggered by a field onInput 395 | 396 | #### Signature 397 | 398 | ```ts 399 | interface onFieldInputValueChange { 400 | (pattern: FormPathPattern, callback: (field: Field, form: Form) => void) 401 | } 402 | ``` 403 | 404 | #### Example 405 | 406 | ```tsx 407 | import React, { useMemo, useState } from 'react' 408 | import { createForm, onFieldInputValueChange } from '@formily/core' 409 | import { ActionResponse } from './ActionResponse' 410 | 411 | export default () => { 412 | const [response, setResponse] = useState('') 413 | const form = useMemo( 414 | () => 415 | createForm({ 416 | effects() { 417 | onFieldInputValueChange('target', (field) => { 418 | setResponse('target value change:' + field.value) 419 | }) 420 | }, 421 | }), 422 | [] 423 | ) 424 | return ( 425 | <ActionResponse response={response}> 426 | <button 427 | onClick={() => { 428 | const field = form.createField({ name: 'target' }) 429 | field.onInput(field.value ? field.value + 1 : 1) 430 | }} 431 | > 432 | Call onInput 433 | </button> 434 | </ActionResponse> 435 | ) 436 | } 437 | ``` 438 | 439 | ## onFieldValidateStart 440 | 441 | #### Description 442 | 443 | Monitor the side effect hook that triggers the start of a certain field verification 444 | 445 | #### Signature 446 | 447 | ```ts 448 | interface onFieldValidateStart { 449 | (pattern: FormPathPattern, callback: (field: Field, form: Form) => void) 450 | } 451 | ``` 452 | 453 | #### Example 454 | 455 | ```tsx 456 | import React, { useMemo, useState } from 'react' 457 | import { createForm, onFieldValidateStart } from '@formily/core' 458 | import { ActionResponse } from './ActionResponse' 459 | 460 | export default () => { 461 | const [response, setResponse] = useState('') 462 | const form = useMemo( 463 | () => 464 | createForm({ 465 | effects() { 466 | onFieldValidateStart('target', () => { 467 | setResponse('target verification start') 468 | }) 469 | }, 470 | }), 471 | [] 472 | ) 473 | return ( 474 | <ActionResponse response={response}> 475 | <button 476 | onClick={() => { 477 | const field = form.createField({ name: 'target', required: true }) 478 | field.onInput('') 479 | }} 480 | > 481 | Trigger verification 482 | </button> 483 | </ActionResponse> 484 | ) 485 | } 486 | ``` 487 | 488 | ## onFieldValidateEnd 489 | 490 | #### Description 491 | 492 | Monitor the side effect hook that triggers the end of a certain field verification 493 | 494 | #### Signature 495 | 496 | ```ts 497 | interface onFieldValidateEnd { 498 | (pattern: FormPathPattern, callback: (field: Field, form: Form) => void) 499 | } 500 | ``` 501 | 502 | #### Example 503 | 504 | ```tsx 505 | import React, { useMemo, useState } from 'react' 506 | import { createForm, onFieldValidateEnd } from '@formily/core' 507 | import { ActionResponse } from './ActionResponse' 508 | 509 | export default () => { 510 | const [response, setResponse] = useState('') 511 | const form = useMemo( 512 | () => 513 | createForm({ 514 | effects() { 515 | onFieldValidateEnd('target', () => { 516 | setResponse('target verification is over') 517 | }) 518 | }, 519 | }), 520 | [] 521 | ) 522 | return ( 523 | <ActionResponse response={response}> 524 | <button 525 | onClick={() => { 526 | const field = form.createField({ name: 'target', required: true }) 527 | field.onInput('') 528 | }} 529 | > 530 | Trigger verification 531 | </button> 532 | </ActionResponse> 533 | ) 534 | } 535 | ``` 536 | 537 | ## onFieldValidateFailed 538 | 539 | #### Description 540 | 541 | Listen to the side-effect hook of a field verification trigger failure 542 | 543 | #### Signature 544 | 545 | ```ts 546 | interface onFieldValidateFailed { 547 | (pattern: FormPathPattern, callback: (field: Field, form: Form) => void) 548 | } 549 | ``` 550 | 551 | #### Example 552 | 553 | ```tsx 554 | import React, { useMemo, useState } from 'react' 555 | import { createForm, onFieldValidateFailed } from '@formily/core' 556 | import { ActionResponse } from './ActionResponse' 557 | 558 | export default () => { 559 | const [response, setResponse] = useState('') 560 | const form = useMemo( 561 | () => 562 | createForm({ 563 | effects() { 564 | onFieldValidateFailed('target', () => { 565 | setResponse('target verification failed') 566 | }) 567 | }, 568 | }), 569 | [] 570 | ) 571 | return ( 572 | <ActionResponse response={response}> 573 | <button 574 | onClick={() => { 575 | const field = form.createField({ name: 'target', required: true }) 576 | field.onInput('') 577 | }} 578 | > 579 | Trigger verification 580 | </button> 581 | </ActionResponse> 582 | ) 583 | } 584 | ``` 585 | 586 | ## onFieldValidateSuccess 587 | 588 | #### Description 589 | 590 | Monitor the side effect hook that triggers a successful verification of a certain field 591 | 592 | #### Signature 593 | 594 | ```ts 595 | interface onFieldValidateSuccess { 596 | (pattern: FormPathPattern, callback: (field: Field, form: Form) => void) 597 | } 598 | ``` 599 | 600 | #### Example 601 | 602 | ```tsx 603 | import React, { useMemo, useState } from 'react' 604 | import { 605 | createForm, 606 | onFieldValidateFailed, 607 | onFieldValidateSuccess, 608 | } from '@formily/core' 609 | import { ActionResponse } from './ActionResponse' 610 | 611 | export default () => { 612 | const [response, setResponse] = useState('') 613 | const form = useMemo( 614 | () => 615 | createForm({ 616 | effects() { 617 | onFieldValidateFailed('target', () => { 618 | setResponse('target verification failed') 619 | }) 620 | onFieldValidateSuccess('target', () => { 621 | setResponse('target verification succeeded') 622 | }) 623 | }, 624 | }), 625 | [] 626 | ) 627 | return ( 628 | <ActionResponse response={response}> 629 | <button 630 | onClick={() => { 631 | const field = form.createField({ name: 'target', required: true }) 632 | field.onInput('') 633 | }} 634 | > 635 | Trigger failed 636 | </button> 637 | <button 638 | onClick={() => { 639 | const field = form.createField({ name: 'target', required: true }) 640 | field.onInput('123') 641 | }} 642 | > 643 | Triggered successfully 644 | </button> 645 | </ActionResponse> 646 | ) 647 | } 648 | ``` 649 | ```