This is page 32 of 52. Use http://codebase.md/alibaba/formily?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .all-contributorsrc ├── .codecov.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github │ ├── CONTRIBUTING.md │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ └── config.yml │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows │ ├── check-pr-title.yml │ ├── ci.yml │ ├── commitlint.yml │ ├── issue-open-check.yml │ ├── package-size.yml │ └── pr-welcome.yml ├── .gitignore ├── .prettierrc.js ├── .umirc.js ├── .vscode │ └── cspell.json ├── .yarnrc ├── CHANGELOG.md ├── commitlint.config.js ├── devtools │ ├── .eslintrc │ └── chrome-extension │ ├── .npmignore │ ├── assets │ │ └── img │ │ ├── loading.svg │ │ └── logo │ │ ├── 128x128.png │ │ ├── 16x16.png │ │ ├── 38x38.png │ │ ├── 48x48.png │ │ ├── error.png │ │ ├── gray.png │ │ └── scalable.png │ ├── config │ │ ├── webpack.base.ts │ │ ├── webpack.dev.ts │ │ └── webpack.prod.ts │ ├── LICENSE.md │ ├── package.json │ ├── src │ │ ├── app │ │ │ ├── components │ │ │ │ ├── FieldTree.tsx │ │ │ │ ├── filter.ts │ │ │ │ ├── LeftPanel.tsx │ │ │ │ ├── RightPanel.tsx │ │ │ │ ├── SearchBox.tsx │ │ │ │ └── Tabs.tsx │ │ │ ├── demo.tsx │ │ │ └── index.tsx │ │ └── extension │ │ ├── backend.ts │ │ ├── background.ts │ │ ├── content.ts │ │ ├── devpanel.tsx │ │ ├── devtools.tsx │ │ ├── inject.ts │ │ ├── manifest.json │ │ ├── popup.tsx │ │ └── views │ │ ├── devpanel.ejs │ │ ├── devtools.ejs │ │ └── popup.ejs │ ├── tsconfig.build.json │ └── tsconfig.json ├── docs │ ├── functions │ │ ├── contributors.ts │ │ └── npm-search.ts │ ├── guide │ │ ├── advanced │ │ │ ├── async.md │ │ │ ├── async.zh-CN.md │ │ │ ├── build.md │ │ │ ├── build.zh-CN.md │ │ │ ├── business-logic.md │ │ │ ├── business-logic.zh-CN.md │ │ │ ├── calculator.md │ │ │ ├── calculator.zh-CN.md │ │ │ ├── controlled.md │ │ │ ├── controlled.zh-CN.md │ │ │ ├── custom.md │ │ │ ├── custom.zh-CN.md │ │ │ ├── destructor.md │ │ │ ├── destructor.zh-CN.md │ │ │ ├── input.less │ │ │ ├── layout.md │ │ │ ├── layout.zh-CN.md │ │ │ ├── linkages.md │ │ │ ├── linkages.zh-CN.md │ │ │ ├── validate.md │ │ │ └── validate.zh-CN.md │ │ ├── contribution.md │ │ ├── contribution.zh-CN.md │ │ ├── form-builder.md │ │ ├── form-builder.zh-CN.md │ │ ├── index.md │ │ ├── index.zh-CN.md │ │ ├── issue-helper.md │ │ ├── issue-helper.zh-CN.md │ │ ├── learn-formily.md │ │ ├── learn-formily.zh-CN.md │ │ ├── quick-start.md │ │ ├── quick-start.zh-CN.md │ │ ├── scenes │ │ │ ├── dialog-drawer.md │ │ │ ├── dialog-drawer.zh-CN.md │ │ │ ├── edit-detail.md │ │ │ ├── edit-detail.zh-CN.md │ │ │ ├── index.less │ │ │ ├── login-register.md │ │ │ ├── login-register.zh-CN.md │ │ │ ├── more.md │ │ │ ├── more.zh-CN.md │ │ │ ├── query-list.md │ │ │ ├── query-list.zh-CN.md │ │ │ ├── step-form.md │ │ │ ├── step-form.zh-CN.md │ │ │ ├── tab-form.md │ │ │ ├── tab-form.zh-CN.md │ │ │ └── VerifyCode.tsx │ │ ├── upgrade.md │ │ └── upgrade.zh-CN.md │ ├── index.md │ ├── index.zh-CN.md │ └── site │ ├── Contributors.less │ ├── Contributors.tsx │ ├── QrCode.less │ ├── QrCode.tsx │ ├── Section.less │ ├── Section.tsx │ └── styles.less ├── global.config.ts ├── jest.config.js ├── lerna.json ├── LICENSE.md ├── package.json ├── packages │ ├── .eslintrc │ ├── antd │ │ ├── __tests__ │ │ │ ├── moment.spec.ts │ │ │ └── sideEffects.spec.ts │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── build-style.ts │ │ ├── create-style.ts │ │ ├── docs │ │ │ ├── components │ │ │ │ ├── ArrayCards.md │ │ │ │ ├── ArrayCards.zh-CN.md │ │ │ │ ├── ArrayCollapse.md │ │ │ │ ├── ArrayCollapse.zh-CN.md │ │ │ │ ├── ArrayItems.md │ │ │ │ ├── ArrayItems.zh-CN.md │ │ │ │ ├── ArrayTable.md │ │ │ │ ├── ArrayTable.zh-CN.md │ │ │ │ ├── ArrayTabs.md │ │ │ │ ├── ArrayTabs.zh-CN.md │ │ │ │ ├── Cascader.md │ │ │ │ ├── Cascader.zh-CN.md │ │ │ │ ├── Checkbox.md │ │ │ │ ├── Checkbox.zh-CN.md │ │ │ │ ├── DatePicker.md │ │ │ │ ├── DatePicker.zh-CN.md │ │ │ │ ├── Editable.md │ │ │ │ ├── Editable.zh-CN.md │ │ │ │ ├── Form.md │ │ │ │ ├── Form.zh-CN.md │ │ │ │ ├── FormButtonGroup.md │ │ │ │ ├── FormButtonGroup.zh-CN.md │ │ │ │ ├── FormCollapse.md │ │ │ │ ├── FormCollapse.zh-CN.md │ │ │ │ ├── FormDialog.md │ │ │ │ ├── FormDialog.zh-CN.md │ │ │ │ ├── FormDrawer.md │ │ │ │ ├── FormDrawer.zh-CN.md │ │ │ │ ├── FormGrid.md │ │ │ │ ├── FormGrid.zh-CN.md │ │ │ │ ├── FormItem.md │ │ │ │ ├── FormItem.zh-CN.md │ │ │ │ ├── FormLayout.md │ │ │ │ ├── FormLayout.zh-CN.md │ │ │ │ ├── FormStep.md │ │ │ │ ├── FormStep.zh-CN.md │ │ │ │ ├── FormTab.md │ │ │ │ ├── FormTab.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ ├── index.zh-CN.md │ │ │ │ ├── Input.md │ │ │ │ ├── Input.zh-CN.md │ │ │ │ ├── NumberPicker.md │ │ │ │ ├── NumberPicker.zh-CN.md │ │ │ │ ├── Password.md │ │ │ │ ├── Password.zh-CN.md │ │ │ │ ├── PreviewText.md │ │ │ │ ├── PreviewText.zh-CN.md │ │ │ │ ├── Radio.md │ │ │ │ ├── Radio.zh-CN.md │ │ │ │ ├── Reset.md │ │ │ │ ├── Reset.zh-CN.md │ │ │ │ ├── Select.md │ │ │ │ ├── Select.zh-CN.md │ │ │ │ ├── SelectTable.md │ │ │ │ ├── SelectTable.zh-CN.md │ │ │ │ ├── Space.md │ │ │ │ ├── Space.zh-CN.md │ │ │ │ ├── Submit.md │ │ │ │ ├── Submit.zh-CN.md │ │ │ │ ├── Switch.md │ │ │ │ ├── Switch.zh-CN.md │ │ │ │ ├── TimePicker.md │ │ │ │ ├── TimePicker.zh-CN.md │ │ │ │ ├── Transfer.md │ │ │ │ ├── Transfer.zh-CN.md │ │ │ │ ├── TreeSelect.md │ │ │ │ ├── TreeSelect.zh-CN.md │ │ │ │ ├── Upload.md │ │ │ │ └── Upload.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __builtins__ │ │ │ │ ├── hooks │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── useClickAway.ts │ │ │ │ │ └── usePrefixCls.ts │ │ │ │ ├── index.ts │ │ │ │ ├── loading.ts │ │ │ │ ├── moment.ts │ │ │ │ ├── pickDataProps.ts │ │ │ │ ├── portal.tsx │ │ │ │ ├── render.ts │ │ │ │ └── sort.tsx │ │ │ ├── array-base │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-cards │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-collapse │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-items │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-table │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── array-tabs │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── cascader │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── checkbox │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── date-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── editable │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form-button-group │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form-collapse │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-dialog │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-drawer │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-grid │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form-item │ │ │ │ ├── animation.less │ │ │ │ ├── grid.less │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── form-layout │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ ├── style.ts │ │ │ │ └── useResponsiveFormLayout.ts │ │ │ ├── form-step │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-tab │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── index.ts │ │ │ ├── input │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── number-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── password │ │ │ │ ├── index.tsx │ │ │ │ ├── PasswordStrength.tsx │ │ │ │ └── style.ts │ │ │ ├── preview-text │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── radio │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── style.ts │ │ │ ├── reset │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── select │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── select-table │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ ├── style.ts │ │ │ │ ├── useCheckSlackly.tsx │ │ │ │ ├── useFilterOptions.tsx │ │ │ │ ├── useFlatOptions.tsx │ │ │ │ ├── useSize.tsx │ │ │ │ ├── useTitleAddon.tsx │ │ │ │ └── utils.ts │ │ │ ├── space │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── style.less │ │ │ ├── style.ts │ │ │ ├── submit │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── switch │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── time-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── transfer │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── tree-select │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ └── upload │ │ │ ├── index.tsx │ │ │ ├── placeholder.ts │ │ │ └── style.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── benchmark │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ └── index.tsx │ │ ├── template.ejs │ │ ├── tsconfig.build.json │ │ ├── tsconfig.json │ │ ├── webpack.base.ts │ │ ├── webpack.dev.ts │ │ └── webpack.prod.ts │ ├── core │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── docs │ │ │ ├── api │ │ │ │ ├── entry │ │ │ │ │ ├── ActionResponse.less │ │ │ │ │ ├── ActionResponse.tsx │ │ │ │ │ ├── createForm.md │ │ │ │ │ ├── createForm.zh-CN.md │ │ │ │ │ ├── FieldEffectHooks.md │ │ │ │ │ ├── FieldEffectHooks.zh-CN.md │ │ │ │ │ ├── FormChecker.md │ │ │ │ │ ├── FormChecker.zh-CN.md │ │ │ │ │ ├── FormEffectHooks.md │ │ │ │ │ ├── FormEffectHooks.zh-CN.md │ │ │ │ │ ├── FormHooksAPI.md │ │ │ │ │ ├── FormHooksAPI.zh-CN.md │ │ │ │ │ ├── FormPath.md │ │ │ │ │ ├── FormPath.zh-CN.md │ │ │ │ │ ├── FormValidatorRegistry.md │ │ │ │ │ └── FormValidatorRegistry.zh-CN.md │ │ │ │ └── models │ │ │ │ ├── ArrayField.md │ │ │ │ ├── ArrayField.zh-CN.md │ │ │ │ ├── Field.md │ │ │ │ ├── Field.zh-CN.md │ │ │ │ ├── Form.md │ │ │ │ ├── Form.zh-CN.md │ │ │ │ ├── ObjectField.md │ │ │ │ ├── ObjectField.zh-CN.md │ │ │ │ ├── Query.md │ │ │ │ ├── Query.zh-CN.md │ │ │ │ ├── VoidField.md │ │ │ │ └── VoidField.zh-CN.md │ │ │ ├── guide │ │ │ │ ├── architecture.md │ │ │ │ ├── architecture.zh-CN.md │ │ │ │ ├── field.md │ │ │ │ ├── field.zh-CN.md │ │ │ │ ├── form.md │ │ │ │ ├── form.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ ├── index.zh-CN.md │ │ │ │ ├── mvvm.md │ │ │ │ ├── mvvm.zh-CN.md │ │ │ │ ├── values.md │ │ │ │ └── values.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── array.spec.ts │ │ │ │ ├── effects.spec.ts │ │ │ │ ├── externals.spec.ts │ │ │ │ ├── field.spec.ts │ │ │ │ ├── form.spec.ts │ │ │ │ ├── graph.spec.ts │ │ │ │ ├── heart.spec.ts │ │ │ │ ├── internals.spec.ts │ │ │ │ ├── lifecycle.spec.ts │ │ │ │ ├── object.spec.ts │ │ │ │ ├── shared.ts │ │ │ │ └── void.spec.ts │ │ │ ├── effects │ │ │ │ ├── index.ts │ │ │ │ ├── onFieldEffects.ts │ │ │ │ └── onFormEffects.ts │ │ │ ├── global.d.ts │ │ │ ├── index.ts │ │ │ ├── models │ │ │ │ ├── ArrayField.ts │ │ │ │ ├── BaseField.ts │ │ │ │ ├── Field.ts │ │ │ │ ├── Form.ts │ │ │ │ ├── Graph.ts │ │ │ │ ├── Heart.ts │ │ │ │ ├── index.ts │ │ │ │ ├── LifeCycle.ts │ │ │ │ ├── ObjectField.ts │ │ │ │ ├── Query.ts │ │ │ │ ├── types.ts │ │ │ │ └── VoidField.ts │ │ │ ├── shared │ │ │ │ ├── checkers.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── effective.ts │ │ │ │ ├── externals.ts │ │ │ │ └── internals.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── element │ │ ├── .npmignore │ │ ├── build-style.ts │ │ ├── create-style.ts │ │ ├── docs │ │ │ ├── .vuepress │ │ │ │ ├── components │ │ │ │ │ ├── createCodeSandBox.js │ │ │ │ │ ├── dumi-previewer.vue │ │ │ │ │ └── highlight.js │ │ │ │ ├── config.js │ │ │ │ ├── enhanceApp.js │ │ │ │ ├── styles │ │ │ │ │ └── index.styl │ │ │ │ └── util.js │ │ │ ├── demos │ │ │ │ ├── guide │ │ │ │ │ ├── array-cards │ │ │ │ │ │ ├── effects-json-schema.vue │ │ │ │ │ │ ├── effects-markup-schema.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── array-collapse │ │ │ │ │ │ ├── effects-json-schema.vue │ │ │ │ │ │ ├── effects-markup-schema.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── array-items │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── array-table │ │ │ │ │ │ ├── effects-json-schema.vue │ │ │ │ │ │ ├── effects-markup-schema.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── array-tabs │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── cascader │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── checkbox │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── date-picker │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── editable │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-button-group.vue │ │ │ │ │ ├── form-collapse │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── form-dialog │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-drawer │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-grid │ │ │ │ │ │ ├── form.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── native.vue │ │ │ │ │ ├── form-item │ │ │ │ │ │ ├── bordered-none.vue │ │ │ │ │ │ ├── common.vue │ │ │ │ │ │ ├── feedback.vue │ │ │ │ │ │ ├── inset.vue │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ ├── size.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-layout │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── form-step │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── form-tab │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ └── markup-schema.vue │ │ │ │ │ ├── form.vue │ │ │ │ │ ├── input │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── input-number │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── password │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── preview-text │ │ │ │ │ │ ├── base.vue │ │ │ │ │ │ └── extend.vue │ │ │ │ │ ├── radio │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── reset │ │ │ │ │ │ ├── base.vue │ │ │ │ │ │ ├── force.vue │ │ │ │ │ │ └── validate.vue │ │ │ │ │ ├── select │ │ │ │ │ │ ├── json-schema-async.vue │ │ │ │ │ │ ├── json-schema-sync.vue │ │ │ │ │ │ ├── markup-schema-async-search.vue │ │ │ │ │ │ ├── markup-schema-async.vue │ │ │ │ │ │ ├── markup-schema-sync.vue │ │ │ │ │ │ ├── template-async.vue │ │ │ │ │ │ └── template-sync.vue │ │ │ │ │ ├── space │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── submit │ │ │ │ │ │ ├── base.vue │ │ │ │ │ │ └── loading.vue │ │ │ │ │ ├── switch │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── time-picker │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ ├── transfer │ │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ │ └── template.vue │ │ │ │ │ └── upload │ │ │ │ │ ├── json-schema.vue │ │ │ │ │ ├── markup-schema.vue │ │ │ │ │ └── template.vue │ │ │ │ └── index.vue │ │ │ ├── guide │ │ │ │ ├── array-cards.md │ │ │ │ ├── array-collapse.md │ │ │ │ ├── array-items.md │ │ │ │ ├── array-table.md │ │ │ │ ├── array-tabs.md │ │ │ │ ├── cascader.md │ │ │ │ ├── checkbox.md │ │ │ │ ├── date-picker.md │ │ │ │ ├── editable.md │ │ │ │ ├── form-button-group.md │ │ │ │ ├── form-collapse.md │ │ │ │ ├── form-dialog.md │ │ │ │ ├── form-drawer.md │ │ │ │ ├── form-grid.md │ │ │ │ ├── form-item.md │ │ │ │ ├── form-layout.md │ │ │ │ ├── form-step.md │ │ │ │ ├── form-tab.md │ │ │ │ ├── form.md │ │ │ │ ├── index.md │ │ │ │ ├── input-number.md │ │ │ │ ├── input.md │ │ │ │ ├── password.md │ │ │ │ ├── preview-text.md │ │ │ │ ├── radio.md │ │ │ │ ├── reset.md │ │ │ │ ├── select.md │ │ │ │ ├── space.md │ │ │ │ ├── submit.md │ │ │ │ ├── switch.md │ │ │ │ ├── time-picker.md │ │ │ │ ├── transfer.md │ │ │ │ └── upload.md │ │ │ └── README.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __builtins__ │ │ │ │ ├── configs │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── shared │ │ │ │ │ ├── create-context.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── loading.ts │ │ │ │ │ ├── portal.ts │ │ │ │ │ ├── resolve-component.ts │ │ │ │ │ ├── transform-component.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ └── styles │ │ │ │ └── common.scss │ │ │ ├── array-base │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-cards │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-collapse │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-items │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-table │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── array-tabs │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── cascader │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── checkbox │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── date-picker │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── editable │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── el-form │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── el-form-item │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── form │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-button-group │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-collapse │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-dialog │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── form-drawer │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-grid │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── form-item │ │ │ │ ├── animation.scss │ │ │ │ ├── grid.scss │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ ├── style.ts │ │ │ │ └── var.scss │ │ │ ├── form-layout │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ ├── style.ts │ │ │ │ └── useResponsiveFormLayout.ts │ │ │ ├── form-step │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── form-tab │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── index.ts │ │ │ ├── input │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── input-number │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── password │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── preview-text │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── radio │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── reset │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── select │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── space │ │ │ │ ├── index.ts │ │ │ │ ├── style.scss │ │ │ │ └── style.ts │ │ │ ├── style.ts │ │ │ ├── submit │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── switch │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── time-picker │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ ├── transfer │ │ │ │ ├── index.ts │ │ │ │ └── style.ts │ │ │ └── upload │ │ │ ├── index.ts │ │ │ └── style.ts │ │ ├── transformer.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── grid │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── index.ts │ │ │ └── observer.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── json-schema │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── schema.spec.ts.snap │ │ │ │ ├── compiler.spec.ts │ │ │ │ ├── patches.spec.ts │ │ │ │ ├── schema.spec.ts │ │ │ │ ├── server-validate.spec.ts │ │ │ │ ├── shared.spec.ts │ │ │ │ ├── transformer.spec.ts │ │ │ │ └── traverse.spec.ts │ │ │ ├── compiler.ts │ │ │ ├── global.d.ts │ │ │ ├── index.ts │ │ │ ├── patches.ts │ │ │ ├── polyfills │ │ │ │ ├── index.ts │ │ │ │ └── SPECIFICATION_1_0.ts │ │ │ ├── schema.ts │ │ │ ├── shared.ts │ │ │ ├── transformer.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── next │ │ ├── __tests__ │ │ │ ├── moment.spec.ts │ │ │ └── sideEffects.spec.ts │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── build-style.ts │ │ ├── create-style.ts │ │ ├── docs │ │ │ ├── components │ │ │ │ ├── ArrayCards.md │ │ │ │ ├── ArrayCards.zh-CN.md │ │ │ │ ├── ArrayCollapse.md │ │ │ │ ├── ArrayCollapse.zh-CN.md │ │ │ │ ├── ArrayItems.md │ │ │ │ ├── ArrayItems.zh-CN.md │ │ │ │ ├── ArrayTable.md │ │ │ │ ├── ArrayTable.zh-CN.md │ │ │ │ ├── Cascader.md │ │ │ │ ├── Cascader.zh-CN.md │ │ │ │ ├── Checkbox.md │ │ │ │ ├── Checkbox.zh-CN.md │ │ │ │ ├── DatePicker.md │ │ │ │ ├── DatePicker.zh-CN.md │ │ │ │ ├── DatePicker2.md │ │ │ │ ├── DatePicker2.zh-CN.md │ │ │ │ ├── Editable.md │ │ │ │ ├── Editable.zh-CN.md │ │ │ │ ├── Form.md │ │ │ │ ├── Form.zh-CN.md │ │ │ │ ├── FormButtonGroup.md │ │ │ │ ├── FormButtonGroup.zh-CN.md │ │ │ │ ├── FormCollapse.md │ │ │ │ ├── FormCollapse.zh-CN.md │ │ │ │ ├── FormDialog.md │ │ │ │ ├── FormDialog.zh-CN.md │ │ │ │ ├── FormDrawer.md │ │ │ │ ├── FormDrawer.zh-CN.md │ │ │ │ ├── FormGrid.md │ │ │ │ ├── FormGrid.zh-CN.md │ │ │ │ ├── FormItem.md │ │ │ │ ├── FormItem.zh-CN.md │ │ │ │ ├── FormLayout.md │ │ │ │ ├── FormLayout.zh-CN.md │ │ │ │ ├── FormStep.md │ │ │ │ ├── FormStep.zh-CN.md │ │ │ │ ├── FormTab.md │ │ │ │ ├── FormTab.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ ├── index.zh-CN.md │ │ │ │ ├── Input.md │ │ │ │ ├── Input.zh-CN.md │ │ │ │ ├── NumberPicker.md │ │ │ │ ├── NumberPicker.zh-CN.md │ │ │ │ ├── Password.md │ │ │ │ ├── Password.zh-CN.md │ │ │ │ ├── PreviewText.md │ │ │ │ ├── PreviewText.zh-CN.md │ │ │ │ ├── Radio.md │ │ │ │ ├── Radio.zh-CN.md │ │ │ │ ├── Reset.md │ │ │ │ ├── Reset.zh-CN.md │ │ │ │ ├── Select.md │ │ │ │ ├── Select.zh-CN.md │ │ │ │ ├── SelectTable.md │ │ │ │ ├── SelectTable.zh-CN.md │ │ │ │ ├── Space.md │ │ │ │ ├── Space.zh-CN.md │ │ │ │ ├── Submit.md │ │ │ │ ├── Submit.zh-CN.md │ │ │ │ ├── Switch.md │ │ │ │ ├── Switch.zh-CN.md │ │ │ │ ├── TimePicker.md │ │ │ │ ├── TimePicker.zh-CN.md │ │ │ │ ├── TimePicker2.md │ │ │ │ ├── TimePicker2.zh-CN.md │ │ │ │ ├── Transfer.md │ │ │ │ ├── Transfer.zh-CN.md │ │ │ │ ├── TreeSelect.md │ │ │ │ ├── TreeSelect.zh-CN.md │ │ │ │ ├── Upload.md │ │ │ │ └── Upload.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LESENCE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __builtins__ │ │ │ │ ├── empty.tsx │ │ │ │ ├── hooks │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── useClickAway.ts │ │ │ │ │ └── usePrefixCls.ts │ │ │ │ ├── icons.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── loading.ts │ │ │ │ ├── mapSize.ts │ │ │ │ ├── mapStatus.ts │ │ │ │ ├── moment.ts │ │ │ │ ├── pickDataProps.ts │ │ │ │ ├── portal.tsx │ │ │ │ ├── render.ts │ │ │ │ └── toArray.ts │ │ │ ├── array-base │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── array-cards │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── array-collapse │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── array-items │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── array-table │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── cascader │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── checkbox │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── date-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── date-picker2 │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── editable │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── form │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── form-button-group │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── form-collapse │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-dialog │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-drawer │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-grid │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── form-item │ │ │ │ ├── animation.scss │ │ │ │ ├── grid.scss │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ ├── scss │ │ │ │ │ └── variable.scss │ │ │ │ └── style.ts │ │ │ ├── form-layout │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ ├── style.ts │ │ │ │ └── useResponsiveFormLayout.ts │ │ │ ├── form-step │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── form-tab │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── index.ts │ │ │ ├── input │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── main.scss │ │ │ ├── number-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── password │ │ │ │ ├── index.tsx │ │ │ │ ├── PasswordStrength.tsx │ │ │ │ └── style.ts │ │ │ ├── preview-text │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── radio │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── reset │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── select │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── select-table │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ ├── style.ts │ │ │ │ ├── useCheckSlackly.tsx │ │ │ │ ├── useFilterOptions.tsx │ │ │ │ ├── useFlatOptions.tsx │ │ │ │ ├── useSize.tsx │ │ │ │ ├── useTitleAddon.tsx │ │ │ │ └── utils.ts │ │ │ ├── space │ │ │ │ ├── index.tsx │ │ │ │ ├── main.scss │ │ │ │ └── style.ts │ │ │ ├── style.ts │ │ │ ├── submit │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── switch │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── time-picker │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── time-picker2 │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── transfer │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ ├── tree-select │ │ │ │ ├── index.tsx │ │ │ │ └── style.ts │ │ │ └── upload │ │ │ ├── index.tsx │ │ │ ├── main.scss │ │ │ ├── placeholder.ts │ │ │ └── style.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── path │ │ ├── .npmignore │ │ ├── benchmark.ts │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── accessor.spec.ts │ │ │ │ ├── basic.spec.ts │ │ │ │ ├── match.spec.ts │ │ │ │ ├── parser.spec.ts │ │ │ │ └── share.spec.ts │ │ │ ├── contexts.ts │ │ │ ├── destructor.ts │ │ │ ├── index.ts │ │ │ ├── matcher.ts │ │ │ ├── parser.ts │ │ │ ├── shared.ts │ │ │ ├── tokenizer.ts │ │ │ ├── tokens.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── react │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── docs │ │ │ ├── api │ │ │ │ ├── components │ │ │ │ │ ├── ArrayField.md │ │ │ │ │ ├── ArrayField.zh-CN.md │ │ │ │ │ ├── ExpressionScope.md │ │ │ │ │ ├── ExpressionScope.zh-CN.md │ │ │ │ │ ├── Field.md │ │ │ │ │ ├── Field.zh-CN.md │ │ │ │ │ ├── FormConsumer.md │ │ │ │ │ ├── FormConsumer.zh-CN.md │ │ │ │ │ ├── FormProvider.md │ │ │ │ │ ├── FormProvider.zh-CN.md │ │ │ │ │ ├── ObjectField.md │ │ │ │ │ ├── ObjectField.zh-CN.md │ │ │ │ │ ├── RecordScope.md │ │ │ │ │ ├── RecordScope.zh-CN.md │ │ │ │ │ ├── RecordsScope.md │ │ │ │ │ ├── RecordsScope.zh-CN.md │ │ │ │ │ ├── RecursionField.md │ │ │ │ │ ├── RecursionField.zh-CN.md │ │ │ │ │ ├── SchemaField.md │ │ │ │ │ ├── SchemaField.zh-CN.md │ │ │ │ │ ├── VoidField.md │ │ │ │ │ └── VoidField.zh-CN.md │ │ │ │ ├── hooks │ │ │ │ │ ├── useExpressionScope.md │ │ │ │ │ ├── useExpressionScope.zh-CN.md │ │ │ │ │ ├── useField.md │ │ │ │ │ ├── useField.zh-CN.md │ │ │ │ │ ├── useFieldSchema.md │ │ │ │ │ ├── useFieldSchema.zh-CN.md │ │ │ │ │ ├── useForm.md │ │ │ │ │ ├── useForm.zh-CN.md │ │ │ │ │ ├── useFormEffects.md │ │ │ │ │ ├── useFormEffects.zh-CN.md │ │ │ │ │ ├── useParentForm.md │ │ │ │ │ └── useParentForm.zh-CN.md │ │ │ │ └── shared │ │ │ │ ├── connect.md │ │ │ │ ├── connect.zh-CN.md │ │ │ │ ├── context.md │ │ │ │ ├── context.zh-CN.md │ │ │ │ ├── mapProps.md │ │ │ │ ├── mapProps.zh-CN.md │ │ │ │ ├── mapReadPretty.md │ │ │ │ ├── mapReadPretty.zh-CN.md │ │ │ │ ├── observer.md │ │ │ │ ├── observer.zh-CN.md │ │ │ │ ├── Schema.md │ │ │ │ └── Schema.zh-CN.md │ │ │ ├── guide │ │ │ │ ├── architecture.md │ │ │ │ ├── architecture.zh-CN.md │ │ │ │ ├── concept.md │ │ │ │ ├── concept.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ └── index.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── expression.spec.tsx │ │ │ │ ├── field.spec.tsx │ │ │ │ ├── form.spec.tsx │ │ │ │ ├── schema.json.spec.tsx │ │ │ │ ├── schema.markup.spec.tsx │ │ │ │ └── shared.tsx │ │ │ ├── components │ │ │ │ ├── ArrayField.tsx │ │ │ │ ├── ExpressionScope.tsx │ │ │ │ ├── Field.tsx │ │ │ │ ├── FormConsumer.tsx │ │ │ │ ├── FormProvider.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── ObjectField.tsx │ │ │ │ ├── ReactiveField.tsx │ │ │ │ ├── RecordScope.tsx │ │ │ │ ├── RecordsScope.tsx │ │ │ │ ├── RecursionField.tsx │ │ │ │ ├── SchemaField.tsx │ │ │ │ └── VoidField.tsx │ │ │ ├── global.d.ts │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ ├── useAttach.ts │ │ │ │ ├── useExpressionScope.ts │ │ │ │ ├── useField.ts │ │ │ │ ├── useFieldSchema.ts │ │ │ │ ├── useForm.ts │ │ │ │ ├── useFormEffects.ts │ │ │ │ └── useParentForm.ts │ │ │ ├── index.ts │ │ │ ├── shared │ │ │ │ ├── connect.ts │ │ │ │ ├── context.ts │ │ │ │ ├── index.ts │ │ │ │ └── render.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── reactive │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── benchmark.ts │ │ ├── docs │ │ │ ├── api │ │ │ │ ├── action.md │ │ │ │ ├── action.zh-CN.md │ │ │ │ ├── autorun.md │ │ │ │ ├── autorun.zh-CN.md │ │ │ │ ├── batch.md │ │ │ │ ├── batch.zh-CN.md │ │ │ │ ├── define.md │ │ │ │ ├── define.zh-CN.md │ │ │ │ ├── hasCollected.md │ │ │ │ ├── hasCollected.zh-CN.md │ │ │ │ ├── markObservable.md │ │ │ │ ├── markObservable.zh-CN.md │ │ │ │ ├── markRaw.md │ │ │ │ ├── markRaw.zh-CN.md │ │ │ │ ├── model.md │ │ │ │ ├── model.zh-CN.md │ │ │ │ ├── observable.md │ │ │ │ ├── observable.zh-CN.md │ │ │ │ ├── observe.md │ │ │ │ ├── observe.zh-CN.md │ │ │ │ ├── raw.md │ │ │ │ ├── raw.zh-CN.md │ │ │ │ ├── react │ │ │ │ │ ├── observer.md │ │ │ │ │ └── observer.zh-CN.md │ │ │ │ ├── reaction.md │ │ │ │ ├── reaction.zh-CN.md │ │ │ │ ├── toJS.md │ │ │ │ ├── toJS.zh-CN.md │ │ │ │ ├── tracker.md │ │ │ │ ├── tracker.zh-CN.md │ │ │ │ ├── typeChecker.md │ │ │ │ ├── typeChecker.zh-CN.md │ │ │ │ ├── untracked.md │ │ │ │ ├── untracked.zh-CN.md │ │ │ │ └── vue │ │ │ │ ├── observer.md │ │ │ │ └── observer.zh-CN.md │ │ │ ├── guide │ │ │ │ ├── best-practice.md │ │ │ │ ├── best-practice.zh-CN.md │ │ │ │ ├── concept.md │ │ │ │ ├── concept.zh-CN.md │ │ │ │ ├── index.md │ │ │ │ └── index.zh-CN.md │ │ │ ├── index.md │ │ │ └── index.zh-CN.md │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── action.spec.ts │ │ │ │ ├── annotations.spec.ts │ │ │ │ ├── array.spec.ts │ │ │ │ ├── autorun.spec.ts │ │ │ │ ├── batch.spec.ts │ │ │ │ ├── collections-map.spec.ts │ │ │ │ ├── collections-set.spec.ts │ │ │ │ ├── collections-weakmap.spec.ts │ │ │ │ ├── collections-weakset.spec.ts │ │ │ │ ├── define.spec.ts │ │ │ │ ├── externals.spec.ts │ │ │ │ ├── hasCollected.spec.ts │ │ │ │ ├── observable.spec.ts │ │ │ │ ├── observe.spec.ts │ │ │ │ ├── tracker.spec.ts │ │ │ │ └── untracked.spec.ts │ │ │ ├── action.ts │ │ │ ├── annotations │ │ │ │ ├── box.ts │ │ │ │ ├── computed.ts │ │ │ │ ├── index.ts │ │ │ │ ├── observable.ts │ │ │ │ ├── ref.ts │ │ │ │ └── shallow.ts │ │ │ ├── array.ts │ │ │ ├── autorun.ts │ │ │ ├── batch.ts │ │ │ ├── checkers.ts │ │ │ ├── environment.ts │ │ │ ├── externals.ts │ │ │ ├── global.d.ts │ │ │ ├── handlers.ts │ │ │ ├── index.ts │ │ │ ├── internals.ts │ │ │ ├── model.ts │ │ │ ├── observable.ts │ │ │ ├── observe.ts │ │ │ ├── reaction.ts │ │ │ ├── tracker.ts │ │ │ ├── tree.ts │ │ │ ├── types.ts │ │ │ └── untracked.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── reactive-react │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ ├── useCompatEffect.ts │ │ │ │ ├── useCompatFactory.ts │ │ │ │ ├── useDidUpdate.ts │ │ │ │ ├── useForceUpdate.ts │ │ │ │ ├── useLayoutEffect.ts │ │ │ │ └── useObserver.ts │ │ │ ├── index.ts │ │ │ ├── observer.ts │ │ │ ├── shared │ │ │ │ ├── gc.ts │ │ │ │ ├── global.ts │ │ │ │ ├── immediate.ts │ │ │ │ └── index.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── reactive-test-cases-for-react18 │ │ ├── .npmignore │ │ ├── .umirc.js │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.js │ │ │ └── MySlowList.js │ │ ├── template.ejs │ │ ├── tsconfig.build.json │ │ ├── tsconfig.json │ │ ├── webpack.base.ts │ │ ├── webpack.dev.ts │ │ └── webpack.prod.ts │ ├── reactive-vue │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ └── observer.spec.ts │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ └── useObserver.ts │ │ │ ├── index.ts │ │ │ ├── observer │ │ │ │ ├── collectData.ts │ │ │ │ ├── index.ts │ │ │ │ ├── observerInVue2.ts │ │ │ │ └── observerInVue3.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── shared │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ └── index.spec.ts │ │ │ ├── array.ts │ │ │ ├── case.ts │ │ │ ├── checkers.ts │ │ │ ├── clone.ts │ │ │ ├── compare.ts │ │ │ ├── defaults.ts │ │ │ ├── deprecate.ts │ │ │ ├── global.ts │ │ │ ├── index.ts │ │ │ ├── instanceof.ts │ │ │ ├── isEmpty.ts │ │ │ ├── merge.ts │ │ │ ├── middleware.ts │ │ │ ├── path.ts │ │ │ ├── string.ts │ │ │ ├── subscribable.ts │ │ │ └── uid.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── validator │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── package.json │ │ ├── README.md │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ ├── parser.spec.ts │ │ │ │ ├── registry.spec.ts │ │ │ │ └── validator.spec.ts │ │ │ ├── formats.ts │ │ │ ├── index.ts │ │ │ ├── locale.ts │ │ │ ├── parser.ts │ │ │ ├── registry.ts │ │ │ ├── rules.ts │ │ │ ├── template.ts │ │ │ ├── types.ts │ │ │ └── validator.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ └── vue │ ├── .npmignore │ ├── bin │ │ ├── formily-vue-fix.js │ │ └── formily-vue-switch.js │ ├── docs │ │ ├── .vuepress │ │ │ ├── components │ │ │ │ ├── createCodeSandBox.js │ │ │ │ ├── dumi-previewer.vue │ │ │ │ └── highlight.js │ │ │ ├── config.js │ │ │ ├── enhanceApp.js │ │ │ └── styles │ │ │ └── index.styl │ │ ├── api │ │ │ ├── components │ │ │ │ ├── array-field.md │ │ │ │ ├── expression-scope.md │ │ │ │ ├── field.md │ │ │ │ ├── form-consumer.md │ │ │ │ ├── form-provider.md │ │ │ │ ├── object-field.md │ │ │ │ ├── recursion-field-with-component.md │ │ │ │ ├── recursion-field.md │ │ │ │ ├── schema-field-with-schema.md │ │ │ │ ├── schema-field.md │ │ │ │ └── void-field.md │ │ │ ├── hooks │ │ │ │ ├── use-field-schema.md │ │ │ │ ├── use-field.md │ │ │ │ ├── use-form-effects.md │ │ │ │ ├── use-form.md │ │ │ │ └── use-parent-form.md │ │ │ └── shared │ │ │ ├── connect.md │ │ │ ├── injections.md │ │ │ ├── map-props.md │ │ │ ├── map-read-pretty.md │ │ │ ├── observer.md │ │ │ └── schema.md │ │ ├── demos │ │ │ ├── api │ │ │ │ ├── components │ │ │ │ │ ├── array-field.vue │ │ │ │ │ ├── expression-scope.vue │ │ │ │ │ ├── field.vue │ │ │ │ │ ├── form-consumer.vue │ │ │ │ │ ├── form-provider.vue │ │ │ │ │ ├── object-field.vue │ │ │ │ │ ├── recursion-field-with-component.vue │ │ │ │ │ ├── recursion-field.vue │ │ │ │ │ ├── schema-field-with-schema.vue │ │ │ │ │ ├── schema-field.vue │ │ │ │ │ └── void-field.vue │ │ │ │ ├── hooks │ │ │ │ │ ├── use-field-schema.vue │ │ │ │ │ ├── use-field.vue │ │ │ │ │ ├── use-form-effects.vue │ │ │ │ │ ├── use-form.vue │ │ │ │ │ └── use-parent-form.vue │ │ │ │ └── shared │ │ │ │ ├── connect.vue │ │ │ │ ├── map-props.vue │ │ │ │ ├── map-read-pretty.vue │ │ │ │ └── observer.vue │ │ │ ├── index.vue │ │ │ └── questions │ │ │ ├── default-slot.vue │ │ │ ├── events.vue │ │ │ ├── named-slot.vue │ │ │ └── scoped-slot.vue │ │ ├── guide │ │ │ ├── architecture.md │ │ │ ├── concept.md │ │ │ └── README.md │ │ ├── questions │ │ │ └── README.md │ │ └── README.md │ ├── package.json │ ├── README.md │ ├── rollup.config.js │ ├── scripts │ │ ├── postinstall.js │ │ ├── switch-cli.js │ │ └── utils.js │ ├── src │ │ ├── __tests__ │ │ │ ├── expression.scope.spec.ts │ │ │ ├── field.spec.ts │ │ │ ├── form.spec.ts │ │ │ ├── schema.json.spec.ts │ │ │ ├── schema.markup.spec.ts │ │ │ ├── shared.spec.ts │ │ │ └── utils.spec.ts │ │ ├── components │ │ │ ├── ArrayField.ts │ │ │ ├── ExpressionScope.ts │ │ │ ├── Field.ts │ │ │ ├── FormConsumer.ts │ │ │ ├── FormProvider.ts │ │ │ ├── index.ts │ │ │ ├── ObjectField.ts │ │ │ ├── ReactiveField.ts │ │ │ ├── RecursionField.ts │ │ │ ├── SchemaField.ts │ │ │ └── VoidField.ts │ │ ├── global.d.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useAttach.ts │ │ │ ├── useField.ts │ │ │ ├── useFieldSchema.ts │ │ │ ├── useForm.ts │ │ │ ├── useFormEffects.ts │ │ │ ├── useInjectionCleaner.ts │ │ │ └── useParentForm.ts │ │ ├── index.ts │ │ ├── shared │ │ │ ├── connect.ts │ │ │ ├── context.ts │ │ │ ├── createForm.ts │ │ │ ├── fragment.ts │ │ │ ├── h.ts │ │ │ └── index.ts │ │ ├── types │ │ │ └── index.ts │ │ ├── utils │ │ │ ├── formatVNodeData.ts │ │ │ ├── getFieldProps.ts │ │ │ ├── getRawComponent.ts │ │ │ └── resolveSchemaProps.ts │ │ └── vue2-components.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── tsconfig.types.json ├── README.md ├── README.zh-cn.md ├── scripts │ ├── build-style │ │ ├── buildAllStyles.ts │ │ ├── copy.ts │ │ ├── helper.ts │ │ └── index.ts │ └── rollup.base.js ├── tsconfig.build.json ├── tsconfig.jest.json ├── tsconfig.json └── yarn.lock ``` # Files -------------------------------------------------------------------------------- /packages/reactive/src/__tests__/autorun.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { observable, reaction, autorun } from '../' 2 | import { batch } from '../batch' 3 | import { define } from '../model' 4 | 5 | const sleep = (duration = 100) => 6 | new Promise((resolve) => setTimeout(resolve, duration)) 7 | 8 | test('autorun', () => { 9 | const obs = observable({ 10 | aa: { 11 | bb: 123, 12 | }, 13 | }) 14 | const handler = jest.fn() 15 | const dispose = autorun(() => { 16 | handler(obs.aa.bb) 17 | }) 18 | obs.aa.bb = 123 19 | expect(handler).toBeCalledTimes(1) 20 | obs.aa.bb = 111 21 | expect(handler).toBeCalledTimes(2) 22 | dispose() 23 | obs.aa.bb = 222 24 | expect(handler).toBeCalledTimes(2) 25 | }) 26 | 27 | test('reaction', () => { 28 | const obs = observable({ 29 | aa: { 30 | bb: 123, 31 | }, 32 | }) 33 | const handler = jest.fn() 34 | const dispose = reaction(() => { 35 | return obs.aa.bb 36 | }, handler) 37 | obs.aa.bb = 123 38 | expect(handler).toBeCalledTimes(0) 39 | obs.aa.bb = 111 40 | expect(handler).toBeCalledTimes(1) 41 | dispose() 42 | obs.aa.bb = 222 43 | expect(handler).toBeCalledTimes(1) 44 | }) 45 | 46 | test('reaction fireImmediately', () => { 47 | const obs = observable({ 48 | aa: { 49 | bb: 123, 50 | }, 51 | }) 52 | const handler = jest.fn() 53 | const dispose = reaction( 54 | () => { 55 | return obs.aa.bb 56 | }, 57 | handler, 58 | { 59 | fireImmediately: true, 60 | } 61 | ) 62 | expect(handler).toBeCalledTimes(1) 63 | obs.aa.bb = 123 64 | expect(handler).toBeCalledTimes(1) 65 | obs.aa.bb = 111 66 | expect(handler).toBeCalledTimes(2) 67 | dispose() 68 | obs.aa.bb = 222 69 | expect(handler).toBeCalledTimes(2) 70 | }) 71 | 72 | test('reaction untrack handler', () => { 73 | const obs = observable({ 74 | aa: { 75 | bb: 123, 76 | cc: 123, 77 | }, 78 | }) 79 | const handler = jest.fn() 80 | const dispose = reaction( 81 | () => { 82 | return obs.aa.bb 83 | }, 84 | () => { 85 | handler(obs.aa.cc) 86 | } 87 | ) 88 | obs.aa.bb = 222 89 | obs.aa.cc = 222 90 | expect(handler).toBeCalledTimes(1) 91 | dispose() 92 | }) 93 | 94 | test('reaction dirty check', () => { 95 | const obs: any = { 96 | aa: 123, 97 | } 98 | define(obs, { 99 | aa: observable.ref, 100 | }) 101 | const handler = jest.fn() 102 | reaction(() => { 103 | return obs.aa 104 | }, handler) 105 | batch(() => { 106 | obs.aa = 123 107 | obs.aa = 123 108 | }) 109 | 110 | expect(handler).toBeCalledTimes(0) 111 | }) 112 | 113 | test('reaction with shallow equals', () => { 114 | const obs: any = { 115 | aa: { bb: 123 }, 116 | } 117 | define(obs, { 118 | aa: observable.ref, 119 | }) 120 | const handler = jest.fn() 121 | reaction(() => { 122 | return obs.aa 123 | }, handler) 124 | obs.aa = { bb: 123 } 125 | expect(handler).toBeCalledTimes(1) 126 | expect(handler.mock.calls[0][0]).toEqual({ bb: 123 }) 127 | }) 128 | 129 | test('reaction with deep equals', () => { 130 | const obs: any = { 131 | aa: { bb: 123 }, 132 | } 133 | define(obs, { 134 | aa: observable.ref, 135 | }) 136 | const handler = jest.fn() 137 | reaction( 138 | () => { 139 | return obs.aa 140 | }, 141 | handler, 142 | { 143 | equals: (a, b) => JSON.stringify(a) === JSON.stringify(b), 144 | } 145 | ) 146 | obs.aa = { bb: 123 } 147 | expect(handler).toBeCalledTimes(0) 148 | }) 149 | 150 | test('autorun direct recursive react', () => { 151 | const obs = observable<any>({ value: 1 }) 152 | autorun(() => { 153 | obs.value++ 154 | }) 155 | expect(obs.value).toEqual(2) 156 | }) 157 | 158 | test('autorun direct recursive react with if', () => { 159 | const obs1 = observable<any>({}) 160 | const obs2 = observable<any>({}) 161 | const fn = jest.fn() 162 | autorun(() => { 163 | if (!obs1.value) { 164 | obs1.value = '111' 165 | return 166 | } 167 | fn(obs1.value, obs2.value) 168 | }) 169 | obs2.value = '222' 170 | expect(fn).toBeCalledTimes(0) 171 | }) 172 | 173 | test('autorun indirect recursive react', () => { 174 | const obs1 = observable<any>({}) 175 | const obs2 = observable<any>({}) 176 | const obs3 = observable<any>({}) 177 | autorun(() => { 178 | obs1.value = obs2.value + 1 179 | }) 180 | autorun(() => { 181 | obs2.value = obs3.value + 1 182 | }) 183 | autorun(() => { 184 | if (obs1.value) { 185 | obs3.value = obs1.value + 1 186 | } else { 187 | obs3.value = 0 188 | } 189 | }) 190 | expect(obs2.value).toEqual(1) 191 | expect(obs1.value).toEqual(2) 192 | obs3.value = 1 193 | expect(obs2.value).toEqual(2) 194 | expect(obs1.value).toEqual(3) 195 | }) 196 | 197 | test('autorun indirect alive recursive react', () => { 198 | const aa = observable<any>({}) 199 | const bb = observable<any>({}) 200 | const cc = observable<any>({}) 201 | 202 | batch(() => { 203 | autorun(() => { 204 | if (aa.value) { 205 | bb.value = aa.value + 1 206 | } 207 | }) 208 | autorun(() => { 209 | if (aa.value && bb.value) { 210 | cc.value = aa.value + bb.value 211 | } 212 | }) 213 | batch(() => { 214 | aa.value = 1 215 | }) 216 | }) 217 | expect(aa.value).toEqual(1) 218 | expect(bb.value).toEqual(2) 219 | expect(cc.value).toEqual(3) 220 | }) 221 | 222 | test('autorun direct recursive react with head track', () => { 223 | const obs1 = observable<any>({}) 224 | const obs2 = observable<any>({}) 225 | const fn = jest.fn() 226 | autorun(() => { 227 | const obs2Value = obs2.value 228 | if (!obs1.value) { 229 | obs1.value = '111' 230 | return 231 | } 232 | fn(obs1.value, obs2Value) 233 | }) 234 | obs2.value = '222' 235 | expect(fn).toBeCalledTimes(1) 236 | expect(fn).lastCalledWith('111', '222') 237 | }) 238 | 239 | test('autorun.memo', () => { 240 | const obs = observable<any>({ 241 | bb: 0, 242 | }) 243 | const fn = jest.fn() 244 | autorun(() => { 245 | const value = autorun.memo(() => ({ 246 | aa: 0, 247 | })) 248 | fn(obs.bb, value.aa++) 249 | }) 250 | obs.bb++ 251 | obs.bb++ 252 | obs.bb++ 253 | obs.bb++ 254 | expect(fn).toBeCalledTimes(5) 255 | expect(fn).nthCalledWith(1, 0, 0) 256 | expect(fn).nthCalledWith(2, 1, 1) 257 | expect(fn).nthCalledWith(3, 2, 2) 258 | expect(fn).nthCalledWith(4, 3, 3) 259 | expect(fn).nthCalledWith(5, 4, 4) 260 | }) 261 | 262 | test('autorun.memo with observable', () => { 263 | const obs1 = observable({ 264 | aa: 0, 265 | }) 266 | const fn = jest.fn() 267 | const dispose = autorun(() => { 268 | const obs2 = autorun.memo(() => 269 | observable({ 270 | bb: 0, 271 | }) 272 | ) 273 | fn(obs1.aa, obs2.bb++) 274 | }) 275 | obs1.aa++ 276 | obs1.aa++ 277 | obs1.aa++ 278 | expect(fn).toBeCalledTimes(4) 279 | expect(fn).nthCalledWith(1, 0, 0) 280 | expect(fn).nthCalledWith(2, 1, 1) 281 | expect(fn).nthCalledWith(3, 2, 2) 282 | expect(fn).nthCalledWith(4, 3, 3) 283 | dispose() 284 | obs1.aa++ 285 | expect(fn).toBeCalledTimes(4) 286 | }) 287 | 288 | test('autorun.memo with observable and effect', async () => { 289 | const obs1 = observable({ 290 | aa: 0, 291 | }) 292 | const fn = jest.fn() 293 | const dispose = autorun(() => { 294 | const obs2 = autorun.memo(() => 295 | observable({ 296 | bb: 0, 297 | }) 298 | ) 299 | fn(obs1.aa, obs2.bb++) 300 | autorun.effect(() => { 301 | obs2.bb++ 302 | }, []) 303 | }) 304 | obs1.aa++ 305 | obs1.aa++ 306 | obs1.aa++ 307 | await sleep(0) 308 | expect(fn).toBeCalledTimes(5) 309 | expect(fn).nthCalledWith(1, 0, 0) 310 | expect(fn).nthCalledWith(2, 1, 1) 311 | expect(fn).nthCalledWith(3, 2, 2) 312 | expect(fn).nthCalledWith(4, 3, 3) 313 | expect(fn).nthCalledWith(5, 3, 5) 314 | dispose() 315 | obs1.aa++ 316 | expect(fn).toBeCalledTimes(5) 317 | }) 318 | 319 | test('autorun.memo with deps', () => { 320 | const obs = observable<any>({ 321 | bb: 0, 322 | cc: 0, 323 | }) 324 | const fn = jest.fn() 325 | autorun(() => { 326 | const value = autorun.memo( 327 | () => ({ 328 | aa: 0, 329 | }), 330 | [obs.cc] 331 | ) 332 | fn(obs.bb, value.aa++) 333 | }) 334 | obs.bb++ 335 | obs.bb++ 336 | obs.bb++ 337 | obs.bb++ 338 | expect(fn).toBeCalledTimes(5) 339 | expect(fn).nthCalledWith(1, 0, 0) 340 | expect(fn).nthCalledWith(2, 1, 1) 341 | expect(fn).nthCalledWith(3, 2, 2) 342 | expect(fn).nthCalledWith(4, 3, 3) 343 | expect(fn).nthCalledWith(5, 4, 4) 344 | obs.cc++ 345 | expect(fn).toBeCalledTimes(6) 346 | expect(fn).nthCalledWith(6, 4, 0) 347 | }) 348 | 349 | test('autorun.memo with deps and dispose', () => { 350 | const obs = observable<any>({ 351 | bb: 0, 352 | cc: 0, 353 | }) 354 | const fn = jest.fn() 355 | const dispose = autorun(() => { 356 | const value = autorun.memo( 357 | () => ({ 358 | aa: 0, 359 | }), 360 | [obs.cc] 361 | ) 362 | fn(obs.bb, value.aa++) 363 | }) 364 | obs.bb++ 365 | obs.bb++ 366 | obs.bb++ 367 | obs.bb++ 368 | expect(fn).toBeCalledTimes(5) 369 | expect(fn).lastCalledWith(4, 4) 370 | obs.cc++ 371 | expect(fn).toBeCalledTimes(6) 372 | expect(fn).lastCalledWith(4, 0) 373 | dispose() 374 | obs.bb++ 375 | obs.cc++ 376 | expect(fn).toBeCalledTimes(6) 377 | }) 378 | 379 | test('autorun.memo with invalid params', () => { 380 | const obs = observable<any>({ 381 | bb: 0, 382 | }) 383 | const fn = jest.fn() 384 | autorun(() => { 385 | const value = autorun.memo({ aa: 0 } as any) 386 | fn(obs.bb, value) 387 | }) 388 | obs.bb++ 389 | obs.bb++ 390 | obs.bb++ 391 | obs.bb++ 392 | expect(fn).toBeCalledTimes(5) 393 | expect(fn).lastCalledWith(4, undefined) 394 | }) 395 | 396 | test('autorun.memo not in autorun', () => { 397 | expect(() => autorun.memo(() => ({ aa: 0 }))).toThrow() 398 | }) 399 | 400 | test('autorun no memo', () => { 401 | const obs = observable<any>({ 402 | bb: 0, 403 | }) 404 | const fn = jest.fn() 405 | autorun(() => { 406 | const value = { 407 | aa: 0, 408 | } 409 | fn(obs.bb, value.aa++) 410 | }) 411 | obs.bb++ 412 | obs.bb++ 413 | obs.bb++ 414 | obs.bb++ 415 | expect(fn).toBeCalledTimes(5) 416 | expect(fn).nthCalledWith(1, 0, 0) 417 | expect(fn).nthCalledWith(2, 1, 0) 418 | expect(fn).nthCalledWith(3, 2, 0) 419 | expect(fn).nthCalledWith(4, 3, 0) 420 | expect(fn).nthCalledWith(5, 4, 0) 421 | }) 422 | 423 | test('autorun.effect', async () => { 424 | const obs = observable<any>({ 425 | bb: 0, 426 | }) 427 | const fn = jest.fn() 428 | const effect = jest.fn() 429 | const disposer = jest.fn() 430 | const dispose = autorun(() => { 431 | autorun.effect(() => { 432 | effect() 433 | return disposer 434 | }, []) 435 | fn(obs.bb) 436 | }) 437 | obs.bb++ 438 | obs.bb++ 439 | obs.bb++ 440 | obs.bb++ 441 | 442 | await sleep(0) 443 | expect(fn).toBeCalledTimes(5) 444 | expect(fn).lastCalledWith(4) 445 | expect(effect).toBeCalledTimes(1) 446 | expect(disposer).toBeCalledTimes(0) 447 | 448 | dispose() 449 | await sleep(0) 450 | expect(effect).toBeCalledTimes(1) 451 | expect(disposer).toBeCalledTimes(1) 452 | }) 453 | 454 | test('autorun.effect dispose when autorun dispose', async () => { 455 | const obs = observable<any>({ 456 | bb: 0, 457 | }) 458 | const fn = jest.fn() 459 | const effect = jest.fn() 460 | const disposer = jest.fn() 461 | const dispose = autorun(() => { 462 | autorun.effect(() => { 463 | effect() 464 | return disposer 465 | }, []) 466 | fn(obs.bb) 467 | }) 468 | obs.bb++ 469 | obs.bb++ 470 | obs.bb++ 471 | obs.bb++ 472 | 473 | dispose() 474 | await sleep(0) 475 | expect(fn).toBeCalledTimes(5) 476 | expect(fn).lastCalledWith(4) 477 | expect(effect).toBeCalledTimes(0) 478 | expect(disposer).toBeCalledTimes(0) 479 | }) 480 | 481 | test('autorun.effect with deps', async () => { 482 | const obs = observable<any>({ 483 | bb: 0, 484 | cc: 0, 485 | }) 486 | const fn = jest.fn() 487 | const effect = jest.fn() 488 | const dispose = autorun(() => { 489 | autorun.effect(() => { 490 | effect() 491 | }, [obs.cc]) 492 | fn(obs.bb) 493 | }) 494 | obs.bb++ 495 | obs.bb++ 496 | obs.bb++ 497 | obs.bb++ 498 | expect(effect).toBeCalledTimes(0) 499 | await sleep(0) 500 | expect(fn).toBeCalledTimes(5) 501 | expect(fn).lastCalledWith(4) 502 | expect(effect).toBeCalledTimes(1) 503 | obs.cc++ 504 | expect(effect).toBeCalledTimes(1) 505 | await sleep(0) 506 | expect(fn).toBeCalledTimes(6) 507 | expect(fn).lastCalledWith(4) 508 | expect(effect).toBeCalledTimes(2) 509 | dispose() 510 | await sleep(0) 511 | expect(effect).toBeCalledTimes(2) 512 | }) 513 | 514 | test('autorun.effect with default deps', async () => { 515 | const obs = observable<any>({ 516 | bb: 0, 517 | }) 518 | const fn = jest.fn() 519 | const effect = jest.fn() 520 | const dispose = autorun(() => { 521 | autorun.effect(() => { 522 | effect() 523 | }) 524 | fn(obs.bb) 525 | }) 526 | obs.bb++ 527 | obs.bb++ 528 | obs.bb++ 529 | obs.bb++ 530 | expect(effect).toBeCalledTimes(0) 531 | await sleep(0) 532 | expect(fn).toBeCalledTimes(5) 533 | expect(fn).lastCalledWith(4) 534 | expect(effect).toBeCalledTimes(5) 535 | dispose() 536 | await sleep(0) 537 | expect(effect).toBeCalledTimes(5) 538 | }) 539 | 540 | test('autorun.effect not in autorun', () => { 541 | expect(() => autorun.effect(() => {})).toThrow() 542 | }) 543 | 544 | test('autorun.effect with invalid params', () => { 545 | autorun.effect({} as any) 546 | }) 547 | 548 | test('autorun dispose in batch', () => { 549 | const obs = observable({ 550 | value: 123, 551 | }) 552 | const handler = jest.fn() 553 | const dispose = autorun(() => { 554 | handler(obs.value) 555 | }) 556 | 557 | batch(() => { 558 | obs.value = 321 559 | dispose() 560 | }) 561 | expect(handler).toBeCalledTimes(1) 562 | }) 563 | 564 | test('set value by computed depend', () => { 565 | const obs = observable<any>({}) 566 | const comp1 = observable.computed(() => { 567 | return obs.aa?.bb 568 | }) 569 | const comp2 = observable.computed(() => { 570 | return obs.aa?.cc 571 | }) 572 | const handler = jest.fn() 573 | autorun(() => { 574 | handler(comp1.value, comp2.value) 575 | }) 576 | obs.aa = { 577 | bb: 123, 578 | cc: 321, 579 | } 580 | expect(handler).toBeCalledTimes(2) 581 | expect(handler).nthCalledWith(1, undefined, undefined) 582 | expect(handler).nthCalledWith(2, 123, 321) 583 | }) 584 | 585 | test('delete value by computed depend', () => { 586 | const handler = jest.fn() 587 | const obs = observable({ 588 | a: { 589 | b: 1, 590 | c: 2, 591 | }, 592 | }) 593 | const comp1 = observable.computed(() => { 594 | return obs.a?.b 595 | }) 596 | const comp2 = observable.computed(() => { 597 | return obs.a?.c 598 | }) 599 | autorun(() => { 600 | handler(comp1.value, comp2.value) 601 | }) 602 | delete obs.a 603 | expect(handler).toBeCalledTimes(2) 604 | expect(handler).nthCalledWith(1, 1, 2) 605 | expect(handler).nthCalledWith(2, undefined, undefined) 606 | }) 607 | 608 | test('set Set value by computed depend', () => { 609 | const handler = jest.fn() 610 | const obs = observable({ 611 | set: new Set(), 612 | }) 613 | const comp1 = observable.computed(() => { 614 | return obs.set.has(1) 615 | }) 616 | const comp2 = observable.computed(() => { 617 | return obs.set.size 618 | }) 619 | autorun(() => { 620 | handler(comp1.value, comp2.value) 621 | }) 622 | obs.set.add(1) 623 | expect(handler).toBeCalledTimes(2) 624 | expect(handler).nthCalledWith(1, false, 0) 625 | expect(handler).nthCalledWith(2, true, 1) 626 | }) 627 | 628 | test('delete Set by computed depend', () => { 629 | const handler = jest.fn() 630 | const obs = observable({ 631 | set: new Set([1]), 632 | }) 633 | const comp1 = observable.computed(() => { 634 | return obs.set.has(1) 635 | }) 636 | const comp2 = observable.computed(() => { 637 | return obs.set.size 638 | }) 639 | autorun(() => { 640 | handler(comp1.value, comp2.value) 641 | }) 642 | obs.set.delete(1) 643 | expect(handler).toBeCalledTimes(2) 644 | expect(handler).nthCalledWith(1, true, 1) 645 | expect(handler).nthCalledWith(2, false, 0) 646 | }) 647 | 648 | test('set Map value by computed depend', () => { 649 | const handler = jest.fn() 650 | const obs = observable({ 651 | map: new Map(), 652 | }) 653 | const comp1 = observable.computed(() => { 654 | return obs.map.has(1) 655 | }) 656 | const comp2 = observable.computed(() => { 657 | return obs.map.size 658 | }) 659 | autorun(() => { 660 | handler(comp1.value, comp2.value) 661 | }) 662 | obs.map.set(1, 1) 663 | expect(handler).toBeCalledTimes(2) 664 | expect(handler).nthCalledWith(1, false, 0) 665 | expect(handler).nthCalledWith(2, true, 1) 666 | }) 667 | 668 | test('delete Map by computed depend', () => { 669 | const handler = jest.fn() 670 | const obs = observable({ 671 | map: new Map([[1, 1]]), 672 | }) 673 | const comp1 = observable.computed(() => { 674 | return obs.map.has(1) 675 | }) 676 | const comp2 = observable.computed(() => { 677 | return obs.map.size 678 | }) 679 | autorun(() => { 680 | handler(comp1.value, comp2.value) 681 | }) 682 | obs.map.delete(1) 683 | expect(handler).toBeCalledTimes(2) 684 | expect(handler).nthCalledWith(1, true, 1) 685 | expect(handler).nthCalledWith(2, false, 0) 686 | }) 687 | 688 | test('autorun recollect dependencies', () => { 689 | const obs = observable<any>({ 690 | aa: 'aaa', 691 | bb: 'bbb', 692 | cc: 'ccc', 693 | }) 694 | const fn = jest.fn() 695 | autorun(() => { 696 | fn() 697 | if (obs.aa === 'aaa') { 698 | return obs.bb 699 | } 700 | return obs.cc 701 | }) 702 | obs.aa = '111' 703 | obs.bb = '222' 704 | expect(fn).toBeCalledTimes(2) 705 | }) 706 | 707 | test('reaction recollect dependencies', () => { 708 | const obs = observable<any>({ 709 | aa: 'aaa', 710 | bb: 'bbb', 711 | cc: 'ccc', 712 | }) 713 | const fn1 = jest.fn() 714 | const fn2 = jest.fn() 715 | const trigger1 = jest.fn() 716 | const trigger2 = jest.fn() 717 | reaction(() => { 718 | fn1() 719 | if (obs.aa === 'aaa') { 720 | return obs.bb 721 | } 722 | return obs.cc 723 | }, trigger1) 724 | reaction( 725 | () => { 726 | fn2() 727 | if (obs.aa === 'aaa') { 728 | return obs.bb 729 | } 730 | return obs.cc 731 | }, 732 | trigger2, 733 | { 734 | fireImmediately: true, 735 | } 736 | ) 737 | obs.aa = '111' 738 | obs.bb = '222' 739 | expect(fn1).toBeCalledTimes(2) 740 | expect(trigger1).toBeCalledTimes(1) 741 | expect(fn2).toBeCalledTimes(2) 742 | expect(trigger2).toBeCalledTimes(2) 743 | }) 744 | ``` -------------------------------------------------------------------------------- /packages/core/src/models/Form.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { define, observable, batch, action, observe } from '@formily/reactive' 2 | import { 3 | FormPath, 4 | FormPathPattern, 5 | isValid, 6 | uid, 7 | globalThisPolyfill, 8 | merge, 9 | isPlainObj, 10 | isArr, 11 | isObj, 12 | } from '@formily/shared' 13 | import { Heart } from './Heart' 14 | import { Field } from './Field' 15 | import { 16 | JSXComponent, 17 | LifeCycleTypes, 18 | HeartSubscriber, 19 | FormPatternTypes, 20 | IFormRequests, 21 | IFormFeedback, 22 | ISearchFeedback, 23 | IFormGraph, 24 | IFormProps, 25 | IFieldResetOptions, 26 | IFormFields, 27 | IFieldFactoryProps, 28 | IVoidFieldFactoryProps, 29 | IFormState, 30 | IModelGetter, 31 | IModelSetter, 32 | IFieldStateGetter, 33 | IFieldStateSetter, 34 | FormDisplayTypes, 35 | IFormMergeStrategy, 36 | } from '../types' 37 | import { 38 | createStateGetter, 39 | createStateSetter, 40 | createBatchStateSetter, 41 | createBatchStateGetter, 42 | triggerFormInitialValuesChange, 43 | triggerFormValuesChange, 44 | batchValidate, 45 | batchReset, 46 | batchSubmit, 47 | setValidating, 48 | setSubmitting, 49 | setLoading, 50 | getValidFormValues, 51 | } from '../shared/internals' 52 | import { isVoidField } from '../shared/checkers' 53 | import { runEffects } from '../shared/effective' 54 | import { ArrayField } from './ArrayField' 55 | import { ObjectField } from './ObjectField' 56 | import { VoidField } from './VoidField' 57 | import { Query } from './Query' 58 | import { Graph } from './Graph' 59 | 60 | const DEV_TOOLS_HOOK = '__FORMILY_DEV_TOOLS_HOOK__' 61 | 62 | export class Form<ValueType extends object = any> { 63 | displayName = 'Form' 64 | id: string 65 | initialized: boolean 66 | validating: boolean 67 | submitting: boolean 68 | loading: boolean 69 | modified: boolean 70 | pattern: FormPatternTypes 71 | display: FormDisplayTypes 72 | values: ValueType 73 | initialValues: ValueType 74 | mounted: boolean 75 | unmounted: boolean 76 | props: IFormProps<ValueType> 77 | heart: Heart 78 | graph: Graph 79 | fields: IFormFields = {} 80 | requests: IFormRequests = {} 81 | indexes: Record<string, string> = {} 82 | disposers: (() => void)[] = [] 83 | 84 | constructor(props: IFormProps<ValueType>) { 85 | this.initialize(props) 86 | this.makeObservable() 87 | this.makeReactive() 88 | this.makeValues() 89 | this.onInit() 90 | } 91 | 92 | protected initialize(props: IFormProps<ValueType>) { 93 | this.id = uid() 94 | this.props = { ...props } 95 | this.initialized = false 96 | this.submitting = false 97 | this.validating = false 98 | this.loading = false 99 | this.modified = false 100 | this.mounted = false 101 | this.unmounted = false 102 | this.display = this.props.display || 'visible' 103 | this.pattern = this.props.pattern || 'editable' 104 | this.editable = this.props.editable 105 | this.disabled = this.props.disabled 106 | this.readOnly = this.props.readOnly 107 | this.readPretty = this.props.readPretty 108 | this.visible = this.props.visible 109 | this.hidden = this.props.hidden 110 | this.graph = new Graph(this) 111 | this.heart = new Heart({ 112 | lifecycles: this.lifecycles, 113 | context: this, 114 | }) 115 | } 116 | 117 | protected makeValues() { 118 | this.values = getValidFormValues(this.props.values) 119 | this.initialValues = getValidFormValues(this.props.initialValues) 120 | } 121 | 122 | protected makeObservable() { 123 | define(this, { 124 | fields: observable.shallow, 125 | indexes: observable.shallow, 126 | initialized: observable.ref, 127 | validating: observable.ref, 128 | submitting: observable.ref, 129 | loading: observable.ref, 130 | modified: observable.ref, 131 | pattern: observable.ref, 132 | display: observable.ref, 133 | mounted: observable.ref, 134 | unmounted: observable.ref, 135 | values: observable, 136 | initialValues: observable, 137 | valid: observable.computed, 138 | invalid: observable.computed, 139 | errors: observable.computed, 140 | warnings: observable.computed, 141 | successes: observable.computed, 142 | hidden: observable.computed, 143 | visible: observable.computed, 144 | editable: observable.computed, 145 | readOnly: observable.computed, 146 | readPretty: observable.computed, 147 | disabled: observable.computed, 148 | setValues: action, 149 | setValuesIn: action, 150 | setInitialValues: action, 151 | setInitialValuesIn: action, 152 | setPattern: action, 153 | setDisplay: action, 154 | setState: action, 155 | deleteInitialValuesIn: action, 156 | deleteValuesIn: action, 157 | setSubmitting: action, 158 | setValidating: action, 159 | reset: action, 160 | submit: action, 161 | validate: action, 162 | onMount: batch, 163 | onUnmount: batch, 164 | onInit: batch, 165 | }) 166 | } 167 | 168 | protected makeReactive() { 169 | this.disposers.push( 170 | observe( 171 | this, 172 | (change) => { 173 | triggerFormInitialValuesChange(this, change) 174 | triggerFormValuesChange(this, change) 175 | }, 176 | true 177 | ) 178 | ) 179 | } 180 | 181 | get valid() { 182 | return !this.invalid 183 | } 184 | 185 | get invalid() { 186 | return this.errors.length > 0 187 | } 188 | 189 | get errors() { 190 | return this.queryFeedbacks({ 191 | type: 'error', 192 | }) 193 | } 194 | 195 | get warnings() { 196 | return this.queryFeedbacks({ 197 | type: 'warning', 198 | }) 199 | } 200 | 201 | get successes() { 202 | return this.queryFeedbacks({ 203 | type: 'success', 204 | }) 205 | } 206 | 207 | get lifecycles() { 208 | return runEffects(this, this.props.effects) 209 | } 210 | 211 | get hidden() { 212 | return this.display === 'hidden' 213 | } 214 | 215 | get visible() { 216 | return this.display === 'visible' 217 | } 218 | 219 | set hidden(hidden: boolean) { 220 | if (!isValid(hidden)) return 221 | if (hidden) { 222 | this.display = 'hidden' 223 | } else { 224 | this.display = 'visible' 225 | } 226 | } 227 | 228 | set visible(visible: boolean) { 229 | if (!isValid(visible)) return 230 | if (visible) { 231 | this.display = 'visible' 232 | } else { 233 | this.display = 'none' 234 | } 235 | } 236 | 237 | get editable() { 238 | return this.pattern === 'editable' 239 | } 240 | 241 | set editable(editable) { 242 | if (!isValid(editable)) return 243 | if (editable) { 244 | this.pattern = 'editable' 245 | } else { 246 | this.pattern = 'readPretty' 247 | } 248 | } 249 | 250 | get readOnly() { 251 | return this.pattern === 'readOnly' 252 | } 253 | 254 | set readOnly(readOnly) { 255 | if (!isValid(readOnly)) return 256 | if (readOnly) { 257 | this.pattern = 'readOnly' 258 | } else { 259 | this.pattern = 'editable' 260 | } 261 | } 262 | 263 | get disabled() { 264 | return this.pattern === 'disabled' 265 | } 266 | 267 | set disabled(disabled) { 268 | if (!isValid(disabled)) return 269 | if (disabled) { 270 | this.pattern = 'disabled' 271 | } else { 272 | this.pattern = 'editable' 273 | } 274 | } 275 | 276 | get readPretty() { 277 | return this.pattern === 'readPretty' 278 | } 279 | 280 | set readPretty(readPretty) { 281 | if (!isValid(readPretty)) return 282 | if (readPretty) { 283 | this.pattern = 'readPretty' 284 | } else { 285 | this.pattern = 'editable' 286 | } 287 | } 288 | 289 | /** 创建字段 **/ 290 | 291 | createField = < 292 | Decorator extends JSXComponent, 293 | Component extends JSXComponent 294 | >( 295 | props: IFieldFactoryProps<Decorator, Component> 296 | ): Field<Decorator, Component> => { 297 | const address = FormPath.parse(props.basePath).concat(props.name) 298 | const identifier = address.toString() 299 | if (!identifier) return 300 | if (!this.fields[identifier] || this.props.designable) { 301 | batch(() => { 302 | new Field(address, props, this, this.props.designable) 303 | }) 304 | this.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE) 305 | } 306 | return this.fields[identifier] as any 307 | } 308 | 309 | createArrayField = < 310 | Decorator extends JSXComponent, 311 | Component extends JSXComponent 312 | >( 313 | props: IFieldFactoryProps<Decorator, Component> 314 | ): ArrayField<Decorator, Component> => { 315 | const address = FormPath.parse(props.basePath).concat(props.name) 316 | const identifier = address.toString() 317 | if (!identifier) return 318 | if (!this.fields[identifier] || this.props.designable) { 319 | batch(() => { 320 | new ArrayField( 321 | address, 322 | { 323 | ...props, 324 | value: isArr(props.value) ? props.value : [], 325 | }, 326 | this, 327 | this.props.designable 328 | ) 329 | }) 330 | this.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE) 331 | } 332 | return this.fields[identifier] as any 333 | } 334 | 335 | createObjectField = < 336 | Decorator extends JSXComponent, 337 | Component extends JSXComponent 338 | >( 339 | props: IFieldFactoryProps<Decorator, Component> 340 | ): ObjectField<Decorator, Component> => { 341 | const address = FormPath.parse(props.basePath).concat(props.name) 342 | const identifier = address.toString() 343 | if (!identifier) return 344 | if (!this.fields[identifier] || this.props.designable) { 345 | batch(() => { 346 | new ObjectField( 347 | address, 348 | { 349 | ...props, 350 | value: isObj(props.value) ? props.value : {}, 351 | }, 352 | this, 353 | this.props.designable 354 | ) 355 | }) 356 | this.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE) 357 | } 358 | return this.fields[identifier] as any 359 | } 360 | 361 | createVoidField = < 362 | Decorator extends JSXComponent, 363 | Component extends JSXComponent 364 | >( 365 | props: IVoidFieldFactoryProps<Decorator, Component> 366 | ): VoidField<Decorator, Component> => { 367 | const address = FormPath.parse(props.basePath).concat(props.name) 368 | const identifier = address.toString() 369 | if (!identifier) return 370 | if (!this.fields[identifier] || this.props.designable) { 371 | batch(() => { 372 | new VoidField(address, props, this, this.props.designable) 373 | }) 374 | this.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE) 375 | } 376 | return this.fields[identifier] as any 377 | } 378 | 379 | /** 状态操作模型 **/ 380 | 381 | setValues = (values: any, strategy: IFormMergeStrategy = 'merge') => { 382 | if (!isPlainObj(values)) return 383 | if (strategy === 'merge' || strategy === 'deepMerge') { 384 | merge(this.values, values, { 385 | // never reach 386 | arrayMerge: (target, source) => source, 387 | assign: true, 388 | }) 389 | } else if (strategy === 'shallowMerge') { 390 | Object.assign(this.values, values) 391 | } else { 392 | this.values = values as any 393 | } 394 | } 395 | 396 | setInitialValues = ( 397 | initialValues: any, 398 | strategy: IFormMergeStrategy = 'merge' 399 | ) => { 400 | if (!isPlainObj(initialValues)) return 401 | if (strategy === 'merge' || strategy === 'deepMerge') { 402 | merge(this.initialValues, initialValues, { 403 | // never reach 404 | arrayMerge: (target, source) => source, 405 | assign: true, 406 | }) 407 | } else if (strategy === 'shallowMerge') { 408 | Object.assign(this.initialValues, initialValues) 409 | } else { 410 | this.initialValues = initialValues as any 411 | } 412 | } 413 | 414 | setValuesIn = (pattern: FormPathPattern, value: any) => { 415 | FormPath.setIn(this.values, pattern, value) 416 | } 417 | 418 | deleteValuesIn = (pattern: FormPathPattern) => { 419 | FormPath.deleteIn(this.values, pattern) 420 | } 421 | 422 | existValuesIn = (pattern: FormPathPattern) => { 423 | return FormPath.existIn(this.values, pattern) 424 | } 425 | 426 | getValuesIn = (pattern: FormPathPattern) => { 427 | return FormPath.getIn(this.values, pattern) 428 | } 429 | 430 | setInitialValuesIn = (pattern: FormPathPattern, initialValue: any) => { 431 | FormPath.setIn(this.initialValues, pattern, initialValue) 432 | } 433 | 434 | deleteInitialValuesIn = (pattern: FormPathPattern) => { 435 | FormPath.deleteIn(this.initialValues, pattern) 436 | } 437 | 438 | existInitialValuesIn = (pattern: FormPathPattern) => { 439 | return FormPath.existIn(this.initialValues, pattern) 440 | } 441 | 442 | getInitialValuesIn = (pattern: FormPathPattern) => { 443 | return FormPath.getIn(this.initialValues, pattern) 444 | } 445 | 446 | setLoading = (loading: boolean) => { 447 | setLoading(this, loading) 448 | } 449 | 450 | setSubmitting = (submitting: boolean) => { 451 | setSubmitting(this, submitting) 452 | } 453 | 454 | setValidating = (validating: boolean) => { 455 | setValidating(this, validating) 456 | } 457 | 458 | setDisplay = (display: FormDisplayTypes) => { 459 | this.display = display 460 | } 461 | 462 | setPattern = (pattern: FormPatternTypes) => { 463 | this.pattern = pattern 464 | } 465 | 466 | addEffects = (id: any, effects: IFormProps['effects']) => { 467 | if (!this.heart.hasLifeCycles(id)) { 468 | this.heart.addLifeCycles(id, runEffects(this, effects)) 469 | } 470 | } 471 | 472 | removeEffects = (id: any) => { 473 | this.heart.removeLifeCycles(id) 474 | } 475 | 476 | setEffects = (effects: IFormProps['effects']) => { 477 | this.heart.setLifeCycles(runEffects(this, effects)) 478 | } 479 | 480 | clearErrors = (pattern: FormPathPattern = '*') => { 481 | this.query(pattern).forEach((field) => { 482 | if (!isVoidField(field)) { 483 | field.setFeedback({ 484 | type: 'error', 485 | messages: [], 486 | }) 487 | } 488 | }) 489 | } 490 | 491 | clearWarnings = (pattern: FormPathPattern = '*') => { 492 | this.query(pattern).forEach((field) => { 493 | if (!isVoidField(field)) { 494 | field.setFeedback({ 495 | type: 'warning', 496 | messages: [], 497 | }) 498 | } 499 | }) 500 | } 501 | 502 | clearSuccesses = (pattern: FormPathPattern = '*') => { 503 | this.query(pattern).forEach((field) => { 504 | if (!isVoidField(field)) { 505 | field.setFeedback({ 506 | type: 'success', 507 | messages: [], 508 | }) 509 | } 510 | }) 511 | } 512 | 513 | query = (pattern: FormPathPattern): Query => { 514 | return new Query({ 515 | pattern, 516 | base: '', 517 | form: this, 518 | }) 519 | } 520 | 521 | queryFeedbacks = (search: ISearchFeedback): IFormFeedback[] => { 522 | return this.query(search.address || search.path || '*').reduce( 523 | (messages, field) => { 524 | if (isVoidField(field)) return messages 525 | return messages.concat( 526 | field 527 | .queryFeedbacks(search) 528 | .map((feedback) => ({ 529 | ...feedback, 530 | address: field.address.toString(), 531 | path: field.path.toString(), 532 | })) 533 | .filter((feedback) => feedback.messages.length > 0) 534 | ) 535 | }, 536 | [] 537 | ) 538 | } 539 | 540 | notify = (type: string, payload?: any) => { 541 | this.heart.publish(type, payload ?? this) 542 | } 543 | 544 | subscribe = (subscriber?: HeartSubscriber) => { 545 | return this.heart.subscribe(subscriber) 546 | } 547 | 548 | unsubscribe = (id: number) => { 549 | this.heart.unsubscribe(id) 550 | } 551 | 552 | /**事件钩子**/ 553 | 554 | onInit = () => { 555 | this.initialized = true 556 | this.notify(LifeCycleTypes.ON_FORM_INIT) 557 | } 558 | 559 | onMount = () => { 560 | this.mounted = true 561 | this.notify(LifeCycleTypes.ON_FORM_MOUNT) 562 | if (globalThisPolyfill[DEV_TOOLS_HOOK] && !this.props.designable) { 563 | globalThisPolyfill[DEV_TOOLS_HOOK].inject(this.id, this) 564 | } 565 | } 566 | 567 | onUnmount = () => { 568 | this.notify(LifeCycleTypes.ON_FORM_UNMOUNT) 569 | this.query('*').forEach((field) => field.destroy(false)) 570 | this.disposers.forEach((dispose) => dispose()) 571 | this.unmounted = true 572 | this.indexes = {} 573 | this.heart.clear() 574 | if (globalThisPolyfill[DEV_TOOLS_HOOK] && !this.props.designable) { 575 | globalThisPolyfill[DEV_TOOLS_HOOK].unmount(this.id) 576 | } 577 | } 578 | 579 | setState: IModelSetter<IFormState<ValueType>> = createStateSetter(this) 580 | 581 | getState: IModelGetter<IFormState<ValueType>> = createStateGetter(this) 582 | 583 | setFormState: IModelSetter<IFormState<ValueType>> = createStateSetter(this) 584 | 585 | getFormState: IModelGetter<IFormState<ValueType>> = createStateGetter(this) 586 | 587 | setFieldState: IFieldStateSetter = createBatchStateSetter(this) 588 | 589 | getFieldState: IFieldStateGetter = createBatchStateGetter(this) 590 | 591 | getFormGraph = () => { 592 | return this.graph.getGraph() 593 | } 594 | 595 | setFormGraph = (graph: IFormGraph) => { 596 | this.graph.setGraph(graph) 597 | } 598 | 599 | clearFormGraph = (pattern: FormPathPattern = '*', forceClear = true) => { 600 | this.query(pattern).forEach((field) => { 601 | field.destroy(forceClear) 602 | }) 603 | } 604 | 605 | validate = (pattern: FormPathPattern = '*') => { 606 | return batchValidate(this, pattern) 607 | } 608 | 609 | submit = <T>( 610 | onSubmit?: (values: ValueType) => Promise<T> | void 611 | ): Promise<T> => { 612 | return batchSubmit(this, onSubmit) 613 | } 614 | 615 | reset = (pattern: FormPathPattern = '*', options?: IFieldResetOptions) => { 616 | return batchReset(this, pattern, options) 617 | } 618 | } 619 | ``` -------------------------------------------------------------------------------- /packages/path/src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Parser } from './parser' 2 | import { isStr, isArr, isFn, isEqual, isObj, isNum, isRegExp } from './shared' 3 | import { 4 | getDestructor, 5 | getInByDestructor, 6 | setInByDestructor, 7 | deleteInByDestructor, 8 | existInByDestructor, 9 | } from './destructor' 10 | import { Segments, Node, Pattern } from './types' 11 | import { Matcher } from './matcher' 12 | 13 | const pathCache = new Map() 14 | 15 | const isMatcher = Symbol('PATH_MATCHER') 16 | 17 | const isValid = (val: any) => val !== undefined && val !== null 18 | 19 | const isSimplePath = (val: string) => 20 | val.indexOf('*') === -1 && 21 | val.indexOf('~') === -1 && 22 | val.indexOf('[') === -1 && 23 | val.indexOf(']') === -1 && 24 | val.indexOf(',') === -1 && 25 | val.indexOf(':') === -1 && 26 | val.indexOf(' ') === -1 && 27 | val[0] !== '.' 28 | 29 | const isAssignable = (val: any) => 30 | typeof val === 'object' || typeof val === 'function' 31 | 32 | const isNumberIndex = (val: any) => 33 | isStr(val) ? /^\d+$/.test(val) : isNum(val) 34 | 35 | const getIn = (segments: Segments, source: any) => { 36 | for (let i = 0; i < segments.length; i++) { 37 | const index = segments[i] 38 | const rules = getDestructor(index as string) 39 | if (!rules) { 40 | if (!isValid(source)) return 41 | source = source[index] 42 | } else { 43 | source = getInByDestructor(source, rules, { setIn, getIn }) 44 | break 45 | } 46 | } 47 | return source 48 | } 49 | 50 | const setIn = (segments: Segments, source: any, value: any) => { 51 | for (let i = 0; i < segments.length; i++) { 52 | const index = segments[i] 53 | const rules = getDestructor(index as string) 54 | if (!rules) { 55 | if (!isValid(source) || !isAssignable(source)) return 56 | if (isArr(source) && !isNumberIndex(index)) { 57 | return 58 | } 59 | if (!isValid(source[index])) { 60 | if (value === undefined) { 61 | if (source[index] === null) source[index] = value 62 | return 63 | } 64 | if (i < segments.length - 1) { 65 | source[index] = isNum(segments[i + 1]) ? [] : {} 66 | } 67 | } 68 | if (i === segments.length - 1) { 69 | source[index] = value 70 | } 71 | source = source[index] 72 | } else { 73 | setInByDestructor(source, rules, value, { setIn, getIn }) 74 | break 75 | } 76 | } 77 | } 78 | 79 | const deleteIn = (segments: Segments, source: any) => { 80 | for (let i = 0; i < segments.length; i++) { 81 | const index = segments[i] 82 | const rules = getDestructor(index as string) 83 | if (!rules) { 84 | if (i === segments.length - 1 && isValid(source)) { 85 | delete source[index] 86 | return 87 | } 88 | 89 | if (!isValid(source) || !isAssignable(source)) return 90 | source = source[index] 91 | if (!isObj(source)) { 92 | return 93 | } 94 | } else { 95 | deleteInByDestructor(source, rules, { 96 | setIn, 97 | getIn, 98 | deleteIn, 99 | }) 100 | break 101 | } 102 | } 103 | } 104 | 105 | const hasOwnProperty = Object.prototype.hasOwnProperty 106 | 107 | const existIn = (segments: Segments, source: any, start: number | Path) => { 108 | if (start instanceof Path) { 109 | start = start.length 110 | } 111 | for (let i = start; i < segments.length; i++) { 112 | const index = segments[i] 113 | const rules = getDestructor(index as string) 114 | if (!rules) { 115 | if (i === segments.length - 1) { 116 | return hasOwnProperty.call(source, index) 117 | } 118 | 119 | if (!isValid(source) || !isAssignable(source)) return false 120 | source = source[index] 121 | 122 | if (!isObj(source)) { 123 | return false 124 | } 125 | } else { 126 | return existInByDestructor(source, rules, start, { 127 | setIn, 128 | getIn, 129 | deleteIn, 130 | existIn, 131 | }) 132 | } 133 | } 134 | } 135 | 136 | const parse = (pattern: Pattern, base?: Pattern) => { 137 | if (pattern instanceof Path) { 138 | return { 139 | entire: pattern.entire, 140 | segments: pattern.segments.slice(), 141 | isRegExp: false, 142 | haveRelativePattern: pattern.haveRelativePattern, 143 | isWildMatchPattern: pattern.isWildMatchPattern, 144 | isMatchPattern: pattern.isMatchPattern, 145 | haveExcludePattern: pattern.haveExcludePattern, 146 | tree: pattern.tree, 147 | } 148 | } else if (isStr(pattern)) { 149 | if (!pattern) { 150 | return { 151 | entire: '', 152 | segments: [], 153 | isRegExp: false, 154 | isWildMatchPattern: false, 155 | haveExcludePattern: false, 156 | isMatchPattern: false, 157 | } 158 | } 159 | if (isSimplePath(pattern)) { 160 | return { 161 | entire: pattern, 162 | segments: pattern.split('.'), 163 | isRegExp: false, 164 | isWildMatchPattern: false, 165 | haveExcludePattern: false, 166 | isMatchPattern: false, 167 | } 168 | } 169 | const parser = new Parser(pattern, Path.parse(base)) 170 | const tree = parser.parse() 171 | if (!parser.isMatchPattern) { 172 | const segments = parser.data.segments 173 | return { 174 | entire: segments.join('.'), 175 | segments, 176 | tree, 177 | isRegExp: false, 178 | haveRelativePattern: parser.haveRelativePattern, 179 | isWildMatchPattern: false, 180 | haveExcludePattern: false, 181 | isMatchPattern: false, 182 | } 183 | } else { 184 | return { 185 | entire: pattern, 186 | segments: [], 187 | isRegExp: false, 188 | haveRelativePattern: false, 189 | isWildMatchPattern: parser.isWildMatchPattern, 190 | haveExcludePattern: parser.haveExcludePattern, 191 | isMatchPattern: true, 192 | tree, 193 | } 194 | } 195 | } else if (isFn(pattern) && pattern[isMatcher]) { 196 | return parse(pattern['path']) 197 | } else if (isArr(pattern)) { 198 | return { 199 | entire: pattern.join('.'), 200 | segments: pattern.reduce((buf, key) => { 201 | return buf.concat(parseString(key)) 202 | }, []), 203 | isRegExp: false, 204 | haveRelativePattern: false, 205 | isWildMatchPattern: false, 206 | haveExcludePattern: false, 207 | isMatchPattern: false, 208 | } 209 | } else if (isRegExp(pattern)) { 210 | return { 211 | entire: pattern, 212 | segments: [], 213 | isRegExp: true, 214 | haveRelativePattern: false, 215 | isWildMatchPattern: false, 216 | haveExcludePattern: false, 217 | isMatchPattern: true, 218 | } 219 | } else { 220 | return { 221 | entire: '', 222 | isRegExp: false, 223 | segments: pattern !== undefined ? [pattern] : [], 224 | haveRelativePattern: false, 225 | isWildMatchPattern: false, 226 | haveExcludePattern: false, 227 | isMatchPattern: false, 228 | } 229 | } 230 | } 231 | 232 | const parseString = (source: any) => { 233 | if (isStr(source)) { 234 | source = source.replace(/\s*/g, '') 235 | try { 236 | const { segments, isMatchPattern } = parse(source) 237 | return !isMatchPattern ? segments : source 238 | } catch (e) { 239 | return source 240 | } 241 | } else if (source instanceof Path) { 242 | return source.segments 243 | } 244 | return source 245 | } 246 | 247 | export class Path { 248 | public entire: string | RegExp 249 | public segments: Segments 250 | public isMatchPattern: boolean 251 | public isWildMatchPattern: boolean 252 | public isRegExp: boolean 253 | public haveRelativePattern: boolean 254 | public haveExcludePattern: boolean 255 | public matchScore: number 256 | public tree: Node 257 | private matchCache: any 258 | private includesCache: any 259 | 260 | constructor(input: Pattern, base?: Pattern) { 261 | const { 262 | tree, 263 | segments, 264 | entire, 265 | isRegExp, 266 | isMatchPattern, 267 | isWildMatchPattern, 268 | haveRelativePattern, 269 | haveExcludePattern, 270 | } = parse(input, base) 271 | this.entire = entire 272 | this.segments = segments 273 | this.isMatchPattern = isMatchPattern 274 | this.isWildMatchPattern = isWildMatchPattern 275 | this.haveRelativePattern = haveRelativePattern 276 | this.isRegExp = isRegExp 277 | this.haveExcludePattern = haveExcludePattern 278 | this.tree = tree as Node 279 | this.matchCache = new Map() 280 | this.includesCache = new Map() 281 | } 282 | 283 | toString() { 284 | return this.entire?.toString() 285 | } 286 | 287 | toArr() { 288 | return this.segments?.slice() 289 | } 290 | 291 | get length() { 292 | return this.segments.length 293 | } 294 | 295 | concat = (...args: Pattern[]) => { 296 | if (this.isMatchPattern || this.isRegExp) { 297 | throw new Error(`${this.entire} cannot be concat`) 298 | } 299 | const path = new Path('') 300 | path.segments = this.segments.concat(...args.map((s) => parseString(s))) 301 | path.entire = path.segments.join('.') 302 | return path 303 | } 304 | 305 | slice = (start?: number, end?: number) => { 306 | if (this.isMatchPattern || this.isRegExp) { 307 | throw new Error(`${this.entire} cannot be slice`) 308 | } 309 | const path = new Path('') 310 | path.segments = this.segments.slice(start, end) 311 | path.entire = path.segments.join('.') 312 | return path 313 | } 314 | 315 | push = (...items: Pattern[]) => { 316 | return this.concat(...items) 317 | } 318 | 319 | pop = () => { 320 | if (this.isMatchPattern || this.isRegExp) { 321 | throw new Error(`${this.entire} cannot be pop`) 322 | } 323 | return new Path(this.segments.slice(0, this.segments.length - 1)) 324 | } 325 | 326 | splice = ( 327 | start: number, 328 | deleteCount?: number, 329 | ...items: Array<string | number> 330 | ) => { 331 | if (this.isMatchPattern || this.isRegExp) { 332 | throw new Error(`${this.entire} cannot be splice`) 333 | } 334 | items = items.reduce((buf, item) => buf.concat(parseString(item)), []) 335 | const segments_ = this.segments.slice() 336 | segments_.splice(start, deleteCount, ...items) 337 | return new Path(segments_) 338 | } 339 | 340 | forEach = (callback: (key: string | number) => any) => { 341 | if (this.isMatchPattern || this.isRegExp) { 342 | throw new Error(`${this.entire} cannot be each`) 343 | } 344 | this.segments.forEach(callback) 345 | } 346 | 347 | map = (callback: (key: string | number) => any) => { 348 | if (this.isMatchPattern || this.isRegExp) { 349 | throw new Error(`${this.entire} cannot be map`) 350 | } 351 | return this.segments.map(callback) 352 | } 353 | 354 | reduce = <T>( 355 | callback: (buf: T, item: string | number, index: number) => T, 356 | initial: T 357 | ): T => { 358 | if (this.isMatchPattern || this.isRegExp) { 359 | throw new Error(`${this.entire} cannot be reduce`) 360 | } 361 | return this.segments.reduce(callback, initial) 362 | } 363 | 364 | parent = () => { 365 | return this.slice(0, this.length - 1) 366 | } 367 | 368 | includes = (pattern: Pattern) => { 369 | const { entire, segments, isMatchPattern } = Path.parse(pattern) 370 | const cache = this.includesCache.get(entire) 371 | if (cache !== undefined) return cache 372 | const cacheWith = (value: boolean): boolean => { 373 | this.includesCache.set(entire, value) 374 | return value 375 | } 376 | if (this.isMatchPattern) { 377 | if (!isMatchPattern) { 378 | return cacheWith(this.match(segments)) 379 | } else { 380 | throw new Error(`${this.entire} cannot be used to match ${entire}`) 381 | } 382 | } 383 | if (isMatchPattern) { 384 | throw new Error(`${this.entire} cannot be used to match ${entire}`) 385 | } 386 | if (segments.length > this.segments.length) return cacheWith(false) 387 | for (let i = 0; i < segments.length; i++) { 388 | if (!isEqual(String(segments[i]), String(this.segments[i]))) { 389 | return cacheWith(false) 390 | } 391 | } 392 | return cacheWith(true) 393 | } 394 | 395 | transform = <T>( 396 | regexp: string | RegExp, 397 | callback: (...args: string[]) => T 398 | ): T | string => { 399 | if (!isFn(callback)) return '' 400 | if (this.isMatchPattern) { 401 | throw new Error(`${this.entire} cannot be transformed`) 402 | } 403 | const reg = new RegExp(regexp) 404 | const args = this.segments.filter((key) => 405 | reg.test(key as string) 406 | ) as string[] 407 | return callback(...args) 408 | } 409 | 410 | match = (pattern: Pattern): boolean => { 411 | const path = Path.parse(pattern) 412 | const cache = this.matchCache.get(path.entire) 413 | if (cache !== undefined) { 414 | if (cache.record && cache.record.score !== undefined) { 415 | this.matchScore = cache.record.score 416 | } 417 | return cache.matched 418 | } 419 | const cacheWith = (value: any) => { 420 | this.matchCache.set(path.entire, value) 421 | return value 422 | } 423 | if (path.isMatchPattern) { 424 | if (this.isMatchPattern) { 425 | throw new Error(`${path.entire} cannot match ${this.entire}`) 426 | } else { 427 | this.matchScore = 0 428 | return cacheWith(path.match(this.segments)) 429 | } 430 | } else { 431 | if (this.isMatchPattern) { 432 | if (this.isRegExp) { 433 | try { 434 | return this['entire']?.['test']?.(path.entire) 435 | } finally { 436 | ;(this.entire as RegExp).lastIndex = 0 437 | } 438 | } 439 | const record = { 440 | score: 0, 441 | } 442 | const result = cacheWith( 443 | new Matcher(this.tree, record).match(path.segments) 444 | ) 445 | this.matchScore = record.score 446 | return result.matched 447 | } else { 448 | const record = { 449 | score: 0, 450 | } 451 | const result = cacheWith( 452 | Matcher.matchSegments(this.segments, path.segments, record) 453 | ) 454 | this.matchScore = record.score 455 | return result.matched 456 | } 457 | } 458 | } 459 | 460 | //别名组匹配 461 | matchAliasGroup = (name: Pattern, alias: Pattern) => { 462 | const namePath = Path.parse(name) 463 | const aliasPath = Path.parse(alias) 464 | const nameMatched = this.match(namePath) 465 | const nameMatchedScore = this.matchScore 466 | const aliasMatched = this.match(aliasPath) 467 | const aliasMatchedScore = this.matchScore 468 | if (this.haveExcludePattern) { 469 | if (nameMatchedScore >= aliasMatchedScore) { 470 | return nameMatched 471 | } else { 472 | return aliasMatched 473 | } 474 | } else { 475 | return nameMatched || aliasMatched 476 | } 477 | } 478 | 479 | existIn = (source?: any, start: number | Path = 0) => { 480 | return existIn(this.segments, source, start) 481 | } 482 | 483 | getIn = (source?: any) => { 484 | return getIn(this.segments, source) 485 | } 486 | 487 | setIn = (source?: any, value?: any) => { 488 | setIn(this.segments, source, value) 489 | return source 490 | } 491 | 492 | deleteIn = (source?: any) => { 493 | deleteIn(this.segments, source) 494 | return source 495 | } 496 | 497 | ensureIn = (source?: any, defaults?: any) => { 498 | const results = this.getIn(source) 499 | if (results === undefined) { 500 | this.setIn(source, defaults) 501 | return this.getIn(source) 502 | } 503 | return results 504 | } 505 | 506 | static match(pattern: Pattern) { 507 | const path = Path.parse(pattern) 508 | const matcher = (target) => { 509 | return path.match(target) 510 | } 511 | matcher[isMatcher] = true 512 | matcher.path = path 513 | return matcher 514 | } 515 | 516 | static isPathPattern(target: any): target is Pattern { 517 | return !!( 518 | isStr(target) || 519 | isArr(target) || 520 | isRegExp(target) || 521 | (isFn(target) && target[isMatcher]) 522 | ) 523 | } 524 | 525 | static transform<T>( 526 | pattern: Pattern, 527 | regexp: string | RegExp, 528 | callback: (...args: string[]) => T 529 | ): any { 530 | return Path.parse(pattern).transform(regexp, callback) 531 | } 532 | 533 | static parse(path: Pattern = '', base?: Pattern): Path { 534 | if (path instanceof Path) { 535 | const found = pathCache.get(path.entire) 536 | if (found) { 537 | return found 538 | } else { 539 | pathCache.set(path.entire, path) 540 | return path 541 | } 542 | } else if (path && path[isMatcher]) { 543 | return Path.parse(path['path']) 544 | } else { 545 | const key_ = base ? Path.parse(base) : '' 546 | const key = `${path}:${key_}` 547 | const found = pathCache.get(key) 548 | if (found) { 549 | return found 550 | } else { 551 | path = new Path(path, base) 552 | pathCache.set(key, path) 553 | return path 554 | } 555 | } 556 | } 557 | 558 | static getIn = (source: any, pattern: Pattern) => { 559 | const path = Path.parse(pattern) 560 | return path.getIn(source) 561 | } 562 | 563 | static setIn = (source: any, pattern: Pattern, value: any) => { 564 | const path = Path.parse(pattern) 565 | return path.setIn(source, value) 566 | } 567 | 568 | static deleteIn = (source: any, pattern: Pattern) => { 569 | const path = Path.parse(pattern) 570 | return path.deleteIn(source) 571 | } 572 | 573 | static existIn = (source: any, pattern: Pattern, start?: number | Path) => { 574 | const path = Path.parse(pattern) 575 | return path.existIn(source, start) 576 | } 577 | 578 | static ensureIn = (source: any, pattern: Pattern, defaultValue?: any) => { 579 | const path = Path.parse(pattern) 580 | return path.ensureIn(source, defaultValue) 581 | } 582 | } 583 | 584 | export { Pattern } 585 | ``` -------------------------------------------------------------------------------- /packages/antd/docs/components/ArrayCollapse.zh-CN.md: -------------------------------------------------------------------------------- ```markdown 1 | # ArrayCollapse 2 | 3 | > 折叠面板,对于每行字段数量较多,联动较多的场景比较适合使用 ArrayCollapse 4 | > 5 | > 注意:该组件只适用于 Schema 场景 6 | 7 | ## Markup Schema 案例 8 | 9 | ```tsx 10 | import React from 'react' 11 | import { 12 | FormItem, 13 | Input, 14 | ArrayCollapse, 15 | FormButtonGroup, 16 | Submit, 17 | } from '@formily/antd' 18 | import { createForm } from '@formily/core' 19 | import { FormProvider, createSchemaField } from '@formily/react' 20 | import { Button } from 'antd' 21 | 22 | const SchemaField = createSchemaField({ 23 | components: { 24 | FormItem, 25 | Input, 26 | ArrayCollapse, 27 | }, 28 | }) 29 | 30 | const form = createForm() 31 | 32 | export default () => { 33 | return ( 34 | <FormProvider form={form}> 35 | <SchemaField> 36 | <SchemaField.Array 37 | name="string_array" 38 | maxItems={3} 39 | x-decorator="FormItem" 40 | x-component="ArrayCollapse" 41 | x-component-props={{ 42 | accordion: true, 43 | defaultOpenPanelCount: 3, 44 | }} 45 | > 46 | <SchemaField.Void 47 | x-component="ArrayCollapse.CollapsePanel" 48 | x-component-props={{ 49 | header: '字符串数组', 50 | }} 51 | > 52 | <SchemaField.Void x-component="ArrayCollapse.Index" /> 53 | <SchemaField.String 54 | name="input" 55 | x-decorator="FormItem" 56 | title="Input" 57 | required 58 | x-component="Input" 59 | /> 60 | <SchemaField.Void x-component="ArrayCollapse.Remove" /> 61 | <SchemaField.Void x-component="ArrayCollapse.MoveUp" /> 62 | <SchemaField.Void x-component="ArrayCollapse.MoveDown" /> 63 | </SchemaField.Void> 64 | <SchemaField.Void 65 | x-component="ArrayCollapse.Addition" 66 | title="添加条目" 67 | /> 68 | </SchemaField.Array> 69 | <SchemaField.Array 70 | name="array" 71 | maxItems={3} 72 | x-decorator="FormItem" 73 | x-component="ArrayCollapse" 74 | > 75 | <SchemaField.Object 76 | x-component="ArrayCollapse.CollapsePanel" 77 | x-component-props={{ 78 | header: '对象数组', 79 | }} 80 | > 81 | <SchemaField.Void x-component="ArrayCollapse.Index" /> 82 | <SchemaField.String 83 | name="input" 84 | x-decorator="FormItem" 85 | title="Input" 86 | required 87 | x-component="Input" 88 | /> 89 | <SchemaField.Void x-component="ArrayCollapse.Remove" /> 90 | <SchemaField.Void x-component="ArrayCollapse.MoveUp" /> 91 | <SchemaField.Void x-component="ArrayCollapse.MoveDown" /> 92 | </SchemaField.Object> 93 | <SchemaField.Void 94 | x-component="ArrayCollapse.Addition" 95 | title="添加条目" 96 | /> 97 | </SchemaField.Array> 98 | <SchemaField.Array 99 | name="string_array_unshift" 100 | maxItems={3} 101 | x-decorator="FormItem" 102 | x-component="ArrayCollapse" 103 | x-component-props={{ 104 | defaultOpenPanelCount: 8, 105 | }} 106 | > 107 | <SchemaField.Void 108 | x-component="ArrayCollapse.CollapsePanel" 109 | x-component-props={{ 110 | header: '字符串数组', 111 | }} 112 | > 113 | <SchemaField.Void x-component="ArrayCollapse.Index" /> 114 | <SchemaField.String 115 | name="input" 116 | x-decorator="FormItem" 117 | title="Input" 118 | required 119 | x-component="Input" 120 | /> 121 | <SchemaField.Void x-component="ArrayCollapse.Remove" /> 122 | <SchemaField.Void x-component="ArrayCollapse.MoveUp" /> 123 | <SchemaField.Void x-component="ArrayCollapse.MoveDown" /> 124 | </SchemaField.Void> 125 | <SchemaField.Void 126 | x-component="ArrayCollapse.Addition" 127 | title="添加条目(unshift)" 128 | x-component-props={{ 129 | method: 'unshift', 130 | }} 131 | /> 132 | </SchemaField.Array> 133 | </SchemaField> 134 | <FormButtonGroup> 135 | <Button 136 | onClick={() => { 137 | form.setInitialValues({ 138 | array: Array.from({ length: 10 }).map(() => ({ 139 | input: 'default value', 140 | })), 141 | string_array: Array.from({ length: 10 }).map( 142 | () => 'default value' 143 | ), 144 | string_array_unshift: Array.from({ length: 10 }).map( 145 | () => 'default value' 146 | ), 147 | }) 148 | }} 149 | > 150 | 加载默认数据 151 | </Button> 152 | <Submit onSubmit={console.log}>提交</Submit> 153 | </FormButtonGroup> 154 | </FormProvider> 155 | ) 156 | } 157 | ``` 158 | 159 | ## JSON Schema 案例 160 | 161 | ```tsx 162 | import React from 'react' 163 | import { 164 | FormItem, 165 | Input, 166 | ArrayCollapse, 167 | FormButtonGroup, 168 | Submit, 169 | } from '@formily/antd' 170 | import { createForm } from '@formily/core' 171 | import { FormProvider, createSchemaField } from '@formily/react' 172 | 173 | const SchemaField = createSchemaField({ 174 | components: { 175 | FormItem, 176 | Input, 177 | ArrayCollapse, 178 | }, 179 | }) 180 | 181 | const form = createForm() 182 | 183 | const schema = { 184 | type: 'object', 185 | properties: { 186 | string_array: { 187 | type: 'array', 188 | 'x-component': 'ArrayCollapse', 189 | 'x-component-props': { 190 | onAdd: (index: number) => { 191 | console.log('Adding ' + index + ' item') 192 | }, 193 | }, 194 | maxItems: 3, 195 | 'x-decorator': 'FormItem', 196 | items: { 197 | type: 'void', 198 | 'x-component': 'ArrayCollapse.CollapsePanel', 199 | 'x-component-props': { 200 | header: '字符串数组', 201 | }, 202 | properties: { 203 | index: { 204 | type: 'void', 205 | 'x-component': 'ArrayCollapse.Index', 206 | }, 207 | input: { 208 | type: 'string', 209 | 'x-decorator': 'FormItem', 210 | title: 'Input', 211 | required: true, 212 | 'x-component': 'Input', 213 | }, 214 | remove: { 215 | type: 'void', 216 | 'x-component': 'ArrayCollapse.Remove', 217 | }, 218 | moveUp: { 219 | type: 'void', 220 | 'x-component': 'ArrayCollapse.MoveUp', 221 | }, 222 | moveDown: { 223 | type: 'void', 224 | 'x-component': 'ArrayCollapse.MoveDown', 225 | }, 226 | }, 227 | }, 228 | properties: { 229 | addition: { 230 | type: 'void', 231 | title: '添加条目', 232 | 'x-component': 'ArrayCollapse.Addition', 233 | }, 234 | }, 235 | }, 236 | array: { 237 | type: 'array', 238 | 'x-component': 'ArrayCollapse', 239 | maxItems: 3, 240 | 'x-decorator': 'FormItem', 241 | items: { 242 | type: 'object', 243 | 'x-component': 'ArrayCollapse.CollapsePanel', 244 | 'x-component-props': { 245 | header: '对象数组', 246 | }, 247 | properties: { 248 | index: { 249 | type: 'void', 250 | 'x-component': 'ArrayCollapse.Index', 251 | }, 252 | input: { 253 | type: 'string', 254 | 'x-decorator': 'FormItem', 255 | title: 'Input', 256 | required: true, 257 | 'x-component': 'Input', 258 | }, 259 | remove: { 260 | type: 'void', 261 | 'x-component': 'ArrayCollapse.Remove', 262 | }, 263 | moveUp: { 264 | type: 'void', 265 | 'x-component': 'ArrayCollapse.MoveUp', 266 | }, 267 | moveDown: { 268 | type: 'void', 269 | 'x-component': 'ArrayCollapse.MoveDown', 270 | }, 271 | }, 272 | }, 273 | properties: { 274 | addition: { 275 | type: 'void', 276 | title: '添加条目', 277 | 'x-component': 'ArrayCollapse.Addition', 278 | }, 279 | }, 280 | }, 281 | array_unshift: { 282 | type: 'array', 283 | 'x-component': 'ArrayCollapse', 284 | maxItems: 3, 285 | 'x-decorator': 'FormItem', 286 | items: { 287 | type: 'object', 288 | 'x-component': 'ArrayCollapse.CollapsePanel', 289 | 'x-component-props': { 290 | header: '对象数组', 291 | }, 292 | properties: { 293 | index: { 294 | type: 'void', 295 | 'x-component': 'ArrayCollapse.Index', 296 | }, 297 | input: { 298 | type: 'string', 299 | 'x-decorator': 'FormItem', 300 | title: 'Input', 301 | required: true, 302 | 'x-component': 'Input', 303 | }, 304 | remove: { 305 | type: 'void', 306 | 'x-component': 'ArrayCollapse.Remove', 307 | }, 308 | moveUp: { 309 | type: 'void', 310 | 'x-component': 'ArrayCollapse.MoveUp', 311 | }, 312 | moveDown: { 313 | type: 'void', 314 | 'x-component': 'ArrayCollapse.MoveDown', 315 | }, 316 | }, 317 | }, 318 | properties: { 319 | addition: { 320 | type: 'void', 321 | title: '添加条目(unshift)', 322 | 'x-component': 'ArrayCollapse.Addition', 323 | 'x-component-props': { 324 | method: 'unshift', 325 | }, 326 | }, 327 | }, 328 | }, 329 | }, 330 | } 331 | 332 | export default () => { 333 | return ( 334 | <FormProvider form={form}> 335 | <SchemaField schema={schema} /> 336 | <FormButtonGroup> 337 | <Submit onSubmit={console.log}>提交</Submit> 338 | </FormButtonGroup> 339 | </FormProvider> 340 | ) 341 | } 342 | ``` 343 | 344 | ## Effects 联动案例 345 | 346 | ```tsx 347 | import React from 'react' 348 | import { 349 | FormItem, 350 | Input, 351 | ArrayCollapse, 352 | FormButtonGroup, 353 | Submit, 354 | } from '@formily/antd' 355 | import { createForm, onFieldChange, onFieldReact } from '@formily/core' 356 | import { FormProvider, createSchemaField } from '@formily/react' 357 | 358 | const SchemaField = createSchemaField({ 359 | components: { 360 | FormItem, 361 | Input, 362 | ArrayCollapse, 363 | }, 364 | }) 365 | 366 | const form = createForm({ 367 | effects: () => { 368 | //主动联动模式 369 | onFieldChange('array.*.aa', ['value'], (field, form) => { 370 | form.setFieldState(field.query('.bb'), (state) => { 371 | state.visible = field.value != '123' 372 | }) 373 | }) 374 | //被动联动模式 375 | onFieldReact('array.*.dd', (field) => { 376 | field.visible = field.query('.cc').get('value') != '123' 377 | }) 378 | }, 379 | }) 380 | 381 | export default () => { 382 | return ( 383 | <FormProvider form={form}> 384 | <SchemaField> 385 | <SchemaField.Array 386 | name="array" 387 | maxItems={3} 388 | x-component="ArrayCollapse" 389 | x-decorator="FormItem" 390 | x-component-props={{ 391 | title: '对象数组', 392 | }} 393 | > 394 | <SchemaField.Object 395 | x-component="ArrayCollapse.CollapsePanel" 396 | x-component-props={{ 397 | header: '对象数组', 398 | }} 399 | > 400 | <SchemaField.Void x-component="ArrayCollapse.Index" /> 401 | <SchemaField.String 402 | name="aa" 403 | x-decorator="FormItem" 404 | title="AA" 405 | required 406 | description="AA输入123时隐藏BB" 407 | x-component="Input" 408 | /> 409 | <SchemaField.String 410 | name="bb" 411 | x-decorator="FormItem" 412 | title="BB" 413 | required 414 | x-component="Input" 415 | /> 416 | <SchemaField.String 417 | name="cc" 418 | x-decorator="FormItem" 419 | title="CC" 420 | required 421 | description="CC输入123时隐藏DD" 422 | x-component="Input" 423 | /> 424 | <SchemaField.String 425 | name="dd" 426 | x-decorator="FormItem" 427 | title="DD" 428 | required 429 | x-component="Input" 430 | /> 431 | <SchemaField.Void x-component="ArrayCollapse.Remove" /> 432 | <SchemaField.Void x-component="ArrayCollapse.MoveUp" /> 433 | <SchemaField.Void x-component="ArrayCollapse.MoveDown" /> 434 | </SchemaField.Object> 435 | <SchemaField.Void 436 | x-component="ArrayCollapse.Addition" 437 | title="添加条目" 438 | /> 439 | </SchemaField.Array> 440 | </SchemaField> 441 | <FormButtonGroup> 442 | <Submit onSubmit={console.log}>提交</Submit> 443 | </FormButtonGroup> 444 | </FormProvider> 445 | ) 446 | } 447 | ``` 448 | 449 | ## JSON Schema 联动案例 450 | 451 | ```tsx 452 | import React from 'react' 453 | import { 454 | FormItem, 455 | Input, 456 | ArrayCollapse, 457 | FormButtonGroup, 458 | Submit, 459 | } from '@formily/antd' 460 | import { createForm } from '@formily/core' 461 | import { FormProvider, createSchemaField } from '@formily/react' 462 | 463 | const SchemaField = createSchemaField({ 464 | components: { 465 | FormItem, 466 | Input, 467 | ArrayCollapse, 468 | }, 469 | }) 470 | 471 | const form = createForm() 472 | 473 | const schema = { 474 | type: 'object', 475 | properties: { 476 | array: { 477 | type: 'array', 478 | 'x-component': 'ArrayCollapse', 479 | maxItems: 3, 480 | title: '对象数组', 481 | items: { 482 | type: 'object', 483 | 'x-component': 'ArrayCollapse.CollapsePanel', 484 | 'x-component-props': { 485 | header: '对象数组', 486 | }, 487 | properties: { 488 | index: { 489 | type: 'void', 490 | 'x-component': 'ArrayCollapse.Index', 491 | }, 492 | aa: { 493 | type: 'string', 494 | 'x-decorator': 'FormItem', 495 | title: 'AA', 496 | required: true, 497 | 'x-component': 'Input', 498 | description: '输入123', 499 | }, 500 | bb: { 501 | type: 'string', 502 | title: 'BB', 503 | required: true, 504 | 'x-decorator': 'FormItem', 505 | 'x-component': 'Input', 506 | 'x-reactions': [ 507 | { 508 | dependencies: ['.aa'], 509 | when: "{{$deps[0] != '123'}}", 510 | fulfill: { 511 | schema: { 512 | title: 'BB', 513 | 'x-disabled': true, 514 | }, 515 | }, 516 | otherwise: { 517 | schema: { 518 | title: 'Changed', 519 | 'x-disabled': false, 520 | }, 521 | }, 522 | }, 523 | ], 524 | }, 525 | remove: { 526 | type: 'void', 527 | 'x-component': 'ArrayCollapse.Remove', 528 | }, 529 | moveUp: { 530 | type: 'void', 531 | 'x-component': 'ArrayCollapse.MoveUp', 532 | }, 533 | moveDown: { 534 | type: 'void', 535 | 'x-component': 'ArrayCollapse.MoveDown', 536 | }, 537 | }, 538 | }, 539 | properties: { 540 | addition: { 541 | type: 'void', 542 | title: '添加条目', 543 | 'x-component': 'ArrayCollapse.Addition', 544 | }, 545 | }, 546 | }, 547 | }, 548 | } 549 | 550 | export default () => { 551 | return ( 552 | <FormProvider form={form}> 553 | <SchemaField schema={schema} /> 554 | <FormButtonGroup> 555 | <Submit onSubmit={console.log}>提交</Submit> 556 | </FormButtonGroup> 557 | </FormProvider> 558 | ) 559 | } 560 | ``` 561 | 562 | ## API 563 | 564 | ### ArrayCollapse 565 | 566 | 参考 https://ant.design/components/collapse-cn/ 567 | 568 | ### ArrayCollapse.CollapsePanel 569 | 570 | 参考 https://ant.design/components/collapse-cn/ 571 | 572 | ### ArrayCollapse.Addition 573 | 574 | > 添加按钮 575 | 576 | 扩展属性 577 | 578 | | 属性名 | 类型 | 描述 | 默认值 | 579 | | ------------ | --------------------- | -------- | -------- | 580 | | title | ReactText | 文案 | | 581 | | method | `'push' \| 'unshift'` | 添加方式 | `'push'` | 582 | | defaultValue | `any` | 默认值 | | 583 | 584 | 其余参考 https://ant.design/components/button-cn/ 585 | 586 | 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 587 | 588 | 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 589 | 590 | ### ArrayCollapse.Remove 591 | 592 | > 删除按钮 593 | 594 | | 属性名 | 类型 | 描述 | 默认值 | 595 | | ------ | --------- | ---- | ------ | 596 | | title | ReactText | 文案 | | 597 | 598 | 其余参考 https://ant.design/components/icon-cn/ 599 | 600 | 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 601 | 602 | 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 603 | 604 | ### ArrayCollapse.MoveDown 605 | 606 | > 下移按钮 607 | 608 | | 属性名 | 类型 | 描述 | 默认值 | 609 | | ------ | --------- | ---- | ------ | 610 | | title | ReactText | 文案 | | 611 | 612 | 其余参考 https://ant.design/components/icon-cn/ 613 | 614 | 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 615 | 616 | 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 617 | 618 | ### ArrayCollapse.MoveUp 619 | 620 | > 上移按钮 621 | 622 | | 属性名 | 类型 | 描述 | 默认值 | 623 | | ------ | --------- | ---- | ------ | 624 | | title | ReactText | 文案 | | 625 | 626 | 其余参考 https://ant.design/components/icon-cn/ 627 | 628 | 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 629 | 630 | 注意:使用`onClick={e => e.preventDefault()}`可禁用默认行为。 631 | 632 | ### ArrayCollapse.Index 633 | 634 | > 索引渲染器 635 | 636 | 无属性 637 | 638 | ### ArrayCollapse.useIndex 639 | 640 | > 读取当前渲染行索引的 React Hook 641 | 642 | ### ArrayCollapse.useRecord 643 | 644 | > 读取当前渲染记录的 React Hook 645 | ``` -------------------------------------------------------------------------------- /packages/next/docs/components/ArrayCollapse.zh-CN.md: -------------------------------------------------------------------------------- ```markdown 1 | # ArrayCollapse 2 | 3 | > 折叠面板,对于每行字段数量较多,联动较多的场景比较适合使用 ArrayCollapse 4 | > 5 | > 注意:该组件只适用于 Schema 场景 6 | 7 | ## Markup Schema 案例 8 | 9 | ```tsx 10 | import React from 'react' 11 | import { 12 | FormItem, 13 | Input, 14 | ArrayCollapse, 15 | FormButtonGroup, 16 | Radio, 17 | Submit, 18 | } from '@formily/next' 19 | import { createForm } from '@formily/core' 20 | import { FormProvider, createSchemaField } from '@formily/react' 21 | import { Button } from '@alifd/next' 22 | 23 | const SchemaField = createSchemaField({ 24 | components: { 25 | Radio, 26 | FormItem, 27 | Input, 28 | ArrayCollapse, 29 | }, 30 | }) 31 | 32 | const form = createForm() 33 | 34 | export default () => { 35 | return ( 36 | <FormProvider form={form}> 37 | <SchemaField> 38 | <SchemaField.Array 39 | name="string_array" 40 | maxItems={3} 41 | x-decorator="FormItem" 42 | x-component="ArrayCollapse" 43 | x-component-props={{ 44 | accordion: true, 45 | defaultOpenPanelCount: 3, 46 | }} 47 | > 48 | <SchemaField.Void 49 | x-component="ArrayCollapse.CollapsePanel" 50 | x-component-props={{ 51 | title: '字符串数组', 52 | }} 53 | > 54 | <SchemaField.Void x-component="ArrayCollapse.Index" /> 55 | <SchemaField.String 56 | name="input" 57 | x-decorator="FormItem" 58 | title="Input" 59 | required 60 | x-component="Input" 61 | /> 62 | <SchemaField.Void x-component="ArrayCollapse.Remove" /> 63 | <SchemaField.Void x-component="ArrayCollapse.MoveUp" /> 64 | <SchemaField.Void x-component="ArrayCollapse.MoveDown" /> 65 | </SchemaField.Void> 66 | <SchemaField.Void 67 | x-component="ArrayCollapse.Addition" 68 | title="添加条目" 69 | /> 70 | </SchemaField.Array> 71 | <SchemaField.Array 72 | name="array" 73 | maxItems={3} 74 | x-decorator="FormItem" 75 | x-component="ArrayCollapse" 76 | > 77 | <SchemaField.Object 78 | x-component="ArrayCollapse.CollapsePanel" 79 | x-component-props={{ 80 | title: '对象数组', 81 | }} 82 | > 83 | <SchemaField.Void x-component="ArrayCollapse.Index" /> 84 | <SchemaField.String 85 | name="input" 86 | x-decorator="FormItem" 87 | title="Input" 88 | required 89 | x-component="Input" 90 | /> 91 | <SchemaField.String 92 | name="radio" 93 | x-decorator="FormItem" 94 | title="Radio" 95 | enum={[1, 2]} 96 | required 97 | x-component="Radio.Group" 98 | /> 99 | <SchemaField.Void x-component="ArrayCollapse.Remove" /> 100 | <SchemaField.Void x-component="ArrayCollapse.MoveUp" /> 101 | <SchemaField.Void x-component="ArrayCollapse.MoveDown" /> 102 | </SchemaField.Object> 103 | <SchemaField.Void 104 | x-component="ArrayCollapse.Addition" 105 | title="添加条目" 106 | /> 107 | </SchemaField.Array> 108 | <SchemaField.Array 109 | name="string_array_unshift" 110 | maxItems={3} 111 | x-decorator="FormItem" 112 | x-component="ArrayCollapse" 113 | x-component-props={{ 114 | defaultOpenPanelCount: 8, 115 | }} 116 | > 117 | <SchemaField.Void 118 | x-component="ArrayCollapse.CollapsePanel" 119 | x-component-props={{ 120 | title: '字符串数组', 121 | }} 122 | > 123 | <SchemaField.Void x-component="ArrayCollapse.Index" /> 124 | <SchemaField.String 125 | name="input" 126 | x-decorator="FormItem" 127 | title="Input" 128 | required 129 | x-component="Input" 130 | /> 131 | <SchemaField.Void x-component="ArrayCollapse.Remove" /> 132 | <SchemaField.Void x-component="ArrayCollapse.MoveUp" /> 133 | <SchemaField.Void x-component="ArrayCollapse.MoveDown" /> 134 | </SchemaField.Void> 135 | <SchemaField.Void 136 | x-component="ArrayCollapse.Addition" 137 | title="添加条目(unshift)" 138 | x-component-props={{ 139 | method: 'unshift', 140 | }} 141 | /> 142 | </SchemaField.Array> 143 | </SchemaField> 144 | <FormButtonGroup> 145 | <Button 146 | onClick={() => { 147 | form.setInitialValues({ 148 | array: Array.from({ length: 10 }).map(() => ({ 149 | input: 'default value', 150 | })), 151 | string_array: Array.from({ length: 10 }).map( 152 | () => 'default value' 153 | ), 154 | string_array_unshift: Array.from({ length: 10 }).map( 155 | () => 'default value' 156 | ), 157 | }) 158 | }} 159 | > 160 | 加载默认数据 161 | </Button> 162 | <Submit onSubmit={console.log}>提交</Submit> 163 | </FormButtonGroup> 164 | </FormProvider> 165 | ) 166 | } 167 | ``` 168 | 169 | ## JSON Schema 案例 170 | 171 | ```tsx 172 | import React from 'react' 173 | import { 174 | FormItem, 175 | Input, 176 | ArrayCollapse, 177 | FormButtonGroup, 178 | Submit, 179 | } from '@formily/next' 180 | import { createForm } from '@formily/core' 181 | import { FormProvider, createSchemaField } from '@formily/react' 182 | 183 | const SchemaField = createSchemaField({ 184 | components: { 185 | FormItem, 186 | Input, 187 | ArrayCollapse, 188 | }, 189 | }) 190 | 191 | const form = createForm() 192 | 193 | const schema = { 194 | type: 'object', 195 | properties: { 196 | string_array: { 197 | type: 'array', 198 | 'x-component': 'ArrayCollapse', 199 | maxItems: 3, 200 | 'x-decorator': 'FormItem', 201 | items: { 202 | type: 'void', 203 | 'x-component': 'ArrayCollapse.CollapsePanel', 204 | 'x-component-props': { 205 | title: '字符串数组', 206 | }, 207 | properties: { 208 | index: { 209 | type: 'void', 210 | 'x-component': 'ArrayCollapse.Index', 211 | }, 212 | input: { 213 | type: 'string', 214 | 'x-decorator': 'FormItem', 215 | title: 'Input', 216 | required: true, 217 | 'x-component': 'Input', 218 | }, 219 | remove: { 220 | type: 'void', 221 | 'x-component': 'ArrayCollapse.Remove', 222 | }, 223 | moveUp: { 224 | type: 'void', 225 | 'x-component': 'ArrayCollapse.MoveUp', 226 | }, 227 | moveDown: { 228 | type: 'void', 229 | 'x-component': 'ArrayCollapse.MoveDown', 230 | }, 231 | }, 232 | }, 233 | properties: { 234 | addition: { 235 | type: 'void', 236 | title: '添加条目', 237 | 'x-component': 'ArrayCollapse.Addition', 238 | }, 239 | }, 240 | }, 241 | array: { 242 | type: 'array', 243 | 'x-component': 'ArrayCollapse', 244 | maxItems: 3, 245 | 'x-decorator': 'FormItem', 246 | items: { 247 | type: 'object', 248 | 'x-component': 'ArrayCollapse.CollapsePanel', 249 | 'x-component-props': { 250 | title: '对象数组', 251 | }, 252 | properties: { 253 | index: { 254 | type: 'void', 255 | 'x-component': 'ArrayCollapse.Index', 256 | }, 257 | input: { 258 | type: 'string', 259 | 'x-decorator': 'FormItem', 260 | title: 'Input', 261 | required: true, 262 | 'x-component': 'Input', 263 | }, 264 | remove: { 265 | type: 'void', 266 | 'x-component': 'ArrayCollapse.Remove', 267 | }, 268 | moveUp: { 269 | type: 'void', 270 | 'x-component': 'ArrayCollapse.MoveUp', 271 | }, 272 | moveDown: { 273 | type: 'void', 274 | 'x-component': 'ArrayCollapse.MoveDown', 275 | }, 276 | }, 277 | }, 278 | properties: { 279 | addition: { 280 | type: 'void', 281 | title: '添加条目', 282 | 'x-component': 'ArrayCollapse.Addition', 283 | }, 284 | }, 285 | }, 286 | array_unshift: { 287 | type: 'array', 288 | 'x-component': 'ArrayCollapse', 289 | maxItems: 3, 290 | 'x-decorator': 'FormItem', 291 | items: { 292 | type: 'object', 293 | 'x-component': 'ArrayCollapse.CollapsePanel', 294 | 'x-component-props': { 295 | title: '对象数组', 296 | }, 297 | properties: { 298 | index: { 299 | type: 'void', 300 | 'x-component': 'ArrayCollapse.Index', 301 | }, 302 | input: { 303 | type: 'string', 304 | 'x-decorator': 'FormItem', 305 | title: 'Input', 306 | required: true, 307 | 'x-component': 'Input', 308 | }, 309 | remove: { 310 | type: 'void', 311 | 'x-component': 'ArrayCollapse.Remove', 312 | }, 313 | moveUp: { 314 | type: 'void', 315 | 'x-component': 'ArrayCollapse.MoveUp', 316 | }, 317 | moveDown: { 318 | type: 'void', 319 | 'x-component': 'ArrayCollapse.MoveDown', 320 | }, 321 | }, 322 | }, 323 | properties: { 324 | addition: { 325 | type: 'void', 326 | title: '添加条目(unshift)', 327 | 'x-component': 'ArrayCollapse.Addition', 328 | 'x-component-props': { 329 | method: 'unshift', 330 | }, 331 | }, 332 | }, 333 | }, 334 | }, 335 | } 336 | 337 | export default () => { 338 | return ( 339 | <FormProvider form={form}> 340 | <SchemaField schema={schema} /> 341 | <FormButtonGroup> 342 | <Submit onSubmit={console.log}>提交</Submit> 343 | </FormButtonGroup> 344 | </FormProvider> 345 | ) 346 | } 347 | ``` 348 | 349 | ## Effects 联动案例 350 | 351 | ```tsx 352 | import React from 'react' 353 | import { 354 | FormItem, 355 | Input, 356 | ArrayCollapse, 357 | FormButtonGroup, 358 | Submit, 359 | } from '@formily/next' 360 | import { createForm, onFieldChange, onFieldReact } from '@formily/core' 361 | import { FormProvider, createSchemaField } from '@formily/react' 362 | 363 | const SchemaField = createSchemaField({ 364 | components: { 365 | FormItem, 366 | Input, 367 | ArrayCollapse, 368 | }, 369 | }) 370 | 371 | const form = createForm({ 372 | effects: () => { 373 | //主动联动模式 374 | onFieldChange('array.*.aa', ['value'], (field, form) => { 375 | form.setFieldState(field.query('.bb'), (state) => { 376 | state.visible = field.value != '123' 377 | }) 378 | }) 379 | //被动联动模式 380 | onFieldReact('array.*.dd', (field) => { 381 | field.visible = field.query('.cc').get('value') != '123' 382 | }) 383 | }, 384 | }) 385 | 386 | export default () => { 387 | return ( 388 | <FormProvider form={form}> 389 | <SchemaField> 390 | <SchemaField.Array 391 | name="array" 392 | maxItems={3} 393 | x-component="ArrayCollapse" 394 | x-decorator="FormItem" 395 | x-component-props={{ 396 | title: '对象数组', 397 | }} 398 | > 399 | <SchemaField.Object 400 | x-component="ArrayCollapse.CollapsePanel" 401 | x-component-props={{ 402 | title: '对象数组', 403 | }} 404 | > 405 | <SchemaField.Void x-component="ArrayCollapse.Index" /> 406 | <SchemaField.String 407 | name="aa" 408 | x-decorator="FormItem" 409 | title="AA" 410 | required 411 | description="AA输入123时隐藏BB" 412 | x-component="Input" 413 | /> 414 | <SchemaField.String 415 | name="bb" 416 | x-decorator="FormItem" 417 | title="BB" 418 | required 419 | x-component="Input" 420 | /> 421 | <SchemaField.String 422 | name="cc" 423 | x-decorator="FormItem" 424 | title="CC" 425 | required 426 | description="CC输入123时隐藏DD" 427 | x-component="Input" 428 | /> 429 | <SchemaField.String 430 | name="dd" 431 | x-decorator="FormItem" 432 | title="DD" 433 | required 434 | x-component="Input" 435 | /> 436 | <SchemaField.Void x-component="ArrayCollapse.Remove" /> 437 | <SchemaField.Void x-component="ArrayCollapse.MoveUp" /> 438 | <SchemaField.Void x-component="ArrayCollapse.MoveDown" /> 439 | </SchemaField.Object> 440 | <SchemaField.Void 441 | x-component="ArrayCollapse.Addition" 442 | title="添加条目" 443 | /> 444 | </SchemaField.Array> 445 | </SchemaField> 446 | <FormButtonGroup> 447 | <Submit onSubmit={console.log}>提交</Submit> 448 | </FormButtonGroup> 449 | </FormProvider> 450 | ) 451 | } 452 | ``` 453 | 454 | ## JSON Schema 联动案例 455 | 456 | ```tsx 457 | import React from 'react' 458 | import { 459 | FormItem, 460 | Input, 461 | ArrayCollapse, 462 | FormButtonGroup, 463 | Submit, 464 | } from '@formily/next' 465 | import { createForm } from '@formily/core' 466 | import { FormProvider, createSchemaField } from '@formily/react' 467 | 468 | const SchemaField = createSchemaField({ 469 | components: { 470 | FormItem, 471 | Input, 472 | ArrayCollapse, 473 | }, 474 | }) 475 | 476 | const form = createForm() 477 | 478 | const schema = { 479 | type: 'object', 480 | properties: { 481 | array: { 482 | type: 'array', 483 | 'x-component': 'ArrayCollapse', 484 | maxItems: 3, 485 | title: '对象数组', 486 | items: { 487 | type: 'object', 488 | 'x-component': 'ArrayCollapse.CollapsePanel', 489 | 'x-component-props': { 490 | title: '对象数组', 491 | }, 492 | properties: { 493 | index: { 494 | type: 'void', 495 | 'x-component': 'ArrayCollapse.Index', 496 | }, 497 | aa: { 498 | type: 'string', 499 | 'x-decorator': 'FormItem', 500 | title: 'AA', 501 | required: true, 502 | 'x-component': 'Input', 503 | description: '输入123', 504 | }, 505 | bb: { 506 | type: 'string', 507 | title: 'BB', 508 | required: true, 509 | 'x-decorator': 'FormItem', 510 | 'x-component': 'Input', 511 | 'x-reactions': [ 512 | { 513 | dependencies: ['.aa'], 514 | when: "{{$deps[0] != '123'}}", 515 | fulfill: { 516 | schema: { 517 | title: 'BB', 518 | 'x-disabled': true, 519 | }, 520 | }, 521 | otherwise: { 522 | schema: { 523 | title: 'Changed', 524 | 'x-disabled': false, 525 | }, 526 | }, 527 | }, 528 | ], 529 | }, 530 | remove: { 531 | type: 'void', 532 | 'x-component': 'ArrayCollapse.Remove', 533 | }, 534 | moveUp: { 535 | type: 'void', 536 | 'x-component': 'ArrayCollapse.MoveUp', 537 | }, 538 | moveDown: { 539 | type: 'void', 540 | 'x-component': 'ArrayCollapse.MoveDown', 541 | }, 542 | }, 543 | }, 544 | properties: { 545 | addition: { 546 | type: 'void', 547 | title: '添加条目', 548 | 'x-component': 'ArrayCollapse.Addition', 549 | }, 550 | }, 551 | }, 552 | }, 553 | } 554 | 555 | export default () => { 556 | return ( 557 | <FormProvider form={form}> 558 | <SchemaField schema={schema} /> 559 | <FormButtonGroup> 560 | <Submit onSubmit={console.log}>提交</Submit> 561 | </FormButtonGroup> 562 | </FormProvider> 563 | ) 564 | } 565 | ``` 566 | 567 | ## API 568 | 569 | ### ArrayCollapse 570 | 571 | 参考 https://fusion.design/pc/component/collapse 572 | 573 | 扩展属性 574 | 575 | | 属性名 | 类型 | 描述 | 默认值 | 576 | | --------------------- | ------ | ------------------- | ------ | 577 | | defaultOpenPanelCount | number | 默认展开 Panel 数量 | 5 | 578 | 579 | ### ArrayCollapse.CollapsePanel 580 | 581 | 参考 https://fusion.design/pc/component/collapse 582 | 583 | ### ArrayCollapse.Addition 584 | 585 | > 添加按钮 586 | 587 | 扩展属性 588 | 589 | | 属性名 | 类型 | 描述 | 默认值 | 590 | | ------------ | --------------------- | -------- | -------- | 591 | | title | ReactText | 文案 | | 592 | | method | `'push' \| 'unshift'` | 添加方式 | `'push'` | 593 | | defaultValue | `any` | 默认值 | | 594 | 595 | 其余参考 https://fusion.design/pc/component/basic/button 596 | 597 | 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 598 | 599 | ### ArrayCollapse.Remove 600 | 601 | > 删除按钮 602 | 603 | | 属性名 | 类型 | 描述 | 默认值 | 604 | | ------ | --------- | ---- | ------ | 605 | | title | ReactText | 文案 | | 606 | 607 | 其余参考 https://ant.design/components/icon-cn/ 608 | 609 | 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 610 | 611 | ### ArrayCollapse.MoveDown 612 | 613 | > 下移按钮 614 | 615 | | 属性名 | 类型 | 描述 | 默认值 | 616 | | ------ | --------- | ---- | ------ | 617 | | title | ReactText | 文案 | | 618 | 619 | 其余参考 https://ant.design/components/icon-cn/ 620 | 621 | 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 622 | 623 | ### ArrayCollapse.MoveUp 624 | 625 | > 上移按钮 626 | 627 | | 属性名 | 类型 | 描述 | 默认值 | 628 | | ------ | --------- | ---- | ------ | 629 | | title | ReactText | 文案 | | 630 | 631 | 其余参考 https://ant.design/components/icon-cn/ 632 | 633 | 注意:title 属性可以接收 Field 模型中的 title 映射,也就是在 Field 上传 title 也是生效的 634 | 635 | ### ArrayCollapse.Index 636 | 637 | > 索引渲染器 638 | 639 | 无属性 640 | 641 | ### ArrayCollapse.useIndex 642 | 643 | > 读取当前渲染行索引的 React Hook 644 | 645 | ### ArrayCollapse.useRecord 646 | 647 | > 读取当前渲染记录的 React Hook 648 | ```