This is page 23 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/core/src/models/Form.ts: -------------------------------------------------------------------------------- ```typescript import { define, observable, batch, action, observe } from '@formily/reactive' import { FormPath, FormPathPattern, isValid, uid, globalThisPolyfill, merge, isPlainObj, isArr, isObj, } from '@formily/shared' import { Heart } from './Heart' import { Field } from './Field' import { JSXComponent, LifeCycleTypes, HeartSubscriber, FormPatternTypes, IFormRequests, IFormFeedback, ISearchFeedback, IFormGraph, IFormProps, IFieldResetOptions, IFormFields, IFieldFactoryProps, IVoidFieldFactoryProps, IFormState, IModelGetter, IModelSetter, IFieldStateGetter, IFieldStateSetter, FormDisplayTypes, IFormMergeStrategy, } from '../types' import { createStateGetter, createStateSetter, createBatchStateSetter, createBatchStateGetter, triggerFormInitialValuesChange, triggerFormValuesChange, batchValidate, batchReset, batchSubmit, setValidating, setSubmitting, setLoading, getValidFormValues, } from '../shared/internals' import { isVoidField } from '../shared/checkers' import { runEffects } from '../shared/effective' import { ArrayField } from './ArrayField' import { ObjectField } from './ObjectField' import { VoidField } from './VoidField' import { Query } from './Query' import { Graph } from './Graph' const DEV_TOOLS_HOOK = '__FORMILY_DEV_TOOLS_HOOK__' export class Form<ValueType extends object = any> { displayName = 'Form' id: string initialized: boolean validating: boolean submitting: boolean loading: boolean modified: boolean pattern: FormPatternTypes display: FormDisplayTypes values: ValueType initialValues: ValueType mounted: boolean unmounted: boolean props: IFormProps<ValueType> heart: Heart graph: Graph fields: IFormFields = {} requests: IFormRequests = {} indexes: Record<string, string> = {} disposers: (() => void)[] = [] constructor(props: IFormProps<ValueType>) { this.initialize(props) this.makeObservable() this.makeReactive() this.makeValues() this.onInit() } protected initialize(props: IFormProps<ValueType>) { this.id = uid() this.props = { ...props } this.initialized = false this.submitting = false this.validating = false this.loading = false this.modified = false this.mounted = false this.unmounted = false this.display = this.props.display || 'visible' this.pattern = this.props.pattern || 'editable' this.editable = this.props.editable this.disabled = this.props.disabled this.readOnly = this.props.readOnly this.readPretty = this.props.readPretty this.visible = this.props.visible this.hidden = this.props.hidden this.graph = new Graph(this) this.heart = new Heart({ lifecycles: this.lifecycles, context: this, }) } protected makeValues() { this.values = getValidFormValues(this.props.values) this.initialValues = getValidFormValues(this.props.initialValues) } protected makeObservable() { define(this, { fields: observable.shallow, indexes: observable.shallow, initialized: observable.ref, validating: observable.ref, submitting: observable.ref, loading: observable.ref, modified: observable.ref, pattern: observable.ref, display: observable.ref, mounted: observable.ref, unmounted: observable.ref, values: observable, initialValues: observable, valid: observable.computed, invalid: observable.computed, errors: observable.computed, warnings: observable.computed, successes: observable.computed, hidden: observable.computed, visible: observable.computed, editable: observable.computed, readOnly: observable.computed, readPretty: observable.computed, disabled: observable.computed, setValues: action, setValuesIn: action, setInitialValues: action, setInitialValuesIn: action, setPattern: action, setDisplay: action, setState: action, deleteInitialValuesIn: action, deleteValuesIn: action, setSubmitting: action, setValidating: action, reset: action, submit: action, validate: action, onMount: batch, onUnmount: batch, onInit: batch, }) } protected makeReactive() { this.disposers.push( observe( this, (change) => { triggerFormInitialValuesChange(this, change) triggerFormValuesChange(this, change) }, true ) ) } get valid() { return !this.invalid } get invalid() { return this.errors.length > 0 } get errors() { return this.queryFeedbacks({ type: 'error', }) } get warnings() { return this.queryFeedbacks({ type: 'warning', }) } get successes() { return this.queryFeedbacks({ type: 'success', }) } get lifecycles() { return runEffects(this, this.props.effects) } get hidden() { return this.display === 'hidden' } get visible() { return this.display === 'visible' } set hidden(hidden: boolean) { if (!isValid(hidden)) return if (hidden) { this.display = 'hidden' } else { this.display = 'visible' } } set visible(visible: boolean) { if (!isValid(visible)) return if (visible) { this.display = 'visible' } else { this.display = 'none' } } get editable() { return this.pattern === 'editable' } set editable(editable) { if (!isValid(editable)) return if (editable) { this.pattern = 'editable' } else { this.pattern = 'readPretty' } } get readOnly() { return this.pattern === 'readOnly' } set readOnly(readOnly) { if (!isValid(readOnly)) return if (readOnly) { this.pattern = 'readOnly' } else { this.pattern = 'editable' } } get disabled() { return this.pattern === 'disabled' } set disabled(disabled) { if (!isValid(disabled)) return if (disabled) { this.pattern = 'disabled' } else { this.pattern = 'editable' } } get readPretty() { return this.pattern === 'readPretty' } set readPretty(readPretty) { if (!isValid(readPretty)) return if (readPretty) { this.pattern = 'readPretty' } else { this.pattern = 'editable' } } /** 创建字段 **/ createField = < Decorator extends JSXComponent, Component extends JSXComponent >( props: IFieldFactoryProps<Decorator, Component> ): Field<Decorator, Component> => { const address = FormPath.parse(props.basePath).concat(props.name) const identifier = address.toString() if (!identifier) return if (!this.fields[identifier] || this.props.designable) { batch(() => { new Field(address, props, this, this.props.designable) }) this.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE) } return this.fields[identifier] as any } createArrayField = < Decorator extends JSXComponent, Component extends JSXComponent >( props: IFieldFactoryProps<Decorator, Component> ): ArrayField<Decorator, Component> => { const address = FormPath.parse(props.basePath).concat(props.name) const identifier = address.toString() if (!identifier) return if (!this.fields[identifier] || this.props.designable) { batch(() => { new ArrayField( address, { ...props, value: isArr(props.value) ? props.value : [], }, this, this.props.designable ) }) this.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE) } return this.fields[identifier] as any } createObjectField = < Decorator extends JSXComponent, Component extends JSXComponent >( props: IFieldFactoryProps<Decorator, Component> ): ObjectField<Decorator, Component> => { const address = FormPath.parse(props.basePath).concat(props.name) const identifier = address.toString() if (!identifier) return if (!this.fields[identifier] || this.props.designable) { batch(() => { new ObjectField( address, { ...props, value: isObj(props.value) ? props.value : {}, }, this, this.props.designable ) }) this.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE) } return this.fields[identifier] as any } createVoidField = < Decorator extends JSXComponent, Component extends JSXComponent >( props: IVoidFieldFactoryProps<Decorator, Component> ): VoidField<Decorator, Component> => { const address = FormPath.parse(props.basePath).concat(props.name) const identifier = address.toString() if (!identifier) return if (!this.fields[identifier] || this.props.designable) { batch(() => { new VoidField(address, props, this, this.props.designable) }) this.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE) } return this.fields[identifier] as any } /** 状态操作模型 **/ setValues = (values: any, strategy: IFormMergeStrategy = 'merge') => { if (!isPlainObj(values)) return if (strategy === 'merge' || strategy === 'deepMerge') { merge(this.values, values, { // never reach arrayMerge: (target, source) => source, assign: true, }) } else if (strategy === 'shallowMerge') { Object.assign(this.values, values) } else { this.values = values as any } } setInitialValues = ( initialValues: any, strategy: IFormMergeStrategy = 'merge' ) => { if (!isPlainObj(initialValues)) return if (strategy === 'merge' || strategy === 'deepMerge') { merge(this.initialValues, initialValues, { // never reach arrayMerge: (target, source) => source, assign: true, }) } else if (strategy === 'shallowMerge') { Object.assign(this.initialValues, initialValues) } else { this.initialValues = initialValues as any } } setValuesIn = (pattern: FormPathPattern, value: any) => { FormPath.setIn(this.values, pattern, value) } deleteValuesIn = (pattern: FormPathPattern) => { FormPath.deleteIn(this.values, pattern) } existValuesIn = (pattern: FormPathPattern) => { return FormPath.existIn(this.values, pattern) } getValuesIn = (pattern: FormPathPattern) => { return FormPath.getIn(this.values, pattern) } setInitialValuesIn = (pattern: FormPathPattern, initialValue: any) => { FormPath.setIn(this.initialValues, pattern, initialValue) } deleteInitialValuesIn = (pattern: FormPathPattern) => { FormPath.deleteIn(this.initialValues, pattern) } existInitialValuesIn = (pattern: FormPathPattern) => { return FormPath.existIn(this.initialValues, pattern) } getInitialValuesIn = (pattern: FormPathPattern) => { return FormPath.getIn(this.initialValues, pattern) } setLoading = (loading: boolean) => { setLoading(this, loading) } setSubmitting = (submitting: boolean) => { setSubmitting(this, submitting) } setValidating = (validating: boolean) => { setValidating(this, validating) } setDisplay = (display: FormDisplayTypes) => { this.display = display } setPattern = (pattern: FormPatternTypes) => { this.pattern = pattern } addEffects = (id: any, effects: IFormProps['effects']) => { if (!this.heart.hasLifeCycles(id)) { this.heart.addLifeCycles(id, runEffects(this, effects)) } } removeEffects = (id: any) => { this.heart.removeLifeCycles(id) } setEffects = (effects: IFormProps['effects']) => { this.heart.setLifeCycles(runEffects(this, effects)) } clearErrors = (pattern: FormPathPattern = '*') => { this.query(pattern).forEach((field) => { if (!isVoidField(field)) { field.setFeedback({ type: 'error', messages: [], }) } }) } clearWarnings = (pattern: FormPathPattern = '*') => { this.query(pattern).forEach((field) => { if (!isVoidField(field)) { field.setFeedback({ type: 'warning', messages: [], }) } }) } clearSuccesses = (pattern: FormPathPattern = '*') => { this.query(pattern).forEach((field) => { if (!isVoidField(field)) { field.setFeedback({ type: 'success', messages: [], }) } }) } query = (pattern: FormPathPattern): Query => { return new Query({ pattern, base: '', form: this, }) } queryFeedbacks = (search: ISearchFeedback): IFormFeedback[] => { return this.query(search.address || search.path || '*').reduce( (messages, field) => { if (isVoidField(field)) return messages return messages.concat( field .queryFeedbacks(search) .map((feedback) => ({ ...feedback, address: field.address.toString(), path: field.path.toString(), })) .filter((feedback) => feedback.messages.length > 0) ) }, [] ) } notify = (type: string, payload?: any) => { this.heart.publish(type, payload ?? this) } subscribe = (subscriber?: HeartSubscriber) => { return this.heart.subscribe(subscriber) } unsubscribe = (id: number) => { this.heart.unsubscribe(id) } /**事件钩子**/ onInit = () => { this.initialized = true this.notify(LifeCycleTypes.ON_FORM_INIT) } onMount = () => { this.mounted = true this.notify(LifeCycleTypes.ON_FORM_MOUNT) if (globalThisPolyfill[DEV_TOOLS_HOOK] && !this.props.designable) { globalThisPolyfill[DEV_TOOLS_HOOK].inject(this.id, this) } } onUnmount = () => { this.notify(LifeCycleTypes.ON_FORM_UNMOUNT) this.query('*').forEach((field) => field.destroy(false)) this.disposers.forEach((dispose) => dispose()) this.unmounted = true this.indexes = {} this.heart.clear() if (globalThisPolyfill[DEV_TOOLS_HOOK] && !this.props.designable) { globalThisPolyfill[DEV_TOOLS_HOOK].unmount(this.id) } } setState: IModelSetter<IFormState<ValueType>> = createStateSetter(this) getState: IModelGetter<IFormState<ValueType>> = createStateGetter(this) setFormState: IModelSetter<IFormState<ValueType>> = createStateSetter(this) getFormState: IModelGetter<IFormState<ValueType>> = createStateGetter(this) setFieldState: IFieldStateSetter = createBatchStateSetter(this) getFieldState: IFieldStateGetter = createBatchStateGetter(this) getFormGraph = () => { return this.graph.getGraph() } setFormGraph = (graph: IFormGraph) => { this.graph.setGraph(graph) } clearFormGraph = (pattern: FormPathPattern = '*', forceClear = true) => { this.query(pattern).forEach((field) => { field.destroy(forceClear) }) } validate = (pattern: FormPathPattern = '*') => { return batchValidate(this, pattern) } submit = <T>( onSubmit?: (values: ValueType) => Promise<T> | void ): Promise<T> => { return batchSubmit(this, onSubmit) } reset = (pattern: FormPathPattern = '*', options?: IFieldResetOptions) => { return batchReset(this, pattern, options) } } ``` -------------------------------------------------------------------------------- /packages/path/src/index.ts: -------------------------------------------------------------------------------- ```typescript import { Parser } from './parser' import { isStr, isArr, isFn, isEqual, isObj, isNum, isRegExp } from './shared' import { getDestructor, getInByDestructor, setInByDestructor, deleteInByDestructor, existInByDestructor, } from './destructor' import { Segments, Node, Pattern } from './types' import { Matcher } from './matcher' const pathCache = new Map() const isMatcher = Symbol('PATH_MATCHER') const isValid = (val: any) => val !== undefined && val !== null const isSimplePath = (val: string) => val.indexOf('*') === -1 && val.indexOf('~') === -1 && val.indexOf('[') === -1 && val.indexOf(']') === -1 && val.indexOf(',') === -1 && val.indexOf(':') === -1 && val.indexOf(' ') === -1 && val[0] !== '.' const isAssignable = (val: any) => typeof val === 'object' || typeof val === 'function' const isNumberIndex = (val: any) => isStr(val) ? /^\d+$/.test(val) : isNum(val) const getIn = (segments: Segments, source: any) => { for (let i = 0; i < segments.length; i++) { const index = segments[i] const rules = getDestructor(index as string) if (!rules) { if (!isValid(source)) return source = source[index] } else { source = getInByDestructor(source, rules, { setIn, getIn }) break } } return source } const setIn = (segments: Segments, source: any, value: any) => { for (let i = 0; i < segments.length; i++) { const index = segments[i] const rules = getDestructor(index as string) if (!rules) { if (!isValid(source) || !isAssignable(source)) return if (isArr(source) && !isNumberIndex(index)) { return } if (!isValid(source[index])) { if (value === undefined) { if (source[index] === null) source[index] = value return } if (i < segments.length - 1) { source[index] = isNum(segments[i + 1]) ? [] : {} } } if (i === segments.length - 1) { source[index] = value } source = source[index] } else { setInByDestructor(source, rules, value, { setIn, getIn }) break } } } const deleteIn = (segments: Segments, source: any) => { for (let i = 0; i < segments.length; i++) { const index = segments[i] const rules = getDestructor(index as string) if (!rules) { if (i === segments.length - 1 && isValid(source)) { delete source[index] return } if (!isValid(source) || !isAssignable(source)) return source = source[index] if (!isObj(source)) { return } } else { deleteInByDestructor(source, rules, { setIn, getIn, deleteIn, }) break } } } const hasOwnProperty = Object.prototype.hasOwnProperty const existIn = (segments: Segments, source: any, start: number | Path) => { if (start instanceof Path) { start = start.length } for (let i = start; i < segments.length; i++) { const index = segments[i] const rules = getDestructor(index as string) if (!rules) { if (i === segments.length - 1) { return hasOwnProperty.call(source, index) } if (!isValid(source) || !isAssignable(source)) return false source = source[index] if (!isObj(source)) { return false } } else { return existInByDestructor(source, rules, start, { setIn, getIn, deleteIn, existIn, }) } } } const parse = (pattern: Pattern, base?: Pattern) => { if (pattern instanceof Path) { return { entire: pattern.entire, segments: pattern.segments.slice(), isRegExp: false, haveRelativePattern: pattern.haveRelativePattern, isWildMatchPattern: pattern.isWildMatchPattern, isMatchPattern: pattern.isMatchPattern, haveExcludePattern: pattern.haveExcludePattern, tree: pattern.tree, } } else if (isStr(pattern)) { if (!pattern) { return { entire: '', segments: [], isRegExp: false, isWildMatchPattern: false, haveExcludePattern: false, isMatchPattern: false, } } if (isSimplePath(pattern)) { return { entire: pattern, segments: pattern.split('.'), isRegExp: false, isWildMatchPattern: false, haveExcludePattern: false, isMatchPattern: false, } } const parser = new Parser(pattern, Path.parse(base)) const tree = parser.parse() if (!parser.isMatchPattern) { const segments = parser.data.segments return { entire: segments.join('.'), segments, tree, isRegExp: false, haveRelativePattern: parser.haveRelativePattern, isWildMatchPattern: false, haveExcludePattern: false, isMatchPattern: false, } } else { return { entire: pattern, segments: [], isRegExp: false, haveRelativePattern: false, isWildMatchPattern: parser.isWildMatchPattern, haveExcludePattern: parser.haveExcludePattern, isMatchPattern: true, tree, } } } else if (isFn(pattern) && pattern[isMatcher]) { return parse(pattern['path']) } else if (isArr(pattern)) { return { entire: pattern.join('.'), segments: pattern.reduce((buf, key) => { return buf.concat(parseString(key)) }, []), isRegExp: false, haveRelativePattern: false, isWildMatchPattern: false, haveExcludePattern: false, isMatchPattern: false, } } else if (isRegExp(pattern)) { return { entire: pattern, segments: [], isRegExp: true, haveRelativePattern: false, isWildMatchPattern: false, haveExcludePattern: false, isMatchPattern: true, } } else { return { entire: '', isRegExp: false, segments: pattern !== undefined ? [pattern] : [], haveRelativePattern: false, isWildMatchPattern: false, haveExcludePattern: false, isMatchPattern: false, } } } const parseString = (source: any) => { if (isStr(source)) { source = source.replace(/\s*/g, '') try { const { segments, isMatchPattern } = parse(source) return !isMatchPattern ? segments : source } catch (e) { return source } } else if (source instanceof Path) { return source.segments } return source } export class Path { public entire: string | RegExp public segments: Segments public isMatchPattern: boolean public isWildMatchPattern: boolean public isRegExp: boolean public haveRelativePattern: boolean public haveExcludePattern: boolean public matchScore: number public tree: Node private matchCache: any private includesCache: any constructor(input: Pattern, base?: Pattern) { const { tree, segments, entire, isRegExp, isMatchPattern, isWildMatchPattern, haveRelativePattern, haveExcludePattern, } = parse(input, base) this.entire = entire this.segments = segments this.isMatchPattern = isMatchPattern this.isWildMatchPattern = isWildMatchPattern this.haveRelativePattern = haveRelativePattern this.isRegExp = isRegExp this.haveExcludePattern = haveExcludePattern this.tree = tree as Node this.matchCache = new Map() this.includesCache = new Map() } toString() { return this.entire?.toString() } toArr() { return this.segments?.slice() } get length() { return this.segments.length } concat = (...args: Pattern[]) => { if (this.isMatchPattern || this.isRegExp) { throw new Error(`${this.entire} cannot be concat`) } const path = new Path('') path.segments = this.segments.concat(...args.map((s) => parseString(s))) path.entire = path.segments.join('.') return path } slice = (start?: number, end?: number) => { if (this.isMatchPattern || this.isRegExp) { throw new Error(`${this.entire} cannot be slice`) } const path = new Path('') path.segments = this.segments.slice(start, end) path.entire = path.segments.join('.') return path } push = (...items: Pattern[]) => { return this.concat(...items) } pop = () => { if (this.isMatchPattern || this.isRegExp) { throw new Error(`${this.entire} cannot be pop`) } return new Path(this.segments.slice(0, this.segments.length - 1)) } splice = ( start: number, deleteCount?: number, ...items: Array<string | number> ) => { if (this.isMatchPattern || this.isRegExp) { throw new Error(`${this.entire} cannot be splice`) } items = items.reduce((buf, item) => buf.concat(parseString(item)), []) const segments_ = this.segments.slice() segments_.splice(start, deleteCount, ...items) return new Path(segments_) } forEach = (callback: (key: string | number) => any) => { if (this.isMatchPattern || this.isRegExp) { throw new Error(`${this.entire} cannot be each`) } this.segments.forEach(callback) } map = (callback: (key: string | number) => any) => { if (this.isMatchPattern || this.isRegExp) { throw new Error(`${this.entire} cannot be map`) } return this.segments.map(callback) } reduce = <T>( callback: (buf: T, item: string | number, index: number) => T, initial: T ): T => { if (this.isMatchPattern || this.isRegExp) { throw new Error(`${this.entire} cannot be reduce`) } return this.segments.reduce(callback, initial) } parent = () => { return this.slice(0, this.length - 1) } includes = (pattern: Pattern) => { const { entire, segments, isMatchPattern } = Path.parse(pattern) const cache = this.includesCache.get(entire) if (cache !== undefined) return cache const cacheWith = (value: boolean): boolean => { this.includesCache.set(entire, value) return value } if (this.isMatchPattern) { if (!isMatchPattern) { return cacheWith(this.match(segments)) } else { throw new Error(`${this.entire} cannot be used to match ${entire}`) } } if (isMatchPattern) { throw new Error(`${this.entire} cannot be used to match ${entire}`) } if (segments.length > this.segments.length) return cacheWith(false) for (let i = 0; i < segments.length; i++) { if (!isEqual(String(segments[i]), String(this.segments[i]))) { return cacheWith(false) } } return cacheWith(true) } transform = <T>( regexp: string | RegExp, callback: (...args: string[]) => T ): T | string => { if (!isFn(callback)) return '' if (this.isMatchPattern) { throw new Error(`${this.entire} cannot be transformed`) } const reg = new RegExp(regexp) const args = this.segments.filter((key) => reg.test(key as string) ) as string[] return callback(...args) } match = (pattern: Pattern): boolean => { const path = Path.parse(pattern) const cache = this.matchCache.get(path.entire) if (cache !== undefined) { if (cache.record && cache.record.score !== undefined) { this.matchScore = cache.record.score } return cache.matched } const cacheWith = (value: any) => { this.matchCache.set(path.entire, value) return value } if (path.isMatchPattern) { if (this.isMatchPattern) { throw new Error(`${path.entire} cannot match ${this.entire}`) } else { this.matchScore = 0 return cacheWith(path.match(this.segments)) } } else { if (this.isMatchPattern) { if (this.isRegExp) { try { return this['entire']?.['test']?.(path.entire) } finally { ;(this.entire as RegExp).lastIndex = 0 } } const record = { score: 0, } const result = cacheWith( new Matcher(this.tree, record).match(path.segments) ) this.matchScore = record.score return result.matched } else { const record = { score: 0, } const result = cacheWith( Matcher.matchSegments(this.segments, path.segments, record) ) this.matchScore = record.score return result.matched } } } //别名组匹配 matchAliasGroup = (name: Pattern, alias: Pattern) => { const namePath = Path.parse(name) const aliasPath = Path.parse(alias) const nameMatched = this.match(namePath) const nameMatchedScore = this.matchScore const aliasMatched = this.match(aliasPath) const aliasMatchedScore = this.matchScore if (this.haveExcludePattern) { if (nameMatchedScore >= aliasMatchedScore) { return nameMatched } else { return aliasMatched } } else { return nameMatched || aliasMatched } } existIn = (source?: any, start: number | Path = 0) => { return existIn(this.segments, source, start) } getIn = (source?: any) => { return getIn(this.segments, source) } setIn = (source?: any, value?: any) => { setIn(this.segments, source, value) return source } deleteIn = (source?: any) => { deleteIn(this.segments, source) return source } ensureIn = (source?: any, defaults?: any) => { const results = this.getIn(source) if (results === undefined) { this.setIn(source, defaults) return this.getIn(source) } return results } static match(pattern: Pattern) { const path = Path.parse(pattern) const matcher = (target) => { return path.match(target) } matcher[isMatcher] = true matcher.path = path return matcher } static isPathPattern(target: any): target is Pattern { return !!( isStr(target) || isArr(target) || isRegExp(target) || (isFn(target) && target[isMatcher]) ) } static transform<T>( pattern: Pattern, regexp: string | RegExp, callback: (...args: string[]) => T ): any { return Path.parse(pattern).transform(regexp, callback) } static parse(path: Pattern = '', base?: Pattern): Path { if (path instanceof Path) { const found = pathCache.get(path.entire) if (found) { return found } else { pathCache.set(path.entire, path) return path } } else if (path && path[isMatcher]) { return Path.parse(path['path']) } else { const key_ = base ? Path.parse(base) : '' const key = `${path}:${key_}` const found = pathCache.get(key) if (found) { return found } else { path = new Path(path, base) pathCache.set(key, path) return path } } } static getIn = (source: any, pattern: Pattern) => { const path = Path.parse(pattern) return path.getIn(source) } static setIn = (source: any, pattern: Pattern, value: any) => { const path = Path.parse(pattern) return path.setIn(source, value) } static deleteIn = (source: any, pattern: Pattern) => { const path = Path.parse(pattern) return path.deleteIn(source) } static existIn = (source: any, pattern: Pattern, start?: number | Path) => { const path = Path.parse(pattern) return path.existIn(source, start) } static ensureIn = (source: any, pattern: Pattern, defaultValue?: any) => { const path = Path.parse(pattern) return path.ensureIn(source, defaultValue) } } export { Pattern } ``` -------------------------------------------------------------------------------- /packages/antd/docs/components/ArrayCollapse.zh-CN.md: -------------------------------------------------------------------------------- ```markdown # ArrayCollapse > 折叠面板,对于每行字段数量较多,联动较多的场景比较适合使用 ArrayCollapse > > 注意:该组件只适用于 Schema 场景 ## Markup Schema 案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm() export default () => { return ( <FormProvider form={form}> <SchemaField> <SchemaField.Array name="string_array" maxItems={3} x-decorator="FormItem" x-component="ArrayCollapse" x-component-props={{ accordion: true, defaultOpenPanelCount: 3, }} > <SchemaField.Void x-component="ArrayCollapse.CollapsePanel" x-component-props={{ header: '字符串数组', }} > <SchemaField.Void x-component="ArrayCollapse.Index" /> <SchemaField.String name="input" x-decorator="FormItem" title="Input" required x-component="Input" /> <SchemaField.Void x-component="ArrayCollapse.Remove" /> <SchemaField.Void x-component="ArrayCollapse.MoveUp" /> <SchemaField.Void x-component="ArrayCollapse.MoveDown" /> </SchemaField.Void> <SchemaField.Void x-component="ArrayCollapse.Addition" title="添加条目" /> </SchemaField.Array> <SchemaField.Array name="array" maxItems={3} x-decorator="FormItem" x-component="ArrayCollapse" > <SchemaField.Object x-component="ArrayCollapse.CollapsePanel" x-component-props={{ header: '对象数组', }} > <SchemaField.Void x-component="ArrayCollapse.Index" /> <SchemaField.String name="input" x-decorator="FormItem" title="Input" required x-component="Input" /> <SchemaField.Void x-component="ArrayCollapse.Remove" /> <SchemaField.Void x-component="ArrayCollapse.MoveUp" /> <SchemaField.Void x-component="ArrayCollapse.MoveDown" /> </SchemaField.Object> <SchemaField.Void x-component="ArrayCollapse.Addition" title="添加条目" /> </SchemaField.Array> <SchemaField.Array name="string_array_unshift" maxItems={3} x-decorator="FormItem" x-component="ArrayCollapse" x-component-props={{ defaultOpenPanelCount: 8, }} > <SchemaField.Void x-component="ArrayCollapse.CollapsePanel" x-component-props={{ header: '字符串数组', }} > <SchemaField.Void x-component="ArrayCollapse.Index" /> <SchemaField.String name="input" x-decorator="FormItem" title="Input" required x-component="Input" /> <SchemaField.Void x-component="ArrayCollapse.Remove" /> <SchemaField.Void x-component="ArrayCollapse.MoveUp" /> <SchemaField.Void x-component="ArrayCollapse.MoveDown" /> </SchemaField.Void> <SchemaField.Void x-component="ArrayCollapse.Addition" title="添加条目(unshift)" x-component-props={{ method: 'unshift', }} /> </SchemaField.Array> </SchemaField> <FormButtonGroup> <Button onClick={() => { form.setInitialValues({ array: Array.from({ length: 10 }).map(() => ({ input: 'default value', })), string_array: Array.from({ length: 10 }).map( () => 'default value' ), string_array_unshift: Array.from({ length: 10 }).map( () => 'default value' ), }) }} > 加载默认数据 </Button> <Submit onSubmit={console.log}>提交</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm() const schema = { type: 'object', properties: { string_array: { type: 'array', 'x-component': 'ArrayCollapse', 'x-component-props': { onAdd: (index: number) => { console.log('Adding ' + index + ' item') }, }, maxItems: 3, 'x-decorator': 'FormItem', items: { type: 'void', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { header: '字符串数组', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayCollapse.Addition', }, }, }, array: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, 'x-decorator': 'FormItem', items: { type: 'object', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { header: '对象数组', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayCollapse.Addition', }, }, }, array_unshift: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, 'x-decorator': 'FormItem', items: { type: 'object', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { header: '对象数组', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目(unshift)', 'x-component': 'ArrayCollapse.Addition', 'x-component-props': { method: 'unshift', }, }, }, }, }, } 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, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/antd' import { createForm, onFieldChange, onFieldReact } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) 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="ArrayCollapse" x-decorator="FormItem" x-component-props={{ title: '对象数组', }} > <SchemaField.Object x-component="ArrayCollapse.CollapsePanel" x-component-props={{ header: '对象数组', }} > <SchemaField.Void x-component="ArrayCollapse.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="ArrayCollapse.Remove" /> <SchemaField.Void x-component="ArrayCollapse.MoveUp" /> <SchemaField.Void x-component="ArrayCollapse.MoveDown" /> </SchemaField.Object> <SchemaField.Void x-component="ArrayCollapse.Addition" title="添加条目" /> </SchemaField.Array> </SchemaField> <FormButtonGroup> <Submit onSubmit={console.log}>提交</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## JSON Schema 联动案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm() const schema = { type: 'object', properties: { array: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, title: '对象数组', items: { type: 'object', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { header: '对象数组', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.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': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayCollapse.Addition', }, }, }, }, } export default () => { return ( <FormProvider form={form}> <SchemaField schema={schema} /> <FormButtonGroup> <Submit onSubmit={console.log}>提交</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## API ### ArrayCollapse 参考 https://ant.design/components/collapse-cn/ ### ArrayCollapse.CollapsePanel 参考 https://ant.design/components/collapse-cn/ ### ArrayCollapse.Addition > 添加按钮 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------------ | --------------------- | -------- | -------- | | title | ReactText | 文案 | | | method | `'push' \| 'unshift'` | 添加方式 | `'push'` | | defaultValue | `any` | 默认值 | | 其余参考 https://ant.design/components/button-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayCollapse.Remove > 删除按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayCollapse.MoveDown > 下移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayCollapse.MoveUp > 上移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 ### ArrayCollapse.Index > 索引渲染器 无属性 ### ArrayCollapse.useIndex > 读取当前渲染行索引的 React Hook ### ArrayCollapse.useRecord > 读取当前渲染记录的 React Hook ``` -------------------------------------------------------------------------------- /packages/next/docs/components/ArrayCollapse.zh-CN.md: -------------------------------------------------------------------------------- ```markdown # ArrayCollapse > 折叠面板,对于每行字段数量较多,联动较多的场景比较适合使用 ArrayCollapse > > 注意:该组件只适用于 Schema 场景 ## Markup Schema 案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Radio, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { Radio, FormItem, Input, ArrayCollapse, }, }) const form = createForm() export default () => { return ( <FormProvider form={form}> <SchemaField> <SchemaField.Array name="string_array" maxItems={3} x-decorator="FormItem" x-component="ArrayCollapse" x-component-props={{ accordion: true, defaultOpenPanelCount: 3, }} > <SchemaField.Void x-component="ArrayCollapse.CollapsePanel" x-component-props={{ title: '字符串数组', }} > <SchemaField.Void x-component="ArrayCollapse.Index" /> <SchemaField.String name="input" x-decorator="FormItem" title="Input" required x-component="Input" /> <SchemaField.Void x-component="ArrayCollapse.Remove" /> <SchemaField.Void x-component="ArrayCollapse.MoveUp" /> <SchemaField.Void x-component="ArrayCollapse.MoveDown" /> </SchemaField.Void> <SchemaField.Void x-component="ArrayCollapse.Addition" title="添加条目" /> </SchemaField.Array> <SchemaField.Array name="array" maxItems={3} x-decorator="FormItem" x-component="ArrayCollapse" > <SchemaField.Object x-component="ArrayCollapse.CollapsePanel" x-component-props={{ title: '对象数组', }} > <SchemaField.Void x-component="ArrayCollapse.Index" /> <SchemaField.String name="input" x-decorator="FormItem" title="Input" required x-component="Input" /> <SchemaField.String name="radio" x-decorator="FormItem" title="Radio" enum={[1, 2]} required x-component="Radio.Group" /> <SchemaField.Void x-component="ArrayCollapse.Remove" /> <SchemaField.Void x-component="ArrayCollapse.MoveUp" /> <SchemaField.Void x-component="ArrayCollapse.MoveDown" /> </SchemaField.Object> <SchemaField.Void x-component="ArrayCollapse.Addition" title="添加条目" /> </SchemaField.Array> <SchemaField.Array name="string_array_unshift" maxItems={3} x-decorator="FormItem" x-component="ArrayCollapse" x-component-props={{ defaultOpenPanelCount: 8, }} > <SchemaField.Void x-component="ArrayCollapse.CollapsePanel" x-component-props={{ title: '字符串数组', }} > <SchemaField.Void x-component="ArrayCollapse.Index" /> <SchemaField.String name="input" x-decorator="FormItem" title="Input" required x-component="Input" /> <SchemaField.Void x-component="ArrayCollapse.Remove" /> <SchemaField.Void x-component="ArrayCollapse.MoveUp" /> <SchemaField.Void x-component="ArrayCollapse.MoveDown" /> </SchemaField.Void> <SchemaField.Void x-component="ArrayCollapse.Addition" title="添加条目(unshift)" x-component-props={{ method: 'unshift', }} /> </SchemaField.Array> </SchemaField> <FormButtonGroup> <Button onClick={() => { form.setInitialValues({ array: Array.from({ length: 10 }).map(() => ({ input: 'default value', })), string_array: Array.from({ length: 10 }).map( () => 'default value' ), string_array_unshift: Array.from({ length: 10 }).map( () => 'default value' ), }) }} > 加载默认数据 </Button> <Submit onSubmit={console.log}>提交</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## JSON Schema 案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm() const schema = { type: 'object', properties: { string_array: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, 'x-decorator': 'FormItem', items: { type: 'void', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { title: '字符串数组', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayCollapse.Addition', }, }, }, array: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, 'x-decorator': 'FormItem', items: { type: 'object', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { title: '对象数组', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayCollapse.Addition', }, }, }, array_unshift: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, 'x-decorator': 'FormItem', items: { type: 'object', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { title: '对象数组', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目(unshift)', 'x-component': 'ArrayCollapse.Addition', 'x-component-props': { method: 'unshift', }, }, }, }, }, } 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, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/next' import { createForm, onFieldChange, onFieldReact } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) 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="ArrayCollapse" x-decorator="FormItem" x-component-props={{ title: '对象数组', }} > <SchemaField.Object x-component="ArrayCollapse.CollapsePanel" x-component-props={{ title: '对象数组', }} > <SchemaField.Void x-component="ArrayCollapse.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="ArrayCollapse.Remove" /> <SchemaField.Void x-component="ArrayCollapse.MoveUp" /> <SchemaField.Void x-component="ArrayCollapse.MoveDown" /> </SchemaField.Object> <SchemaField.Void x-component="ArrayCollapse.Addition" title="添加条目" /> </SchemaField.Array> </SchemaField> <FormButtonGroup> <Submit onSubmit={console.log}>提交</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## JSON Schema 联动案例 ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm() const schema = { type: 'object', properties: { array: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, title: '对象数组', items: { type: 'object', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { title: '对象数组', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.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': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: '添加条目', 'x-component': 'ArrayCollapse.Addition', }, }, }, }, } export default () => { return ( <FormProvider form={form}> <SchemaField schema={schema} /> <FormButtonGroup> <Submit onSubmit={console.log}>提交</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## API ### ArrayCollapse 参考 https://fusion.design/pc/component/collapse 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | --------------------- | ------ | ------------------- | ------ | | defaultOpenPanelCount | number | 默认展开 Panel 数量 | 5 | ### ArrayCollapse.CollapsePanel 参考 https://fusion.design/pc/component/collapse ### ArrayCollapse.Addition > 添加按钮 扩展属性 | 属性名 | 类型 | 描述 | 默认值 | | ------------ | --------------------- | -------- | -------- | | title | ReactText | 文案 | | | method | `'push' \| 'unshift'` | 添加方式 | `'push'` | | defaultValue | `any` | 默认值 | | 其余参考 https://fusion.design/pc/component/basic/button 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCollapse.Remove > 删除按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCollapse.MoveDown > 下移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCollapse.MoveUp > 上移按钮 | 属性名 | 类型 | 描述 | 默认值 | | ------ | --------- | ---- | ------ | | title | ReactText | 文案 | | 其余参考 https://ant.design/components/icon-cn/ 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 ### ArrayCollapse.Index > 索引渲染器 无属性 ### ArrayCollapse.useIndex > 读取当前渲染行索引的 React Hook ### ArrayCollapse.useRecord > 读取当前渲染记录的 React Hook ``` -------------------------------------------------------------------------------- /packages/element/src/form-item/index.ts: -------------------------------------------------------------------------------- ```typescript import { isVoidField } from '@formily/core' import { connect, h, mapProps } from '@formily/vue' import { Tooltip } from 'element-ui' import ResizeObserver from 'resize-observer-polyfill' import { Component } from 'vue' import { defineComponent, onBeforeUnmount, provide, ref, Ref, watch, } from 'vue-demi' import { FormLayoutShallowContext, useFormLayout } from '../form-layout' import { stylePrefix } from '../__builtins__/configs' import { composeExport, resolveComponent, useCompatRef, } from '../__builtins__/shared' export type FormItemProps = { className?: string required?: boolean label?: string | Component colon?: boolean tooltip?: string | Component layout?: 'vertical' | 'horizontal' | 'inline' labelStyle?: Record<string, any> labelAlign?: 'left' | 'right' labelWrap?: boolean labelWidth?: number wrapperWidth?: number labelCol?: number wrapperCol?: number wrapperAlign?: 'left' | 'right' wrapperWrap?: boolean wrapperStyle?: Record<string, any> fullness?: boolean addonBefore?: string | Component addonAfter?: string | Component size?: 'small' | 'default' | 'large' extra?: string feedbackText?: string | Component feedbackLayout?: 'loose' | 'terse' | 'popover' | 'none' | (string & {}) feedbackStatus?: 'error' | 'warning' | 'success' | 'pending' | (string & {}) tooltipLayout?: 'icon' | 'text' feedbackIcon?: string | Component asterisk?: boolean gridSpan?: number bordered?: boolean inset?: boolean } const useOverflow = (containerRef: Ref<HTMLElement>) => { const overflow = ref(false) let resizeObserver: ResizeObserver | undefined const cleanup = () => { if (resizeObserver) { resizeObserver.unobserve(containerRef.value) resizeObserver = null } } const observer = () => { const container = containerRef.value const content = container.querySelector('label') const containerWidth = container.getBoundingClientRect().width const contentWidth = content?.getBoundingClientRect().width if (containerWidth !== 0) { if (contentWidth > containerWidth) { overflow.value = true } else { overflow.value = false } } } const stopWatch = watch( () => containerRef.value, (el) => { cleanup() if (el) { resizeObserver = new ResizeObserver(observer) resizeObserver.observe(el) } }, { immediate: true, flush: 'post' } ) onBeforeUnmount(() => { cleanup() stopWatch() }) return overflow } const ICON_MAP = { error: () => h('i', { class: 'el-icon-circle-close' }, {}), success: () => h('i', { class: 'el-icon-circle-check' }, {}), warning: () => h('i', { class: 'el-icon-warning-outline' }, {}), } export const FormBaseItem = defineComponent<FormItemProps>({ name: 'FormItem', props: { className: {}, required: {}, label: {}, colon: {}, layout: {}, tooltip: {}, labelStyle: {}, labelAlign: {}, labelWrap: {}, labelWidth: {}, wrapperWidth: {}, labelCol: {}, wrapperCol: {}, wrapperAlign: {}, wrapperWrap: {}, wrapperStyle: {}, fullness: {}, addonBefore: {}, addonAfter: {}, size: {}, extra: {}, feedbackText: {}, feedbackLayout: {}, tooltipLayout: {}, feedbackStatus: {}, feedbackIcon: {}, asterisk: {}, gridSpan: {}, bordered: { default: true }, inset: { default: false }, }, setup(props, { slots, refs }) { const active = ref(false) const deepLayoutRef = useFormLayout() const prefixCls = `${stylePrefix}-form-item` const { elRef: containerRef, elRefBinder } = useCompatRef(refs) const overflow = useOverflow(containerRef) provide(FormLayoutShallowContext, ref(null)) return () => { const gridStyles: Record<string, any> = {} const deepLayout = deepLayoutRef.value const { label, colon = deepLayout.colon ?? true, layout = deepLayout.layout ?? 'horizontal', tooltip, labelStyle = {}, labelWrap = deepLayout.labelWrap ?? false, labelWidth = deepLayout.labelWidth, wrapperWidth = deepLayout.wrapperWidth, labelCol = deepLayout.labelCol, wrapperCol = deepLayout.wrapperCol, wrapperAlign = deepLayout.wrapperAlign ?? 'left', wrapperWrap = deepLayout.wrapperWrap, wrapperStyle = {}, fullness = deepLayout.fullness, addonBefore, addonAfter, size = deepLayout.size, extra, feedbackText, feedbackLayout = deepLayout.feedbackLayout ?? 'loose', tooltipLayout = deepLayout.tooltipLayout ?? 'icon', feedbackStatus, feedbackIcon, asterisk, bordered = deepLayout.bordered, inset = deepLayout.inset, } = props const labelAlign = deepLayout.layout === 'vertical' ? props.labelAlign ?? deepLayout.labelAlign ?? 'left' : props.labelAlign ?? deepLayout.labelAlign ?? 'right' // 固定宽度 let enableCol = false if (labelWidth || wrapperWidth) { if (labelWidth) { labelStyle.width = `${labelWidth}px` labelStyle.maxWidth = `${labelWidth}px` } if (wrapperWidth) { wrapperStyle.width = `${wrapperWidth}px` wrapperStyle.maxWidth = `${wrapperWidth}px` } // 栅格模式 } else if (labelCol || wrapperCol) { enableCol = true } const formatChildren = feedbackLayout === 'popover' ? h( 'el-popover', { props: { disabled: !feedbackText, placement: 'top', }, }, { reference: () => h('div', {}, { default: () => slots.default?.() }), default: () => [ h( 'div', { class: { [`${prefixCls}-${feedbackStatus}-help`]: !!feedbackStatus, [`${prefixCls}-help`]: true, }, }, { default: () => [ feedbackStatus && ['error', 'success', 'warning'].includes(feedbackStatus) ? ICON_MAP[ feedbackStatus as 'error' | 'success' | 'warning' ]() : '', resolveComponent(feedbackText), ], } ), ], } ) : slots.default?.() const renderLabelText = () => { const labelChildren = h( 'div', { class: `${prefixCls}-label-content`, ref: elRefBinder, }, { default: () => [ asterisk && h( 'span', { class: `${prefixCls}-asterisk` }, { default: () => ['*'] } ), h('label', {}, { default: () => [resolveComponent(label)] }), ], } ) const isTextTooltip = tooltip && tooltipLayout === 'text' if (isTextTooltip || overflow.value) { return h( Tooltip, { props: { placement: 'top', }, }, { default: () => [labelChildren], content: () => h( 'div', {}, { default: () => [ overflow.value && resolveComponent(label), isTextTooltip && resolveComponent(tooltip), ], } ), } ) } else { return labelChildren } } const renderTooltipIcon = () => { if (tooltip && tooltipLayout === 'icon') { return h( 'span', { class: `${prefixCls}-label-tooltip`, }, { default: () => [ h( Tooltip, { props: { placement: 'top', }, }, { default: () => [h('i', { class: 'el-icon-info' }, {})], content: () => h( 'div', { class: `${prefixCls}-label-tooltip-content`, }, { default: () => [resolveComponent(tooltip)], } ), } ), ], } ) } } const renderLabel = label && h( 'div', { class: { [`${prefixCls}-label`]: true, [`${prefixCls}-label-tooltip`]: (tooltip && tooltipLayout === 'text') || overflow.value, [`${prefixCls}-item-col-${labelCol}`]: enableCol && !!labelCol, }, style: labelStyle, }, { default: () => [ // label content renderLabelText(), // label tooltip renderTooltipIcon(), // label colon label && h( 'span', { class: `${prefixCls}-colon`, }, { default: () => [colon ? ':' : ''] } ), ], } ) const renderFeedback = !!feedbackText && feedbackLayout !== 'popover' && feedbackLayout !== 'none' && h( 'div', { class: { [`${prefixCls}-${feedbackStatus}-help`]: !!feedbackStatus, [`${prefixCls}-help`]: true, [`${prefixCls}-help-enter`]: true, [`${prefixCls}-help-enter-active`]: true, }, }, { default: () => [resolveComponent(feedbackText)] } ) const renderExtra = extra && h( 'div', { class: `${prefixCls}-extra` }, { default: () => [resolveComponent(extra)] } ) const renderContent = h( 'div', { class: { [`${prefixCls}-control`]: true, [`${prefixCls}-item-col-${wrapperCol}`]: enableCol && !!wrapperCol, }, }, { default: () => [ h( 'div', { class: `${prefixCls}-control-content` }, { default: () => [ addonBefore && h( 'div', { class: `${prefixCls}-addon-before` }, { default: () => [resolveComponent(addonBefore)], } ), h( 'div', { class: { [`${prefixCls}-control-content-component`]: true, [`${prefixCls}-control-content-component-has-feedback-icon`]: !!feedbackIcon, }, style: wrapperStyle, }, { default: () => [ formatChildren, feedbackIcon && h( 'div', { class: `${prefixCls}-feedback-icon` }, { default: () => [ typeof feedbackIcon === 'string' ? h('i', { class: feedbackIcon }, {}) : resolveComponent(feedbackIcon), ], } ), ], } ), addonAfter && h( 'div', { class: `${prefixCls}-addon-after` }, { default: () => [resolveComponent(addonAfter)], } ), ], } ), renderFeedback, renderExtra, ], } ) return h( 'div', { style: { ...gridStyles, }, attrs: { 'data-grid-span': props.gridSpan, }, class: { [`${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, [`${prefixCls}-active`]: active.value, [`${prefixCls}-inset-active`]: !!inset && active.value, [`${prefixCls}-label-align-${labelAlign}`]: true, [`${prefixCls}-control-align-${wrapperAlign}`]: true, [`${prefixCls}-label-wrap`]: !!labelWrap, [`${prefixCls}-control-wrap`]: !!wrapperWrap, [`${prefixCls}-bordered-none`]: bordered === false || !!inset || !!feedbackIcon, [`${props.className}`]: !!props.className, }, on: { focus: () => { if (feedbackIcon || inset) { active.value = true } }, blur: () => { if (feedbackIcon || inset) { active.value = false } }, }, }, { default: () => [renderLabel, renderContent], } ) } }, }) const Item = connect( FormBaseItem, mapProps( { validateStatus: true, title: 'label', required: true }, (props, field) => { if (isVoidField(field)) return props if (!field) return props const takeMessage = () => { if (field.validating) return if (props.feedbackText) return props.feedbackText if (field.selfErrors.length) return field.selfErrors if (field.selfWarnings.length) return field.selfWarnings if (field.selfSuccesses.length) return field.selfSuccesses } const errorMessages = takeMessage() return { feedbackText: Array.isArray(errorMessages) ? errorMessages.join(', ') : errorMessages, extra: props.extra || field.description, } }, (props, field) => { if (isVoidField(field)) return props if (!field) return props return { feedbackStatus: field.validateStatus === 'validating' ? 'pending' : (Array.isArray(field.decorator) && field.decorator[1]?.feedbackStatus) || field.validateStatus, } }, (props, field) => { if (isVoidField(field)) return props if (!field) return props let asterisk = false if (field.required && field.pattern !== 'readPretty') { asterisk = true } if ('asterisk' in props) { asterisk = props.asterisk } return { asterisk, } } ) ) export const FormItem = composeExport(Item, { BaseItem: FormBaseItem, }) export default FormItem ``` -------------------------------------------------------------------------------- /packages/next/docs/components/ArrayCollapse.md: -------------------------------------------------------------------------------- ```markdown # ArrayCollapse > Folding panel, it is more suitable to use ArrayCollapse for scenes with more fields in each row and more linkage > > Note: This component is only applicable to Schema scenarios ## Markup Schema example ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from '@alifd/next' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm() export default () => { return ( <FormProvider form={form}> <SchemaField> <SchemaField.Array name="string_array" maxItems={3} x-decorator="FormItem" x-component="ArrayCollapse" x-component-props={{ accordion: true, defaultOpenPanelCount: 3, }} > <SchemaField.Void x-component="ArrayCollapse.CollapsePanel" x-component-props={{ title: 'String array', }} > <SchemaField.Void x-component="ArrayCollapse.Index" /> <SchemaField.String name="input" x-decorator="FormItem" title="Input" required x-component="Input" /> <SchemaField.Void x-component="ArrayCollapse.Remove" /> <SchemaField.Void x-component="ArrayCollapse.MoveUp" /> <SchemaField.Void x-component="ArrayCollapse.MoveDown" /> </SchemaField.Void> <SchemaField.Void x-component="ArrayCollapse.Addition" title="Add entry" /> </SchemaField.Array> <SchemaField.Array name="array" maxItems={3} x-decorator="FormItem" x-component="ArrayCollapse" > <SchemaField.Object x-component="ArrayCollapse.CollapsePanel" x-component-props={{ title: 'Object array', }} > <SchemaField.Void x-component="ArrayCollapse.Index" /> <SchemaField.String name="input" x-decorator="FormItem" title="Input" required x-component="Input" /> <SchemaField.Void x-component="ArrayCollapse.Remove" /> <SchemaField.Void x-component="ArrayCollapse.MoveUp" /> <SchemaField.Void x-component="ArrayCollapse.MoveDown" /> </SchemaField.Object> <SchemaField.Void x-component="ArrayCollapse.Addition" title="Add entry" /> </SchemaField.Array> <SchemaField.Array name="string_array_unshift" maxItems={3} x-decorator="FormItem" x-component="ArrayCollapse" x-component-props={{ defaultOpenPanelCount: 8, }} > <SchemaField.Void x-component="ArrayCollapse.CollapsePanel" x-component-props={{ title: 'String array', }} > <SchemaField.Void x-component="ArrayCollapse.Index" /> <SchemaField.String name="input" x-decorator="FormItem" title="Input" required x-component="Input" /> <SchemaField.Void x-component="ArrayCollapse.Remove" /> <SchemaField.Void x-component="ArrayCollapse.MoveUp" /> <SchemaField.Void x-component="ArrayCollapse.MoveDown" /> </SchemaField.Void> <SchemaField.Void x-component="ArrayCollapse.Addition" title="Add entry (unshift)" x-component-props={{ method: 'unshift', }} /> </SchemaField.Array> </SchemaField> <FormButtonGroup> <Button onClick={() => { form.setInitialValues({ array: Array.from({ length: 10 }).map(() => ({ input: 'default value', })), string_array: Array.from({ length: 10 }).map( () => 'default value' ), string_array_unshift: Array.from({ length: 10 }).map( () => 'default value' ), }) }} > Load default data </Button> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## JSON Schema case ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm() const schema = { type: 'object', properties: { string_array: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, 'x-decorator': 'FormItem', items: { type: 'void', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { title: 'String array', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: 'Add entry', 'x-component': 'ArrayCollapse.Addition', }, }, }, array: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, 'x-decorator': 'FormItem', items: { type: 'object', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { title: 'Object array', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: 'Add entry', 'x-component': 'ArrayCollapse.Addition', }, }, }, array_unshift: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, 'x-decorator': 'FormItem', items: { type: 'object', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { title: 'Object array', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: 'Add entry (unshift)', 'x-component': 'ArrayCollapse.Addition', 'x-component-props': { method: 'unshift', }, }, }, }, }, } export default () => { return ( <FormProvider form={form}> <SchemaField schema={schema} /> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## Effects linkage case ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/next' import { createForm, onFieldChange, onFieldReact } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm({ effects: () => { //Active linkage mode onFieldChange('array.*.aa', ['value'], (field, form) => { form.setFieldState(field.query('.bb'), (state) => { state.visible = field.value != '123' }) }) //Passive linkage mode 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="ArrayCollapse" x-decorator="FormItem" x-component-props={{ title: 'Object array', }} > <SchemaField.Object x-component="ArrayCollapse.CollapsePanel" x-component-props={{ title: 'Object array', }} > <SchemaField.Void x-component="ArrayCollapse.Index" /> <SchemaField.String name="aa" x-decorator="FormItem" title="AA" required description="AA hide BB when entering 123" 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="Hide DD when CC enters 123" x-component="Input" /> <SchemaField.String name="dd" x-decorator="FormItem" title="DD" required x-component="Input" /> <SchemaField.Void x-component="ArrayCollapse.Remove" /> <SchemaField.Void x-component="ArrayCollapse.MoveUp" /> <SchemaField.Void x-component="ArrayCollapse.MoveDown" /> </SchemaField.Object> <SchemaField.Void x-component="ArrayCollapse.Addition" title="Add entry" /> </SchemaField.Array> </SchemaField> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## JSON Schema linkage case ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm() const schema = { type: 'object', properties: { array: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, title: 'Object array', items: { type: 'object', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { title: 'Object array', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, aa: { type: 'string', 'x-decorator': 'FormItem', title: 'AA', required: true, 'x-component': 'Input', description: 'Enter 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': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: 'Add entry', 'x-component': 'ArrayCollapse.Addition', }, }, }, }, } export default () => { return ( <FormProvider form={form}> <SchemaField schema={schema} /> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## API ### ArrayCollapse Reference https://fusion.design/pc/component/collapse Extended attributes | Property name | Type | Description | Default value | | --------------------- | ------ | ---------------------------- | ------------- | | defaultOpenPanelCount | number | Default expanded Panel count | 5 | ### ArrayCollapse.CollapsePanel Reference https://fusion.design/pc/component/collapse ### ArrayCollapse.Addition > Add button Extended attributes | Property name | Type | Description | Default value | | ------------- | -------------------- | ------------- | ------------- | | title | ReactText | Copywriting | | | method | `'push' \|'unshift'` | add method | `'push'` | | defaultValue | `any` | Default value | | Other references https://fusion.design/pc/component/basic/button Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective ### ArrayCollapse.Remove > Delete button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective ### ArrayCollapse.MoveDown > Move down button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective ### ArrayCollapse.MoveUp > Move up button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective ### ArrayCollapse.Index > Index Renderer No attributes ### ArrayCollapse.useIndex > Read the React Hook of the current rendering row index ### ArrayCollapse.useRecord > Read the React Hook of the current rendering row ``` -------------------------------------------------------------------------------- /packages/antd/docs/components/ArrayCollapse.md: -------------------------------------------------------------------------------- ```markdown # ArrayCollapse > Folding panel, it is more suitable to use ArrayCollapse for scenes with more fields in each row and more linkage > > Note: This component is only applicable to Schema scenarios ## Markup Schema example ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' import { Button } from 'antd' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm() export default () => { return ( <FormProvider form={form}> <SchemaField> <SchemaField.Array name="string_array" maxItems={3} x-decorator="FormItem" x-component="ArrayCollapse" x-component-props={{ accordion: true, defaultOpenPanelCount: 3, }} > <SchemaField.Void x-component="ArrayCollapse.CollapsePanel" x-component-props={{ header: 'String array', }} > <SchemaField.Void x-component="ArrayCollapse.Index" /> <SchemaField.String name="input" x-decorator="FormItem" title="Input" required x-component="Input" /> <SchemaField.Void x-component="ArrayCollapse.Remove" /> <SchemaField.Void x-component="ArrayCollapse.MoveUp" /> <SchemaField.Void x-component="ArrayCollapse.MoveDown" /> </SchemaField.Void> <SchemaField.Void x-component="ArrayCollapse.Addition" title="Add entry" /> </SchemaField.Array> <SchemaField.Array name="array" maxItems={3} x-decorator="FormItem" x-component="ArrayCollapse" > <SchemaField.Object x-component="ArrayCollapse.CollapsePanel" x-component-props={{ header: 'Object array', }} > <SchemaField.Void x-component="ArrayCollapse.Index" /> <SchemaField.String name="input" x-decorator="FormItem" title="Input" required x-component="Input" /> <SchemaField.Void x-component="ArrayCollapse.Remove" /> <SchemaField.Void x-component="ArrayCollapse.MoveUp" /> <SchemaField.Void x-component="ArrayCollapse.MoveDown" /> </SchemaField.Object> <SchemaField.Void x-component="ArrayCollapse.Addition" title="Add entry" /> </SchemaField.Array> <SchemaField.Array name="string_array_unshift" maxItems={3} x-decorator="FormItem" x-component="ArrayCollapse" x-component-props={{ defaultOpenPanelCount: 8, }} > <SchemaField.Void x-component="ArrayCollapse.CollapsePanel" x-component-props={{ header: 'String array', }} > <SchemaField.Void x-component="ArrayCollapse.Index" /> <SchemaField.String name="input" x-decorator="FormItem" title="Input" required x-component="Input" /> <SchemaField.Void x-component="ArrayCollapse.Remove" /> <SchemaField.Void x-component="ArrayCollapse.MoveUp" /> <SchemaField.Void x-component="ArrayCollapse.MoveDown" /> </SchemaField.Void> <SchemaField.Void x-component="ArrayCollapse.Addition" title="Add entry (unshift)" x-component-props={{ method: 'unshift', }} /> </SchemaField.Array> </SchemaField> <FormButtonGroup> <Button onClick={() => { form.setInitialValues({ array: Array.from({ length: 10 }).map(() => ({ input: 'default value', })), string_array: Array.from({ length: 10 }).map( () => 'default value' ), string_array_unshift: Array.from({ length: 10 }).map( () => 'default value' ), }) }} > Load default data </Button> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## JSON Schema case ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm() const schema = { type: 'object', properties: { string_array: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, 'x-decorator': 'FormItem', items: { type: 'void', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { header: 'String array', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: 'Add entry', 'x-component': 'ArrayCollapse.Addition', }, }, }, array: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, 'x-decorator': 'FormItem', items: { type: 'object', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { header: 'Object array', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: 'Add entry', 'x-component': 'ArrayCollapse.Addition', }, }, }, array_unshift: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, 'x-decorator': 'FormItem', items: { type: 'object', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { header: 'Object array', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, input: { type: 'string', 'x-decorator': 'FormItem', title: 'Input', required: true, 'x-component': 'Input', }, remove: { type: 'void', 'x-component': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: 'Add entry (unshift)', 'x-component': 'ArrayCollapse.Addition', 'x-component-props': { method: 'unshift', }, }, }, }, }, } export default () => { return ( <FormProvider form={form}> <SchemaField schema={schema} /> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## Effects linkage case ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/antd' import { createForm, onFieldChange, onFieldReact } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm({ effects: () => { //Active linkage mode onFieldChange('array.*.aa', ['value'], (field, form) => { form.setFieldState(field.query('.bb'), (state) => { state.visible = field.value != '123' }) }) //Passive linkage mode 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="ArrayCollapse" x-decorator="FormItem" x-component-props={{ title: 'Object array', }} > <SchemaField.Object x-component="ArrayCollapse.CollapsePanel" x-component-props={{ header: 'Object array', }} > <SchemaField.Void x-component="ArrayCollapse.Index" /> <SchemaField.String name="aa" x-decorator="FormItem" title="AA" required description="AA hide BB when entering 123" 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="Hide DD when CC enters 123" x-component="Input" /> <SchemaField.String name="dd" x-decorator="FormItem" title="DD" required x-component="Input" /> <SchemaField.Void x-component="ArrayCollapse.Remove" /> <SchemaField.Void x-component="ArrayCollapse.MoveUp" /> <SchemaField.Void x-component="ArrayCollapse.MoveDown" /> </SchemaField.Object> <SchemaField.Void x-component="ArrayCollapse.Addition" title="Add entry" /> </SchemaField.Array> </SchemaField> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## JSON Schema linkage case ```tsx import React from 'react' import { FormItem, Input, ArrayCollapse, FormButtonGroup, Submit, } from '@formily/antd' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { FormItem, Input, ArrayCollapse, }, }) const form = createForm() const schema = { type: 'object', properties: { array: { type: 'array', 'x-component': 'ArrayCollapse', maxItems: 3, title: 'Object array', items: { type: 'object', 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { header: 'Object array', }, properties: { index: { type: 'void', 'x-component': 'ArrayCollapse.Index', }, aa: { type: 'string', 'x-decorator': 'FormItem', title: 'AA', required: true, 'x-component': 'Input', description: 'Enter 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': 'ArrayCollapse.Remove', }, moveUp: { type: 'void', 'x-component': 'ArrayCollapse.MoveUp', }, moveDown: { type: 'void', 'x-component': 'ArrayCollapse.MoveDown', }, }, }, properties: { addition: { type: 'void', title: 'Add entry', 'x-component': 'ArrayCollapse.Addition', }, }, }, }, } export default () => { return ( <FormProvider form={form}> <SchemaField schema={schema} /> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> ) } ``` ## API ### ArrayCollapse Reference https://ant.design/components/collapse-cn/ ### ArrayCollapse.CollapsePanel Reference https://ant.design/components/collapse-cn/ ### ArrayCollapse.Addition > Add button Extended attributes | Property name | Type | Description | Default value | | ------------- | -------------------- | ------------- | ------------- | | title | ReactText | Copywriting | | | method | `'push' \|'unshift'` | add method | `'push'` | | defaultValue | `any` | Default value | | Other references https://ant.design/components/button-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayCollapse.Remove > Delete button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayCollapse.MoveDown > Move down button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayCollapse.MoveUp > Move up button | Property name | Type | Description | Default value | | ------------- | --------- | ----------- | ------------- | | title | ReactText | Copywriting | | Other references https://ant.design/components/icon-cn/ Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. ### ArrayCollapse.Index > Index Renderer No attributes ### ArrayCollapse.useIndex > Read the React Hook of the current rendering row index ### ArrayCollapse.useRecord > Read the React Hook of the current rendering row ``` -------------------------------------------------------------------------------- /packages/element/src/array-table/index.ts: -------------------------------------------------------------------------------- ```typescript import { ArrayField, FieldDisplayTypes, GeneralField, IVoidFieldFactoryProps, } from '@formily/core' import type { Schema } from '@formily/json-schema' import { observer } from '@formily/reactive-vue' import { isArr, isBool, isFn } from '@formily/shared' import { Fragment, h, RecursionField as _RecursionField, useField, useFieldSchema, } from '@formily/vue' import type { Pagination as PaginationProps, Table as TableProps, TableColumn as ElColumnProps, } from 'element-ui' import { Badge, Option, Pagination, Select, Table as ElTable, TableColumn as ElTableColumn, } from 'element-ui' import type { Component, VNode } from 'vue' import { computed, defineComponent, inject, provide, ref, Ref } from 'vue-demi' import { ArrayBase } from '../array-base' import { Space } from '../space' import { stylePrefix } from '../__builtins__/configs' import { composeExport } from '../__builtins__/shared' const RecursionField = _RecursionField as unknown as Component interface IArrayTableProps extends TableProps { pagination?: PaginationProps | boolean } interface IArrayTablePaginationProps extends PaginationProps { dataSource?: any[] } interface ObservableColumnSource { field: GeneralField fieldProps: IVoidFieldFactoryProps<any, any> columnProps: ElColumnProps & { title: string; asterisk: boolean } schema: Schema display: FieldDisplayTypes required: boolean name: string } type ColumnProps = ElColumnProps & { key: string | number asterisk: boolean render?: ( startIndex?: Ref<number> ) => (props: { row: Record<string, any> column: ElColumnProps $index: number }) => VNode } interface PaginationAction { totalPage?: number pageSize?: number changePage?: (page: number) => void } const PaginationSymbol = Symbol('pagination') 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 getArrayTableSources = ( arrayFieldRef: Ref<ArrayField>, schemaRef: Ref<Schema> ) => { const arrayField = arrayFieldRef.value const parseSources = (schema: Schema): ObservableColumnSource[] => { if ( isColumnComponent(schema) || isOperationsComponent(schema) || isAdditionComponent(schema) ) { if (!schema['x-component-props']?.['prop'] && !schema['name']) return [] const name = schema['x-component-props']?.['prop'] || schema['name'] const field = arrayField.query(arrayField.address.concat(name)).take() const fieldProps = field?.props || schema.toFieldProps() const columnProps = (field?.component as any[])?.[1] || schema['x-component-props'] || {} const display = field?.display || schema['x-display'] const required = schema.reduceProperties((required, property) => { if (required) { return required } return !!property.required }, false) return [ { name, display, required, field, fieldProps, schema, columnProps, }, ] } else if (schema.properties) { return schema.reduceProperties((buf: any[], schema) => { return buf.concat(parseSources(schema)) }, []) } else { return [] } } const parseArrayTable = (schema: Schema['items']) => { if (!schema) return [] const sources: ObservableColumnSource[] = [] const items = isArr(schema) ? schema : ([schema] as Schema[]) return items.reduce((columns, schema) => { const item = parseSources(schema) if (item) { return columns.concat(item) } return columns }, sources) } if (!schemaRef.value) throw new Error('can not found schema object') return parseArrayTable(schemaRef.value.items) } const getArrayTableColumns = ( sources: ObservableColumnSource[] ): ColumnProps[] => { return sources.reduce( ( buf: ColumnProps[], { name, columnProps, schema, display, required }, key ) => { const { title, asterisk, ...props } = columnProps if (display !== 'visible') return buf if (!isColumnComponent(schema)) return buf const render = (startIndex?: Ref<number>) => { return columnProps?.type && columnProps?.type !== 'default' ? undefined : (props: { row: Record<string, any> column: ElColumnProps $index: number }): VNode => { let index = (startIndex?.value ?? 0) + props.$index // const index = reactiveDataSource.value.indexOf(props.row) const children = h( ArrayBase.Item, { props: { index, record: props.row }, key: `${key}${index}` }, { default: () => h( RecursionField, { props: { schema, name: index, onlyRenderProperties: true, }, }, {} ), } ) return children } } return buf.concat({ label: title, ...props, key, prop: name, asterisk: asterisk ?? required, render, }) }, [] ) } const renderAddition = () => { const schema = useFieldSchema() return schema.value.reduceProperties((addition, schema) => { if (isAdditionComponent(schema)) { return h( RecursionField, { props: { schema, name: 'addition', }, }, {} ) } return addition }, null) } const schedulerRequest = { request: null, } const StatusSelect = observer( defineComponent({ props: { value: Number, onChange: Function, options: Array, pageSize: Number, }, setup(props) { const fieldRef = useField<ArrayField>() const prefixCls = `${stylePrefix}-array-table` return () => { const field = fieldRef.value const width = String(props.options?.length).length * 15 const errors = field.errors const parseIndex = (address: string) => { return Number( address .slice(address.indexOf(field.address.toString()) + 1) .match(/(\d+)/)?.[1] ) } return h( Select, { style: { width: `${width < 60 ? 60 : width}px`, }, class: [ `${prefixCls}-status-select`, { 'has-error': errors?.length, }, ], props: { value: props.value, popperClass: `${prefixCls}-status-select-dropdown`, }, on: { input: props.onChange, }, }, { default: () => { return props.options?.map(({ label, value }) => { const hasError = errors.some(({ address }) => { const currentIndex = parseIndex(address) const startIndex = (value - 1) * props.pageSize const endIndex = value * props.pageSize return currentIndex >= startIndex && currentIndex <= endIndex }) return h( Option, { key: value, props: { label, value, }, }, { default: () => { if (hasError) { return h( Badge, { props: { isDot: true, }, }, { default: () => label } ) } return label }, } ) }) }, } ) } }, }), { scheduler: (update) => { clearTimeout(schedulerRequest.request) schedulerRequest.request = setTimeout(() => { update() }, 100) }, } ) const usePagination = () => { return inject<Ref<PaginationAction>>(PaginationSymbol, ref({})) } const ArrayTablePagination = defineComponent<IArrayTablePaginationProps>({ inheritAttrs: false, props: ['pageSize', 'dataSource'], setup(props, { attrs, slots }) { const prefixCls = `${stylePrefix}-array-table` const current = ref(1) const pageSize = computed(() => props.pageSize || 10) const dataSource = computed(() => props.dataSource || []) const startIndex = computed(() => (current.value - 1) * pageSize.value) const endIndex = computed(() => startIndex.value + pageSize.value - 1) const total = computed(() => dataSource.value?.length || 0) const totalPage = computed(() => Math.ceil(total.value / pageSize.value)) const pages = computed(() => { return Array.from(new Array(totalPage.value)).map((_, index) => { const page = index + 1 return { label: page, value: page, } }) }) const renderPagination = function () { if (totalPage.value <= 1) return return h( 'div', { class: [`${prefixCls}-pagination`], }, { default: () => h( Space, {}, { default: () => [ h( StatusSelect, { props: { value: current.value, onChange: (val: number) => { current.value = val }, pageSize: pageSize.value, options: pages.value, }, }, {} ), h( Pagination, { props: { background: true, layout: 'prev, pager, next', ...attrs, pageSize: pageSize.value, pageCount: totalPage.value, currentPage: current.value, }, on: { 'current-change': (val: number) => { current.value = val }, }, }, {} ), ], } ), } ) } const paginationContext = computed<PaginationAction>(() => { return { totalPage: totalPage.value, pageSize: pageSize.value, changePage: (page: number) => (current.value = page), } }) provide(PaginationSymbol, paginationContext) return () => { return h( Fragment, {}, { default: () => slots?.default?.( dataSource.value?.slice(startIndex.value, endIndex.value + 1), renderPagination, startIndex ), } ) } }, }) const ArrayTableInner = observer( defineComponent<IArrayTableProps>({ name: 'FArrayTable', inheritAttrs: false, setup(props, { attrs, listeners, slots }) { const fieldRef = useField<ArrayField>() const schemaRef = useFieldSchema() const prefixCls = `${stylePrefix}-array-table` const { getKey, keyMap } = ArrayBase.useKey(schemaRef.value) const defaultRowKey = (record: any) => { return getKey(record) } return () => { const props = attrs as unknown as IArrayTableProps const field = fieldRef.value const dataSource = Array.isArray(field.value) ? field.value.slice() : [] const pagination = props.pagination const sources = getArrayTableSources(fieldRef, schemaRef) const columns = getArrayTableColumns(sources) const renderColumns = (startIndex?: Ref<number>) => { return columns.map(({ key, render, asterisk, ...props }) => { const children = {} as Record<string, any> if (render) { children.default = render(startIndex) } if (asterisk) { children.header = ({ column }: { column: ElColumnProps }) => h( 'span', {}, { default: () => [ h( 'span', { class: `${prefixCls}-asterisk` }, { default: () => ['*'] } ), column.label, ], } ) } return h( ElTableColumn, { key, props, }, children ) }) } const renderStateManager = () => sources.map((column, key) => { //专门用来承接对Column的状态管理 if (!isColumnComponent(column.schema)) return return h( RecursionField, { props: { name: column.name, schema: column.schema, onlyRenderSelf: true, }, key, }, {} ) }) const renderTable = ( dataSource?: any[], pager?: () => VNode, startIndex?: Ref<number> ) => { return h( 'div', { class: prefixCls }, { default: () => h( ArrayBase, { props: { keyMap, }, }, { default: () => [ h( ElTable, { props: { rowKey: defaultRowKey, ...attrs, data: dataSource, }, on: listeners, }, { ...slots, default: () => renderColumns(startIndex), } ), pager?.(), renderStateManager(), renderAddition(), ], } ), } ) } if (!pagination) { return renderTable(dataSource, null) } return h( ArrayTablePagination, { attrs: { ...(isBool(pagination) ? {} : pagination), dataSource, }, }, { default: renderTable } ) } }, }) ) const ArrayTableColumn: Component = { name: 'FArrayTableColumn', render(h) { return h() }, } const ArrayAddition = defineComponent({ name: 'ArrayAddition', setup(props, { attrs, listeners, slots }) { const array = ArrayBase.useArray() const paginationRef = usePagination() const onClick = listeners['click'] listeners['click'] = (e) => { const { totalPage = 0, pageSize = 10, changePage } = paginationRef.value // 如果添加数据后超过当前页,则自动切换到下一页 const total = array?.field?.value?.value.length || 0 if (total === (totalPage - 1) * pageSize + 1 && isFn(changePage)) { changePage(totalPage) } if (onClick) onClick(e) } return () => { return h( ArrayBase.Addition, { props, attrs, on: listeners, }, slots ) } }, }) export const ArrayTable = composeExport(ArrayTableInner, { Column: ArrayTableColumn, Index: ArrayBase.Index, SortHandle: ArrayBase.SortHandle, Addition: ArrayAddition, Remove: ArrayBase.Remove, MoveDown: ArrayBase.MoveDown, MoveUp: ArrayBase.MoveUp, useArray: ArrayBase.useArray, useIndex: ArrayBase.useIndex, useRecord: ArrayBase.useRecord, }) export default ArrayTable ```