This is page 28 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/element/src/form-dialog/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { createForm, Form, IFormProps } from '@formily/core' 2 | import { toJS } from '@formily/reactive' 3 | import { observer } from '@formily/reactive-vue' 4 | import { 5 | applyMiddleware, 6 | IMiddleware, 7 | isBool, 8 | isFn, 9 | isNum, 10 | isStr, 11 | } from '@formily/shared' 12 | import { FormProvider, Fragment, h } from '@formily/vue' 13 | import type { Button as ButtonProps, Dialog as DialogProps } from 'element-ui' 14 | import { Button, Dialog } from 'element-ui' 15 | import { t } from 'element-ui/src/locale' 16 | import { Portal, PortalTarget } from 'portal-vue' 17 | import Vue, { Component, VNode } from 'vue' 18 | import { defineComponent } from 'vue-demi' 19 | import { stylePrefix } from '../__builtins__/configs' 20 | import { 21 | createPortalProvider, 22 | getProtalContext, 23 | isValidElement, 24 | loading, 25 | resolveComponent, 26 | } from '../__builtins__/shared' 27 | 28 | type FormDialogContentProps = { form: Form } 29 | 30 | type FormDialogContent = Component | ((props: FormDialogContentProps) => VNode) 31 | 32 | type DialogTitle = string | number | Component | VNode | (() => VNode) 33 | 34 | type IFormDialogProps = Omit<DialogProps, 'title'> & { 35 | title?: DialogTitle 36 | footer?: null | Component | VNode | (() => VNode) 37 | cancelText?: string | Component | VNode | (() => VNode) 38 | cancelButtonProps?: ButtonProps 39 | okText?: string | Component | VNode | (() => VNode) 40 | okButtonProps?: ButtonProps 41 | onOpen?: () => void 42 | onOpened?: () => void 43 | onClose?: () => void 44 | onClosed?: () => void 45 | onCancel?: () => void 46 | onOK?: () => void 47 | loadingText?: string 48 | } 49 | 50 | const PORTAL_TARGET_NAME = 'FormDialogFooter' 51 | 52 | const isDialogTitle = (props: any): props is DialogTitle => { 53 | return isNum(props) || isStr(props) || isBool(props) || isValidElement(props) 54 | } 55 | 56 | const getDialogProps = (props: any): IFormDialogProps => { 57 | if (isDialogTitle(props)) { 58 | return { 59 | title: props, 60 | } as IFormDialogProps 61 | } else { 62 | return props 63 | } 64 | } 65 | 66 | export interface IFormDialog { 67 | forOpen(middleware: IMiddleware<IFormProps>): IFormDialog 68 | forConfirm(middleware: IMiddleware<Form>): IFormDialog 69 | forCancel(middleware: IMiddleware<Form>): IFormDialog 70 | open(props?: IFormProps): Promise<any> 71 | close(): void 72 | } 73 | 74 | export interface IFormDialogComponentProps { 75 | content: FormDialogContent 76 | resolve: () => any 77 | reject: () => any 78 | } 79 | 80 | export function FormDialog( 81 | title: IFormDialogProps | DialogTitle, 82 | content: FormDialogContent 83 | ): IFormDialog 84 | 85 | export function FormDialog( 86 | title: IFormDialogProps | DialogTitle, 87 | id: string | symbol, 88 | content: FormDialogContent 89 | ): IFormDialog 90 | 91 | export function FormDialog( 92 | title: DialogTitle, 93 | id: string, 94 | content: FormDialogContent 95 | ): IFormDialog 96 | 97 | export function FormDialog( 98 | title: IFormDialogProps | DialogTitle, 99 | id: string | symbol | FormDialogContent, 100 | content?: FormDialogContent 101 | ): IFormDialog { 102 | if (isFn(id) || isValidElement(id)) { 103 | content = id as FormDialogContent 104 | id = 'form-dialog' 105 | } 106 | 107 | const prefixCls = `${stylePrefix}-form-dialog` 108 | const env = { 109 | root: document.createElement('div'), 110 | form: null, 111 | promise: null, 112 | instance: null, 113 | openMiddlewares: [], 114 | confirmMiddlewares: [], 115 | cancelMiddlewares: [], 116 | } 117 | 118 | document.body.appendChild(env.root) 119 | 120 | const props = getDialogProps(title) 121 | const dialogProps = { 122 | ...props, 123 | onClosed: () => { 124 | props.onClosed?.() 125 | env.instance.$destroy() 126 | env.instance = null 127 | env.root?.parentNode?.removeChild(env.root) 128 | env.root = undefined 129 | }, 130 | } 131 | 132 | const component = observer( 133 | defineComponent({ 134 | setup() { 135 | return () => 136 | h( 137 | Fragment, 138 | {}, 139 | { 140 | default: () => 141 | resolveComponent(content, { 142 | form: env.form, 143 | }), 144 | } 145 | ) 146 | }, 147 | }) 148 | ) 149 | 150 | const render = (visible = true, resolve?: () => any, reject?: () => any) => { 151 | if (!env.instance) { 152 | const ComponentConstructor = observer( 153 | Vue.extend({ 154 | props: ['dialogProps'], 155 | data() { 156 | return { 157 | visible: false, 158 | } 159 | }, 160 | render() { 161 | const { 162 | onClose, 163 | onClosed, 164 | onOpen, 165 | onOpened, 166 | onOK, 167 | onCancel, 168 | title, 169 | footer, 170 | okText, 171 | cancelText, 172 | okButtonProps, 173 | cancelButtonProps, 174 | ...dialogProps 175 | } = this.dialogProps 176 | 177 | return h( 178 | FormProvider, 179 | { 180 | props: { 181 | form: env.form, 182 | }, 183 | }, 184 | { 185 | default: () => 186 | h( 187 | Dialog, 188 | { 189 | class: [`${prefixCls}`], 190 | attrs: { 191 | visible: this.visible, 192 | ...dialogProps, 193 | }, 194 | on: { 195 | 'update:visible': (val) => { 196 | this.visible = val 197 | }, 198 | close: () => { 199 | onClose?.() 200 | }, 201 | 202 | closed: () => { 203 | onClosed?.() 204 | }, 205 | open: () => { 206 | onOpen?.() 207 | }, 208 | opened: () => { 209 | onOpened?.() 210 | }, 211 | }, 212 | }, 213 | { 214 | default: () => [h(component, {}, {})], 215 | title: () => 216 | h( 217 | 'div', 218 | {}, 219 | { default: () => resolveComponent(title) } 220 | ), 221 | footer: () => 222 | h( 223 | 'div', 224 | {}, 225 | { 226 | default: () => { 227 | const FooterProtalTarget = h( 228 | PortalTarget, 229 | { 230 | props: { 231 | name: PORTAL_TARGET_NAME, 232 | slim: true, 233 | }, 234 | }, 235 | {} 236 | ) 237 | if (footer === null) { 238 | return [null, FooterProtalTarget] 239 | } else if (footer) { 240 | return [ 241 | resolveComponent(footer), 242 | FooterProtalTarget, 243 | ] 244 | } 245 | 246 | return [ 247 | h( 248 | Button, 249 | { 250 | attrs: { 251 | ...cancelButtonProps 252 | }, 253 | on: { 254 | click: (e) => { 255 | onCancel?.(e) 256 | reject() 257 | }, 258 | }, 259 | }, 260 | { 261 | default: () => 262 | resolveComponent( 263 | cancelText || 264 | t('el.popconfirm.cancelButtonText') 265 | ), 266 | } 267 | ), 268 | 269 | h( 270 | Button, 271 | { 272 | attrs: { 273 | type: 'primary', 274 | ...okButtonProps, 275 | loading: env.form.submitting, 276 | }, 277 | on: { 278 | click: (e) => { 279 | onOK?.(e) 280 | resolve() 281 | }, 282 | }, 283 | }, 284 | { 285 | default: () => 286 | resolveComponent( 287 | okText || 288 | t('el.popconfirm.confirmButtonText') 289 | ), 290 | } 291 | ), 292 | FooterProtalTarget, 293 | ] 294 | }, 295 | } 296 | ), 297 | } 298 | ), 299 | } 300 | ) 301 | }, 302 | }) 303 | ) 304 | env.instance = new ComponentConstructor({ 305 | propsData: { 306 | dialogProps, 307 | }, 308 | parent: getProtalContext(id as string | symbol), 309 | }) 310 | env.instance.$mount(env.root) 311 | env.root = env.instance.$el 312 | } 313 | 314 | env.instance.visible = visible 315 | } 316 | 317 | const formDialog = { 318 | forOpen: (middleware: IMiddleware<IFormProps>) => { 319 | if (isFn(middleware)) { 320 | env.openMiddlewares.push(middleware) 321 | } 322 | return formDialog 323 | }, 324 | forConfirm: (middleware: IMiddleware<Form>) => { 325 | if (isFn(middleware)) { 326 | env.confirmMiddlewares.push(middleware) 327 | } 328 | return formDialog 329 | }, 330 | forCancel: (middleware: IMiddleware<Form>) => { 331 | if (isFn(middleware)) { 332 | env.cancelMiddlewares.push(middleware) 333 | } 334 | return formDialog 335 | }, 336 | open: (props: IFormProps) => { 337 | if (env.promise) return env.promise 338 | 339 | env.promise = new Promise(async (resolve, reject) => { 340 | try { 341 | props = await loading(dialogProps.loadingText, () => 342 | applyMiddleware(props, env.openMiddlewares) 343 | ) 344 | env.form = env.form || createForm(props) 345 | } catch (e) { 346 | reject(e) 347 | } 348 | 349 | render( 350 | true, 351 | () => { 352 | env.form 353 | .submit(async () => { 354 | await applyMiddleware(env.form, env.confirmMiddlewares) 355 | resolve(toJS(env.form.values)) 356 | if (dialogProps.beforeClose) { 357 | setTimeout(() => { 358 | dialogProps.beforeClose(() => { 359 | formDialog.close() 360 | }) 361 | }) 362 | } else { 363 | formDialog.close() 364 | } 365 | }) 366 | .catch(() => {}) 367 | }, 368 | async () => { 369 | await loading(dialogProps.loadingText, () => 370 | applyMiddleware(env.form, env.cancelMiddlewares) 371 | ) 372 | 373 | if (dialogProps.beforeClose) { 374 | dialogProps.beforeClose(() => { 375 | formDialog.close() 376 | }) 377 | } else { 378 | formDialog.close() 379 | } 380 | } 381 | ) 382 | }) 383 | return env.promise 384 | }, 385 | close: () => { 386 | if (!env.root) return 387 | render(false) 388 | }, 389 | } 390 | return formDialog 391 | } 392 | 393 | const FormDialogFooter = defineComponent({ 394 | name: 'FFormDialogFooter', 395 | setup(props, { slots }) { 396 | return () => { 397 | return h( 398 | Portal, 399 | { 400 | props: { 401 | to: PORTAL_TARGET_NAME, 402 | }, 403 | }, 404 | slots 405 | ) 406 | } 407 | }, 408 | }) 409 | 410 | FormDialog.Footer = FormDialogFooter 411 | FormDialog.Portal = createPortalProvider('form-dialog') 412 | 413 | export default FormDialog 414 | ``` -------------------------------------------------------------------------------- /docs/guide/issue-helper.md: -------------------------------------------------------------------------------- ```markdown 1 | # Issue Helper 2 | 3 | ## Before You Start... 4 | 5 | The issue list is reserved exclusively for bug reports and feature requests. That means we do not accept usage questions. If you open an issue that does not conform to the requirements, it will be closed immediately. 6 | 7 | For usage questions, please use the following resources: 8 | 9 | - Read the introduce and components documentation 10 | - Make sure you have search your question in FAQ and changelog 11 | - Look for / ask questions on [Discussions](https://github.com/alibaba/formily/discussions) 12 | 13 | Also try to search for your issue 14 | 15 | it may have already been answered or even fixed in the development branch. However, if you find that an old, closed issue still persists in the latest version, you should open a new issue using the form below instead of commenting on the old issue. 16 | 17 | ```tsx 18 | import React from 'react' 19 | import { createForm, onFieldMount, onFieldReact } from '@formily/core' 20 | import { Field, VoidField } from '@formily/react' 21 | import { 22 | Form, 23 | Input, 24 | Select, 25 | Radio, 26 | FormItem, 27 | FormButtonGroup, 28 | Submit, 29 | } from '@formily/antd' 30 | import semver from 'semver' 31 | import ReactMde from 'react-mde' 32 | import * as Showdown from 'showdown' 33 | import 'react-mde/lib/styles/css/react-mde-all.css' 34 | 35 | const converter = new Showdown.Converter({ 36 | tables: true, 37 | simplifiedAutoLink: true, 38 | strikethrough: true, 39 | tasklists: true, 40 | }) 41 | 42 | const MdInput = ({ value, onChange }) => { 43 | const [selectedTab, setSelectedTab] = React.useState('write') 44 | return ( 45 | <div style={{ fontSize: 12, lineHeight: 1 }}> 46 | <ReactMde 47 | value={value} 48 | onChange={onChange} 49 | selectedTab={selectedTab} 50 | onTabChange={setSelectedTab} 51 | generateMarkdownPreview={(markdown) => 52 | Promise.resolve( 53 | `<div class="markdown" style="margin:0 20px;">${ 54 | converter.makeHtml(markdown) || '' 55 | }</div>` 56 | ) 57 | } 58 | /> 59 | </div> 60 | ) 61 | } 62 | 63 | const form = createForm({ 64 | validateFirst: true, 65 | effects() { 66 | onFieldMount('version', async (field) => { 67 | const { versions: unsort } = await fetch( 68 | 'https://registry.npmmirror.com/@formily/core' 69 | ).then((res) => res.json()) 70 | 71 | const versions = Object.keys(unsort).sort((v1, v2) => 72 | semver.gte(v1, v2) ? -1 : 1 73 | ) 74 | field.dataSource = versions.map((version) => ({ 75 | label: version, 76 | value: version, 77 | })) 78 | }) 79 | onFieldMount('package', async (field) => { 80 | const packages = await fetch( 81 | 'https://formilyjs.org/.netlify/functions/npm-search?q=@formily' 82 | ).then((res) => res.json()) 83 | field.dataSource = packages.map(({ name }) => { 84 | return { 85 | label: name, 86 | value: name, 87 | } 88 | }) 89 | }) 90 | onFieldReact('bug-desc', (field) => { 91 | field.visible = field.query('type').value() === 'Bug Report' 92 | }) 93 | onFieldReact('feature-desc', (field) => { 94 | field.visible = field.query('type').value() === 'Feature Request' 95 | }) 96 | }, 97 | }) 98 | 99 | const createIssueURL = ({ 100 | type, 101 | title, 102 | version, 103 | package: pkg, 104 | reproduceLink, 105 | reproduceStep, 106 | expected, 107 | actually, 108 | comment, 109 | feature, 110 | api, 111 | }) => { 112 | const url = new URL('https://github.com/alibaba/formily/issues/new') 113 | 114 | const bugInfo = ` 115 | - [ ] I have searched the [issues](https://github.com/alibaba/formily/issues) of this repository and believe that this is not a duplicate. 116 | 117 | ### Reproduction link 118 | [](${ 119 | reproduceLink || '' 120 | }) 121 | 122 | ### Steps to reproduce 123 | ${reproduceStep || ''} 124 | 125 | ### What is expected? 126 | ${expected || ''} 127 | 128 | ### What is actually happening? 129 | ${actually || ''} 130 | 131 | ### Package 132 | ${pkg}@${version} 133 | 134 | --- 135 | 136 | ${comment || ''} 137 | 138 | <!-- generated by formily-issue-helper. DO NOT REMOVE --> 139 | ` 140 | 141 | const prInfo = ` 142 | - [ ] I have searched the [issues](https://github.com/alibaba/formily/issues) of this repository and believe that this is not a duplicate. 143 | 144 | ### What problem does this feature solve? 145 | ${feature || ''} 146 | 147 | ### What does the proposed API look like? 148 | ${api || ''} 149 | 150 | 151 | <!-- generated by formily-issue-helper. DO NOT REMOVE --> 152 | ` 153 | 154 | url.searchParams.set('title', `[${type}] ${title}`) 155 | url.searchParams.set('body', type === 'Bug Report' ? bugInfo : prInfo) 156 | 157 | return url.href 158 | } 159 | 160 | export default () => { 161 | return ( 162 | <Form form={form} layout="vertical" size="large"> 163 | <Field 164 | title="This is a" 165 | name="type" 166 | required 167 | initialValue="Bug Report" 168 | decorator={[FormItem]} 169 | component={[Radio.Group, { optionType: 'button' }]} 170 | dataSource={[ 171 | { label: 'Bug Report', value: 'Bug Report' }, 172 | { label: 'Feature Request', value: 'Feature Request' }, 173 | ]} 174 | /> 175 | <Field 176 | title="Title" 177 | name="title" 178 | required 179 | decorator={[FormItem]} 180 | component={[Input]} 181 | /> 182 | <VoidField name="bug-desc"> 183 | <Field 184 | title="Package" 185 | name="package" 186 | required 187 | decorator={[FormItem]} 188 | component={[Select, { showSearch: true }]} 189 | /> 190 | <Field 191 | title="Version" 192 | description="Check if the issue is reproducible with the latest stable version." 193 | name="version" 194 | required 195 | decorator={[FormItem]} 196 | component={[Select, { showSearch: true }]} 197 | /> 198 | 199 | <Field 200 | title="Link to minimal reproduction" 201 | name="reproduceLink" 202 | decorator={[FormItem]} 203 | component={[Input]} 204 | required 205 | validator={[ 206 | 'url', 207 | (value) => { 208 | return /\/\/(codesandbox\.io|github)/.test(value) 209 | ? '' 210 | : 'Must Be Codesandbox Link or Github Repo' 211 | }, 212 | ]} 213 | description={ 214 | <div> 215 | This is Codesandbox templates.If you are: 216 | <ul> 217 | <li> 218 | React + Antd User: 219 | <ul> 220 | <li> 221 | <a 222 | href="https://codesandbox.io/s/formily-react-antd-pure-jsx-omncis" 223 | target="_blank" 224 | rel="noreferrer" 225 | > 226 | Pure JSX 227 | </a> 228 | </li> 229 | <li> 230 | <a 231 | href="https://codesandbox.io/s/formily-react-antd-markup-schema-fvpevx" 232 | target="_blank" 233 | rel="noreferrer" 234 | > 235 | Markup Schema 236 | </a> 237 | </li> 238 | <li> 239 | <a 240 | href="https://codesandbox.io/s/formily-react-antd-json-schema-28p0fh" 241 | target="_blank" 242 | rel="noreferrer" 243 | > 244 | JSON Schema 245 | </a> 246 | </li> 247 | </ul> 248 | </li> 249 | <li> 250 | React + Fusion User: 251 | <ul> 252 | <li> 253 | <a 254 | href="https://codesandbox.io/s/formily-react-next-pure-jsx-ji9iiu" 255 | target="_blank" 256 | rel="noreferrer" 257 | > 258 | Pure JSX 259 | </a> 260 | </li> 261 | <li> 262 | <a 263 | href="https://codesandbox.io/s/formily-react-next-markup-schema-i7dm17" 264 | target="_blank" 265 | rel="noreferrer" 266 | > 267 | Markup Schema 268 | </a> 269 | </li> 270 | <li> 271 | <a 272 | href="https://codesandbox.io/s/formily-react-next-json-schema-1lm35h" 273 | target="_blank" 274 | rel="noreferrer" 275 | > 276 | JSON Schema 277 | </a> 278 | </li> 279 | </ul> 280 | </li> 281 | <li> 282 | Vue3 + ant-design-vue User: 283 | <ul> 284 | <li> 285 | <a 286 | href="https://codesandbox.io/s/formily-antd-vue-pure-jsx-pp3gvv" 287 | target="_blank" 288 | rel="noreferrer" 289 | > 290 | Pure JSX 291 | </a> 292 | </li> 293 | <li> 294 | <a 295 | href="https://codesandbox.io/s/formily-vue-ant-design-vue-markup-schema-donivp" 296 | target="_blank" 297 | rel="noreferrer" 298 | > 299 | Markup Schema 300 | </a> 301 | </li> 302 | <li> 303 | <a 304 | href="https://codesandbox.io/s/formily-vue-ant-design-vue-json-schema-25g4z1" 305 | target="_blank" 306 | rel="noreferrer" 307 | > 308 | JSON Schema 309 | </a> 310 | </li> 311 | </ul> 312 | </li> 313 | </ul> 314 | </div> 315 | } 316 | /> 317 | <Field 318 | title="Step to reproduce" 319 | description="Clear and concise reproduction instructions are important for us to be able to triage your issue in a timely manner. Note that you can use Markdown to format lists and code." 320 | name="reproduceStep" 321 | decorator={[FormItem]} 322 | component={[MdInput]} 323 | required 324 | /> 325 | <Field 326 | title="What is expected?" 327 | name="expected" 328 | decorator={[FormItem]} 329 | component={[MdInput]} 330 | required 331 | /> 332 | <Field 333 | title="What is actually happening?" 334 | name="actually" 335 | decorator={[FormItem]} 336 | component={[MdInput]} 337 | required 338 | /> 339 | <Field 340 | title="Any additional comments? (optional)" 341 | name="comment" 342 | decorator={[FormItem]} 343 | component={[MdInput]} 344 | /> 345 | </VoidField> 346 | <VoidField name="feature-desc"> 347 | <Field 348 | title="What problem does this feature solve?" 349 | description={ 350 | <div> 351 | <p> 352 | Explain your use case, context, and rationale behind this 353 | feature request. More importantly, what is the end user 354 | experience you are trying to build that led to the need for this 355 | feature? 356 | </p> 357 | <p> 358 | An important design goal of Formily is keeping the API surface 359 | small and straightforward. In general, we only consider adding 360 | new features that solve a problem that cannot be easily dealt 361 | with using existing APIs (i.e. not just an alternative way of 362 | doing things that can already be done). The problem should also 363 | be common enough to justify the addition. 364 | </p> 365 | </div> 366 | } 367 | name="feature" 368 | required 369 | decorator={[FormItem]} 370 | component={[MdInput]} 371 | /> 372 | 373 | <Field 374 | title="What does the proposed API look like?" 375 | description="Describe how you propose to solve the problem and provide code samples of how the API would work once implemented." 376 | name="api" 377 | required 378 | decorator={[FormItem]} 379 | component={[MdInput]} 380 | /> 381 | </VoidField> 382 | <FormButtonGroup.Sticky align="center"> 383 | <Submit 384 | size="large" 385 | onSubmit={(values) => { 386 | window.open(createIssueURL(values)) 387 | }} 388 | > 389 | Submit 390 | </Submit> 391 | </FormButtonGroup.Sticky> 392 | </Form> 393 | ) 394 | } 395 | ``` 396 | ``` -------------------------------------------------------------------------------- /packages/reactive/src/__tests__/batch.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { observable, batch, autorun, reaction } from '..' 2 | import { define } from '../model' 3 | 4 | describe('normal batch', () => { 5 | test('no batch', () => { 6 | const obs = observable({ 7 | aa: { 8 | bb: 123, 9 | }, 10 | }) 11 | const handler = jest.fn() 12 | autorun(() => { 13 | handler(obs.aa.bb) 14 | }) 15 | obs.aa.bb = 111 16 | obs.aa.bb = 222 17 | expect(handler).toBeCalledTimes(3) 18 | 19 | obs.aa.bb = 333 20 | obs.aa.bb = 444 21 | 22 | expect(handler).toBeCalledTimes(5) 23 | }) 24 | 25 | test('batch', () => { 26 | const obs = observable({ 27 | aa: { 28 | bb: 123, 29 | }, 30 | }) 31 | const handler = jest.fn() 32 | autorun(() => { 33 | handler(obs.aa.bb) 34 | }) 35 | obs.aa.bb = 111 36 | obs.aa.bb = 222 37 | expect(handler).toBeCalledTimes(3) 38 | expect(handler).lastCalledWith(222) 39 | batch(() => { 40 | obs.aa.bb = 333 41 | obs.aa.bb = 444 42 | }) 43 | batch(() => {}) 44 | batch() 45 | expect(handler).toBeCalledTimes(4) 46 | expect(handler).lastCalledWith(444) 47 | }) 48 | 49 | test('batch track', () => { 50 | const obs = observable({ 51 | aa: { 52 | bb: 123, 53 | }, 54 | cc: 1, 55 | }) 56 | const handler = jest.fn() 57 | autorun(() => { 58 | batch(() => { 59 | if (obs.cc > 0) { 60 | handler(obs.aa.bb) 61 | obs.cc = obs.cc + 20 62 | } 63 | }) 64 | }) 65 | expect(handler).toBeCalledTimes(1) 66 | expect(obs.cc).toEqual(21) 67 | obs.aa.bb = 321 68 | expect(handler).toBeCalledTimes(2) 69 | expect(obs.cc).toEqual(41) 70 | }) 71 | 72 | test('batch.bound', () => { 73 | const obs = observable({ 74 | aa: { 75 | bb: 123, 76 | }, 77 | }) 78 | const handler = jest.fn() 79 | const setData = batch.bound(() => { 80 | obs.aa.bb = 333 81 | obs.aa.bb = 444 82 | }) 83 | autorun(() => { 84 | handler(obs.aa.bb) 85 | }) 86 | obs.aa.bb = 111 87 | obs.aa.bb = 222 88 | expect(handler).toBeCalledTimes(3) 89 | expect(handler).lastCalledWith(222) 90 | setData() 91 | batch(() => {}) 92 | expect(handler).toBeCalledTimes(4) 93 | expect(handler).lastCalledWith(444) 94 | }) 95 | 96 | test('batch.bound track', () => { 97 | const obs = observable({ 98 | aa: { 99 | bb: 123, 100 | }, 101 | cc: 1, 102 | }) 103 | const handler = jest.fn() 104 | autorun(() => { 105 | batch.bound(() => { 106 | if (obs.cc > 0) { 107 | handler(obs.aa.bb) 108 | obs.cc = obs.cc + 20 109 | } 110 | })() 111 | }) 112 | expect(handler).toBeCalledTimes(1) 113 | expect(obs.cc).toEqual(21) 114 | obs.aa.bb = 321 115 | expect(handler).toBeCalledTimes(2) 116 | expect(obs.cc).toEqual(41) 117 | }) 118 | 119 | test('batch.scope', () => { 120 | const obs = observable<any>({}) 121 | 122 | const handler = jest.fn() 123 | 124 | autorun(() => { 125 | handler(obs.aa, obs.bb, obs.cc, obs.dd) 126 | }) 127 | 128 | batch(() => { 129 | batch.scope(() => { 130 | obs.aa = 123 131 | }) 132 | batch.scope(() => { 133 | obs.cc = 'ccccc' 134 | }) 135 | obs.bb = 321 136 | obs.dd = 'ddddd' 137 | }) 138 | 139 | expect(handler).toBeCalledTimes(4) 140 | expect(handler).nthCalledWith(1, undefined, undefined, undefined, undefined) 141 | expect(handler).nthCalledWith(2, 123, undefined, undefined, undefined) 142 | expect(handler).nthCalledWith(3, 123, undefined, 'ccccc', undefined) 143 | expect(handler).nthCalledWith(4, 123, 321, 'ccccc', 'ddddd') 144 | }) 145 | 146 | test('batch.scope bound', () => { 147 | const obs = observable<any>({}) 148 | 149 | const handler = jest.fn() 150 | 151 | autorun(() => { 152 | handler(obs.aa, obs.bb, obs.cc, obs.dd) 153 | }) 154 | 155 | const scope1 = batch.scope.bound(() => { 156 | obs.aa = 123 157 | }) 158 | batch(() => { 159 | scope1() 160 | batch.scope.bound(() => { 161 | obs.cc = 'ccccc' 162 | })() 163 | obs.bb = 321 164 | obs.dd = 'ddddd' 165 | }) 166 | 167 | expect(handler).toBeCalledTimes(4) 168 | expect(handler).nthCalledWith(1, undefined, undefined, undefined, undefined) 169 | expect(handler).nthCalledWith(2, 123, undefined, undefined, undefined) 170 | expect(handler).nthCalledWith(3, 123, undefined, 'ccccc', undefined) 171 | expect(handler).nthCalledWith(4, 123, 321, 'ccccc', 'ddddd') 172 | }) 173 | 174 | test('batch.scope track', () => { 175 | const obs = observable({ 176 | aa: { 177 | bb: 123, 178 | }, 179 | cc: 1, 180 | }) 181 | const handler = jest.fn() 182 | autorun(() => { 183 | batch.scope(() => { 184 | if (obs.cc > 0) { 185 | handler(obs.aa.bb) 186 | obs.cc = obs.cc + 20 187 | } 188 | }) 189 | }) 190 | expect(handler).toBeCalledTimes(1) 191 | expect(obs.cc).toEqual(21) 192 | obs.aa.bb = 321 193 | expect(handler).toBeCalledTimes(2) 194 | expect(obs.cc).toEqual(41) 195 | }) 196 | 197 | test('batch.scope bound track', () => { 198 | const obs = observable({ 199 | aa: { 200 | bb: 123, 201 | }, 202 | cc: 1, 203 | }) 204 | const handler = jest.fn() 205 | autorun(() => { 206 | batch.scope.bound(() => { 207 | if (obs.cc > 0) { 208 | handler(obs.aa.bb) 209 | obs.cc = obs.cc + 20 210 | } 211 | })() 212 | }) 213 | expect(handler).toBeCalledTimes(1) 214 | expect(obs.cc).toEqual(21) 215 | obs.aa.bb = 321 216 | expect(handler).toBeCalledTimes(2) 217 | expect(obs.cc).toEqual(41) 218 | }) 219 | 220 | test('batch error', () => { 221 | let error = null 222 | try { 223 | batch(() => { 224 | throw '123' 225 | }) 226 | } catch (e) { 227 | error = e 228 | } 229 | expect(error).toEqual('123') 230 | }) 231 | }) 232 | 233 | describe('annotation batch', () => { 234 | test('batch', () => { 235 | const obs = define( 236 | { 237 | aa: { 238 | bb: 123, 239 | }, 240 | setData() { 241 | this.aa.bb = 333 242 | this.aa.bb = 444 243 | }, 244 | }, 245 | { 246 | aa: observable, 247 | setData: batch, 248 | } 249 | ) 250 | const handler = jest.fn() 251 | autorun(() => { 252 | handler(obs.aa.bb) 253 | }) 254 | obs.aa.bb = 111 255 | obs.aa.bb = 222 256 | expect(handler).toBeCalledTimes(3) 257 | expect(handler).lastCalledWith(222) 258 | obs.setData() 259 | expect(handler).toBeCalledTimes(4) 260 | expect(handler).lastCalledWith(444) 261 | }) 262 | 263 | test('batch track', () => { 264 | const obs = define( 265 | { 266 | aa: { 267 | bb: 123, 268 | }, 269 | cc: 1, 270 | setData() { 271 | if (obs.cc > 0) { 272 | handler(obs.aa.bb) 273 | obs.cc = obs.cc + 20 274 | } 275 | }, 276 | }, 277 | { 278 | aa: observable, 279 | setData: batch, 280 | } 281 | ) 282 | const handler = jest.fn() 283 | autorun(() => { 284 | obs.setData() 285 | }) 286 | expect(handler).toBeCalledTimes(1) 287 | expect(obs.cc).toEqual(21) 288 | obs.aa.bb = 321 289 | expect(handler).toBeCalledTimes(2) 290 | expect(obs.cc).toEqual(41) 291 | }) 292 | 293 | test('batch.bound', () => { 294 | const obs = define( 295 | { 296 | aa: { 297 | bb: 123, 298 | }, 299 | setData() { 300 | this.aa.bb = 333 301 | this.aa.bb = 444 302 | }, 303 | }, 304 | { 305 | aa: observable, 306 | setData: batch.bound, 307 | } 308 | ) 309 | const handler = jest.fn() 310 | autorun(() => { 311 | handler(obs.aa.bb) 312 | }) 313 | obs.aa.bb = 111 314 | obs.aa.bb = 222 315 | expect(handler).toBeCalledTimes(3) 316 | expect(handler).lastCalledWith(222) 317 | obs.setData() 318 | expect(handler).toBeCalledTimes(4) 319 | expect(handler).lastCalledWith(444) 320 | }) 321 | 322 | test('batch.bound track', () => { 323 | const obs = define( 324 | { 325 | aa: { 326 | bb: 123, 327 | }, 328 | cc: 1, 329 | setData() { 330 | if (obs.cc > 0) { 331 | handler(obs.aa.bb) 332 | obs.cc = obs.cc + 20 333 | } 334 | }, 335 | }, 336 | { 337 | aa: observable, 338 | setData: batch.bound, 339 | } 340 | ) 341 | const handler = jest.fn() 342 | autorun(() => { 343 | obs.setData() 344 | }) 345 | expect(handler).toBeCalledTimes(1) 346 | expect(obs.cc).toEqual(21) 347 | obs.aa.bb = 321 348 | expect(handler).toBeCalledTimes(2) 349 | expect(obs.cc).toEqual(41) 350 | }) 351 | 352 | test('batch.scope', () => { 353 | const obs = define( 354 | { 355 | aa: null, 356 | bb: null, 357 | cc: null, 358 | dd: null, 359 | scope1() { 360 | this.aa = 123 361 | }, 362 | scope2() { 363 | this.cc = 'ccccc' 364 | }, 365 | }, 366 | { 367 | aa: observable, 368 | bb: observable, 369 | cc: observable, 370 | dd: observable, 371 | scope1: batch.scope, 372 | scope2: batch.scope, 373 | } 374 | ) 375 | 376 | const handler = jest.fn() 377 | 378 | autorun(() => { 379 | handler(obs.aa, obs.bb, obs.cc, obs.dd) 380 | }) 381 | 382 | batch(() => { 383 | obs.scope1() 384 | obs.scope2() 385 | obs.bb = 321 386 | obs.dd = 'ddddd' 387 | }) 388 | 389 | expect(handler).toBeCalledTimes(4) 390 | expect(handler).nthCalledWith(1, null, null, null, null) 391 | expect(handler).nthCalledWith(2, 123, null, null, null) 392 | expect(handler).nthCalledWith(3, 123, null, 'ccccc', null) 393 | expect(handler).nthCalledWith(4, 123, 321, 'ccccc', 'ddddd') 394 | }) 395 | 396 | test('batch.scope bound', () => { 397 | const obs = define( 398 | { 399 | aa: null, 400 | bb: null, 401 | cc: null, 402 | dd: null, 403 | scope1() { 404 | this.aa = 123 405 | }, 406 | scope2() { 407 | this.cc = 'ccccc' 408 | }, 409 | }, 410 | { 411 | aa: observable, 412 | bb: observable, 413 | cc: observable, 414 | dd: observable, 415 | scope1: batch.scope.bound, 416 | scope2: batch.scope.bound, 417 | } 418 | ) 419 | 420 | const handler = jest.fn() 421 | 422 | autorun(() => { 423 | handler(obs.aa, obs.bb, obs.cc, obs.dd) 424 | }) 425 | 426 | batch(() => { 427 | obs.scope1() 428 | obs.scope2() 429 | obs.bb = 321 430 | obs.dd = 'ddddd' 431 | }) 432 | 433 | expect(handler).toBeCalledTimes(4) 434 | expect(handler).nthCalledWith(1, null, null, null, null) 435 | expect(handler).nthCalledWith(2, 123, null, null, null) 436 | expect(handler).nthCalledWith(3, 123, null, 'ccccc', null) 437 | expect(handler).nthCalledWith(4, 123, 321, 'ccccc', 'ddddd') 438 | }) 439 | 440 | test('batch.scope track', () => { 441 | const obs = define( 442 | { 443 | aa: { 444 | bb: 123, 445 | }, 446 | cc: 1, 447 | scope() { 448 | if (this.cc > 0) { 449 | handler(this.aa.bb) 450 | this.cc = this.cc + 20 451 | } 452 | }, 453 | }, 454 | { 455 | aa: observable, 456 | cc: observable, 457 | scope: batch.scope, 458 | } 459 | ) 460 | const handler = jest.fn() 461 | autorun(() => { 462 | obs.scope() 463 | }) 464 | expect(handler).toBeCalledTimes(1) 465 | expect(obs.cc).toEqual(21) 466 | obs.aa.bb = 321 467 | expect(handler).toBeCalledTimes(2) 468 | expect(obs.cc).toEqual(41) 469 | }) 470 | 471 | test('batch.scope bound track', () => { 472 | const obs = define( 473 | { 474 | aa: { 475 | bb: 123, 476 | }, 477 | cc: 1, 478 | scope() { 479 | if (this.cc > 0) { 480 | handler(this.aa.bb) 481 | this.cc = this.cc + 20 482 | } 483 | }, 484 | }, 485 | { 486 | aa: observable, 487 | cc: observable, 488 | scope: batch.scope.bound, 489 | } 490 | ) 491 | const handler = jest.fn() 492 | autorun(() => { 493 | obs.scope() 494 | }) 495 | expect(handler).toBeCalledTimes(1) 496 | expect(obs.cc).toEqual(21) 497 | obs.aa.bb = 321 498 | expect(handler).toBeCalledTimes(2) 499 | expect(obs.cc).toEqual(41) 500 | }) 501 | }) 502 | 503 | describe('batch endpoint', () => { 504 | test('normal endpoint', () => { 505 | const tokens = [] 506 | const inner = batch.bound(() => { 507 | batch.endpoint(() => { 508 | tokens.push('endpoint') 509 | }) 510 | tokens.push('inner') 511 | }) 512 | const wrapper = batch.bound(() => { 513 | inner() 514 | tokens.push('wrapper') 515 | }) 516 | wrapper() 517 | expect(tokens).toEqual(['inner', 'wrapper', 'endpoint']) 518 | }) 519 | 520 | test('unexpect endpoint', () => { 521 | const tokens = [] 522 | const inner = batch.bound(() => { 523 | batch.endpoint() 524 | tokens.push('inner') 525 | }) 526 | const wrapper = batch.bound(() => { 527 | inner() 528 | tokens.push('wrapper') 529 | }) 530 | wrapper() 531 | expect(tokens).toEqual(['inner', 'wrapper']) 532 | }) 533 | 534 | test('no wrapper endpoint', () => { 535 | const tokens = [] 536 | batch.endpoint(() => { 537 | tokens.push('endpoint') 538 | }) 539 | expect(tokens).toEqual(['endpoint']) 540 | }) 541 | }) 542 | 543 | test('reaction collect in batch valid', () => { 544 | const obs = observable({ 545 | aa: 11, 546 | bb: 22, 547 | cc: 33, 548 | }) 549 | reaction( 550 | () => obs.aa, 551 | () => { 552 | void obs.cc 553 | } 554 | ) 555 | const fn = jest.fn() 556 | 557 | autorun(() => { 558 | batch.scope(() => { 559 | obs.aa = obs.bb 560 | }) 561 | fn() 562 | }) 563 | 564 | obs.bb = 44 565 | expect(fn).toBeCalledTimes(2) 566 | }) 567 | 568 | test('reaction collect in batch invalid', () => { 569 | const obs = observable({ 570 | aa: 11, 571 | bb: 22, 572 | cc: 33, 573 | }) 574 | reaction( 575 | () => obs.aa, 576 | () => { 577 | void obs.cc 578 | } 579 | ) 580 | const fn = jest.fn() 581 | 582 | autorun(() => { 583 | batch.scope(() => { 584 | obs.aa = obs.bb 585 | }) 586 | fn() 587 | }) 588 | 589 | obs.bb = 44 590 | obs.cc = 55 591 | expect(fn).toBeCalledTimes(3) 592 | }) 593 | ``` -------------------------------------------------------------------------------- /packages/antd/docs/components/Select.md: -------------------------------------------------------------------------------- ```markdown 1 | # Select 2 | 3 | > Drop-down box components 4 | 5 | ## Markup Schema synchronization data source case 6 | 7 | ```tsx 8 | import React from 'react' 9 | import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' 10 | import { createForm } from '@formily/core' 11 | import { FormProvider, createSchemaField } from '@formily/react' 12 | 13 | const SchemaField = createSchemaField({ 14 | components: { 15 | Select, 16 | FormItem, 17 | }, 18 | }) 19 | 20 | const form = createForm() 21 | 22 | export default () => ( 23 | <FormProvider form={form}> 24 | <SchemaField> 25 | <SchemaField.Number 26 | name="select" 27 | title="select box" 28 | x-decorator="FormItem" 29 | x-component="Select" 30 | enum={[ 31 | { label: 'Option 1', value: 1 }, 32 | { label: 'Option 2', value: 2 }, 33 | ]} 34 | x-component-props={{ 35 | style: { 36 | width: 120, 37 | }, 38 | }} 39 | /> 40 | </SchemaField> 41 | <FormButtonGroup> 42 | <Submit onSubmit={console.log}>Submit</Submit> 43 | </FormButtonGroup> 44 | </FormProvider> 45 | ) 46 | ``` 47 | 48 | ## Markup Schema Asynchronous Search Case 49 | 50 | ```tsx 51 | import React from 'react' 52 | import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' 53 | import { 54 | createForm, 55 | onFieldReact, 56 | onFieldInit, 57 | FormPathPattern, 58 | Field, 59 | } from '@formily/core' 60 | import { FormProvider, createSchemaField } from '@formily/react' 61 | import { action, observable } from '@formily/reactive' 62 | import { fetch } from 'mfetch' 63 | 64 | let timeout 65 | let currentValue 66 | 67 | function fetchData(value, callback) { 68 | if (timeout) { 69 | clearTimeout(timeout) 70 | timeout = null 71 | } 72 | currentValue = value 73 | 74 | function fake() { 75 | fetch(`https://suggest.taobao.com/sug?q=${value}`, { 76 | method: 'jsonp', 77 | }) 78 | .then((response) => response.json()) 79 | .then((d) => { 80 | if (currentValue === value) { 81 | const { result } = d 82 | const data = [] 83 | result.forEach((r) => { 84 | data.push({ 85 | value: r[0], 86 | text: r[0], 87 | }) 88 | }) 89 | callback(data) 90 | } 91 | }) 92 | } 93 | 94 | timeout = setTimeout(fake, 300) 95 | } 96 | 97 | const SchemaField = createSchemaField({ 98 | components: { 99 | Select, 100 | FormItem, 101 | }, 102 | }) 103 | 104 | const useAsyncDataSource = ( 105 | pattern: FormPathPattern, 106 | service: (param: { 107 | keyword: string 108 | field: Field 109 | }) => Promise<{ label: string; value: any }[]> 110 | ) => { 111 | const keyword = observable.ref('') 112 | 113 | onFieldInit(pattern, (field) => { 114 | field.setComponentProps({ 115 | onSearch: (value) => { 116 | keyword.value = value 117 | }, 118 | }) 119 | }) 120 | 121 | onFieldReact(pattern, (field) => { 122 | field.loading = true 123 | service({ field, keyword: keyword.value }).then( 124 | action.bound((data) => { 125 | field.dataSource = data 126 | field.loading = false 127 | }) 128 | ) 129 | }) 130 | } 131 | 132 | const form = createForm({ 133 | effects: () => { 134 | useAsyncDataSource('select', async ({ keyword }) => { 135 | if (!keyword) { 136 | return [] 137 | } 138 | return new Promise((resolve) => { 139 | fetchData(keyword, resolve) 140 | }) 141 | }) 142 | }, 143 | }) 144 | 145 | export default () => ( 146 | <FormProvider form={form}> 147 | <SchemaField> 148 | <SchemaField.String 149 | name="select" 150 | title="Asynchronous search select box" 151 | x-decorator="FormItem" 152 | x-component="Select" 153 | x-component-props={{ 154 | showSearch: true, 155 | filterOption: false, 156 | style: { 157 | width: 300, 158 | }, 159 | }} 160 | /> 161 | </SchemaField> 162 | <FormButtonGroup> 163 | <Submit onSubmit={console.log}>Submit</Submit> 164 | </FormButtonGroup> 165 | </FormProvider> 166 | ) 167 | ``` 168 | 169 | ## Markup Schema Asynchronous Linkage Data Source Case 170 | 171 | ```tsx 172 | import React from 'react' 173 | import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' 174 | import { createForm, onFieldReact, FormPathPattern, Field } from '@formily/core' 175 | import { FormProvider, createSchemaField } from '@formily/react' 176 | import { action } from '@formily/reactive' 177 | 178 | const SchemaField = createSchemaField({ 179 | components: { 180 | Select, 181 | FormItem, 182 | }, 183 | }) 184 | 185 | const useAsyncDataSource = ( 186 | pattern: FormPathPattern, 187 | service: (field: Field) => Promise<{ label: string; value: any }[]> 188 | ) => { 189 | onFieldReact(pattern, (field) => { 190 | field.loading = true 191 | service(field).then( 192 | action.bound((data) => { 193 | field.dataSource = data 194 | field.loading = false 195 | }) 196 | ) 197 | }) 198 | } 199 | 200 | const form = createForm({ 201 | effects: () => { 202 | useAsyncDataSource('select', async (field) => { 203 | const linkage = field.query('linkage').get('value') 204 | if (!linkage) return [] 205 | return new Promise((resolve) => { 206 | setTimeout(() => { 207 | if (linkage === 1) { 208 | resolve([ 209 | { 210 | label: 'AAA', 211 | value: 'aaa', 212 | }, 213 | { 214 | label: 'BBB', 215 | value: 'ccc', 216 | }, 217 | ]) 218 | } else if (linkage === 2) { 219 | resolve([ 220 | { 221 | label: 'CCC', 222 | value: 'ccc', 223 | }, 224 | { 225 | label: 'DDD', 226 | value: 'ddd', 227 | }, 228 | ]) 229 | } 230 | }, 1500) 231 | }) 232 | }) 233 | }, 234 | }) 235 | 236 | export default () => ( 237 | <FormProvider form={form}> 238 | <SchemaField> 239 | <SchemaField.Number 240 | name="linkage" 241 | title="Linkage selection box" 242 | x-decorator="FormItem" 243 | x-component="Select" 244 | enum={[ 245 | { label: 'Request 1', value: 1 }, 246 | { label: 'Request 2', value: 2 }, 247 | ]} 248 | x-component-props={{ 249 | style: { 250 | width: 120, 251 | }, 252 | }} 253 | /> 254 | <SchemaField.String 255 | name="select" 256 | title="Asynchronous select box" 257 | x-decorator="FormItem" 258 | x-component="Select" 259 | x-component-props={{ 260 | style: { 261 | width: 120, 262 | }, 263 | }} 264 | /> 265 | </SchemaField> 266 | <FormButtonGroup> 267 | <Submit onSubmit={console.log}>Submit</Submit> 268 | </FormButtonGroup> 269 | </FormProvider> 270 | ) 271 | ``` 272 | 273 | ## JSON Schema synchronization data source case 274 | 275 | ```tsx 276 | import React from 'react' 277 | import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' 278 | import { createForm } from '@formily/core' 279 | import { FormProvider, createSchemaField } from '@formily/react' 280 | 281 | const SchemaField = createSchemaField({ 282 | components: { 283 | Select, 284 | FormItem, 285 | }, 286 | }) 287 | 288 | const form = createForm() 289 | 290 | const schema = { 291 | type: 'object', 292 | properties: { 293 | select: { 294 | type: 'string', 295 | title: 'Select box', 296 | 'x-decorator': 'FormItem', 297 | 'x-component': 'Select', 298 | enum: [ 299 | { label: 'Option 1', value: 1 }, 300 | { label: 'Option 2', value: 2 }, 301 | ], 302 | 'x-component-props': { 303 | style: { 304 | width: 120, 305 | }, 306 | }, 307 | }, 308 | }, 309 | } 310 | 311 | export default () => ( 312 | <FormProvider form={form}> 313 | <SchemaField schema={schema} /> 314 | <FormButtonGroup> 315 | <Submit onSubmit={console.log}>Submit</Submit> 316 | </FormButtonGroup> 317 | </FormProvider> 318 | ) 319 | ``` 320 | 321 | ## JSON Schema asynchronous linkage data source case 322 | 323 | ```tsx 324 | import React from 'react' 325 | import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' 326 | import { createForm } from '@formily/core' 327 | import { FormProvider, createSchemaField } from '@formily/react' 328 | import { action } from '@formily/reactive' 329 | 330 | const SchemaField = createSchemaField({ 331 | components: { 332 | Select, 333 | FormItem, 334 | }, 335 | }) 336 | 337 | const loadData = async (field) => { 338 | const linkage = field.query('linkage').get('value') 339 | if (!linkage) return [] 340 | return new Promise((resolve) => { 341 | setTimeout(() => { 342 | if (linkage === 1) { 343 | resolve([ 344 | { 345 | label: 'AAA', 346 | value: 'aaa', 347 | }, 348 | { 349 | label: 'BBB', 350 | value: 'ccc', 351 | }, 352 | ]) 353 | } else if (linkage === 2) { 354 | resolve([ 355 | { 356 | label: 'CCC', 357 | value: 'ccc', 358 | }, 359 | { 360 | label: 'DDD', 361 | value: 'ddd', 362 | }, 363 | ]) 364 | } 365 | }, 1500) 366 | }) 367 | } 368 | 369 | const useAsyncDataSource = (service) => (field) => { 370 | field.loading = true 371 | service(field).then( 372 | action.bound((data) => { 373 | field.dataSource = data 374 | field.loading = false 375 | }) 376 | ) 377 | } 378 | 379 | const form = createForm() 380 | 381 | const schema = { 382 | type: 'object', 383 | properties: { 384 | linkage: { 385 | type: 'string', 386 | title: 'Linkage selection box', 387 | enum: [ 388 | { label: 'Request 1', value: 1 }, 389 | { label: 'Request 2', value: 2 }, 390 | ], 391 | 'x-decorator': 'FormItem', 392 | 'x-component': 'Select', 393 | 'x-component-props': { 394 | style: { 395 | width: 120, 396 | }, 397 | }, 398 | }, 399 | select: { 400 | type: 'string', 401 | title: 'Asynchronous selection box', 402 | 'x-decorator': 'FormItem', 403 | 'x-component': 'Select', 404 | 'x-component-props': { 405 | style: { 406 | width: 120, 407 | }, 408 | }, 409 | 'x-reactions': ['{{useAsyncDataSource(loadData)}}'], 410 | }, 411 | }, 412 | } 413 | 414 | export default () => ( 415 | <FormProvider form={form}> 416 | <SchemaField schema={schema} scope={{ useAsyncDataSource, loadData }} /> 417 | <FormButtonGroup> 418 | <Submit onSubmit={console.log}>Submit</Submit> 419 | </FormButtonGroup> 420 | </FormProvider> 421 | ) 422 | ``` 423 | 424 | ## Pure JSX synchronization data source case 425 | 426 | ```tsx 427 | import React from 'react' 428 | import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' 429 | import { createForm } from '@formily/core' 430 | import { FormProvider, Field } from '@formily/react' 431 | 432 | const form = createForm() 433 | 434 | export default () => ( 435 | <FormProvider form={form}> 436 | <Field 437 | name="select" 438 | title="select box" 439 | dataSource={[ 440 | { label: 'Option 1', value: 1 }, 441 | { label: 'Option 2', value: 2 }, 442 | ]} 443 | decorator={[FormItem]} 444 | component={[ 445 | Select, 446 | { 447 | style: { 448 | width: 120, 449 | }, 450 | }, 451 | ]} 452 | /> 453 | <FormButtonGroup> 454 | <Submit onSubmit={console.log}>Submit</Submit> 455 | </FormButtonGroup> 456 | </FormProvider> 457 | ) 458 | ``` 459 | 460 | ## Pure JSX asynchronous linkage data source case 461 | 462 | ```tsx 463 | import React from 'react' 464 | import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' 465 | import { 466 | createForm, 467 | onFieldReact, 468 | FormPathPattern, 469 | Field as FieldType, 470 | } from '@formily/core' 471 | import { FormProvider, Field } from '@formily/react' 472 | import { action } from '@formily/reactive' 473 | 474 | const useAsyncDataSource = ( 475 | pattern: FormPathPattern, 476 | service: (field: FieldType) => Promise<{ label: string; value: any }[]> 477 | ) => { 478 | onFieldReact(pattern, (field) => { 479 | field.loading = true 480 | service(field).then( 481 | action.bound((data) => { 482 | field.dataSource = data 483 | field.loading = false 484 | }) 485 | ) 486 | }) 487 | } 488 | 489 | const form = createForm({ 490 | effects: () => { 491 | useAsyncDataSource('select', async (field) => { 492 | const linkage = field.query('linkage').get('value') 493 | if (!linkage) return [] 494 | return new Promise((resolve) => { 495 | setTimeout(() => { 496 | if (linkage === 1) { 497 | resolve([ 498 | { 499 | label: 'AAA', 500 | value: 'aaa', 501 | }, 502 | { 503 | label: 'BBB', 504 | value: 'ccc', 505 | }, 506 | ]) 507 | } else if (linkage === 2) { 508 | resolve([ 509 | { 510 | label: 'CCC', 511 | value: 'ccc', 512 | }, 513 | { 514 | label: 'DDD', 515 | value: 'ddd', 516 | }, 517 | ]) 518 | } 519 | }, 1500) 520 | }) 521 | }) 522 | }, 523 | }) 524 | 525 | export default () => ( 526 | <FormProvider form={form}> 527 | <Field 528 | name="linkage" 529 | title="Linkage selection box" 530 | dataSource={[ 531 | { label: 'Request 1', value: 1 }, 532 | { label: 'Request 2', value: 2 }, 533 | ]} 534 | decorator={[FormItem]} 535 | component={[ 536 | Select, 537 | { 538 | style: { 539 | width: 120, 540 | }, 541 | }, 542 | ]} 543 | /> 544 | <Field 545 | name="select" 546 | title="Asynchronous select box" 547 | decorator={[FormItem]} 548 | component={[ 549 | Select, 550 | { 551 | style: { 552 | width: 120, 553 | }, 554 | }, 555 | ]} 556 | /> 557 | <FormButtonGroup> 558 | <Submit onSubmit={console.log}>Submit</Submit> 559 | </FormButtonGroup> 560 | </FormProvider> 561 | ) 562 | ``` 563 | 564 | ## API 565 | 566 | Reference https://ant.design/components/select-cn/ 567 | ``` -------------------------------------------------------------------------------- /packages/next/docs/components/ArrayCards.zh-CN.md: -------------------------------------------------------------------------------- ```markdown 1 | # ArrayCards 2 | 3 | > 卡片列表,对于每行字段数量较多,联动较多的场景比较适合使用 ArrayCards 4 | > 5 | > 注意:该组件只适用于 Schema 场景 6 | 7 | ## Markup Schema 案例 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: '字符串数组', 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="添加条目" 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: '对象数组', 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="添加条目" 88 | /> 89 | </SchemaField.Array> 90 | </SchemaField> 91 | <FormButtonGroup> 92 | <Submit onSubmit={console.log}>提交</Submit> 93 | </FormButtonGroup> 94 | </FormProvider> 95 | ) 96 | } 97 | ``` 98 | 99 | ## JSON Schema 案例 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: '字符串数组', 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: '添加条目', 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: '对象数组', 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: '添加条目', 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> 223 | </FormButtonGroup> 224 | </FormProvider> 225 | ) 226 | } 227 | ``` 228 | 229 | ## Effects 联动案例 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 | //主动联动模式 254 | onFieldChange('array.*.aa', ['value'], (field, form) => { 255 | form.setFieldState(field.query('.bb'), (state) => { 256 | state.visible = field.value != '123' 257 | }) 258 | }) 259 | //被动联动模式 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: '对象数组', 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输入123时隐藏BB" 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="CC输入123时隐藏DD" 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="添加条目" 318 | /> 319 | </SchemaField.Array> 320 | </SchemaField> 321 | <FormButtonGroup> 322 | <Submit onSubmit={console.log}>提交</Submit> 323 | </FormButtonGroup> 324 | </FormProvider> 325 | ) 326 | } 327 | ``` 328 | 329 | ## JSON Schema 联动案例 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: '对象数组', 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: '输入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: '添加条目', 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> 432 | </FormButtonGroup> 433 | </FormProvider> 434 | ) 435 | } 436 | ``` 437 | 438 | ## API 439 | 440 | ### ArrayCards 441 | 442 | 扩展属性 443 | 444 | | 属性名 | 类型 | 描述 | 默认值 | 445 | | ---------- | ------------------------- | ------------ | ------ | 446 | | onAdd | `(index: number) => void` | 增加方法 | | 447 | | onRemove | `(index: number) => void` | 删除方法 | | 448 | | onCopy | `(index: number) => void` | 复制方法 | | 449 | | onMoveUp | `(index: number) => void` | 向上移动方法 | | 450 | | onMoveDown | `(index: number) => void` | 向下移动方法 | | 451 | 452 | 其余参考 https://fusion.design/pc/component/basic/card 453 | 454 | ### ArrayCards.Addition 455 | 456 | > 添加按钮 457 | 458 | 扩展属性 459 | 460 | | 属性名 | 类型 | 描述 | 默认值 | 461 | | ------------ | --------------------- | -------- | -------- | 462 | | title | ReactText | 文案 | | 463 | | method | `'push' \| 'unshift'` | 添加方式 | `'push'` | 464 | | defaultValue | `any` | 默认值 | | 465 | 466 | 其余参考 https://fusion.design/pc/component/basic/button 467 | 468 | 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 469 | 470 | ### ArrayCards.Copy 471 | 472 | > 复制按钮 473 | 474 | 扩展属性 475 | 476 | | 属性名 | 类型 | 描述 | 默认值 | 477 | | ------ | --------------------- | -------- | -------- | 478 | | title | ReactText | 文案 | | 479 | | method | `'push' \| 'unshift'` | 添加方式 | `'push'` | 480 | 481 | 其余参考 https://fusion.design/pc/component/basic/button 482 | 483 | 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 484 | 485 | ### ArrayCards.Remove 486 | 487 | > 删除按钮 488 | 489 | | 属性名 | 类型 | 描述 | 默认值 | 490 | | ------ | --------- | ---- | ------ | 491 | | title | ReactText | 文案 | | 492 | 493 | 其余参考 https://ant.design/components/icon-cn/ 494 | 495 | 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 496 | 497 | ### ArrayCards.MoveDown 498 | 499 | > 下移按钮 500 | 501 | | 属性名 | 类型 | 描述 | 默认值 | 502 | | ------ | --------- | ---- | ------ | 503 | | title | ReactText | 文案 | | 504 | 505 | 其余参考 https://ant.design/components/icon-cn/ 506 | 507 | 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 508 | 509 | ### ArrayCards.MoveUp 510 | 511 | > 上移按钮 512 | 513 | | 属性名 | 类型 | 描述 | 默认值 | 514 | | ------ | --------- | ---- | ------ | 515 | | title | ReactText | 文案 | | 516 | 517 | 其余参考 https://ant.design/components/icon-cn/ 518 | 519 | 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 520 | 521 | ### ArrayCards.Index 522 | 523 | > 索引渲染器 524 | 525 | 无属性 526 | 527 | ### ArrayCards.useIndex 528 | 529 | > 读取当前渲染行索引的 React Hook 530 | 531 | ### ArrayCards.useRecord 532 | 533 | > 读取当前渲染记录的 React Hook 534 | ``` -------------------------------------------------------------------------------- /packages/antd/src/array-table/index.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { 2 | Fragment, 3 | useState, 4 | useRef, 5 | useEffect, 6 | createContext, 7 | useContext, 8 | useCallback, 9 | } from 'react' 10 | import { Table, Pagination, Space, Select, Badge } from 'antd' 11 | import { PaginationProps } from 'antd/lib/pagination' 12 | import { TableProps, ColumnProps } from 'antd/lib/table' 13 | import { SelectProps } from 'antd/lib/select' 14 | import cls from 'classnames' 15 | import { GeneralField, FieldDisplayTypes, ArrayField } from '@formily/core' 16 | import { 17 | useField, 18 | observer, 19 | useFieldSchema, 20 | RecursionField, 21 | ReactFC, 22 | } from '@formily/react' 23 | import { isArr, isBool, isFn } from '@formily/shared' 24 | import { Schema } from '@formily/json-schema' 25 | import { 26 | usePrefixCls, 27 | SortableContainer, 28 | SortableElement, 29 | } from '../__builtins__' 30 | import { ArrayBase, ArrayBaseMixins, IArrayBaseProps } from '../array-base' 31 | 32 | interface ObservableColumnSource { 33 | field: GeneralField 34 | columnProps: ColumnProps<any> 35 | schema: Schema 36 | display: FieldDisplayTypes 37 | name: string 38 | } 39 | interface IArrayTablePaginationProps extends PaginationProps { 40 | dataSource?: any[] 41 | showPagination?: boolean 42 | children?: ( 43 | dataSource: any[], 44 | pagination: React.ReactNode, 45 | options: { 46 | startIndex: number 47 | } 48 | ) => React.ReactElement 49 | } 50 | 51 | interface IStatusSelectProps extends SelectProps<any> { 52 | pageSize?: number 53 | } 54 | 55 | type ComposedArrayTable = React.FC< 56 | React.PropsWithChildren<TableProps<any> & IArrayBaseProps> 57 | > & 58 | ArrayBaseMixins & { 59 | Column?: React.FC<React.PropsWithChildren<ColumnProps<any>>> 60 | } 61 | 62 | interface PaginationAction { 63 | totalPage?: number 64 | pageSize?: number 65 | showPagination?: boolean 66 | changePage?: (page: number) => void 67 | } 68 | 69 | const SortableRow = SortableElement((props: any) => <tr {...props} />) 70 | const SortableBody = SortableContainer((props: any) => <tbody {...props} />) 71 | 72 | const isColumnComponent = (schema: Schema) => { 73 | return schema['x-component']?.indexOf('Column') > -1 74 | } 75 | 76 | const isOperationsComponent = (schema: Schema) => { 77 | return schema['x-component']?.indexOf('Operations') > -1 78 | } 79 | 80 | const isAdditionComponent = (schema: Schema) => { 81 | return schema['x-component']?.indexOf('Addition') > -1 82 | } 83 | 84 | const useArrayTableSources = () => { 85 | const arrayField = useField() 86 | const schema = useFieldSchema() 87 | const parseSources = (schema: Schema): ObservableColumnSource[] => { 88 | if ( 89 | isColumnComponent(schema) || 90 | isOperationsComponent(schema) || 91 | isAdditionComponent(schema) 92 | ) { 93 | if (!schema['x-component-props']?.['dataIndex'] && !schema['name']) 94 | return [] 95 | const name = schema['x-component-props']?.['dataIndex'] || schema['name'] 96 | const field = arrayField.query(arrayField.address.concat(name)).take() 97 | const columnProps = 98 | field?.component?.[1] || schema['x-component-props'] || {} 99 | const display = field?.display || schema['x-display'] || 'visible' 100 | return [ 101 | { 102 | name, 103 | display, 104 | field, 105 | schema, 106 | columnProps, 107 | }, 108 | ] 109 | } else if (schema.properties) { 110 | return schema.reduceProperties((buf, schema) => { 111 | return buf.concat(parseSources(schema)) 112 | }, []) 113 | } 114 | } 115 | 116 | const parseArrayItems = (schema: Schema['items']) => { 117 | if (!schema) return [] 118 | const sources: ObservableColumnSource[] = [] 119 | const items = isArr(schema) ? schema : [schema] 120 | return items.reduce((columns, schema) => { 121 | const item = parseSources(schema) 122 | if (item) { 123 | return columns.concat(item) 124 | } 125 | return columns 126 | }, sources) 127 | } 128 | 129 | if (!schema) throw new Error('can not found schema object') 130 | 131 | return parseArrayItems(schema.items) 132 | } 133 | 134 | const useArrayTableColumns = ( 135 | dataSource: any[], 136 | field: ArrayField, 137 | sources: ObservableColumnSource[] 138 | ): TableProps<any>['columns'] => { 139 | return sources.reduce((buf, { name, columnProps, schema, display }, key) => { 140 | if (display !== 'visible') return buf 141 | if (!isColumnComponent(schema)) return buf 142 | return buf.concat({ 143 | ...columnProps, 144 | key, 145 | dataIndex: name, 146 | render: (value: any, record: any) => { 147 | const index = dataSource?.indexOf(record) 148 | const children = ( 149 | <ArrayBase.Item index={index} record={() => field?.value?.[index]}> 150 | <RecursionField schema={schema} name={index} onlyRenderProperties /> 151 | </ArrayBase.Item> 152 | ) 153 | return children 154 | }, 155 | }) 156 | }, []) 157 | } 158 | 159 | const useAddition = () => { 160 | const schema = useFieldSchema() 161 | return schema.reduceProperties((addition, schema, key) => { 162 | if (isAdditionComponent(schema)) { 163 | return <RecursionField schema={schema} name={key} /> 164 | } 165 | return addition 166 | }, null) 167 | } 168 | 169 | const schedulerRequest = { 170 | request: null, 171 | } 172 | 173 | const StatusSelect: ReactFC<IStatusSelectProps> = observer( 174 | (props) => { 175 | const field = useField<ArrayField>() 176 | const prefixCls = usePrefixCls('formily-array-table') 177 | const errors = field.errors 178 | const parseIndex = (address: string) => { 179 | return Number( 180 | address 181 | .slice(address.indexOf(field.address.toString()) + 1) 182 | .match(/(\d+)/)?.[1] 183 | ) 184 | } 185 | const options = props.options?.map(({ label, value }) => { 186 | const val = Number(value) 187 | const hasError = errors.some(({ address }) => { 188 | const currentIndex = parseIndex(address) 189 | const startIndex = (val - 1) * props.pageSize 190 | const endIndex = val * props.pageSize 191 | return currentIndex >= startIndex && currentIndex <= endIndex 192 | }) 193 | return { 194 | label: hasError ? <Badge dot>{label}</Badge> : label, 195 | value, 196 | } 197 | }) 198 | 199 | const width = String(options?.length).length * 15 200 | 201 | return ( 202 | <Select 203 | value={props.value} 204 | onChange={props.onChange} 205 | options={options} 206 | virtual 207 | style={{ 208 | width: width < 60 ? 60 : width, 209 | }} 210 | className={cls(`${prefixCls}-status-select`, { 211 | 'has-error': errors?.length, 212 | })} 213 | /> 214 | ) 215 | }, 216 | { 217 | scheduler: (update) => { 218 | clearTimeout(schedulerRequest.request) 219 | schedulerRequest.request = setTimeout(() => { 220 | update() 221 | }, 100) 222 | }, 223 | } 224 | ) 225 | 226 | const PaginationContext = createContext<PaginationAction>({}) 227 | const usePagination = () => { 228 | return useContext(PaginationContext) 229 | } 230 | 231 | const ArrayTablePagination: ReactFC<IArrayTablePaginationProps> = (props) => { 232 | const [current, setCurrent] = useState(1) 233 | const prefixCls = usePrefixCls('formily-array-table') 234 | const showPagination = props.showPagination ?? true 235 | const pageSize = props.pageSize || 10 236 | const size = props.size || 'default' 237 | const dataSource = props.dataSource || [] 238 | const startIndex = (current - 1) * pageSize 239 | const endIndex = startIndex + pageSize - 1 240 | const total = dataSource?.length || 0 241 | const totalPage = Math.ceil(total / pageSize) 242 | const pages = Array.from(new Array(totalPage)).map((_, index) => { 243 | const page = index + 1 244 | return { 245 | label: page, 246 | value: page, 247 | } 248 | }) 249 | const handleChange = (current: number) => { 250 | setCurrent(current) 251 | } 252 | 253 | useEffect(() => { 254 | if (totalPage > 0 && totalPage < current) { 255 | handleChange(totalPage) 256 | } 257 | }, [totalPage, current]) 258 | 259 | const renderPagination = () => { 260 | if (totalPage <= 1 || !showPagination) return 261 | return ( 262 | <div className={`${prefixCls}-pagination`}> 263 | <Space> 264 | <StatusSelect 265 | value={current} 266 | pageSize={pageSize} 267 | onChange={handleChange} 268 | options={pages} 269 | notFoundContent={false} 270 | /> 271 | <Pagination 272 | {...props} 273 | pageSize={pageSize} 274 | current={current} 275 | total={dataSource.length} 276 | size={size} 277 | showSizeChanger={false} 278 | onChange={handleChange} 279 | /> 280 | </Space> 281 | </div> 282 | ) 283 | } 284 | 285 | return ( 286 | <Fragment> 287 | <PaginationContext.Provider 288 | value={{ 289 | totalPage, 290 | pageSize, 291 | changePage: handleChange, 292 | showPagination, 293 | }} 294 | > 295 | {props.children?.( 296 | showPagination 297 | ? dataSource?.slice(startIndex, endIndex + 1) 298 | : dataSource, 299 | renderPagination(), 300 | { startIndex } 301 | )} 302 | </PaginationContext.Provider> 303 | </Fragment> 304 | ) 305 | } 306 | 307 | const RowComp: ReactFC<React.HTMLAttributes<HTMLTableRowElement>> = (props) => { 308 | const prefixCls = usePrefixCls('formily-array-table') 309 | const index = props['data-row-key'] || 0 310 | return ( 311 | <SortableRow 312 | lockAxis="y" 313 | {...props} 314 | index={index} 315 | className={cls(props.className, `${prefixCls}-row-${index + 1}`)} 316 | /> 317 | ) 318 | } 319 | 320 | export const ArrayTable: ComposedArrayTable = observer((props) => { 321 | const ref = useRef<HTMLDivElement>() 322 | const field = useField<ArrayField>() 323 | const prefixCls = usePrefixCls('formily-array-table') 324 | const dataSource = Array.isArray(field.value) ? field.value.slice() : [] 325 | const sources = useArrayTableSources() 326 | const columns = useArrayTableColumns(dataSource, field, sources) 327 | const pagination = isBool(props.pagination) 328 | ? { showPagination: props.pagination } 329 | : props.pagination 330 | const addition = useAddition() 331 | const { onAdd, onCopy, onRemove, onMoveDown, onMoveUp } = props 332 | const defaultRowKey = (record: any) => { 333 | return dataSource.indexOf(record) 334 | } 335 | const addTdStyles = (id: number) => { 336 | const node = ref.current?.querySelector(`.${prefixCls}-row-${id}`) 337 | const helper = document.body.querySelector(`.${prefixCls}-sort-helper`) 338 | if (!helper) return 339 | const tds = node?.querySelectorAll('td') 340 | if (!tds) return 341 | requestAnimationFrame(() => { 342 | helper.querySelectorAll('td').forEach((td, index) => { 343 | if (tds[index]) { 344 | td.style.width = getComputedStyle(tds[index]).width 345 | } 346 | }) 347 | }) 348 | } 349 | const getWrapperComp = useCallback( 350 | (dataSource: any[], start: number) => (props: any) => 351 | ( 352 | <SortableBody 353 | {...props} 354 | start={start} 355 | list={dataSource.slice()} 356 | accessibility={{ 357 | container: ref.current || undefined, 358 | }} 359 | onSortStart={(event) => { 360 | addTdStyles(event.active.id as number) 361 | }} 362 | onSortEnd={({ oldIndex, newIndex }) => { 363 | field.move(oldIndex, newIndex) 364 | }} 365 | className={cls(`${prefixCls}-sort-helper`, props.className)} 366 | /> 367 | ), 368 | [field] 369 | ) 370 | return ( 371 | <ArrayTablePagination {...pagination} dataSource={dataSource}> 372 | {(dataSource, pager, { startIndex }) => ( 373 | <div ref={ref} className={prefixCls}> 374 | <ArrayBase 375 | onAdd={onAdd} 376 | onCopy={onCopy} 377 | onRemove={onRemove} 378 | onMoveUp={onMoveUp} 379 | onMoveDown={onMoveDown} 380 | > 381 | <Table 382 | size="small" 383 | bordered 384 | rowKey={defaultRowKey} 385 | {...props} 386 | onChange={() => {}} 387 | pagination={false} 388 | columns={columns} 389 | dataSource={dataSource} 390 | components={{ 391 | body: { 392 | wrapper: getWrapperComp(dataSource, startIndex), 393 | row: RowComp, 394 | }, 395 | }} 396 | /> 397 | <div style={{ marginTop: 5, marginBottom: 5 }}>{pager}</div> 398 | {sources.map((column, key) => { 399 | //专门用来承接对Column的状态管理 400 | if (!isColumnComponent(column.schema)) return 401 | return React.createElement(RecursionField, { 402 | name: column.name, 403 | schema: column.schema, 404 | onlyRenderSelf: true, 405 | key, 406 | }) 407 | })} 408 | {addition} 409 | </ArrayBase> 410 | </div> 411 | )} 412 | </ArrayTablePagination> 413 | ) 414 | }) 415 | 416 | ArrayTable.displayName = 'ArrayTable' 417 | 418 | ArrayTable.Column = () => { 419 | return <Fragment /> 420 | } 421 | 422 | ArrayBase.mixin(ArrayTable) 423 | 424 | const Addition: ArrayBaseMixins['Addition'] = (props) => { 425 | const array = ArrayBase.useArray() 426 | const { 427 | totalPage = 0, 428 | pageSize = 10, 429 | changePage, 430 | showPagination, 431 | } = usePagination() 432 | return ( 433 | <ArrayBase.Addition 434 | {...props} 435 | onClick={(e) => { 436 | // 如果添加数据后将超过当前页,则自动切换到下一页 437 | const total = array?.field?.value.length || 0 438 | if ( 439 | showPagination && 440 | total === totalPage * pageSize + 1 && 441 | isFn(changePage) 442 | ) { 443 | changePage(totalPage + 1) 444 | } 445 | props.onClick?.(e) 446 | }} 447 | /> 448 | ) 449 | } 450 | ArrayTable.Addition = Addition 451 | 452 | export default ArrayTable 453 | ``` -------------------------------------------------------------------------------- /packages/next/src/form-item/index.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { useState, useRef, useEffect } from 'react' 2 | import cls from 'classnames' 3 | import { 4 | usePrefixCls, 5 | pickDataProps, 6 | QuestionCircleOutlinedIcon, 7 | CloseCircleOutlinedIcon, 8 | CheckCircleOutlinedIcon, 9 | ExclamationCircleOutlinedIcon, 10 | } from '../__builtins__' 11 | import { isVoidField } from '@formily/core' 12 | import { connect, mapProps } from '@formily/react' 13 | import { useFormLayout, FormLayoutShallowContext } from '../form-layout' 14 | import { Balloon } from '@alifd/next' 15 | 16 | export interface IFormItemProps { 17 | className?: string 18 | style?: React.CSSProperties 19 | prefix?: string 20 | label?: React.ReactNode 21 | colon?: boolean 22 | layout?: 'vertical' | 'horizontal' | 'inline' 23 | tooltip?: React.ReactNode 24 | tooltipLayout?: 'icon' | 'text' 25 | tooltipIcon?: React.ReactNode 26 | labelFor?: string 27 | labelStyle?: React.CSSProperties 28 | labelAlign?: 'left' | 'right' 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 | asterisk?: boolean 48 | gridSpan?: number 49 | bordered?: boolean 50 | } 51 | 52 | type ComposeFormItem = React.FC<React.PropsWithChildren<IFormItemProps>> & { 53 | BaseItem?: React.FC<React.PropsWithChildren<IFormItemProps>> 54 | } 55 | 56 | const useFormItemLayout = (props: IFormItemProps) => { 57 | const layout = useFormLayout() 58 | const layoutType = props.layout ?? layout.layout ?? 'horizontal' 59 | return { 60 | ...props, 61 | layout: layoutType, 62 | colon: props.colon ?? layout.colon, 63 | labelAlign: 64 | layoutType === 'vertical' 65 | ? props.labelAlign ?? 'left' 66 | : props.labelAlign ?? layout.labelAlign ?? 'right', 67 | labelWrap: props.labelWrap ?? layout.labelWrap, 68 | labelWidth: props.labelWidth ?? layout.labelWidth, 69 | wrapperWidth: props.wrapperWidth ?? layout.wrapperWidth, 70 | labelCol: props.labelCol ?? layout.labelCol, 71 | wrapperCol: props.wrapperCol ?? layout.wrapperCol, 72 | wrapperAlign: props.wrapperAlign ?? layout.wrapperAlign, 73 | wrapperWrap: props.wrapperWrap ?? layout.wrapperWrap, 74 | fullness: props.fullness ?? layout.fullness, 75 | size: props.size ?? layout.size, 76 | inset: props.inset ?? layout.inset, 77 | asterisk: props.asterisk, 78 | bordered: props.bordered ?? layout.bordered, 79 | feedbackIcon: props.feedbackIcon, 80 | feedbackLayout: props.feedbackLayout ?? layout.feedbackLayout ?? 'loose', 81 | tooltipLayout: props.tooltipLayout ?? layout.tooltipLayout ?? 'icon', 82 | tooltipIcon: props.tooltipIcon ?? layout.tooltipIcon ?? ( 83 | <QuestionCircleOutlinedIcon /> 84 | ), 85 | } 86 | } 87 | 88 | function useOverflow< 89 | Container extends HTMLElement, 90 | Content extends HTMLElement 91 | >() { 92 | const [overflow, setOverflow] = useState(false) 93 | const containerRef = useRef<Container>() 94 | const contentRef = useRef<Content>() 95 | const layout = useFormLayout() 96 | const labelCol = JSON.stringify(layout.labelCol) 97 | 98 | useEffect(() => { 99 | requestAnimationFrame(() => { 100 | if (containerRef.current && contentRef.current) { 101 | const contentWidth = contentRef.current.getBoundingClientRect().width 102 | const containerWidth = 103 | containerRef.current.getBoundingClientRect().width 104 | if (contentWidth && containerWidth && containerWidth < contentWidth) { 105 | if (!overflow) setOverflow(true) 106 | } else { 107 | if (overflow) setOverflow(false) 108 | } 109 | } 110 | }) 111 | }, [labelCol]) 112 | 113 | return { 114 | overflow, 115 | containerRef, 116 | contentRef, 117 | } 118 | } 119 | 120 | const ICON_MAP = { 121 | error: <CloseCircleOutlinedIcon />, 122 | success: <CheckCircleOutlinedIcon />, 123 | warning: <ExclamationCircleOutlinedIcon />, 124 | } 125 | 126 | export const BaseItem: React.FC<React.PropsWithChildren<IFormItemProps>> = ( 127 | props 128 | ) => { 129 | const { children, ...others } = props 130 | const [active, setActive] = useState(false) 131 | const formLayout = useFormItemLayout(others) 132 | const { containerRef, contentRef, overflow } = useOverflow< 133 | HTMLDivElement, 134 | HTMLSpanElement 135 | >() 136 | const { 137 | label, 138 | style, 139 | layout, 140 | colon = true, 141 | addonBefore, 142 | addonAfter, 143 | asterisk, 144 | feedbackStatus, 145 | extra, 146 | feedbackText, 147 | fullness = true, 148 | feedbackLayout, 149 | feedbackIcon, 150 | inset, 151 | bordered = true, 152 | labelWidth, 153 | wrapperWidth, 154 | labelCol, 155 | wrapperCol, 156 | labelAlign, 157 | wrapperAlign = 'left', 158 | size, 159 | labelWrap, 160 | wrapperWrap, 161 | tooltip, 162 | tooltipLayout, 163 | tooltipIcon, 164 | } = formLayout 165 | const labelStyle = { ...formLayout.labelStyle } 166 | const wrapperStyle = { ...formLayout.wrapperStyle } 167 | // 固定宽度 168 | let enableCol = false 169 | if (labelWidth || wrapperWidth) { 170 | if (labelWidth) { 171 | labelStyle.width = labelWidth === 'auto' ? undefined : labelWidth 172 | labelStyle.maxWidth = labelWidth === 'auto' ? undefined : labelWidth 173 | } 174 | if (wrapperWidth) { 175 | wrapperStyle.width = wrapperWidth === 'auto' ? undefined : wrapperWidth 176 | wrapperStyle.maxWidth = wrapperWidth === 'auto' ? undefined : wrapperWidth 177 | } 178 | // 栅格模式 179 | } 180 | if (labelCol || wrapperCol) { 181 | if (!labelStyle.width && !wrapperStyle.width && layout !== 'vertical') { 182 | enableCol = true 183 | } 184 | } 185 | const prefixCls = usePrefixCls('formily-item', props) 186 | const prefix = usePrefixCls() 187 | const formatChildren = 188 | feedbackLayout === 'popover' ? ( 189 | <Balloon 190 | needAdjust 191 | align="t" 192 | closable={false} 193 | trigger={children} 194 | visible={!!feedbackText} 195 | > 196 | <div 197 | className={cls({ 198 | [`${prefixCls}-${feedbackStatus}-help`]: !!feedbackStatus, 199 | [`${prefixCls}-help`]: true, 200 | })} 201 | > 202 | {ICON_MAP[feedbackStatus]} {feedbackText} 203 | </div> 204 | </Balloon> 205 | ) : ( 206 | children 207 | ) 208 | 209 | const gridStyles: React.CSSProperties = {} 210 | 211 | const getOverflowTooltip = () => { 212 | if (overflow) { 213 | return ( 214 | <div> 215 | <div>{label}</div> 216 | <div>{tooltip}</div> 217 | </div> 218 | ) 219 | } 220 | return tooltip 221 | } 222 | 223 | const renderLabelText = () => { 224 | const labelChildren = ( 225 | <div className={cls(`${prefixCls}-label-content`)} ref={containerRef}> 226 | <span ref={contentRef}> 227 | {asterisk && ( 228 | <span className={cls(`${prefixCls}-asterisk`)}>{'*'}</span> 229 | )} 230 | <label htmlFor={props.labelFor}>{label}</label> 231 | </span> 232 | </div> 233 | ) 234 | 235 | if ((tooltipLayout === 'text' && tooltip) || overflow) { 236 | return ( 237 | <Balloon.Tooltip align="t" trigger={labelChildren}> 238 | {getOverflowTooltip()} 239 | </Balloon.Tooltip> 240 | ) 241 | } 242 | return labelChildren 243 | } 244 | 245 | const renderTooltipIcon = () => { 246 | if (tooltip && tooltipLayout === 'icon' && !overflow) { 247 | return ( 248 | <span className={cls(`${prefixCls}-label-tooltip-icon`)}> 249 | <Balloon.Tooltip align="t" trigger={tooltipIcon}> 250 | {tooltip} 251 | </Balloon.Tooltip> 252 | </span> 253 | ) 254 | } 255 | } 256 | 257 | const renderLabel = () => { 258 | if (!label) return null 259 | return ( 260 | <div 261 | className={cls({ 262 | [`${prefixCls}-label`]: true, 263 | [`${prefixCls}-label-tooltip`]: 264 | (tooltip && tooltipLayout === 'text') || overflow, 265 | [`${prefixCls}-item-col-${labelCol}`]: enableCol && !!labelCol, 266 | })} 267 | style={labelStyle} 268 | > 269 | {renderLabelText()} 270 | {renderTooltipIcon()} 271 | {label !== ' ' && ( 272 | <span className={cls(`${prefixCls}-colon`)}>{colon ? ':' : ''}</span> 273 | )} 274 | </div> 275 | ) 276 | } 277 | 278 | return ( 279 | <div 280 | {...pickDataProps(props)} 281 | style={{ 282 | ...style, 283 | ...gridStyles, 284 | }} 285 | data-grid-span={props.gridSpan} 286 | className={cls({ 287 | [`${prefixCls}`]: true, 288 | [`${prefixCls}-layout-${layout}`]: true, 289 | [`${prefixCls}-${feedbackStatus}`]: !!feedbackStatus, 290 | [`${prefixCls}-feedback-has-text`]: !!feedbackText, 291 | [`${prefixCls}-size-${size}`]: !!size, 292 | [`${prefixCls}-feedback-layout-${feedbackLayout}`]: !!feedbackLayout, 293 | [`${prefixCls}-fullness`]: !!fullness || !!inset || !!feedbackIcon, 294 | [`${prefixCls}-inset`]: !!inset, 295 | [`${prefix}input`]: !!inset, 296 | [`${prefixCls}-active`]: active, 297 | [`${prefix}focus`]: active, 298 | [`${prefixCls}-inset-active`]: !!inset && active, 299 | [`${prefixCls}-label-align-${labelAlign}`]: true, 300 | [`${prefixCls}-control-align-${wrapperAlign}`]: true, 301 | [`${prefixCls}-label-wrap`]: !!labelWrap, 302 | [`${prefixCls}-control-wrap`]: !!wrapperWrap, 303 | [`${prefixCls}-bordered-none`]: bordered === false, 304 | [props.className]: !!props.className, 305 | })} 306 | onFocus={() => { 307 | if (feedbackIcon || inset) { 308 | setActive(true) 309 | } 310 | }} 311 | onBlur={() => { 312 | if (feedbackIcon || inset) { 313 | setActive(false) 314 | } 315 | }} 316 | > 317 | {renderLabel()} 318 | <div 319 | className={cls({ 320 | [`${prefixCls}-control`]: true, 321 | [`${prefixCls}-item-col-${wrapperCol}`]: 322 | enableCol && !!wrapperCol && label, 323 | })} 324 | > 325 | <div className={cls(`${prefixCls}-control-content`)}> 326 | {addonBefore && ( 327 | <div className={cls(`${prefixCls}-addon-before`)}> 328 | {addonBefore} 329 | </div> 330 | )} 331 | <div 332 | style={wrapperStyle} 333 | className={cls({ 334 | [`${prefixCls}-control-content-component`]: true, 335 | [`${prefixCls}-control-content-component-has-feedback-icon`]: 336 | !!feedbackIcon, 337 | [`${prefix}input`]: !!feedbackIcon, 338 | [`${prefixCls}-active`]: active, 339 | [`${prefix}focus`]: active, 340 | })} 341 | > 342 | <FormLayoutShallowContext.Provider value={{ size }}> 343 | {formatChildren} 344 | </FormLayoutShallowContext.Provider> 345 | {feedbackIcon && ( 346 | <div className={cls(`${prefixCls}-feedback-icon`)}> 347 | {feedbackIcon} 348 | </div> 349 | )} 350 | </div> 351 | {addonAfter && ( 352 | <div className={cls(`${prefixCls}-addon-after`)}>{addonAfter}</div> 353 | )} 354 | </div> 355 | {!!feedbackText && 356 | feedbackLayout !== 'popover' && 357 | feedbackLayout !== 'none' && ( 358 | <div 359 | className={cls({ 360 | [`${prefixCls}-${feedbackStatus}-help`]: !!feedbackStatus, 361 | [`${prefixCls}-help`]: true, 362 | [`${prefixCls}-help-enter`]: true, 363 | [`${prefixCls}-help-enter-active`]: true, 364 | })} 365 | > 366 | {feedbackText} 367 | </div> 368 | )} 369 | {extra && <div className={cls(`${prefixCls}-extra`)}>{extra}</div>} 370 | </div> 371 | </div> 372 | ) 373 | } 374 | 375 | // 适配 376 | export const FormItem: ComposeFormItem = connect( 377 | BaseItem, 378 | mapProps((props, field) => { 379 | if (isVoidField(field)) 380 | return { 381 | label: field.title || props.label, 382 | asterisk: props.asterisk, 383 | extra: props.extra || field.description, 384 | } 385 | if (!field) return props 386 | const takeFeedbackStatus = () => { 387 | if (field.validating) return 'pending' 388 | return field.decoratorProps.feedbackStatus || field.validateStatus 389 | } 390 | const takeMessage = () => { 391 | const split = (messages: any[]) => { 392 | return messages.reduce((buf, text, index) => { 393 | if (!text) return buf 394 | return index < messages.length - 1 395 | ? buf.concat([text, ', ']) 396 | : buf.concat([text]) 397 | }, []) 398 | } 399 | if (field.validating) return 400 | if (props.feedbackText) return props.feedbackText 401 | if (field.selfErrors.length) return split(field.selfErrors) 402 | if (field.selfWarnings.length) return split(field.selfWarnings) 403 | if (field.selfSuccesses.length) return split(field.selfSuccesses) 404 | } 405 | const takeAsterisk = () => { 406 | if (field.required && field.pattern !== 'readPretty') { 407 | return true 408 | } 409 | if ('asterisk' in props) { 410 | return props.asterisk 411 | } 412 | return false 413 | } 414 | return { 415 | label: props.label || field.title, 416 | feedbackStatus: takeFeedbackStatus(), 417 | feedbackText: takeMessage(), 418 | asterisk: takeAsterisk(), 419 | extra: props.extra || field.description, 420 | } 421 | }) 422 | ) 423 | 424 | FormItem.BaseItem = BaseItem 425 | 426 | export default FormItem 427 | ```