This is page 20 of 35. Use http://codebase.md/alibaba/formily?lines=false&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 import { createForm, Form, IFormProps } from '@formily/core' import { toJS } from '@formily/reactive' import { observer } from '@formily/reactive-vue' import { applyMiddleware, IMiddleware, isBool, isFn, isNum, isStr, } from '@formily/shared' import { FormProvider, Fragment, h } from '@formily/vue' import type { Button as ButtonProps, Dialog as DialogProps } from 'element-ui' import { Button, Dialog } from 'element-ui' import { t } from 'element-ui/src/locale' import { Portal, PortalTarget } from 'portal-vue' import Vue, { Component, VNode } from 'vue' import { defineComponent } from 'vue-demi' import { stylePrefix } from '../__builtins__/configs' import { createPortalProvider, getProtalContext, isValidElement, loading, resolveComponent, } from '../__builtins__/shared' type FormDialogContentProps = { form: Form } type FormDialogContent = Component | ((props: FormDialogContentProps) => VNode) type DialogTitle = string | number | Component | VNode | (() => VNode) type IFormDialogProps = Omit<DialogProps, 'title'> & { title?: DialogTitle footer?: null | Component | VNode | (() => VNode) cancelText?: string | Component | VNode | (() => VNode) cancelButtonProps?: ButtonProps okText?: string | Component | VNode | (() => VNode) okButtonProps?: ButtonProps onOpen?: () => void onOpened?: () => void onClose?: () => void onClosed?: () => void onCancel?: () => void onOK?: () => void loadingText?: string } const PORTAL_TARGET_NAME = 'FormDialogFooter' const isDialogTitle = (props: any): props is DialogTitle => { return isNum(props) || isStr(props) || isBool(props) || isValidElement(props) } const getDialogProps = (props: any): IFormDialogProps => { if (isDialogTitle(props)) { return { title: props, } as IFormDialogProps } else { return props } } export interface IFormDialog { forOpen(middleware: IMiddleware<IFormProps>): IFormDialog forConfirm(middleware: IMiddleware<Form>): IFormDialog forCancel(middleware: IMiddleware<Form>): IFormDialog open(props?: IFormProps): Promise<any> close(): void } export interface IFormDialogComponentProps { content: FormDialogContent resolve: () => any reject: () => any } export function FormDialog( title: IFormDialogProps | DialogTitle, content: FormDialogContent ): IFormDialog export function FormDialog( title: IFormDialogProps | DialogTitle, id: string | symbol, content: FormDialogContent ): IFormDialog export function FormDialog( title: DialogTitle, id: string, content: FormDialogContent ): IFormDialog export function FormDialog( title: IFormDialogProps | DialogTitle, id: string | symbol | FormDialogContent, content?: FormDialogContent ): IFormDialog { if (isFn(id) || isValidElement(id)) { content = id as FormDialogContent id = 'form-dialog' } const prefixCls = `${stylePrefix}-form-dialog` const env = { root: document.createElement('div'), form: null, promise: null, instance: null, openMiddlewares: [], confirmMiddlewares: [], cancelMiddlewares: [], } document.body.appendChild(env.root) const props = getDialogProps(title) const dialogProps = { ...props, onClosed: () => { props.onClosed?.() env.instance.$destroy() env.instance = null env.root?.parentNode?.removeChild(env.root) env.root = undefined }, } const component = observer( defineComponent({ setup() { return () => h( Fragment, {}, { default: () => resolveComponent(content, { form: env.form, }), } ) }, }) ) const render = (visible = true, resolve?: () => any, reject?: () => any) => { if (!env.instance) { const ComponentConstructor = observer( Vue.extend({ props: ['dialogProps'], data() { return { visible: false, } }, render() { const { onClose, onClosed, onOpen, onOpened, onOK, onCancel, title, footer, okText, cancelText, okButtonProps, cancelButtonProps, ...dialogProps } = this.dialogProps return h( FormProvider, { props: { form: env.form, }, }, { default: () => h( Dialog, { class: [`${prefixCls}`], attrs: { visible: this.visible, ...dialogProps, }, on: { 'update:visible': (val) => { this.visible = val }, close: () => { onClose?.() }, closed: () => { onClosed?.() }, open: () => { onOpen?.() }, opened: () => { onOpened?.() }, }, }, { default: () => [h(component, {}, {})], title: () => h( 'div', {}, { default: () => resolveComponent(title) } ), footer: () => h( 'div', {}, { default: () => { const FooterProtalTarget = h( PortalTarget, { props: { name: PORTAL_TARGET_NAME, slim: true, }, }, {} ) if (footer === null) { return [null, FooterProtalTarget] } else if (footer) { return [ resolveComponent(footer), FooterProtalTarget, ] } return [ h( Button, { attrs: { ...cancelButtonProps }, on: { click: (e) => { onCancel?.(e) reject() }, }, }, { default: () => resolveComponent( cancelText || t('el.popconfirm.cancelButtonText') ), } ), h( Button, { attrs: { type: 'primary', ...okButtonProps, loading: env.form.submitting, }, on: { click: (e) => { onOK?.(e) resolve() }, }, }, { default: () => resolveComponent( okText || t('el.popconfirm.confirmButtonText') ), } ), FooterProtalTarget, ] }, } ), } ), } ) }, }) ) env.instance = new ComponentConstructor({ propsData: { dialogProps, }, parent: getProtalContext(id as string | symbol), }) env.instance.$mount(env.root) env.root = env.instance.$el } env.instance.visible = visible } const formDialog = { forOpen: (middleware: IMiddleware<IFormProps>) => { if (isFn(middleware)) { env.openMiddlewares.push(middleware) } return formDialog }, forConfirm: (middleware: IMiddleware<Form>) => { if (isFn(middleware)) { env.confirmMiddlewares.push(middleware) } return formDialog }, forCancel: (middleware: IMiddleware<Form>) => { if (isFn(middleware)) { env.cancelMiddlewares.push(middleware) } return formDialog }, open: (props: IFormProps) => { if (env.promise) return env.promise env.promise = new Promise(async (resolve, reject) => { try { props = await loading(dialogProps.loadingText, () => applyMiddleware(props, env.openMiddlewares) ) env.form = env.form || createForm(props) } catch (e) { reject(e) } render( true, () => { env.form .submit(async () => { await applyMiddleware(env.form, env.confirmMiddlewares) resolve(toJS(env.form.values)) if (dialogProps.beforeClose) { setTimeout(() => { dialogProps.beforeClose(() => { formDialog.close() }) }) } else { formDialog.close() } }) .catch(() => {}) }, async () => { await loading(dialogProps.loadingText, () => applyMiddleware(env.form, env.cancelMiddlewares) ) if (dialogProps.beforeClose) { dialogProps.beforeClose(() => { formDialog.close() }) } else { formDialog.close() } } ) }) return env.promise }, close: () => { if (!env.root) return render(false) }, } return formDialog } const FormDialogFooter = defineComponent({ name: 'FFormDialogFooter', setup(props, { slots }) { return () => { return h( Portal, { props: { to: PORTAL_TARGET_NAME, }, }, slots ) } }, }) FormDialog.Footer = FormDialogFooter FormDialog.Portal = createPortalProvider('form-dialog') export default FormDialog ``` -------------------------------------------------------------------------------- /docs/guide/issue-helper.md: -------------------------------------------------------------------------------- ```markdown # Issue Helper ## Before You Start... 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. For usage questions, please use the following resources: - Read the introduce and components documentation - Make sure you have search your question in FAQ and changelog - Look for / ask questions on [Discussions](https://github.com/alibaba/formily/discussions) Also try to search for your issue 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. ```tsx import React from 'react' import { createForm, onFieldMount, onFieldReact } from '@formily/core' import { Field, VoidField } from '@formily/react' import { Form, Input, Select, Radio, FormItem, FormButtonGroup, Submit, } from '@formily/antd' import semver from 'semver' import ReactMde from 'react-mde' import * as Showdown from 'showdown' import 'react-mde/lib/styles/css/react-mde-all.css' const converter = new Showdown.Converter({ tables: true, simplifiedAutoLink: true, strikethrough: true, tasklists: true, }) const MdInput = ({ value, onChange }) => { const [selectedTab, setSelectedTab] = React.useState('write') return ( <div style={{ fontSize: 12, lineHeight: 1 }}> <ReactMde value={value} onChange={onChange} selectedTab={selectedTab} onTabChange={setSelectedTab} generateMarkdownPreview={(markdown) => Promise.resolve( `<div class="markdown" style="margin:0 20px;">${ converter.makeHtml(markdown) || '' }</div>` ) } /> </div> ) } const form = createForm({ validateFirst: true, effects() { onFieldMount('version', async (field) => { const { versions: unsort } = await fetch( 'https://registry.npmmirror.com/@formily/core' ).then((res) => res.json()) const versions = Object.keys(unsort).sort((v1, v2) => semver.gte(v1, v2) ? -1 : 1 ) field.dataSource = versions.map((version) => ({ label: version, value: version, })) }) onFieldMount('package', async (field) => { const packages = await fetch( 'https://formilyjs.org/.netlify/functions/npm-search?q=@formily' ).then((res) => res.json()) field.dataSource = packages.map(({ name }) => { return { label: name, value: name, } }) }) onFieldReact('bug-desc', (field) => { field.visible = field.query('type').value() === 'Bug Report' }) onFieldReact('feature-desc', (field) => { field.visible = field.query('type').value() === 'Feature Request' }) }, }) const createIssueURL = ({ type, title, version, package: pkg, reproduceLink, reproduceStep, expected, actually, comment, feature, api, }) => { const url = new URL('https://github.com/alibaba/formily/issues/new') const bugInfo = ` - [ ] I have searched the [issues](https://github.com/alibaba/formily/issues) of this repository and believe that this is not a duplicate. ### Reproduction link [](${ reproduceLink || '' }) ### Steps to reproduce ${reproduceStep || ''} ### What is expected? ${expected || ''} ### What is actually happening? ${actually || ''} ### Package ${pkg}@${version} --- ${comment || ''} <!-- generated by formily-issue-helper. DO NOT REMOVE --> ` const prInfo = ` - [ ] I have searched the [issues](https://github.com/alibaba/formily/issues) of this repository and believe that this is not a duplicate. ### What problem does this feature solve? ${feature || ''} ### What does the proposed API look like? ${api || ''} <!-- generated by formily-issue-helper. DO NOT REMOVE --> ` url.searchParams.set('title', `[${type}] ${title}`) url.searchParams.set('body', type === 'Bug Report' ? bugInfo : prInfo) return url.href } export default () => { return ( <Form form={form} layout="vertical" size="large"> <Field title="This is a" name="type" required initialValue="Bug Report" decorator={[FormItem]} component={[Radio.Group, { optionType: 'button' }]} dataSource={[ { label: 'Bug Report', value: 'Bug Report' }, { label: 'Feature Request', value: 'Feature Request' }, ]} /> <Field title="Title" name="title" required decorator={[FormItem]} component={[Input]} /> <VoidField name="bug-desc"> <Field title="Package" name="package" required decorator={[FormItem]} component={[Select, { showSearch: true }]} /> <Field title="Version" description="Check if the issue is reproducible with the latest stable version." name="version" required decorator={[FormItem]} component={[Select, { showSearch: true }]} /> <Field title="Link to minimal reproduction" name="reproduceLink" decorator={[FormItem]} component={[Input]} required validator={[ 'url', (value) => { return /\/\/(codesandbox\.io|github)/.test(value) ? '' : 'Must Be Codesandbox Link or Github Repo' }, ]} description={ <div> This is Codesandbox templates.If you are: <ul> <li> React + Antd User: <ul> <li> <a href="https://codesandbox.io/s/formily-react-antd-pure-jsx-omncis" target="_blank" rel="noreferrer" > Pure JSX </a> </li> <li> <a href="https://codesandbox.io/s/formily-react-antd-markup-schema-fvpevx" target="_blank" rel="noreferrer" > Markup Schema </a> </li> <li> <a href="https://codesandbox.io/s/formily-react-antd-json-schema-28p0fh" target="_blank" rel="noreferrer" > JSON Schema </a> </li> </ul> </li> <li> React + Fusion User: <ul> <li> <a href="https://codesandbox.io/s/formily-react-next-pure-jsx-ji9iiu" target="_blank" rel="noreferrer" > Pure JSX </a> </li> <li> <a href="https://codesandbox.io/s/formily-react-next-markup-schema-i7dm17" target="_blank" rel="noreferrer" > Markup Schema </a> </li> <li> <a href="https://codesandbox.io/s/formily-react-next-json-schema-1lm35h" target="_blank" rel="noreferrer" > JSON Schema </a> </li> </ul> </li> <li> Vue3 + ant-design-vue User: <ul> <li> <a href="https://codesandbox.io/s/formily-antd-vue-pure-jsx-pp3gvv" target="_blank" rel="noreferrer" > Pure JSX </a> </li> <li> <a href="https://codesandbox.io/s/formily-vue-ant-design-vue-markup-schema-donivp" target="_blank" rel="noreferrer" > Markup Schema </a> </li> <li> <a href="https://codesandbox.io/s/formily-vue-ant-design-vue-json-schema-25g4z1" target="_blank" rel="noreferrer" > JSON Schema </a> </li> </ul> </li> </ul> </div> } /> <Field title="Step to reproduce" 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." name="reproduceStep" decorator={[FormItem]} component={[MdInput]} required /> <Field title="What is expected?" name="expected" decorator={[FormItem]} component={[MdInput]} required /> <Field title="What is actually happening?" name="actually" decorator={[FormItem]} component={[MdInput]} required /> <Field title="Any additional comments? (optional)" name="comment" decorator={[FormItem]} component={[MdInput]} /> </VoidField> <VoidField name="feature-desc"> <Field title="What problem does this feature solve?" description={ <div> <p> Explain your use case, context, and rationale behind this feature request. More importantly, what is the end user experience you are trying to build that led to the need for this feature? </p> <p> An important design goal of Formily is keeping the API surface small and straightforward. In general, we only consider adding new features that solve a problem that cannot be easily dealt with using existing APIs (i.e. not just an alternative way of doing things that can already be done). The problem should also be common enough to justify the addition. </p> </div> } name="feature" required decorator={[FormItem]} component={[MdInput]} /> <Field title="What does the proposed API look like?" description="Describe how you propose to solve the problem and provide code samples of how the API would work once implemented." name="api" required decorator={[FormItem]} component={[MdInput]} /> </VoidField> <FormButtonGroup.Sticky align="center"> <Submit size="large" onSubmit={(values) => { window.open(createIssueURL(values)) }} > Submit </Submit> </FormButtonGroup.Sticky> </Form> ) } ``` ``` -------------------------------------------------------------------------------- /packages/reactive/src/__tests__/batch.spec.ts: -------------------------------------------------------------------------------- ```typescript import { observable, batch, autorun, reaction } from '..' import { define } from '../model' describe('normal batch', () => { test('no batch', () => { const obs = observable({ aa: { bb: 123, }, }) const handler = jest.fn() autorun(() => { handler(obs.aa.bb) }) obs.aa.bb = 111 obs.aa.bb = 222 expect(handler).toBeCalledTimes(3) obs.aa.bb = 333 obs.aa.bb = 444 expect(handler).toBeCalledTimes(5) }) test('batch', () => { const obs = observable({ aa: { bb: 123, }, }) const handler = jest.fn() autorun(() => { handler(obs.aa.bb) }) obs.aa.bb = 111 obs.aa.bb = 222 expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(222) batch(() => { obs.aa.bb = 333 obs.aa.bb = 444 }) batch(() => {}) batch() expect(handler).toBeCalledTimes(4) expect(handler).lastCalledWith(444) }) test('batch track', () => { const obs = observable({ aa: { bb: 123, }, cc: 1, }) const handler = jest.fn() autorun(() => { batch(() => { if (obs.cc > 0) { handler(obs.aa.bb) obs.cc = obs.cc + 20 } }) }) expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) obs.aa.bb = 321 expect(handler).toBeCalledTimes(2) expect(obs.cc).toEqual(41) }) test('batch.bound', () => { const obs = observable({ aa: { bb: 123, }, }) const handler = jest.fn() const setData = batch.bound(() => { obs.aa.bb = 333 obs.aa.bb = 444 }) autorun(() => { handler(obs.aa.bb) }) obs.aa.bb = 111 obs.aa.bb = 222 expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(222) setData() batch(() => {}) expect(handler).toBeCalledTimes(4) expect(handler).lastCalledWith(444) }) test('batch.bound track', () => { const obs = observable({ aa: { bb: 123, }, cc: 1, }) const handler = jest.fn() autorun(() => { batch.bound(() => { if (obs.cc > 0) { handler(obs.aa.bb) obs.cc = obs.cc + 20 } })() }) expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) obs.aa.bb = 321 expect(handler).toBeCalledTimes(2) expect(obs.cc).toEqual(41) }) test('batch.scope', () => { const obs = observable<any>({}) const handler = jest.fn() autorun(() => { handler(obs.aa, obs.bb, obs.cc, obs.dd) }) batch(() => { batch.scope(() => { obs.aa = 123 }) batch.scope(() => { obs.cc = 'ccccc' }) obs.bb = 321 obs.dd = 'ddddd' }) expect(handler).toBeCalledTimes(4) expect(handler).nthCalledWith(1, undefined, undefined, undefined, undefined) expect(handler).nthCalledWith(2, 123, undefined, undefined, undefined) expect(handler).nthCalledWith(3, 123, undefined, 'ccccc', undefined) expect(handler).nthCalledWith(4, 123, 321, 'ccccc', 'ddddd') }) test('batch.scope bound', () => { const obs = observable<any>({}) const handler = jest.fn() autorun(() => { handler(obs.aa, obs.bb, obs.cc, obs.dd) }) const scope1 = batch.scope.bound(() => { obs.aa = 123 }) batch(() => { scope1() batch.scope.bound(() => { obs.cc = 'ccccc' })() obs.bb = 321 obs.dd = 'ddddd' }) expect(handler).toBeCalledTimes(4) expect(handler).nthCalledWith(1, undefined, undefined, undefined, undefined) expect(handler).nthCalledWith(2, 123, undefined, undefined, undefined) expect(handler).nthCalledWith(3, 123, undefined, 'ccccc', undefined) expect(handler).nthCalledWith(4, 123, 321, 'ccccc', 'ddddd') }) test('batch.scope track', () => { const obs = observable({ aa: { bb: 123, }, cc: 1, }) const handler = jest.fn() autorun(() => { batch.scope(() => { if (obs.cc > 0) { handler(obs.aa.bb) obs.cc = obs.cc + 20 } }) }) expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) obs.aa.bb = 321 expect(handler).toBeCalledTimes(2) expect(obs.cc).toEqual(41) }) test('batch.scope bound track', () => { const obs = observable({ aa: { bb: 123, }, cc: 1, }) const handler = jest.fn() autorun(() => { batch.scope.bound(() => { if (obs.cc > 0) { handler(obs.aa.bb) obs.cc = obs.cc + 20 } })() }) expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) obs.aa.bb = 321 expect(handler).toBeCalledTimes(2) expect(obs.cc).toEqual(41) }) test('batch error', () => { let error = null try { batch(() => { throw '123' }) } catch (e) { error = e } expect(error).toEqual('123') }) }) describe('annotation batch', () => { test('batch', () => { const obs = define( { aa: { bb: 123, }, setData() { this.aa.bb = 333 this.aa.bb = 444 }, }, { aa: observable, setData: batch, } ) const handler = jest.fn() autorun(() => { handler(obs.aa.bb) }) obs.aa.bb = 111 obs.aa.bb = 222 expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(222) obs.setData() expect(handler).toBeCalledTimes(4) expect(handler).lastCalledWith(444) }) test('batch track', () => { const obs = define( { aa: { bb: 123, }, cc: 1, setData() { if (obs.cc > 0) { handler(obs.aa.bb) obs.cc = obs.cc + 20 } }, }, { aa: observable, setData: batch, } ) const handler = jest.fn() autorun(() => { obs.setData() }) expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) obs.aa.bb = 321 expect(handler).toBeCalledTimes(2) expect(obs.cc).toEqual(41) }) test('batch.bound', () => { const obs = define( { aa: { bb: 123, }, setData() { this.aa.bb = 333 this.aa.bb = 444 }, }, { aa: observable, setData: batch.bound, } ) const handler = jest.fn() autorun(() => { handler(obs.aa.bb) }) obs.aa.bb = 111 obs.aa.bb = 222 expect(handler).toBeCalledTimes(3) expect(handler).lastCalledWith(222) obs.setData() expect(handler).toBeCalledTimes(4) expect(handler).lastCalledWith(444) }) test('batch.bound track', () => { const obs = define( { aa: { bb: 123, }, cc: 1, setData() { if (obs.cc > 0) { handler(obs.aa.bb) obs.cc = obs.cc + 20 } }, }, { aa: observable, setData: batch.bound, } ) const handler = jest.fn() autorun(() => { obs.setData() }) expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) obs.aa.bb = 321 expect(handler).toBeCalledTimes(2) expect(obs.cc).toEqual(41) }) test('batch.scope', () => { const obs = define( { aa: null, bb: null, cc: null, dd: null, scope1() { this.aa = 123 }, scope2() { this.cc = 'ccccc' }, }, { aa: observable, bb: observable, cc: observable, dd: observable, scope1: batch.scope, scope2: batch.scope, } ) const handler = jest.fn() autorun(() => { handler(obs.aa, obs.bb, obs.cc, obs.dd) }) batch(() => { obs.scope1() obs.scope2() obs.bb = 321 obs.dd = 'ddddd' }) expect(handler).toBeCalledTimes(4) expect(handler).nthCalledWith(1, null, null, null, null) expect(handler).nthCalledWith(2, 123, null, null, null) expect(handler).nthCalledWith(3, 123, null, 'ccccc', null) expect(handler).nthCalledWith(4, 123, 321, 'ccccc', 'ddddd') }) test('batch.scope bound', () => { const obs = define( { aa: null, bb: null, cc: null, dd: null, scope1() { this.aa = 123 }, scope2() { this.cc = 'ccccc' }, }, { aa: observable, bb: observable, cc: observable, dd: observable, scope1: batch.scope.bound, scope2: batch.scope.bound, } ) const handler = jest.fn() autorun(() => { handler(obs.aa, obs.bb, obs.cc, obs.dd) }) batch(() => { obs.scope1() obs.scope2() obs.bb = 321 obs.dd = 'ddddd' }) expect(handler).toBeCalledTimes(4) expect(handler).nthCalledWith(1, null, null, null, null) expect(handler).nthCalledWith(2, 123, null, null, null) expect(handler).nthCalledWith(3, 123, null, 'ccccc', null) expect(handler).nthCalledWith(4, 123, 321, 'ccccc', 'ddddd') }) test('batch.scope track', () => { const obs = define( { aa: { bb: 123, }, cc: 1, scope() { if (this.cc > 0) { handler(this.aa.bb) this.cc = this.cc + 20 } }, }, { aa: observable, cc: observable, scope: batch.scope, } ) const handler = jest.fn() autorun(() => { obs.scope() }) expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) obs.aa.bb = 321 expect(handler).toBeCalledTimes(2) expect(obs.cc).toEqual(41) }) test('batch.scope bound track', () => { const obs = define( { aa: { bb: 123, }, cc: 1, scope() { if (this.cc > 0) { handler(this.aa.bb) this.cc = this.cc + 20 } }, }, { aa: observable, cc: observable, scope: batch.scope.bound, } ) const handler = jest.fn() autorun(() => { obs.scope() }) expect(handler).toBeCalledTimes(1) expect(obs.cc).toEqual(21) obs.aa.bb = 321 expect(handler).toBeCalledTimes(2) expect(obs.cc).toEqual(41) }) }) describe('batch endpoint', () => { test('normal endpoint', () => { const tokens = [] const inner = batch.bound(() => { batch.endpoint(() => { tokens.push('endpoint') }) tokens.push('inner') }) const wrapper = batch.bound(() => { inner() tokens.push('wrapper') }) wrapper() expect(tokens).toEqual(['inner', 'wrapper', 'endpoint']) }) test('unexpect endpoint', () => { const tokens = [] const inner = batch.bound(() => { batch.endpoint() tokens.push('inner') }) const wrapper = batch.bound(() => { inner() tokens.push('wrapper') }) wrapper() expect(tokens).toEqual(['inner', 'wrapper']) }) test('no wrapper endpoint', () => { const tokens = [] batch.endpoint(() => { tokens.push('endpoint') }) expect(tokens).toEqual(['endpoint']) }) }) test('reaction collect in batch valid', () => { const obs = observable({ aa: 11, bb: 22, cc: 33, }) reaction( () => obs.aa, () => { void obs.cc } ) const fn = jest.fn() autorun(() => { batch.scope(() => { obs.aa = obs.bb }) fn() }) obs.bb = 44 expect(fn).toBeCalledTimes(2) }) test('reaction collect in batch invalid', () => { const obs = observable({ aa: 11, bb: 22, cc: 33, }) reaction( () => obs.aa, () => { void obs.cc } ) const fn = jest.fn() autorun(() => { batch.scope(() => { obs.aa = obs.bb }) fn() }) obs.bb = 44 obs.cc = 55 expect(fn).toBeCalledTimes(3) }) ``` -------------------------------------------------------------------------------- /packages/antd/docs/components/Select.md: -------------------------------------------------------------------------------- ```markdown # Select > Drop-down box components ## Markup Schema synchronization data source case ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Select, FormItem, }, }) const form = createForm() export default () => ( <FormProvider form={form}> <SchemaField> <SchemaField.Number name="select" title="select box" x-decorator="FormItem" x-component="Select" enum={[ { label: 'Option 1', value: 1 }, { label: 'Option 2', value: 2 }, ]} x-component-props={{ style: { width: 120, }, }} /> </SchemaField> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) ``` ## Markup Schema Asynchronous Search Case ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm, onFieldReact, onFieldInit, FormPathPattern, Field, } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action, observable } from '@formily/reactive' import { fetch } from 'mfetch' let timeout let currentValue function fetchData(value, callback) { if (timeout) { clearTimeout(timeout) timeout = null } currentValue = value function fake() { fetch(`https://suggest.taobao.com/sug?q=${value}`, { method: 'jsonp', }) .then((response) => response.json()) .then((d) => { if (currentValue === value) { const { result } = d const data = [] result.forEach((r) => { data.push({ value: r[0], text: r[0], }) }) callback(data) } }) } timeout = setTimeout(fake, 300) } const SchemaField = createSchemaField({ components: { Select, FormItem, }, }) const useAsyncDataSource = ( pattern: FormPathPattern, service: (param: { keyword: string field: Field }) => Promise<{ label: string; value: any }[]> ) => { const keyword = observable.ref('') onFieldInit(pattern, (field) => { field.setComponentProps({ onSearch: (value) => { keyword.value = value }, }) }) onFieldReact(pattern, (field) => { field.loading = true service({ field, keyword: keyword.value }).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) }) } const form = createForm({ effects: () => { useAsyncDataSource('select', async ({ keyword }) => { if (!keyword) { return [] } return new Promise((resolve) => { fetchData(keyword, resolve) }) }) }, }) export default () => ( <FormProvider form={form}> <SchemaField> <SchemaField.String name="select" title="Asynchronous search select box" x-decorator="FormItem" x-component="Select" x-component-props={{ showSearch: true, filterOption: false, style: { width: 300, }, }} /> </SchemaField> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) ``` ## Markup Schema Asynchronous Linkage Data Source Case ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm, onFieldReact, FormPathPattern, Field } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Select, FormItem, }, }) const useAsyncDataSource = ( pattern: FormPathPattern, service: (field: Field) => Promise<{ label: string; value: any }[]> ) => { onFieldReact(pattern, (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) }) } const form = createForm({ effects: () => { useAsyncDataSource('select', async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', }, { label: 'BBB', value: 'ccc', }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', }, { label: 'DDD', value: 'ddd', }, ]) } }, 1500) }) }) }, }) export default () => ( <FormProvider form={form}> <SchemaField> <SchemaField.Number name="linkage" title="Linkage selection box" x-decorator="FormItem" x-component="Select" enum={[ { label: 'Request 1', value: 1 }, { label: 'Request 2', value: 2 }, ]} x-component-props={{ style: { width: 120, }, }} /> <SchemaField.String name="select" title="Asynchronous select box" x-decorator="FormItem" x-component="Select" x-component-props={{ style: { width: 120, }, }} /> </SchemaField> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) ``` ## JSON Schema synchronization data source case ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Select, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { select: { type: 'string', title: 'Select box', 'x-decorator': 'FormItem', 'x-component': 'Select', enum: [ { label: 'Option 1', value: 1 }, { label: 'Option 2', value: 2 }, ], 'x-component-props': { style: { width: 120, }, }, }, }, } export default () => ( <FormProvider form={form}> <SchemaField schema={schema} /> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) ``` ## JSON Schema asynchronous linkage data source case ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { action } from '@formily/reactive' const SchemaField = createSchemaField({ components: { Select, FormItem, }, }) const loadData = async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', }, { label: 'BBB', value: 'ccc', }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', }, { label: 'DDD', value: 'ddd', }, ]) } }, 1500) }) } const useAsyncDataSource = (service) => (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) } const form = createForm() const schema = { type: 'object', properties: { linkage: { type: 'string', title: 'Linkage selection box', enum: [ { label: 'Request 1', value: 1 }, { label: 'Request 2', value: 2 }, ], 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { style: { width: 120, }, }, }, select: { type: 'string', title: 'Asynchronous selection box', 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { style: { width: 120, }, }, 'x-reactions': ['{{useAsyncDataSource(loadData)}}'], }, }, } export default () => ( <FormProvider form={form}> <SchemaField schema={schema} scope={{ useAsyncDataSource, loadData }} /> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) ``` ## Pure JSX synchronization data source case ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( <FormProvider form={form}> <Field name="select" title="select box" dataSource={[ { label: 'Option 1', value: 1 }, { label: 'Option 2', value: 2 }, ]} decorator={[FormItem]} component={[ Select, { style: { width: 120, }, }, ]} /> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) ``` ## Pure JSX asynchronous linkage data source case ```tsx import React from 'react' import { Select, FormItem, FormButtonGroup, Submit } from '@formily/antd' import { createForm, onFieldReact, FormPathPattern, Field as FieldType, } from '@formily/core' import { FormProvider, Field } from '@formily/react' import { action } from '@formily/reactive' const useAsyncDataSource = ( pattern: FormPathPattern, service: (field: FieldType) => Promise<{ label: string; value: any }[]> ) => { onFieldReact(pattern, (field) => { field.loading = true service(field).then( action.bound((data) => { field.dataSource = data field.loading = false }) ) }) } const form = createForm({ effects: () => { useAsyncDataSource('select', async (field) => { const linkage = field.query('linkage').get('value') if (!linkage) return [] return new Promise((resolve) => { setTimeout(() => { if (linkage === 1) { resolve([ { label: 'AAA', value: 'aaa', }, { label: 'BBB', value: 'ccc', }, ]) } else if (linkage === 2) { resolve([ { label: 'CCC', value: 'ccc', }, { label: 'DDD', value: 'ddd', }, ]) } }, 1500) }) }) }, }) export default () => ( <FormProvider form={form}> <Field name="linkage" title="Linkage selection box" dataSource={[ { label: 'Request 1', value: 1 }, { label: 'Request 2', value: 2 }, ]} decorator={[FormItem]} component={[ Select, { style: { width: 120, }, }, ]} /> <Field name="select" title="Asynchronous select box" decorator={[FormItem]} component={[ Select, { style: { width: 120, }, }, ]} /> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) ``` ## API Reference https://ant.design/components/select-cn/ ``` -------------------------------------------------------------------------------- /packages/next/docs/components/ArrayCards.zh-CN.md: -------------------------------------------------------------------------------- ```markdown # ArrayCards > 卡片列表,对于每行字段数量较多,联动较多的场景比较适合使用 ArrayCards > > 注意:该组件只适用于 Schema 场景 ## Markup Schema 案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCards, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCards, }, }) const form = createForm() export default () => { return ( <FormProvider form={form}> <SchemaField> <SchemaField.Array name="string_array" maxItems={3} x-decorator="FormItem" x-component="ArrayCards" x-component-props={{ title: '字符串数组', }} > <SchemaField.Void> <SchemaField.Void x-component="ArrayCards.Index" /> <SchemaField.String name="input" x-decorator="FormItem" title="Input" required x-component="Input" /> <SchemaField.Void x-component="ArrayCards.Remove" /> <SchemaField.Void x-component="ArrayCards.Copy" /> <SchemaField.Void x-component="ArrayCards.MoveUp" /> <SchemaField.Void x-component="ArrayCards.MoveDown" /> </SchemaField.Void> <SchemaField.Void x-component="ArrayCards.Addition" title="添加条目" /> </SchemaField.Array> <SchemaField.Array name="array" maxItems={3} x-decorator="FormItem" x-component="ArrayCards" x-component-props={{ title: '对象数组', }} > <SchemaField.Object> <SchemaField.Void x-component="ArrayCards.Index" /> <SchemaField.String name="input" x-decorator="FormItem" title="Input" required x-component="Input" /> <SchemaField.Void x-component="ArrayCards.Remove" /> <SchemaField.Void x-component="ArrayCards.MoveUp" /> <SchemaField.Void x-component="ArrayCards.MoveDown" /> </SchemaField.Object> <SchemaField.Void x-component="ArrayCards.Addition" title="添加条目" /> </SchemaField.Array> </SchemaField> <FormButtonGroup> <Submit onSubmit={console.log}>提交</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCards, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCards, }, }) const form = createForm() const schema = { type: 'object', properties: { string_array: { type: 'array', 'x-component': 'ArrayCards', maxItems: 3, 'x-decorator': 'FormItem', 'x-component-props': { title: '字符串数组', }, items: { type: 'void', properties: { index: { type: 'void', 'x-component': 'ArrayCards.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCards.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCards.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCards.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayCards.Addition', }, }, }, array: { type: 'array', 'x-component': 'ArrayCards', maxItems: 3, 'x-decorator': 'FormItem', 'x-component-props': { title: '对象数组', }, items: { type: 'object', properties: { index: { type: 'void', 'x-component': 'ArrayCards.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCards.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCards.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCards.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayCards.Addition', }, }, }, }, } export default () => { return ( <FormProvider form={form}> <SchemaField schema={schema} /> <FormButtonGroup> <Submit onSubmit={console.log}>提交</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## Effects 联动案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCards, FormButtonGroup, Submit, } from '@formily/next' import { createForm, onFieldChange, onFieldReact } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCards, }, }) const form = createForm({ effects: () => { //主动联动模式 onFieldChange('array.*.aa', ['value'], (field, form) => { form.setFieldState(field.query('.bb'), (state) => { state.visible = field.value != '123' }) }) //被动联动模式 onFieldReact('array.*.dd', (field) => { field.visible = field.query('.cc').get('value') != '123' }) }, }) export default () => { return ( <FormProvider form={form}> <SchemaField> <SchemaField.Array name="array" maxItems={3} x-component="ArrayCards" x-decorator="FormItem" x-component-props={{ title: '对象数组', }} > <SchemaField.Object> <SchemaField.Void x-component="ArrayCards.Index" /> <SchemaField.String name="aa" x-decorator="FormItem" title="AA" required description="AA输入123时隐藏BB" x-component="Input" /> <SchemaField.String name="bb" x-decorator="FormItem" title="BB" required x-component="Input" /> <SchemaField.String name="cc" x-decorator="FormItem" title="CC" required description="CC输入123时隐藏DD" x-component="Input" /> <SchemaField.String name="dd" x-decorator="FormItem" title="DD" required x-component="Input" /> <SchemaField.Void x-component="ArrayCards.Remove" /> <SchemaField.Void x-component="ArrayCards.MoveUp" /> <SchemaField.Void x-component="ArrayCards.MoveDown" /> </SchemaField.Object> <SchemaField.Void x-component="ArrayCards.Addition" title="添加条目" /> </SchemaField.Array> </SchemaField> <FormButtonGroup> <Submit onSubmit={console.log}>提交</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## JSON Schema 联动案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCards, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCards, }, }) const form = createForm() const schema = { type: 'object', properties: { array: { type: 'array', 'x-component': 'ArrayCards', maxItems: 3, title: '对象数组', items: { type: 'object', properties: { index: { type: 'void', 'x-component': 'ArrayCards.Index', }, aa: { type: 'string', 'x-decorator': 'FormItem', title: 'AA', required: true, 'x-component': 'Input', description: '输入123', }, bb: { type: 'string', title: 'BB', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-reactions': [ { dependencies: ['.aa'], when: "{{$deps[0] != '123'}}", fulfill: { schema: { title: 'BB', 'x-disabled': true, }, }, otherwise: { schema: { title: 'Changed', 'x-disabled': false, }, }, }, ], }, remove: { type: 'void', 'x-component': 'ArrayCards.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCards.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCards.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayCards.Addition', }, }, }, }, } export default () => { return ( <FormProvider form={form}> <SchemaField schema={schema} /> <FormButtonGroup> <Submit onSubmit={console.log}>提交</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## API ### ArrayCards 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ---------- | ------------------------- | ------------ | ------ | | onAdd | `(index: number) => void` | 增加方法 | | | onRemove | `(index: number) => void` | 删除方法 | | | onCopy | `(index: number) => void` | 复制方法 | | | onMoveUp | `(index: number) => void` | 向上移动方法 | | | onMoveDown | `(index: number) => void` | 向下移动方法 | | 其余参考 https://fusion.design/pc/component/basic/card ### ArrayCards.Addition > 添加按钮 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------------ | --------------------- | -------- | -------- | | title | ReactText | 文案 | | | method | `'push' \| 'unshift'` | 添加方式 | `'push'` | | defaultValue | `any` | 默认值 | | 其余参考 https://fusion.design/pc/component/basic/button 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCards.Copy > 复制按钮 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------------------- | -------- | -------- | | title | ReactText | 文案 | | | method | `'push' \| 'unshift'` | 添加方式 | `'push'` | 其余参考 https://fusion.design/pc/component/basic/button 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCards.Remove > 删除按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCards.MoveDown > 下移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCards.MoveUp > 上移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCards.Index > 索引渲染器 无属性 ### ArrayCards.useIndex > 读取当前渲染行索引的 React Hook ### ArrayCards.useRecord > 读取当前渲染记录的 React Hook ``` -------------------------------------------------------------------------------- /packages/antd/src/array-table/index.tsx: -------------------------------------------------------------------------------- ```typescript import React, { Fragment, useState, useRef, useEffect, createContext, useContext, useCallback, } from 'react' import { Table, Pagination, Space, Select, Badge } from 'antd' import { PaginationProps } from 'antd/lib/pagination' import { TableProps, ColumnProps } from 'antd/lib/table' import { SelectProps } from 'antd/lib/select' import cls from 'classnames' import { GeneralField, FieldDisplayTypes, ArrayField } from '@formily/core' import { useField, observer, useFieldSchema, RecursionField, ReactFC, } from '@formily/react' import { isArr, isBool, isFn } from '@formily/shared' import { Schema } from '@formily/json-schema' import { usePrefixCls, SortableContainer, SortableElement, } from '../__builtins__' import { ArrayBase, ArrayBaseMixins, IArrayBaseProps } from '../array-base' interface ObservableColumnSource { field: GeneralField columnProps: ColumnProps<any> schema: Schema display: FieldDisplayTypes name: string } interface IArrayTablePaginationProps extends PaginationProps { dataSource?: any[] showPagination?: boolean children?: ( dataSource: any[], pagination: React.ReactNode, options: { startIndex: number } ) => React.ReactElement } interface IStatusSelectProps extends SelectProps<any> { pageSize?: number } type ComposedArrayTable = React.FC< React.PropsWithChildren<TableProps<any> & IArrayBaseProps> > & ArrayBaseMixins & { Column?: React.FC<React.PropsWithChildren<ColumnProps<any>>> } interface PaginationAction { totalPage?: number pageSize?: number showPagination?: boolean changePage?: (page: number) => void } const SortableRow = SortableElement((props: any) => <tr {...props} />) const SortableBody = SortableContainer((props: any) => <tbody {...props} />) const isColumnComponent = (schema: Schema) => { return schema['x-component']?.indexOf('Column') > -1 } const isOperationsComponent = (schema: Schema) => { return schema['x-component']?.indexOf('Operations') > -1 } const isAdditionComponent = (schema: Schema) => { return schema['x-component']?.indexOf('Addition') > -1 } const useArrayTableSources = () => { const arrayField = useField() const schema = useFieldSchema() const parseSources = (schema: Schema): ObservableColumnSource[] => { if ( isColumnComponent(schema) || isOperationsComponent(schema) || isAdditionComponent(schema) ) { if (!schema['x-component-props']?.['dataIndex'] && !schema['name']) return [] const name = schema['x-component-props']?.['dataIndex'] || schema['name'] const field = arrayField.query(arrayField.address.concat(name)).take() const columnProps = field?.component?.[1] || schema['x-component-props'] || {} const display = field?.display || schema['x-display'] || 'visible' return [ { name, display, field, schema, columnProps, }, ] } else if (schema.properties) { return schema.reduceProperties((buf, schema) => { return buf.concat(parseSources(schema)) }, []) } } const parseArrayItems = (schema: Schema['items']) => { if (!schema) return [] const sources: ObservableColumnSource[] = [] const items = isArr(schema) ? schema : [schema] return items.reduce((columns, schema) => { const item = parseSources(schema) if (item) { return columns.concat(item) } return columns }, sources) } if (!schema) throw new Error('can not found schema object') return parseArrayItems(schema.items) } const useArrayTableColumns = ( dataSource: any[], field: ArrayField, sources: ObservableColumnSource[] ): TableProps<any>['columns'] => { return sources.reduce((buf, { name, columnProps, schema, display }, key) => { if (display !== 'visible') return buf if (!isColumnComponent(schema)) return buf return buf.concat({ ...columnProps, key, dataIndex: name, render: (value: any, record: any) => { const index = dataSource?.indexOf(record) const children = ( <ArrayBase.Item index={index} record={() => field?.value?.[index]}> <RecursionField schema={schema} name={index} onlyRenderProperties /> </ArrayBase.Item> ) return children }, }) }, []) } const useAddition = () => { const schema = useFieldSchema() return schema.reduceProperties((addition, schema, key) => { if (isAdditionComponent(schema)) { return <RecursionField schema={schema} name={key} /> } return addition }, null) } const schedulerRequest = { request: null, } const StatusSelect: ReactFC<IStatusSelectProps> = observer( (props) => { const field = useField<ArrayField>() const prefixCls = usePrefixCls('formily-array-table') const errors = field.errors const parseIndex = (address: string) => { return Number( address .slice(address.indexOf(field.address.toString()) + 1) .match(/(\d+)/)?.[1] ) } const options = props.options?.map(({ label, value }) => { const val = Number(value) const hasError = errors.some(({ address }) => { const currentIndex = parseIndex(address) const startIndex = (val - 1) * props.pageSize const endIndex = val * props.pageSize return currentIndex >= startIndex && currentIndex <= endIndex }) return { label: hasError ? <Badge dot>{label}</Badge> : label, value, } }) const width = String(options?.length).length * 15 return ( <Select value={props.value} onChange={props.onChange} options={options} virtual style={{ width: width < 60 ? 60 : width, }} className={cls(`${prefixCls}-status-select`, { 'has-error': errors?.length, })} /> ) }, { scheduler: (update) => { clearTimeout(schedulerRequest.request) schedulerRequest.request = setTimeout(() => { update() }, 100) }, } ) const PaginationContext = createContext<PaginationAction>({}) const usePagination = () => { return useContext(PaginationContext) } const ArrayTablePagination: ReactFC<IArrayTablePaginationProps> = (props) => { const [current, setCurrent] = useState(1) const prefixCls = usePrefixCls('formily-array-table') const showPagination = props.showPagination ?? true const pageSize = props.pageSize || 10 const size = props.size || 'default' const dataSource = props.dataSource || [] const startIndex = (current - 1) * pageSize const endIndex = startIndex + pageSize - 1 const total = dataSource?.length || 0 const totalPage = Math.ceil(total / pageSize) const pages = Array.from(new Array(totalPage)).map((_, index) => { const page = index + 1 return { label: page, value: page, } }) const handleChange = (current: number) => { setCurrent(current) } useEffect(() => { if (totalPage > 0 && totalPage < current) { handleChange(totalPage) } }, [totalPage, current]) const renderPagination = () => { if (totalPage <= 1 || !showPagination) return return ( <div className={`${prefixCls}-pagination`}> <Space> <StatusSelect value={current} pageSize={pageSize} onChange={handleChange} options={pages} notFoundContent={false} /> <Pagination {...props} pageSize={pageSize} current={current} total={dataSource.length} size={size} showSizeChanger={false} onChange={handleChange} /> </Space> </div> ) } return ( <Fragment> <PaginationContext.Provider value={{ totalPage, pageSize, changePage: handleChange, showPagination, }} > {props.children?.( showPagination ? dataSource?.slice(startIndex, endIndex + 1) : dataSource, renderPagination(), { startIndex } )} </PaginationContext.Provider> </Fragment> ) } const RowComp: ReactFC<React.HTMLAttributes<HTMLTableRowElement>> = (props) => { const prefixCls = usePrefixCls('formily-array-table') const index = props['data-row-key'] || 0 return ( <SortableRow lockAxis="y" {...props} index={index} className={cls(props.className, `${prefixCls}-row-${index + 1}`)} /> ) } export const ArrayTable: ComposedArrayTable = observer((props) => { const ref = useRef<HTMLDivElement>() const field = useField<ArrayField>() const prefixCls = usePrefixCls('formily-array-table') const dataSource = Array.isArray(field.value) ? field.value.slice() : [] const sources = useArrayTableSources() const columns = useArrayTableColumns(dataSource, field, sources) const pagination = isBool(props.pagination) ? { showPagination: props.pagination } : props.pagination const addition = useAddition() const { onAdd, onCopy, onRemove, onMoveDown, onMoveUp } = props const defaultRowKey = (record: any) => { return dataSource.indexOf(record) } const addTdStyles = (id: number) => { const node = ref.current?.querySelector(`.${prefixCls}-row-${id}`) const helper = document.body.querySelector(`.${prefixCls}-sort-helper`) if (!helper) return const tds = node?.querySelectorAll('td') if (!tds) return requestAnimationFrame(() => { helper.querySelectorAll('td').forEach((td, index) => { if (tds[index]) { td.style.width = getComputedStyle(tds[index]).width } }) }) } const getWrapperComp = useCallback( (dataSource: any[], start: number) => (props: any) => ( <SortableBody {...props} start={start} list={dataSource.slice()} accessibility={{ container: ref.current || undefined, }} onSortStart={(event) => { addTdStyles(event.active.id as number) }} onSortEnd={({ oldIndex, newIndex }) => { field.move(oldIndex, newIndex) }} className={cls(`${prefixCls}-sort-helper`, props.className)} /> ), [field] ) return ( <ArrayTablePagination {...pagination} dataSource={dataSource}> {(dataSource, pager, { startIndex }) => ( <div ref={ref} className={prefixCls}> <ArrayBase onAdd={onAdd} onCopy={onCopy} onRemove={onRemove} onMoveUp={onMoveUp} onMoveDown={onMoveDown} > <Table size="small" bordered rowKey={defaultRowKey} {...props} onChange={() => {}} pagination={false} columns={columns} dataSource={dataSource} components={{ body: { wrapper: getWrapperComp(dataSource, startIndex), row: RowComp, }, }} /> <div style={{ marginTop: 5, marginBottom: 5 }}>{pager}</div> {sources.map((column, key) => { //专门用来承接对Column的状态管理 if (!isColumnComponent(column.schema)) return return React.createElement(RecursionField, { name: column.name, schema: column.schema, onlyRenderSelf: true, key, }) })} {addition} </ArrayBase> </div> )} </ArrayTablePagination> ) }) ArrayTable.displayName = 'ArrayTable' ArrayTable.Column = () => { return <Fragment /> } ArrayBase.mixin(ArrayTable) const Addition: ArrayBaseMixins['Addition'] = (props) => { const array = ArrayBase.useArray() const { totalPage = 0, pageSize = 10, changePage, showPagination, } = usePagination() return ( <ArrayBase.Addition {...props} onClick={(e) => { // 如果添加数据后将超过当前页,则自动切换到下一页 const total = array?.field?.value.length || 0 if ( showPagination && total === totalPage * pageSize + 1 && isFn(changePage) ) { changePage(totalPage + 1) } props.onClick?.(e) }} /> ) } ArrayTable.Addition = Addition export default ArrayTable ``` -------------------------------------------------------------------------------- /packages/next/src/form-item/index.tsx: -------------------------------------------------------------------------------- ```typescript import React, { useState, useRef, useEffect } from 'react' import cls from 'classnames' import { usePrefixCls, pickDataProps, QuestionCircleOutlinedIcon, CloseCircleOutlinedIcon, CheckCircleOutlinedIcon, ExclamationCircleOutlinedIcon, } from '../__builtins__' import { isVoidField } from '@formily/core' import { connect, mapProps } from '@formily/react' import { useFormLayout, FormLayoutShallowContext } from '../form-layout' import { Balloon } from '@alifd/next' export interface IFormItemProps { className?: string style?: React.CSSProperties prefix?: string label?: React.ReactNode colon?: boolean layout?: 'vertical' | 'horizontal' | 'inline' tooltip?: React.ReactNode tooltipLayout?: 'icon' | 'text' tooltipIcon?: React.ReactNode labelFor?: string labelStyle?: React.CSSProperties labelAlign?: 'left' | 'right' labelWrap?: boolean labelWidth?: number | string wrapperWidth?: number | string labelCol?: number wrapperCol?: number wrapperAlign?: 'left' | 'right' wrapperWrap?: boolean wrapperStyle?: React.CSSProperties fullness?: boolean addonBefore?: React.ReactNode addonAfter?: React.ReactNode size?: 'small' | 'default' | 'large' inset?: boolean extra?: React.ReactNode feedbackText?: React.ReactNode feedbackLayout?: 'loose' | 'terse' | 'popover' | 'none' | (string & {}) feedbackStatus?: 'error' | 'warning' | 'success' | 'pending' | (string & {}) feedbackIcon?: React.ReactNode asterisk?: boolean gridSpan?: number bordered?: boolean } type ComposeFormItem = React.FC<React.PropsWithChildren<IFormItemProps>> & { BaseItem?: React.FC<React.PropsWithChildren<IFormItemProps>> } const useFormItemLayout = (props: IFormItemProps) => { const layout = useFormLayout() const layoutType = props.layout ?? layout.layout ?? 'horizontal' return { ...props, layout: layoutType, colon: props.colon ?? layout.colon, labelAlign: layoutType === 'vertical' ? props.labelAlign ?? 'left' : props.labelAlign ?? layout.labelAlign ?? 'right', labelWrap: props.labelWrap ?? layout.labelWrap, labelWidth: props.labelWidth ?? layout.labelWidth, wrapperWidth: props.wrapperWidth ?? layout.wrapperWidth, labelCol: props.labelCol ?? layout.labelCol, wrapperCol: props.wrapperCol ?? layout.wrapperCol, wrapperAlign: props.wrapperAlign ?? layout.wrapperAlign, wrapperWrap: props.wrapperWrap ?? layout.wrapperWrap, fullness: props.fullness ?? layout.fullness, size: props.size ?? layout.size, inset: props.inset ?? layout.inset, asterisk: props.asterisk, bordered: props.bordered ?? layout.bordered, feedbackIcon: props.feedbackIcon, feedbackLayout: props.feedbackLayout ?? layout.feedbackLayout ?? 'loose', tooltipLayout: props.tooltipLayout ?? layout.tooltipLayout ?? 'icon', tooltipIcon: props.tooltipIcon ?? layout.tooltipIcon ?? ( <QuestionCircleOutlinedIcon /> ), } } function useOverflow< Container extends HTMLElement, Content extends HTMLElement >() { const [overflow, setOverflow] = useState(false) const containerRef = useRef<Container>() const contentRef = useRef<Content>() const layout = useFormLayout() const labelCol = JSON.stringify(layout.labelCol) useEffect(() => { requestAnimationFrame(() => { if (containerRef.current && contentRef.current) { const contentWidth = contentRef.current.getBoundingClientRect().width const containerWidth = containerRef.current.getBoundingClientRect().width if (contentWidth && containerWidth && containerWidth < contentWidth) { if (!overflow) setOverflow(true) } else { if (overflow) setOverflow(false) } } }) }, [labelCol]) return { overflow, containerRef, contentRef, } } const ICON_MAP = { error: <CloseCircleOutlinedIcon />, success: <CheckCircleOutlinedIcon />, warning: <ExclamationCircleOutlinedIcon />, } export const BaseItem: React.FC<React.PropsWithChildren<IFormItemProps>> = ( props ) => { const { children, ...others } = props const [active, setActive] = useState(false) const formLayout = useFormItemLayout(others) const { containerRef, contentRef, overflow } = useOverflow< HTMLDivElement, HTMLSpanElement >() const { label, style, layout, colon = true, addonBefore, addonAfter, asterisk, feedbackStatus, extra, feedbackText, fullness = true, feedbackLayout, feedbackIcon, inset, bordered = true, labelWidth, wrapperWidth, labelCol, wrapperCol, labelAlign, wrapperAlign = 'left', size, labelWrap, wrapperWrap, tooltip, tooltipLayout, tooltipIcon, } = formLayout const labelStyle = { ...formLayout.labelStyle } const wrapperStyle = { ...formLayout.wrapperStyle } // 固定宽度 let enableCol = false if (labelWidth || wrapperWidth) { if (labelWidth) { labelStyle.width = labelWidth === 'auto' ? undefined : labelWidth labelStyle.maxWidth = labelWidth === 'auto' ? undefined : labelWidth } if (wrapperWidth) { wrapperStyle.width = wrapperWidth === 'auto' ? undefined : wrapperWidth wrapperStyle.maxWidth = wrapperWidth === 'auto' ? undefined : wrapperWidth } // 栅格模式 } if (labelCol || wrapperCol) { if (!labelStyle.width && !wrapperStyle.width && layout !== 'vertical') { enableCol = true } } const prefixCls = usePrefixCls('formily-item', props) const prefix = usePrefixCls() const formatChildren = feedbackLayout === 'popover' ? ( <Balloon needAdjust align="t" closable={false} trigger={children} visible={!!feedbackText} > <div className={cls({ [`${prefixCls}-${feedbackStatus}-help`]: !!feedbackStatus, [`${prefixCls}-help`]: true, })} > {ICON_MAP[feedbackStatus]} {feedbackText} </div> </Balloon> ) : ( children ) const gridStyles: React.CSSProperties = {} const getOverflowTooltip = () => { if (overflow) { return ( <div> <div>{label}</div> <div>{tooltip}</div> </div> ) } return tooltip } const renderLabelText = () => { const labelChildren = ( <div className={cls(`${prefixCls}-label-content`)} ref={containerRef}> <span ref={contentRef}> {asterisk && ( <span className={cls(`${prefixCls}-asterisk`)}>{'*'}</span> )} <label htmlFor={props.labelFor}>{label}</label> </span> </div> ) if ((tooltipLayout === 'text' && tooltip) || overflow) { return ( <Balloon.Tooltip align="t" trigger={labelChildren}> {getOverflowTooltip()} </Balloon.Tooltip> ) } return labelChildren } const renderTooltipIcon = () => { if (tooltip && tooltipLayout === 'icon' && !overflow) { return ( <span className={cls(`${prefixCls}-label-tooltip-icon`)}> <Balloon.Tooltip align="t" trigger={tooltipIcon}> {tooltip} </Balloon.Tooltip> </span> ) } } const renderLabel = () => { if (!label) return null return ( <div className={cls({ [`${prefixCls}-label`]: true, [`${prefixCls}-label-tooltip`]: (tooltip && tooltipLayout === 'text') || overflow, [`${prefixCls}-item-col-${labelCol}`]: enableCol && !!labelCol, })} style={labelStyle} > {renderLabelText()} {renderTooltipIcon()} {label !== ' ' && ( <span className={cls(`${prefixCls}-colon`)}>{colon ? ':' : ''}</span> )} </div> ) } return ( <div {...pickDataProps(props)} style={{ ...style, ...gridStyles, }} data-grid-span={props.gridSpan} className={cls({ [`${prefixCls}`]: true, [`${prefixCls}-layout-${layout}`]: true, [`${prefixCls}-${feedbackStatus}`]: !!feedbackStatus, [`${prefixCls}-feedback-has-text`]: !!feedbackText, [`${prefixCls}-size-${size}`]: !!size, [`${prefixCls}-feedback-layout-${feedbackLayout}`]: !!feedbackLayout, [`${prefixCls}-fullness`]: !!fullness || !!inset || !!feedbackIcon, [`${prefixCls}-inset`]: !!inset, [`${prefix}input`]: !!inset, [`${prefixCls}-active`]: active, [`${prefix}focus`]: active, [`${prefixCls}-inset-active`]: !!inset && active, [`${prefixCls}-label-align-${labelAlign}`]: true, [`${prefixCls}-control-align-${wrapperAlign}`]: true, [`${prefixCls}-label-wrap`]: !!labelWrap, [`${prefixCls}-control-wrap`]: !!wrapperWrap, [`${prefixCls}-bordered-none`]: bordered === false, [props.className]: !!props.className, })} onFocus={() => { if (feedbackIcon || inset) { setActive(true) } }} onBlur={() => { if (feedbackIcon || inset) { setActive(false) } }} > {renderLabel()} <div className={cls({ [`${prefixCls}-control`]: true, [`${prefixCls}-item-col-${wrapperCol}`]: enableCol && !!wrapperCol && label, })} > <div className={cls(`${prefixCls}-control-content`)}> {addonBefore && ( <div className={cls(`${prefixCls}-addon-before`)}> {addonBefore} </div> )} <div style={wrapperStyle} className={cls({ [`${prefixCls}-control-content-component`]: true, [`${prefixCls}-control-content-component-has-feedback-icon`]: !!feedbackIcon, [`${prefix}input`]: !!feedbackIcon, [`${prefixCls}-active`]: active, [`${prefix}focus`]: active, })} > <FormLayoutShallowContext.Provider value={{ size }}> {formatChildren} </FormLayoutShallowContext.Provider> {feedbackIcon && ( <div className={cls(`${prefixCls}-feedback-icon`)}> {feedbackIcon} </div> )} </div> {addonAfter && ( <div className={cls(`${prefixCls}-addon-after`)}>{addonAfter}</div> )} </div> {!!feedbackText && feedbackLayout !== 'popover' && feedbackLayout !== 'none' && ( <div className={cls({ [`${prefixCls}-${feedbackStatus}-help`]: !!feedbackStatus, [`${prefixCls}-help`]: true, [`${prefixCls}-help-enter`]: true, [`${prefixCls}-help-enter-active`]: true, })} > {feedbackText} </div> )} {extra && <div className={cls(`${prefixCls}-extra`)}>{extra}</div>} </div> </div> ) } // 适配 export const FormItem: ComposeFormItem = connect( BaseItem, mapProps((props, field) => { if (isVoidField(field)) return { label: field.title || props.label, asterisk: props.asterisk, extra: props.extra || field.description, } if (!field) return props const takeFeedbackStatus = () => { if (field.validating) return 'pending' return field.decoratorProps.feedbackStatus || field.validateStatus } const takeMessage = () => { const split = (messages: any[]) => { return messages.reduce((buf, text, index) => { if (!text) return buf return index < messages.length - 1 ? buf.concat([text, ', ']) : buf.concat([text]) }, []) } if (field.validating) return if (props.feedbackText) return props.feedbackText if (field.selfErrors.length) return split(field.selfErrors) if (field.selfWarnings.length) return split(field.selfWarnings) if (field.selfSuccesses.length) return split(field.selfSuccesses) } const takeAsterisk = () => { if (field.required && field.pattern !== 'readPretty') { return true } if ('asterisk' in props) { return props.asterisk } return false } return { label: props.label || field.title, feedbackStatus: takeFeedbackStatus(), feedbackText: takeMessage(), asterisk: takeAsterisk(), extra: props.extra || field.description, } }) ) FormItem.BaseItem = BaseItem export default FormItem ``` -------------------------------------------------------------------------------- /packages/grid/src/index.ts: -------------------------------------------------------------------------------- ```typescript import { define, observable, batch, reaction } from '@formily/reactive' import { ChildListMutationObserver } from './observer' import { ResizeObserver } from '@juggle/resize-observer' export interface IGridOptions { maxRows?: number maxColumns?: number | number[] minColumns?: number | number[] maxWidth?: number | number[] minWidth?: number | number[] breakpoints?: number[] columnGap?: number rowGap?: number colWrap?: boolean strictAutoFit?: boolean shouldVisible?: (node: GridNode, grid: Grid<HTMLElement>) => boolean onDigest?: (grid: Grid<HTMLElement>) => void onInitialized?: (grid: Grid<HTMLElement>) => void } const SpanRegExp = /span\s*(\d+)/ const isValid = (value: any) => value !== undefined && value !== null const calcBreakpointIndex = (breakpoints: number[], width: number) => { if (Array.isArray(breakpoints)) { for (let i = 0; i < breakpoints.length; i++) { if (width <= breakpoints[i]) { return i } } } return -1 } const calcFactor = <T>(value: T | T[], breakpointIndex: number): T => { if (Array.isArray(value)) { if (breakpointIndex === -1) return value[0] return value[breakpointIndex] ?? value[value.length - 1] } else { return value } } const parseGridNode = (elements: HTMLCollection): GridNode[] => { return Array.from(elements).reduce((buf, element: HTMLElement, index) => { const style = getComputedStyle(element) const visible = !(style.display === 'none') const origin = element.getAttribute('data-grid-span') const span = parseSpan(style.gridColumnStart) ?? 1 const originSpan = Number(origin ?? span) const node: GridNode = { index, span, visible, originSpan, element, } if (!origin) { element.setAttribute('data-grid-span', String(span)) } return buf.concat(node) }, []) } const calcChildTotalColumns = (nodes: GridNode[], shadow = false) => nodes.reduce((buf, node) => { if (!shadow) { if (!node.visible) return buf } if (node.originSpan === -1) return buf + (node.span ?? 1) return buf + node.span }, 0) const calcChildOriginTotalColumns = (nodes: GridNode[], shadow = false) => nodes.reduce((buf, node) => { if (!shadow) { if (!node.visible) return buf } if (node.originSpan === -1) return buf + (node.span ?? 1) return buf + node.originSpan }, 0) const calcSatisfyColumns = ( width: number, maxColumns: number, minColumns: number, maxWidth: number, minWidth: number, gap: number ) => { const results = [] for (let columns = minColumns; columns <= maxColumns; columns++) { const innerWidth = width - (columns - 1) * gap const columnWidth = innerWidth / columns if (columnWidth >= minWidth && columnWidth <= maxWidth) { results.push(columns) } else if (columnWidth < minWidth) { results.push(Math.min(Math.floor(innerWidth / minWidth), maxColumns)) } else if (columnWidth > maxWidth) { results.push(Math.min(Math.floor(innerWidth / maxWidth), maxColumns)) } } return Math.max(...results) } const parseSpan = (gridColumnStart: string) => { return Number(String(gridColumnStart).match(SpanRegExp)?.[1] ?? 1) } const factor = <T>(value: T | T[], grid: Grid<HTMLElement>): T => isValid(value) ? calcFactor(value as any, grid.breakpoint) : value const resolveChildren = (grid: Grid<HTMLElement>) => { let walked = 0, shadowWalked = 0, rowIndex = 0, shadowRowIndex = 0 if (!grid.ready) return grid.children = grid.children.map((node) => { const columnIndex = walked % grid.columns const shadowColumnIndex = shadowWalked % grid.columns const remainColumns = grid.columns - columnIndex const originSpan = node.originSpan const targetSpan = originSpan > grid.columns ? grid.columns : originSpan const span = grid.options.strictAutoFit ? targetSpan : targetSpan > remainColumns ? remainColumns : targetSpan const gridColumn = originSpan === -1 ? `span ${remainColumns} / -1` : `span ${span} / auto` if (node.element.style.gridColumn !== gridColumn) { node.element.style.gridColumn = gridColumn } if (node.visible) { walked += span } shadowWalked += span if (columnIndex === 0) { rowIndex++ } if (shadowColumnIndex == 0) { shadowRowIndex++ } node.shadowRow = shadowRowIndex node.shadowColumn = shadowColumnIndex + 1 if (node.visible) { node.row = rowIndex node.column = columnIndex + 1 } if (grid.options?.shouldVisible) { if (!grid.options.shouldVisible(node, grid)) { if (node.visible) { node.element.style.display = 'none' } node.visible = false } else { if (!node.visible) { node.element.style.display = '' } node.visible = true } } return node }) } const nextTick = (callback?: () => void) => Promise.resolve(0).then(callback) export type GridNode = { index?: number visible?: boolean column?: number shadowColumn?: number row?: number shadowRow?: number span?: number originSpan?: number element?: HTMLElement } export class Grid<Container extends HTMLElement> { options: IGridOptions width = 0 height = 0 container: Container children: GridNode[] = [] childTotalColumns = 0 shadowChildTotalColumns = 0 childOriginTotalColumns = 0 shadowChildOriginTotalColumns = 0 ready = false constructor(options?: IGridOptions) { this.options = { breakpoints: [720, 1280, 1920], columnGap: 8, rowGap: 4, minWidth: 100, colWrap: true, strictAutoFit: false, ...options, } define(this, { options: observable.shallow, width: observable.ref, height: observable.ref, ready: observable.ref, children: observable.ref, childOriginTotalColumns: observable.ref, shadowChildOriginTotalColumns: observable.ref, shadowChildTotalColumns: observable.ref, childTotalColumns: observable.ref, columns: observable.computed, templateColumns: observable.computed, gap: observable.computed, maxColumns: observable.computed, minColumns: observable.computed, maxWidth: observable.computed, minWidth: observable.computed, breakpoints: observable.computed, breakpoint: observable.computed, rowGap: observable.computed, columnGap: observable.computed, colWrap: observable.computed, }) } set breakpoints(breakpoints) { this.options.breakpoints = breakpoints } get breakpoints() { return this.options.breakpoints } get breakpoint() { return calcBreakpointIndex(this.options.breakpoints, this.width) } set maxWidth(maxWidth) { this.options.maxWidth = maxWidth } get maxWidth() { return factor(this.options.maxWidth, this) ?? Infinity } set minWidth(minWidth) { this.options.minWidth = minWidth } get minWidth() { return factor(this.options.minWidth, this) ?? 100 } set maxColumns(maxColumns) { this.options.maxColumns = maxColumns } get maxColumns() { return factor(this.options.maxColumns, this) ?? Infinity } set maxRows(maxRows) { this.options.maxRows = maxRows } get maxRows() { return this.options.maxRows ?? Infinity } set minColumns(minColumns) { this.options.minColumns = minColumns } get minColumns() { return factor(this.options.minColumns, this) ?? 1 } set rowGap(rowGap) { this.options.rowGap = rowGap } get rowGap() { return factor(this.options.rowGap, this) ?? 5 } set columnGap(columnGap) { this.options.columnGap = columnGap } get columnGap() { return factor(this.options.columnGap, this) ?? 10 } set colWrap(colWrap) { this.options.colWrap = colWrap } get colWrap() { return factor(this.options.colWrap, this) ?? true } get columns() { if (!this.ready) return 0 const originTotalColumns = this.childOriginTotalColumns if (this.colWrap === false) { return originTotalColumns } const baseColumns = this.childSize const strictMaxWidthColumns = Math.round( this.width / (this.maxWidth + this.columnGap) ) const looseMaxWidthColumns = Math.min( originTotalColumns, strictMaxWidthColumns ) const maxWidthColumns = this.options.strictAutoFit ? strictMaxWidthColumns : looseMaxWidthColumns const strictMinWidthColumns = Math.round( this.width / (this.minWidth + this.columnGap) ) const looseMinWidthColumns = Math.min( originTotalColumns, strictMinWidthColumns ) const minWidthColumns = this.options.strictAutoFit ? strictMinWidthColumns : looseMinWidthColumns const minCalculatedColumns = Math.min( baseColumns, originTotalColumns, maxWidthColumns, minWidthColumns ) const maxCalculatedColumns = Math.max( baseColumns, originTotalColumns, maxWidthColumns, minWidthColumns ) const finalColumns = calcSatisfyColumns( this.width, maxCalculatedColumns, minCalculatedColumns, this.maxWidth, this.minWidth, this.columnGap ) if (finalColumns >= this.maxColumns) { return this.maxColumns } if (finalColumns <= this.minColumns) { return this.minColumns } return finalColumns } get rows() { return Math.ceil(this.childTotalColumns / this.columns) } get shadowRows() { return Math.ceil(this.shadowChildTotalColumns / this.columns) } get templateColumns() { if (!this.width) return '' if (this.maxWidth === Infinity) { return `repeat(${this.columns},minmax(0,1fr))` } if (this.options.strictAutoFit !== true) { const columnWidth = (this.width - (this.columns - 1) * this.columnGap) / this.columns if (columnWidth < this.minWidth || columnWidth > this.maxWidth) { return `repeat(${this.columns},minmax(0,1fr))` } } return `repeat(${this.columns},minmax(${this.minWidth}px,${this.maxWidth}px))` } get gap() { return `${this.rowGap}px ${this.columnGap}px` } get childSize() { return this.children.length } get fullnessLastColumn() { return this.columns === this.children[this.childSize - 1]?.span } connect = (container: Container) => { if (container) { this.container = container const initialize = batch.bound(() => { digest() this.ready = true }) const digest = batch.bound(() => { this.children = parseGridNode(this.container.children) this.childTotalColumns = calcChildTotalColumns(this.children) this.shadowChildTotalColumns = calcChildTotalColumns( this.children, true ) this.childOriginTotalColumns = calcChildOriginTotalColumns( this.children ) this.shadowChildOriginTotalColumns = calcChildOriginTotalColumns( this.children, true ) const rect = this.container.getBoundingClientRect() if (rect.width && rect.height) { this.width = rect.width this.height = rect.height } resolveChildren(this) nextTick(() => { this.options?.onDigest?.(this) }) if (!this.ready) { nextTick(() => { this.options?.onInitialized?.(this) }) } }) const mutationObserver = new ChildListMutationObserver(digest) // add requestAnimationFrame to smooth digest const smoothDigest = () => { requestAnimationFrame(() => { digest() }) } const resizeObserver = new ResizeObserver(smoothDigest) const dispose = reaction(() => ({ ...this.options }), digest) resizeObserver.observe(this.container) mutationObserver.observe(this.container, { attributeFilter: ['data-grid-span'], attributes: true, }) initialize() return () => { resizeObserver.unobserve(this.container) resizeObserver.disconnect() mutationObserver.disconnect() dispose() this.children = [] } } return () => {} } static id = (options: IGridOptions = {}) => JSON.stringify( [ 'maxRows', 'maxColumns', 'minColumns', 'maxWidth', 'minWidth', 'breakpoints', 'columnGap', 'rowGap', 'colWrap', 'strictAutoFit', ].map((key) => options[key]) ) } ``` -------------------------------------------------------------------------------- /packages/antd/docs/components/ArrayCards.zh-CN.md: -------------------------------------------------------------------------------- ```markdown # ArrayCards > 卡片列表,对于每行字段数量较多,联动较多的场景比较适合使用 ArrayCards > > 注意:该组件只适用于 Schema 场景 ## Markup Schema 案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCards, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCards, }, }) const form = createForm() export default () => { return ( <FormProvider form={form}> <SchemaField> <SchemaField.Array name="string_array" maxItems={3} x-decorator="FormItem" x-component="ArrayCards" x-component-props={{ title: '字符串数组', }} > <SchemaField.Void> <SchemaField.Void x-component="ArrayCards.Index" /> <SchemaField.String name="input" x-decorator="FormItem" title="Input" required x-component="Input" /> <SchemaField.Void x-component="ArrayCards.Remove" /> <SchemaField.Void x-component="ArrayCards.Copy" /> <SchemaField.Void x-component="ArrayCards.MoveUp" /> <SchemaField.Void x-component="ArrayCards.MoveDown" /> </SchemaField.Void> <SchemaField.Void x-component="ArrayCards.Addition" title="添加条目" /> </SchemaField.Array> <SchemaField.Array name="array" maxItems={3} x-decorator="FormItem" x-component="ArrayCards" x-component-props={{ title: '对象数组', }} > <SchemaField.Object> <SchemaField.Void x-component="ArrayCards.Index" /> <SchemaField.String name="input" x-decorator="FormItem" title="Input" required x-component="Input" /> <SchemaField.Void x-component="ArrayCards.Remove" /> <SchemaField.Void x-component="ArrayCards.MoveUp" /> <SchemaField.Void x-component="ArrayCards.MoveDown" /> </SchemaField.Object> <SchemaField.Void x-component="ArrayCards.Addition" title="添加条目" /> </SchemaField.Array> </SchemaField> <FormButtonGroup> <Submit onSubmit={console.log}>提交</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCards, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCards, }, }) const form = createForm() const schema = { type: 'object', properties: { string_array: { type: 'array', 'x-component': 'ArrayCards', maxItems: 3, 'x-decorator': 'FormItem', 'x-component-props': { title: '字符串数组', }, items: { type: 'void', properties: { index: { type: 'void', 'x-component': 'ArrayCards.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCards.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCards.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCards.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayCards.Addition', }, }, }, array: { type: 'array', 'x-component': 'ArrayCards', maxItems: 3, 'x-decorator': 'FormItem', 'x-component-props': { title: '对象数组', }, items: { type: 'object', properties: { index: { type: 'void', 'x-component': 'ArrayCards.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCards.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCards.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCards.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayCards.Addition', }, }, }, }, } export default () => { return ( <FormProvider form={form}> <SchemaField schema={schema} /> <FormButtonGroup> <Submit onSubmit={console.log}>提交</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## Effects 联动案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCards, FormButtonGroup, Submit, } from '@formily/antd' import { createForm, onFieldChange, onFieldReact } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCards, }, }) const form = createForm({ effects: () => { //主动联动模式 onFieldChange('array.*.aa', ['value'], (field, form) => { form.setFieldState(field.query('.bb'), (state) => { state.visible = field.value != '123' }) }) //被动联动模式 onFieldReact('array.*.dd', (field) => { field.visible = field.query('.cc').get('value') != '123' }) }, }) export default () => { return ( <FormProvider form={form}> <SchemaField> <SchemaField.Array name="array" maxItems={3} x-component="ArrayCards" x-decorator="FormItem" x-component-props={{ title: '对象数组', }} > <SchemaField.Object> <SchemaField.Void x-component="ArrayCards.Index" /> <SchemaField.String name="aa" x-decorator="FormItem" title="AA" required description="AA输入123时隐藏BB" x-component="Input" /> <SchemaField.String name="bb" x-decorator="FormItem" title="BB" required x-component="Input" /> <SchemaField.String name="cc" x-decorator="FormItem" title="CC" required description="CC输入123时隐藏DD" x-component="Input" /> <SchemaField.String name="dd" x-decorator="FormItem" title="DD" required x-component="Input" /> <SchemaField.Void x-component="ArrayCards.Remove" /> <SchemaField.Void x-component="ArrayCards.MoveUp" /> <SchemaField.Void x-component="ArrayCards.MoveDown" /> </SchemaField.Object> <SchemaField.Void x-component="ArrayCards.Addition" title="添加条目" /> </SchemaField.Array> </SchemaField> <FormButtonGroup> <Submit onSubmit={console.log}>提交</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## JSON Schema 联动案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCards, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCards, }, }) const form = createForm() const schema = { type: 'object', properties: { array: { type: 'array', 'x-component': 'ArrayCards', maxItems: 3, title: '对象数组', items: { type: 'object', properties: { index: { type: 'void', 'x-component': 'ArrayCards.Index', }, aa: { type: 'string', 'x-decorator': 'FormItem', title: 'AA', required: true, 'x-component': 'Input', description: '输入123', }, bb: { type: 'string', title: 'BB', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-reactions': [ { dependencies: ['.aa'], when: "{{$deps[0] != '123'}}", fulfill: { schema: { title: 'BB', 'x-disabled': true, }, }, otherwise: { schema: { title: 'Changed', 'x-disabled': false, }, }, }, ], }, remove: { type: 'void', 'x-component': 'ArrayCards.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCards.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCards.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayCards.Addition', }, }, }, }, } export default () => { return ( <FormProvider form={form}> <SchemaField schema={schema} /> <FormButtonGroup> <Submit onSubmit={console.log}>提交</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## API ### ArrayCards 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ---------- | ------------------------- | ------------ | ------ | | onAdd | `(index: number) => void` | 增加方法 | | | onRemove | `(index: number) => void` | 删除方法 | | | onCopy | `(index: number) => void` | 复制方法 | | | onMoveUp | `(index: number) => void` | 向上移动方法 | | | onMoveDown | `(index: number) => void` | 向下移动方法 | | 其余参考 https://ant.design/components/card-cn/ ### ArrayCards.Addition > 添加按钮 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------------ | --------------------- | -------- | -------- | | title | ReactText | 文案 | | | method | `'push' \| 'unshift'` | 添加方式 | `'push'` | | defaultValue | `any` | 默认值 | | 其余参考 https://ant.design/components/button-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayCards.Copy > 复制按钮 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------------------- | -------- | -------- | | title | ReactText | 文案 | | | method | `'push' \| 'unshift'` | 添加方式 | `'push'` | 其余参考 https://ant.design/components/button-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayCards.Remove > 删除按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayCards.MoveDown > 下移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayCards.MoveUp > 上移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayCards.Index > 索引渲染器 无属性 ### ArrayCards.useIndex > 读取当前渲染行索引的 React Hook ### ArrayCards.useRecord > 读取当前渲染记录的 React Hook ``` -------------------------------------------------------------------------------- /packages/core/src/__tests__/effects.spec.ts: -------------------------------------------------------------------------------- ```typescript import { createForm, createEffectContext, onFieldChange, onFieldInit, onFieldInitialValueChange, onFieldInputValueChange, onFieldMount, onFieldReact, onFieldUnmount, onFieldValidateEnd, onFieldValidateStart, onFieldValidateFailed, onFieldValidateSuccess, onFieldValueChange, onFormInit, onFormInitialValuesChange, onFormInputChange, onFormMount, onFormReact, onFormReset, onFormSubmit, onFormSubmitEnd, onFormSubmitFailed, onFormSubmitStart, onFormSubmitSuccess, onFormSubmitValidateFailed, onFormSubmitValidateStart, onFormSubmitValidateSuccess, onFormSubmitValidateEnd, onFormUnmount, onFormValidateEnd, onFormValidateStart, onFormValidateFailed, onFormValidateSuccess, onFormValuesChange, isVoidField, } from '../' import { runEffects } from '../shared/effective' import { attach, sleep } from './shared' test('onFormInit/onFormMount/onFormUnmount', () => { const mount = jest.fn() const init = jest.fn() const unmount = jest.fn() const form = attach( createForm({ effects() { onFormInit(init) onFormMount(mount) onFormUnmount(unmount) }, }) ) expect(init).toBeCalled() expect(mount).toBeCalled() expect(unmount).not.toBeCalled() form.onUnmount() expect(unmount).toBeCalled() }) test('onFormValuesChange/onFormInitialValuesChange', () => { const valuesChange = jest.fn() const initialValuesChange = jest.fn() const form = attach( createForm({ effects() { onFormValuesChange(valuesChange) onFormInitialValuesChange(initialValuesChange) }, }) ) expect(valuesChange).not.toBeCalled() expect(initialValuesChange).not.toBeCalled() form.setValues({ aa: '123', }) expect(form.values.aa).toEqual('123') expect(valuesChange).toBeCalled() form.setInitialValues({ aa: '321', bb: '123', }) expect(form.values.aa).toEqual('321') expect(form.values.bb).toEqual('123') expect(initialValuesChange).toBeCalled() }) test('onFormInputChange', () => { const inputChange = jest.fn() const valuesChange = jest.fn() const form = attach( createForm({ effects() { onFormValuesChange(valuesChange) onFormInputChange(inputChange) }, }) ) const field = attach( form.createField({ name: 'aa', }) ) expect(inputChange).not.toBeCalled() expect(valuesChange).not.toBeCalled() field.setValue('123') expect(inputChange).not.toBeCalled() expect(valuesChange).toBeCalledTimes(1) field.onInput('123') expect(inputChange).toBeCalled() expect(valuesChange).toBeCalledTimes(1) field.onInput('321') expect(inputChange).toBeCalledTimes(2) expect(valuesChange).toBeCalledTimes(2) }) test('onFormReact', () => { const react = jest.fn() const form = attach( createForm({ effects() { onFormReact((form) => { if (form.values.aa) { react() } }) }, }) ) expect(react).not.toBeCalled() form.setValues({ aa: 123 }) expect(react).toBeCalled() form.onUnmount() // will not throw error const form2 = attach( createForm({ effects() { onFormReact() }, }) ) form2.onUnmount() }) test('onFormReset', async () => { const reset = jest.fn() const form = attach( createForm({ initialValues: { aa: 123, }, effects() { onFormReset(reset) }, }) ) const field = attach( form.createField({ name: 'aa', }) ) field.setValue('xxxx') expect(field.value).toEqual('xxxx') expect(form.values.aa).toEqual('xxxx') expect(reset).not.toBeCalled() await form.reset() expect(field.value).toEqual(123) expect(form.values.aa).toEqual(123) expect(reset).toBeCalled() }) test('onFormSubmit', async () => { const submit = jest.fn() const submitStart = jest.fn() const submitEnd = jest.fn() const submitSuccess = jest.fn() const submitFailed = jest.fn() const submitValidateStart = jest.fn() const submitValidateFailed = jest.fn() const submitValidateSuccess = jest.fn() const submitValidateEnd = jest.fn() const form = attach( createForm({ effects() { onFormSubmitStart(submitStart) onFormSubmit(submit) onFormSubmitEnd(submitEnd) onFormSubmitFailed(submitFailed) onFormSubmitSuccess(submitSuccess) onFormSubmitValidateStart(submitValidateStart) onFormSubmitValidateFailed(submitValidateFailed) onFormSubmitValidateSuccess(submitValidateSuccess) onFormSubmitValidateEnd(submitValidateEnd) }, }) ) const field = attach( form.createField({ name: 'aa', required: true, }) ) try { await form.submit() } catch {} expect(submitStart).toBeCalled() expect(submit).toBeCalled() expect(submitEnd).toBeCalled() expect(submitSuccess).not.toBeCalled() expect(submitFailed).toBeCalled() expect(submitValidateStart).toBeCalled() expect(submitValidateFailed).toBeCalled() expect(submitValidateSuccess).not.toBeCalled() expect(submitValidateEnd).toBeCalled() field.onInput('123') try { await form.submit() } catch (e) {} expect(submitStart).toBeCalledTimes(2) expect(submit).toBeCalledTimes(2) expect(submitEnd).toBeCalledTimes(2) expect(submitSuccess).toBeCalledTimes(1) expect(submitFailed).toBeCalledTimes(1) expect(submitValidateStart).toBeCalledTimes(2) expect(submitValidateFailed).toBeCalledTimes(1) expect(submitValidateSuccess).toBeCalledTimes(1) expect(submitValidateEnd).toBeCalledTimes(2) }) test('onFormValidate', async () => { const validateStart = jest.fn() const validateEnd = jest.fn() const validateFailed = jest.fn() const validateSuccess = jest.fn() const form = attach( createForm({ effects() { onFormValidateStart(validateStart) onFormValidateEnd(validateEnd) onFormValidateFailed(validateFailed) onFormValidateSuccess(validateSuccess) }, }) ) const field = attach( form.createField({ name: 'aa', required: true, }) ) try { await form.validate() } catch {} expect(validateStart).toBeCalled() expect(validateEnd).toBeCalled() expect(validateFailed).toBeCalled() expect(validateSuccess).not.toBeCalled() field.onInput('123') try { await form.validate() } catch {} expect(validateStart).toBeCalledTimes(2) expect(validateEnd).toBeCalledTimes(2) expect(validateFailed).toBeCalledTimes(1) expect(validateSuccess).toBeCalledTimes(1) }) test('onFieldChange', async () => { const fieldChange = jest.fn() const valueChange = jest.fn() const valueChange2 = jest.fn() const form = attach( createForm({ effects() { onFieldChange( 'aa', [ 'value', 'disabled', 'initialized', 'inputValue', 'loading', 'visible', 'editable', ], fieldChange ) onFieldChange('aa', valueChange) onFieldChange('aa', undefined, valueChange2) onFieldChange('aa') }, }) ) const field = attach( form.createField({ name: 'aa', }) ) expect(fieldChange).toBeCalledTimes(1) field.setValue('123') expect(fieldChange).toBeCalledTimes(2) field.onInput('321') expect(fieldChange).toBeCalledTimes(3) field.setLoading(true) expect(fieldChange).toBeCalledTimes(3) await sleep() expect(fieldChange).toBeCalledTimes(4) field.setPattern('disabled') expect(fieldChange).toBeCalledTimes(5) field.setDisplay('none') expect(fieldChange).toBeCalledTimes(6) form.onUnmount() expect(valueChange).toBeCalledTimes(4) expect(valueChange2).toBeCalledTimes(4) }) test('onFieldInit/onFieldMount/onFieldUnmount', () => { const fieldInit = jest.fn() const fieldMount = jest.fn() const fieldUnmount = jest.fn() const form = attach( createForm({ effects() { onFieldInit('aa', fieldInit) onFieldMount('aa', fieldMount) onFieldUnmount('aa', fieldUnmount) }, }) ) const field = attach( form.createField({ name: 'aa', }) ) expect(fieldInit).toBeCalledTimes(1) expect(fieldMount).toBeCalledTimes(1) expect(fieldUnmount).toBeCalledTimes(0) field.onUnmount() expect(fieldUnmount).toBeCalledTimes(1) }) test('onFieldInitialValueChange/onFieldValueChange/onFieldInputValueChange', () => { const fieldValueChange = jest.fn() const fieldInitialValueChange = jest.fn() const fieldInputValueChange = jest.fn() const notTrigger = jest.fn() const form = attach( createForm({ effects() { onFieldInitialValueChange('aa', fieldInitialValueChange) onFieldValueChange('aa', fieldValueChange) onFieldInputValueChange('aa', fieldInputValueChange) onFieldValueChange('xx', notTrigger) }, }) ) const field = attach( form.createField({ name: 'aa', }) ) field.setValue('123') expect(fieldValueChange).toBeCalledTimes(1) expect(fieldInitialValueChange).toBeCalledTimes(0) expect(fieldInputValueChange).toBeCalledTimes(0) field.setInitialValue('xxx') expect(fieldValueChange).toBeCalledTimes(2) expect(fieldInitialValueChange).toBeCalledTimes(1) expect(fieldInputValueChange).toBeCalledTimes(0) field.onInput('321') expect(fieldValueChange).toBeCalledTimes(3) expect(fieldInitialValueChange).toBeCalledTimes(1) expect(fieldInputValueChange).toBeCalledTimes(1) expect(notTrigger).toBeCalledTimes(0) }) test('onFieldReact', () => { const react = jest.fn() const form = attach( createForm({ effects() { onFieldReact('aa', (field) => { if (isVoidField(field)) return if (field.value) { react() } if (field.display === 'hidden') { react() } }) onFieldReact('aa', null) }, }) ) const field = attach( form.createField({ name: 'aa', }) ) expect(react).not.toBeCalled() form.setValues({ aa: 123 }) expect(react).toBeCalledTimes(1) field.setDisplay('hidden') expect(react).toBeCalledTimes(3) form.onUnmount() }) test('onFieldValidate', async () => { const validateStart = jest.fn() const validateFailed = jest.fn() const validateSuccess = jest.fn() const validateEnd = jest.fn() const form = attach( createForm({ effects() { onFieldValidateStart('aa', validateStart) onFieldValidateEnd('aa', validateEnd) onFieldValidateFailed('aa', validateFailed) onFieldValidateSuccess('aa', validateSuccess) }, }) ) const field = attach( form.createField({ name: 'aa', required: true, }) ) try { await field.validate() } catch {} expect(validateStart).toBeCalled() expect(validateFailed).toBeCalled() expect(validateSuccess).not.toBeCalled() expect(validateEnd).toBeCalled() field.setValue('123') try { await field.validate() } catch {} expect(validateStart).toBeCalledTimes(2) expect(validateFailed).toBeCalledTimes(1) expect(validateSuccess).toBeCalledTimes(1) expect(validateEnd).toBeCalledTimes(2) }) test('async use will throw error', async () => { const valueChange = jest.fn() let error const form = attach( createForm({ effects() { setTimeout(() => { try { onFieldValueChange('aa', valueChange) } catch (e) { error = e } }, 0) }, }) ) const aa = attach( form.createField({ name: 'aa', }) ) await sleep(10) aa.setValue('123') expect(valueChange).toBeCalledTimes(0) expect(error).not.toBeUndefined() }) test('effect context', async () => { const context = createEffectContext<number>() const context2 = createEffectContext<number>() const context3 = createEffectContext<number>(123) let results: any let error: any let error2: any const consumer = () => { results = context.consume() } const consumer2 = () => { setTimeout(() => { try { results = context2.consume() } catch (e) { error2 = e } }, 0) } attach( createForm({ effects() { context.provide(123) context3.provide() consumer() setTimeout(() => { try { context2.provide(123) } catch (e) { error = e } }, 0) consumer2() }, }) ) await sleep(10) expect(results).toEqual(123) expect(error).not.toBeUndefined() expect(error2).not.toBeUndefined() }) test('runEffects', () => { expect( runEffects(123, () => { onFormMount(() => {}) }).length ).toEqual(1) }) ```