This is page 38 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/next/docs/components/ArrayItems.md: -------------------------------------------------------------------------------- ```markdown 1 | # ArrayItems 2 | 3 | > Self-increment list, suitable for simple self-increment editing scenes, or for scenes with high space requirements 4 | > 5 | > Note: This component is only applicable to Schema scenarios 6 | 7 | ## Markup Schema example 8 | 9 | ```tsx 10 | import React from 'react' 11 | import { 12 | FormItem, 13 | Input, 14 | Editable, 15 | Select, 16 | DatePicker, 17 | ArrayItems, 18 | FormButtonGroup, 19 | Submit, 20 | Space, 21 | } from '@formily/next' 22 | import { createForm } from '@formily/core' 23 | import { FormProvider, createSchemaField } from '@formily/react' 24 | 25 | const SchemaField = createSchemaField({ 26 | components: { 27 | FormItem, 28 | DatePicker, 29 | Editable, 30 | Space, 31 | Input, 32 | Select, 33 | ArrayItems, 34 | }, 35 | }) 36 | 37 | const form = createForm() 38 | 39 | export default () => { 40 | return ( 41 | <FormProvider form={form}> 42 | <SchemaField> 43 | <SchemaField.Array 44 | name="string_array" 45 | title="string array" 46 | x-decorator="FormItem" 47 | x-component="ArrayItems" 48 | > 49 | <SchemaField.Void x-component="Space"> 50 | <SchemaField.Void 51 | x-decorator="FormItem" 52 | x-component="ArrayItems.SortHandle" 53 | /> 54 | <SchemaField.String 55 | x-decorator="FormItem" 56 | required 57 | name="input" 58 | x-component="Input" 59 | /> 60 | <SchemaField.Void 61 | x-decorator="FormItem" 62 | x-component="ArrayItems.Remove" 63 | /> 64 | <SchemaField.Void 65 | x-decorator="FormItem" 66 | x-component="ArrayItems.Copy" 67 | /> 68 | </SchemaField.Void> 69 | <SchemaField.Void 70 | x-component="ArrayItems.Addition" 71 | title="Add entry" 72 | /> 73 | </SchemaField.Array> 74 | <SchemaField.Array 75 | name="array" 76 | title="Object array" 77 | x-decorator="FormItem" 78 | x-component="ArrayItems" 79 | > 80 | <SchemaField.Object> 81 | <SchemaField.Void x-component="Space"> 82 | <SchemaField.Void 83 | x-decorator="FormItem" 84 | x-component="ArrayItems.SortHandle" 85 | /> 86 | <SchemaField.String 87 | x-decorator="FormItem" 88 | required 89 | title="date" 90 | name="date" 91 | x-component="DatePicker.RangePicker" 92 | x-component-props={{ 93 | style: { 94 | width: 160, 95 | }, 96 | }} 97 | /> 98 | <SchemaField.String 99 | x-decorator="FormItem" 100 | required 101 | title="input box" 102 | name="input" 103 | x-component="Input" 104 | /> 105 | <SchemaField.String 106 | x-decorator="FormItem" 107 | required 108 | title="select box" 109 | name="select" 110 | enum={[ 111 | { label: 'Option 1', value: 1 }, 112 | { label: 'Option 2', value: 2 }, 113 | ]} 114 | x-component="Select" 115 | x-component-props={{ 116 | style: { 117 | width: 160, 118 | }, 119 | }} 120 | /> 121 | <SchemaField.Void 122 | x-decorator="FormItem" 123 | x-component="ArrayItems.Remove" 124 | /> 125 | <SchemaField.Void 126 | x-decorator="FormItem" 127 | x-component="ArrayItems.Copy" 128 | /> 129 | </SchemaField.Void> 130 | </SchemaField.Object> 131 | <SchemaField.Void 132 | x-component="ArrayItems.Addition" 133 | title="Add entry" 134 | /> 135 | </SchemaField.Array> 136 | <SchemaField.Array 137 | name="array2" 138 | title="Object array" 139 | x-decorator="FormItem" 140 | x-component="ArrayItems" 141 | x-component-props={{ style: { width: 300 } }} 142 | > 143 | <SchemaField.Object x-decorator="ArrayItems.Item"> 144 | <SchemaField.Void 145 | x-decorator="FormItem" 146 | x-component="ArrayItems.SortHandle" 147 | /> 148 | <SchemaField.String 149 | x-decorator="Editable" 150 | title="input box" 151 | name="input" 152 | x-component="Input" 153 | /> 154 | <SchemaField.Object 155 | name="config" 156 | x-component="Editable.Popover" 157 | required 158 | title="Configure complex data" 159 | x-reactions={(field) => 160 | (field.title = field.value?.input || field.title) 161 | } 162 | > 163 | <SchemaField.String 164 | x-decorator="FormItem" 165 | required 166 | title="date" 167 | name="date" 168 | x-component="DatePicker.RangePicker" 169 | x-component-props={{ 170 | style: { width: '100%' }, 171 | followTrigger: true, 172 | }} 173 | /> 174 | <SchemaField.String 175 | x-decorator="FormItem" 176 | required 177 | title="input box" 178 | name="input" 179 | x-component="Input" 180 | /> 181 | </SchemaField.Object> 182 | <SchemaField.Void 183 | x-decorator="FormItem" 184 | x-component="ArrayItems.Remove" 185 | /> 186 | </SchemaField.Object> 187 | <SchemaField.Void 188 | x-component="ArrayItems.Addition" 189 | title="Add entry" 190 | /> 191 | </SchemaField.Array> 192 | </SchemaField> 193 | <FormButtonGroup> 194 | <Submit onSubmit={console.log}>Submit</Submit> 195 | </FormButtonGroup> 196 | </FormProvider> 197 | ) 198 | } 199 | ``` 200 | 201 | ## JSON Schema case 202 | 203 | ```tsx 204 | import React from 'react' 205 | import { 206 | FormItem, 207 | Editable, 208 | Input, 209 | Select, 210 | Radio, 211 | DatePicker, 212 | ArrayItems, 213 | FormButtonGroup, 214 | Submit, 215 | Space, 216 | } from '@formily/next' 217 | import { createForm } from '@formily/core' 218 | import { FormProvider, createSchemaField } from '@formily/react' 219 | 220 | const SchemaField = createSchemaField({ 221 | components: { 222 | FormItem, 223 | Editable, 224 | DatePicker, 225 | Space, 226 | Radio, 227 | Input, 228 | Select, 229 | ArrayItems, 230 | }, 231 | }) 232 | 233 | const form = createForm() 234 | 235 | const schema = { 236 | type: 'object', 237 | properties: { 238 | string_array: { 239 | type: 'array', 240 | 'x-component': 'ArrayItems', 241 | 'x-decorator': 'FormItem', 242 | title: 'String array', 243 | items: { 244 | type: 'void', 245 | 'x-component': 'Space', 246 | properties: { 247 | sort: { 248 | type: 'void', 249 | 'x-decorator': 'FormItem', 250 | 'x-component': 'ArrayItems.SortHandle', 251 | }, 252 | input: { 253 | type: 'string', 254 | 'x-decorator': 'FormItem', 255 | 'x-component': 'Input', 256 | }, 257 | remove: { 258 | type: 'void', 259 | 'x-decorator': 'FormItem', 260 | 'x-component': 'ArrayItems.Remove', 261 | }, 262 | }, 263 | }, 264 | properties: { 265 | add: { 266 | type: 'void', 267 | title: 'Add entry', 268 | 'x-component': 'ArrayItems.Addition', 269 | }, 270 | }, 271 | }, 272 | array: { 273 | type: 'array', 274 | 'x-component': 'ArrayItems', 275 | 'x-decorator': 'FormItem', 276 | title: 'Object array', 277 | items: { 278 | type: 'object', 279 | properties: { 280 | space: { 281 | type: 'void', 282 | 'x-component': 'Space', 283 | properties: { 284 | sort: { 285 | type: 'void', 286 | 'x-decorator': 'FormItem', 287 | 'x-component': 'ArrayItems.SortHandle', 288 | }, 289 | date: { 290 | type: 'string', 291 | title: 'Date', 292 | 'x-decorator': 'FormItem', 293 | 'x-component': 'DatePicker.RangePicker', 294 | 'x-component-props': { 295 | style: { 296 | width: 160, 297 | }, 298 | }, 299 | }, 300 | input: { 301 | type: 'string', 302 | title: 'input box', 303 | 'x-decorator': 'FormItem', 304 | 'x-component': 'Input', 305 | }, 306 | select: { 307 | type: 'string', 308 | title: 'drop-down box', 309 | enum: [ 310 | { label: 'Option 1', value: 1 }, 311 | { label: 'Option 2', value: 2 }, 312 | ], 313 | 'x-decorator': 'FormItem', 314 | 'x-component': 'Select', 315 | 'x-component-props': { 316 | style: { 317 | width: 160, 318 | }, 319 | }, 320 | }, 321 | remove: { 322 | type: 'void', 323 | 'x-decorator': 'FormItem', 324 | 'x-component': 'ArrayItems.Remove', 325 | }, 326 | }, 327 | }, 328 | }, 329 | }, 330 | properties: { 331 | add: { 332 | type: 'void', 333 | title: 'Add entry', 334 | 'x-component': 'ArrayItems.Addition', 335 | }, 336 | }, 337 | }, 338 | array2: { 339 | type: 'array', 340 | 'x-component': 'ArrayItems', 341 | 'x-decorator': 'FormItem', 342 | 'x-component-props': { style: { width: 300 } }, 343 | title: 'Object array', 344 | items: { 345 | type: 'object', 346 | 'x-decorator': 'ArrayItems.Item', 347 | properties: { 348 | sort: { 349 | type: 'void', 350 | 'x-decorator': 'FormItem', 351 | 'x-component': 'ArrayItems.SortHandle', 352 | }, 353 | 354 | input: { 355 | type: 'string', 356 | title: 'input box', 357 | 'x-decorator': 'Editable', 358 | 'x-component': 'Input', 359 | }, 360 | config: { 361 | type: 'object', 362 | title: 'Configure complex data', 363 | 'x-component': 'Editable.Popover', 364 | 'x-reactions': 365 | '{{(field)=>field.title = field.value && field.value.input || field.title}}', 366 | properties: { 367 | date: { 368 | type: 'string', 369 | title: 'Date', 370 | 'x-decorator': 'FormItem', 371 | 'x-component': 'DatePicker.RangePicker', 372 | 'x-component-props': { 373 | style: { 374 | width: 160, 375 | }, 376 | followTrigger: true, 377 | }, 378 | }, 379 | input: { 380 | type: 'string', 381 | title: 'input box', 382 | 'x-decorator': 'FormItem', 383 | 'x-component': 'Input', 384 | }, 385 | select: { 386 | type: 'string', 387 | title: 'drop-down box', 388 | enum: [ 389 | { label: 'Option 1', value: 1 }, 390 | { label: 'Option 2', value: 2 }, 391 | ], 392 | 'x-decorator': 'FormItem', 393 | 'x-component': 'Select', 394 | 'x-component-props': { 395 | style: { 396 | width: 160, 397 | }, 398 | }, 399 | }, 400 | }, 401 | }, 402 | remove: { 403 | type: 'void', 404 | 'x-decorator': 'FormItem', 405 | 'x-component': 'ArrayItems.Remove', 406 | }, 407 | }, 408 | }, 409 | properties: { 410 | add: { 411 | type: 'void', 412 | title: 'Add entry', 413 | 'x-component': 'ArrayItems.Addition', 414 | }, 415 | }, 416 | }, 417 | }, 418 | } 419 | 420 | export default () => { 421 | return ( 422 | <FormProvider form={form}> 423 | <SchemaField schema={schema} /> 424 | <FormButtonGroup> 425 | <Submit onSubmit={console.log}>Submit</Submit> 426 | </FormButtonGroup> 427 | </FormProvider> 428 | ) 429 | } 430 | ``` 431 | 432 | ## Effects linkage case 433 | 434 | ```tsx 435 | import React from 'react' 436 | import { 437 | FormItem, 438 | Input, 439 | ArrayItems, 440 | Editable, 441 | FormButtonGroup, 442 | Submit, 443 | Space, 444 | } from '@formily/next' 445 | import { createForm, onFieldChange, onFieldReact } from '@formily/core' 446 | import { FormProvider, createSchemaField } from '@formily/react' 447 | 448 | const SchemaField = createSchemaField({ 449 | components: { 450 | Space, 451 | Editable, 452 | FormItem, 453 | Input, 454 | ArrayItems, 455 | }, 456 | }) 457 | 458 | const form = createForm({ 459 | effects: () => { 460 | //Active linkage mode 461 | onFieldChange('array.*.aa', ['value'], (field, form) => { 462 | form.setFieldState(field.query('.bb'), (state) => { 463 | state.visible = field.value != '123' 464 | }) 465 | }) 466 | //Passive linkage mode 467 | onFieldReact('array.*.dd', (field) => { 468 | field.visible = field.query('.cc').get('value') != '123' 469 | }) 470 | }, 471 | }) 472 | 473 | export default () => { 474 | return ( 475 | <FormProvider form={form}> 476 | <SchemaField> 477 | <SchemaField.Array 478 | name="array" 479 | title="Object array" 480 | maxItems={3} 481 | x-decorator="FormItem" 482 | x-component="ArrayItems" 483 | x-component-props={{ 484 | style: { 485 | width: 300, 486 | }, 487 | }} 488 | > 489 | <SchemaField.Object x-decorator="ArrayItems.Item"> 490 | <SchemaField.Void x-component="Space"> 491 | <SchemaField.Void 492 | x-decorator="FormItem" 493 | x-component="ArrayItems.SortHandle" 494 | /> 495 | <SchemaField.Void 496 | x-decorator="FormItem" 497 | x-component="ArrayItems.Index" 498 | /> 499 | </SchemaField.Void> 500 | <SchemaField.Void 501 | x-component="Editable.Popover" 502 | title="Configuration data" 503 | > 504 | <SchemaField.String 505 | name="aa" 506 | x-decorator="FormItem" 507 | title="AA" 508 | required 509 | description="AA hide BB when entering 123" 510 | x-component="Input" 511 | /> 512 | <SchemaField.String 513 | name="bb" 514 | x-decorator="FormItem" 515 | title="BB" 516 | required 517 | x-component="Input" 518 | /> 519 | <SchemaField.String 520 | name="cc" 521 | x-decorator="FormItem" 522 | title="CC" 523 | required 524 | description="Hide DD when CC enters 123" 525 | x-component="Input" 526 | /> 527 | <SchemaField.String 528 | name="dd" 529 | x-decorator="FormItem" 530 | title="DD" 531 | required 532 | x-component="Input" 533 | /> 534 | </SchemaField.Void> 535 | <SchemaField.Void x-component="Space"> 536 | <SchemaField.Void 537 | x-decorator="FormItem" 538 | x-component="ArrayItems.Remove" 539 | /> 540 | <SchemaField.Void 541 | x-decorator="FormItem" 542 | x-component="ArrayItems.MoveUp" 543 | /> 544 | <SchemaField.Void 545 | x-decorator="FormItem" 546 | x-component="ArrayItems.MoveDown" 547 | /> 548 | </SchemaField.Void> 549 | </SchemaField.Object> 550 | <SchemaField.Void 551 | x-component="ArrayItems.Addition" 552 | title="Add entry" 553 | /> 554 | </SchemaField.Array> 555 | </SchemaField> 556 | <FormButtonGroup> 557 | <Submit onSubmit={console.log}>Submit</Submit> 558 | </FormButtonGroup> 559 | </FormProvider> 560 | ) 561 | } 562 | ``` 563 | 564 | ## JSON Schema linkage case 565 | 566 | ```tsx 567 | import React from 'react' 568 | import { 569 | FormItem, 570 | Input, 571 | ArrayItems, 572 | Editable, 573 | FormButtonGroup, 574 | Submit, 575 | Space, 576 | } from '@formily/next' 577 | import { createForm } from '@formily/core' 578 | import { FormProvider, createSchemaField } from '@formily/react' 579 | 580 | const SchemaField = createSchemaField({ 581 | components: { 582 | Space, 583 | Editable, 584 | FormItem, 585 | Input, 586 | ArrayItems, 587 | }, 588 | }) 589 | 590 | const form = createForm() 591 | 592 | const schema = { 593 | type: 'object', 594 | properties: { 595 | array: { 596 | type: 'array', 597 | 'x-component': 'ArrayItems', 598 | 'x-decorator': 'FormItem', 599 | maxItems: 3, 600 | title: 'Object array', 601 | 'x-component-props': { style: { width: 300 } }, 602 | items: { 603 | type: 'object', 604 | 'x-decorator': 'ArrayItems.Item', 605 | properties: { 606 | left: { 607 | type: 'void', 608 | 'x-component': 'Space', 609 | properties: { 610 | sort: { 611 | type: 'void', 612 | 'x-decorator': 'FormItem', 613 | 'x-component': 'ArrayItems.SortHandle', 614 | }, 615 | index: { 616 | type: 'void', 617 | 'x-decorator': 'FormItem', 618 | 'x-component': 'ArrayItems.Index', 619 | }, 620 | }, 621 | }, 622 | edit: { 623 | type: 'void', 624 | 'x-component': 'Editable.Popover', 625 | title: 'Configuration data', 626 | properties: { 627 | aa: { 628 | type: 'string', 629 | 'x-decorator': 'FormItem', 630 | title: 'AA', 631 | required: true, 632 | 'x-component': 'Input', 633 | description: 'Enter 123', 634 | }, 635 | bb: { 636 | type: 'string', 637 | title: 'BB', 638 | required: true, 639 | 'x-decorator': 'FormItem', 640 | 'x-component': 'Input', 641 | 'x-reactions': [ 642 | { 643 | dependencies: ['.aa'], 644 | when: "{{$deps[0] != '123'}}", 645 | fulfill: { 646 | schema: { 647 | title: 'BB', 648 | 'x-disabled': true, 649 | }, 650 | }, 651 | otherwise: { 652 | schema: { 653 | title: 'Changed', 654 | 'x-disabled': false, 655 | }, 656 | }, 657 | }, 658 | ], 659 | }, 660 | }, 661 | }, 662 | right: { 663 | type: 'void', 664 | 'x-component': 'Space', 665 | properties: { 666 | remove: { 667 | type: 'void', 668 | 'x-component': 'ArrayItems.Remove', 669 | }, 670 | moveUp: { 671 | type: 'void', 672 | 'x-component': 'ArrayItems.MoveUp', 673 | }, 674 | moveDown: { 675 | type: 'void', 676 | 'x-component': 'ArrayItems.MoveDown', 677 | }, 678 | }, 679 | }, 680 | }, 681 | }, 682 | properties: { 683 | addition: { 684 | type: 'void', 685 | title: 'Add entry', 686 | 'x-component': 'ArrayItems.Addition', 687 | }, 688 | }, 689 | }, 690 | }, 691 | } 692 | 693 | export default () => { 694 | return ( 695 | <FormProvider form={form}> 696 | <SchemaField schema={schema} /> 697 | <FormButtonGroup> 698 | <Submit onSubmit={console.log}>Submit</Submit> 699 | </FormButtonGroup> 700 | </FormProvider> 701 | ) 702 | } 703 | ``` 704 | 705 | ## API 706 | 707 | ### ArrayItems 708 | 709 | Extended attributes 710 | 711 | | Property name | Type | Description | Default value | 712 | | ------------- | ------------------------- | --------------- | ------------- | 713 | | onAdd | `(index: number) => void` | add method | | 714 | | onRemove | `(index: number) => void` | remove method | | 715 | | onCopy | `(index: number) => void` | copy method | | 716 | | onMoveUp | `(index: number) => void` | moveUp method | | 717 | | onMoveDown | `(index: number) => void` | moveDown method | | 718 | 719 | Other Inherit HTMLDivElement Props 720 | 721 | ### ArrayItems.Item 722 | 723 | > List block 724 | 725 | Inherit HTMLDivElement Props 726 | 727 | Extended attributes 728 | 729 | | Property name | Type | Description | Default value | 730 | | ------------- | ------------------- | --------------------- | ------------- | 731 | | type | `'card' \|'divide'` | card or dividing line | | 732 | 733 | ### ArrayItems.SortHandle 734 | 735 | > Drag handle 736 | 737 | Reference https://ant.design/components/icon-cn/ 738 | 739 | ### ArrayItems.Addition 740 | 741 | > Add button 742 | 743 | Extended attributes 744 | 745 | | Property name | Type | Description | Default value | 746 | | ------------- | -------------------- | ------------- | ------------- | 747 | | title | ReactText | Copywriting | | 748 | | method | `'push' \|'unshift'` | add method | `'push'` | 749 | | defaultValue | `any` | Default value | | 750 | 751 | Other references https://fusion.design/pc/component/basic/button 752 | 753 | Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective 754 | 755 | ### ArrayItems.Copy 756 | 757 | > Copy button 758 | 759 | Extended attributes 760 | 761 | | Property name | Type | Description | Default value | 762 | | ------------- | -------------------- | ----------- | ------------- | 763 | | title | ReactText | Copywriting | | 764 | | method | `'push' \|'unshift'` | add method | `'push'` | 765 | 766 | Other references https://fusion.design/pc/component/basic/button 767 | 768 | Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective 769 | 770 | ### ArrayItems.Remove 771 | 772 | > Delete button 773 | 774 | | Property name | Type | Description | Default value | 775 | | ------------- | --------- | ----------- | ------------- | 776 | | title | ReactText | Copywriting | | 777 | 778 | Other references https://ant.design/components/icon-cn/ 779 | 780 | Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective 781 | 782 | ### ArrayItems.MoveDown 783 | 784 | > Move down button 785 | 786 | | Property name | Type | Description | Default value | 787 | | ------------- | --------- | ----------- | ------------- | 788 | | title | ReactText | Copywriting | | 789 | 790 | Other references https://ant.design/components/icon-cn/ 791 | 792 | Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective 793 | 794 | ### ArrayItems.MoveUp 795 | 796 | > Move up button 797 | 798 | | Property name | Type | Description | Default value | 799 | | ------------- | --------- | ----------- | ------------- | 800 | | title | ReactText | Copywriting | | 801 | 802 | Other references https://ant.design/components/icon-cn/ 803 | 804 | Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective 805 | 806 | ### ArrayItems.Index 807 | 808 | > Index Renderer 809 | 810 | No attributes 811 | 812 | ### ArrayItems.useIndex 813 | 814 | > Read the React Hook of the current rendering row index 815 | 816 | ### ArrayItems.useRecord 817 | 818 | > Read the React Hook of the current rendering row 819 | ``` -------------------------------------------------------------------------------- /packages/antd/docs/components/ArrayItems.md: -------------------------------------------------------------------------------- ```markdown 1 | # ArrayItems 2 | 3 | > Self-increment list, suitable for simple self-increment editing scenes, or for scenes with high space requirements 4 | > 5 | > Note: This component is only applicable to Schema scenarios 6 | 7 | ## Markup Schema example 8 | 9 | ```tsx 10 | import React from 'react' 11 | import { 12 | FormItem, 13 | Input, 14 | Editable, 15 | Select, 16 | DatePicker, 17 | ArrayItems, 18 | FormButtonGroup, 19 | Submit, 20 | Space, 21 | } from '@formily/antd' 22 | import { createForm } from '@formily/core' 23 | import { FormProvider, createSchemaField } from '@formily/react' 24 | 25 | const SchemaField = createSchemaField({ 26 | components: { 27 | FormItem, 28 | DatePicker, 29 | Editable, 30 | Space, 31 | Input, 32 | Select, 33 | ArrayItems, 34 | }, 35 | }) 36 | 37 | const form = createForm() 38 | 39 | export default () => { 40 | return ( 41 | <FormProvider form={form}> 42 | <SchemaField> 43 | <SchemaField.Array 44 | name="string_array" 45 | title="string array" 46 | x-decorator="FormItem" 47 | x-component="ArrayItems" 48 | > 49 | <SchemaField.Void x-component="Space"> 50 | <SchemaField.Void 51 | x-decorator="FormItem" 52 | x-component="ArrayItems.SortHandle" 53 | /> 54 | <SchemaField.String 55 | x-decorator="FormItem" 56 | required 57 | name="input" 58 | x-component="Input" 59 | /> 60 | <SchemaField.Void 61 | x-decorator="FormItem" 62 | x-component="ArrayItems.Remove" 63 | /> 64 | <SchemaField.Void 65 | x-decorator="FormItem" 66 | x-component="ArrayItems.Copy" 67 | /> 68 | </SchemaField.Void> 69 | <SchemaField.Void 70 | x-component="ArrayItems.Addition" 71 | title="Add entry" 72 | /> 73 | </SchemaField.Array> 74 | <SchemaField.Array 75 | name="array" 76 | title="Object array" 77 | x-decorator="FormItem" 78 | x-component="ArrayItems" 79 | > 80 | <SchemaField.Object> 81 | <SchemaField.Void x-component="Space"> 82 | <SchemaField.Void 83 | x-decorator="FormItem" 84 | x-component="ArrayItems.SortHandle" 85 | /> 86 | <SchemaField.String 87 | x-decorator="FormItem" 88 | required 89 | title="date" 90 | name="date" 91 | x-component="DatePicker.RangePicker" 92 | x-component-props={{ 93 | style: { 94 | width: 160, 95 | }, 96 | }} 97 | /> 98 | <SchemaField.String 99 | x-decorator="FormItem" 100 | required 101 | title="input box" 102 | name="input" 103 | x-component="Input" 104 | /> 105 | <SchemaField.String 106 | x-decorator="FormItem" 107 | required 108 | title="select box" 109 | name="select" 110 | enum={[ 111 | { label: 'Option 1', value: 1 }, 112 | { label: 'Option 2', value: 2 }, 113 | ]} 114 | x-component="Select" 115 | x-component-props={{ 116 | style: { 117 | width: 160, 118 | }, 119 | }} 120 | /> 121 | <SchemaField.Void 122 | x-decorator="FormItem" 123 | x-component="ArrayItems.Remove" 124 | /> 125 | </SchemaField.Void> 126 | </SchemaField.Object> 127 | <SchemaField.Void 128 | x-component="ArrayItems.Addition" 129 | title="Add entry" 130 | /> 131 | </SchemaField.Array> 132 | <SchemaField.Array 133 | name="array2" 134 | title="Object array" 135 | x-decorator="FormItem" 136 | x-component="ArrayItems" 137 | x-component-props={{ style: { width: 300 } }} 138 | > 139 | <SchemaField.Object x-decorator="ArrayItems.Item"> 140 | <SchemaField.Void 141 | x-decorator="FormItem" 142 | x-component="ArrayItems.SortHandle" 143 | /> 144 | <SchemaField.String 145 | x-decorator="Editable" 146 | title="input box" 147 | name="input" 148 | x-component="Input" 149 | x-component-props={{ bordered: false }} 150 | /> 151 | <SchemaField.Object 152 | name="config" 153 | x-component="Editable.Popover" 154 | required 155 | title="Configure complex data" 156 | x-reactions={(field) => { 157 | field.title = field.value?.input || field.title 158 | }} 159 | > 160 | <SchemaField.String 161 | x-decorator="FormItem" 162 | required 163 | title="date" 164 | name="date" 165 | x-component="DatePicker.RangePicker" 166 | x-component-props={{ style: { width: '100%' } }} 167 | /> 168 | <SchemaField.String 169 | x-decorator="FormItem" 170 | required 171 | title="input box" 172 | name="input" 173 | x-component="Input" 174 | /> 175 | </SchemaField.Object> 176 | <SchemaField.Void 177 | x-decorator="FormItem" 178 | x-component="ArrayItems.Remove" 179 | /> 180 | </SchemaField.Object> 181 | <SchemaField.Void 182 | x-component="ArrayItems.Addition" 183 | title="Add entry" 184 | /> 185 | </SchemaField.Array> 186 | </SchemaField> 187 | <FormButtonGroup> 188 | <Submit onSubmit={console.log}>Submit</Submit> 189 | </FormButtonGroup> 190 | </FormProvider> 191 | ) 192 | } 193 | ``` 194 | 195 | ## JSON Schema case 196 | 197 | ```tsx 198 | import React from 'react' 199 | import { 200 | FormItem, 201 | Editable, 202 | Input, 203 | Select, 204 | Radio, 205 | DatePicker, 206 | ArrayItems, 207 | FormButtonGroup, 208 | Submit, 209 | Space, 210 | } from '@formily/antd' 211 | import { createForm } from '@formily/core' 212 | import { FormProvider, createSchemaField } from '@formily/react' 213 | 214 | const SchemaField = createSchemaField({ 215 | components: { 216 | FormItem, 217 | Editable, 218 | DatePicker, 219 | Space, 220 | Radio, 221 | Input, 222 | Select, 223 | ArrayItems, 224 | }, 225 | }) 226 | 227 | const form = createForm() 228 | 229 | const schema = { 230 | type: 'object', 231 | properties: { 232 | string_array: { 233 | type: 'array', 234 | 'x-component': 'ArrayItems', 235 | 'x-decorator': 'FormItem', 236 | title: 'String array', 237 | items: { 238 | type: 'void', 239 | 'x-component': 'Space', 240 | properties: { 241 | sort: { 242 | type: 'void', 243 | 'x-decorator': 'FormItem', 244 | 'x-component': 'ArrayItems.SortHandle', 245 | }, 246 | input: { 247 | type: 'string', 248 | 'x-decorator': 'FormItem', 249 | 'x-component': 'Input', 250 | }, 251 | remove: { 252 | type: 'void', 253 | 'x-decorator': 'FormItem', 254 | 'x-component': 'ArrayItems.Remove', 255 | }, 256 | }, 257 | }, 258 | properties: { 259 | add: { 260 | type: 'void', 261 | title: 'Add entry', 262 | 'x-component': 'ArrayItems.Addition', 263 | }, 264 | }, 265 | }, 266 | array: { 267 | type: 'array', 268 | 'x-component': 'ArrayItems', 269 | 'x-decorator': 'FormItem', 270 | title: 'Object array', 271 | items: { 272 | type: 'object', 273 | properties: { 274 | space: { 275 | type: 'void', 276 | 'x-component': 'Space', 277 | properties: { 278 | sort: { 279 | type: 'void', 280 | 'x-decorator': 'FormItem', 281 | 'x-component': 'ArrayItems.SortHandle', 282 | }, 283 | date: { 284 | type: 'string', 285 | title: 'Date', 286 | 'x-decorator': 'FormItem', 287 | 'x-component': 'DatePicker.RangePicker', 288 | 'x-component-props': { 289 | style: { 290 | width: 160, 291 | }, 292 | }, 293 | }, 294 | input: { 295 | type: 'string', 296 | title: 'input box', 297 | 'x-decorator': 'FormItem', 298 | 'x-component': 'Input', 299 | }, 300 | select: { 301 | type: 'string', 302 | title: 'drop-down box', 303 | enum: [ 304 | { label: 'Option 1', value: 1 }, 305 | { label: 'Option 2', value: 2 }, 306 | ], 307 | 'x-decorator': 'FormItem', 308 | 'x-component': 'Select', 309 | 'x-component-props': { 310 | style: { 311 | width: 160, 312 | }, 313 | }, 314 | }, 315 | remove: { 316 | type: 'void', 317 | 'x-decorator': 'FormItem', 318 | 'x-component': 'ArrayItems.Remove', 319 | }, 320 | }, 321 | }, 322 | }, 323 | }, 324 | properties: { 325 | add: { 326 | type: 'void', 327 | title: 'Add entry', 328 | 'x-component': 'ArrayItems.Addition', 329 | }, 330 | }, 331 | }, 332 | array2: { 333 | type: 'array', 334 | 'x-component': 'ArrayItems', 335 | 'x-decorator': 'FormItem', 336 | 'x-component-props': { style: { width: 300 } }, 337 | title: 'Object array', 338 | items: { 339 | type: 'object', 340 | 'x-decorator': 'ArrayItems.Item', 341 | properties: { 342 | sort: { 343 | type: 'void', 344 | 'x-decorator': 'FormItem', 345 | 'x-component': 'ArrayItems.SortHandle', 346 | }, 347 | 348 | input: { 349 | type: 'string', 350 | title: 'input box', 351 | 'x-decorator': 'Editable', 352 | 'x-component': 'Input', 353 | 'x-component-props': { 354 | bordered: false, 355 | }, 356 | }, 357 | config: { 358 | type: 'object', 359 | title: 'Configure complex data', 360 | 'x-component': 'Editable.Popover', 361 | 'x-reactions': 362 | '{{(field)=>field.title = field.value && field.value.input || field.title}}', 363 | properties: { 364 | date: { 365 | type: 'string', 366 | title: 'Date', 367 | 'x-decorator': 'FormItem', 368 | 'x-component': 'DatePicker.RangePicker', 369 | 'x-component-props': { 370 | style: { 371 | width: 160, 372 | }, 373 | }, 374 | }, 375 | input: { 376 | type: 'string', 377 | title: 'input box', 378 | 'x-decorator': 'FormItem', 379 | 'x-component': 'Input', 380 | }, 381 | select: { 382 | type: 'string', 383 | title: 'drop-down box', 384 | enum: [ 385 | { label: 'Option 1', value: 1 }, 386 | { label: 'Option 2', value: 2 }, 387 | ], 388 | 'x-decorator': 'FormItem', 389 | 'x-component': 'Select', 390 | 'x-component-props': { 391 | style: { 392 | width: 160, 393 | }, 394 | }, 395 | }, 396 | }, 397 | }, 398 | remove: { 399 | type: 'void', 400 | 'x-decorator': 'FormItem', 401 | 'x-component': 'ArrayItems.Remove', 402 | }, 403 | }, 404 | }, 405 | properties: { 406 | add: { 407 | type: 'void', 408 | title: 'Add entry', 409 | 'x-component': 'ArrayItems.Addition', 410 | }, 411 | }, 412 | }, 413 | }, 414 | } 415 | 416 | export default () => { 417 | return ( 418 | <FormProvider form={form}> 419 | <SchemaField schema={schema} /> 420 | <FormButtonGroup> 421 | <Submit onSubmit={console.log}>Submit</Submit> 422 | </FormButtonGroup> 423 | </FormProvider> 424 | ) 425 | } 426 | ``` 427 | 428 | ## Effects linkage case 429 | 430 | ```tsx 431 | import React from 'react' 432 | import { 433 | FormItem, 434 | Input, 435 | ArrayItems, 436 | Editable, 437 | FormButtonGroup, 438 | Submit, 439 | Space, 440 | } from '@formily/antd' 441 | import { createForm, onFieldChange, onFieldReact } from '@formily/core' 442 | import { FormProvider, createSchemaField } from '@formily/react' 443 | 444 | const SchemaField = createSchemaField({ 445 | components: { 446 | Space, 447 | Editable, 448 | FormItem, 449 | Input, 450 | ArrayItems, 451 | }, 452 | }) 453 | 454 | const form = createForm({ 455 | effects: () => { 456 | //Active linkage mode 457 | onFieldChange('array.*.aa', ['value'], (field, form) => { 458 | form.setFieldState(field.query('.bb'), (state) => { 459 | state.visible = field.value != '123' 460 | }) 461 | }) 462 | //Passive linkage mode 463 | onFieldReact('array.*.dd', (field) => { 464 | field.visible = field.query('.cc').get('value') != '123' 465 | }) 466 | }, 467 | }) 468 | 469 | export default () => { 470 | return ( 471 | <FormProvider form={form}> 472 | <SchemaField> 473 | <SchemaField.Array 474 | name="array" 475 | title="Object array" 476 | maxItems={3} 477 | x-decorator="FormItem" 478 | x-component="ArrayItems" 479 | x-component-props={{ 480 | style: { 481 | width: 300, 482 | }, 483 | }} 484 | > 485 | <SchemaField.Object x-decorator="ArrayItems.Item"> 486 | <SchemaField.Void x-component="Space"> 487 | <SchemaField.Void 488 | x-decorator="FormItem" 489 | x-component="ArrayItems.SortHandle" 490 | /> 491 | <SchemaField.Void 492 | x-decorator="FormItem" 493 | x-component="ArrayItems.Index" 494 | /> 495 | </SchemaField.Void> 496 | <SchemaField.Void 497 | x-component="Editable.Popover" 498 | title="Configuration data" 499 | > 500 | <SchemaField.String 501 | name="aa" 502 | x-decorator="FormItem" 503 | title="AA" 504 | required 505 | description="AA hide BB when entering 123" 506 | x-component="Input" 507 | /> 508 | <SchemaField.String 509 | name="bb" 510 | x-decorator="FormItem" 511 | title="BB" 512 | required 513 | x-component="Input" 514 | /> 515 | <SchemaField.String 516 | name="cc" 517 | x-decorator="FormItem" 518 | title="CC" 519 | required 520 | description="Hide DD when CC enters 123" 521 | x-component="Input" 522 | /> 523 | <SchemaField.String 524 | name="dd" 525 | x-decorator="FormItem" 526 | title="DD" 527 | required 528 | x-component="Input" 529 | /> 530 | </SchemaField.Void> 531 | <SchemaField.Void x-component="Space"> 532 | <SchemaField.Void 533 | x-decorator="FormItem" 534 | x-component="ArrayItems.Remove" 535 | /> 536 | <SchemaField.Void 537 | x-decorator="FormItem" 538 | x-component="ArrayItems.MoveUp" 539 | /> 540 | <SchemaField.Void 541 | x-decorator="FormItem" 542 | x-component="ArrayItems.MoveDown" 543 | /> 544 | </SchemaField.Void> 545 | </SchemaField.Object> 546 | <SchemaField.Void 547 | x-component="ArrayItems.Addition" 548 | title="Add entry" 549 | /> 550 | </SchemaField.Array> 551 | </SchemaField> 552 | <FormButtonGroup> 553 | <Submit onSubmit={console.log}>Submit</Submit> 554 | </FormButtonGroup> 555 | </FormProvider> 556 | ) 557 | } 558 | ``` 559 | 560 | ## JSON Schema linkage case 561 | 562 | ```tsx 563 | import React from 'react' 564 | import { 565 | FormItem, 566 | Input, 567 | ArrayItems, 568 | Editable, 569 | FormButtonGroup, 570 | Submit, 571 | Space, 572 | } from '@formily/antd' 573 | import { createForm } from '@formily/core' 574 | import { FormProvider, createSchemaField } from '@formily/react' 575 | 576 | const SchemaField = createSchemaField({ 577 | components: { 578 | Space, 579 | Editable, 580 | FormItem, 581 | Input, 582 | ArrayItems, 583 | }, 584 | }) 585 | 586 | const form = createForm() 587 | 588 | const schema = { 589 | type: 'object', 590 | properties: { 591 | array: { 592 | type: 'array', 593 | 'x-component': 'ArrayItems', 594 | 'x-decorator': 'FormItem', 595 | maxItems: 3, 596 | title: 'Object array', 597 | 'x-component-props': { style: { width: 300 } }, 598 | items: { 599 | type: 'object', 600 | 'x-decorator': 'ArrayItems.Item', 601 | properties: { 602 | left: { 603 | type: 'void', 604 | 'x-component': 'Space', 605 | properties: { 606 | sort: { 607 | type: 'void', 608 | 'x-decorator': 'FormItem', 609 | 'x-component': 'ArrayItems.SortHandle', 610 | }, 611 | index: { 612 | type: 'void', 613 | 'x-decorator': 'FormItem', 614 | 'x-component': 'ArrayItems.Index', 615 | }, 616 | }, 617 | }, 618 | edit: { 619 | type: 'void', 620 | 'x-component': 'Editable.Popover', 621 | title: 'Configuration data', 622 | properties: { 623 | aa: { 624 | type: 'string', 625 | 'x-decorator': 'FormItem', 626 | title: 'AA', 627 | required: true, 628 | 'x-component': 'Input', 629 | description: 'Enter 123', 630 | }, 631 | bb: { 632 | type: 'string', 633 | title: 'BB', 634 | required: true, 635 | 'x-decorator': 'FormItem', 636 | 'x-component': 'Input', 637 | 'x-reactions': [ 638 | { 639 | dependencies: ['.aa'], 640 | when: "{{$deps[0] != '123'}}", 641 | fulfill: { 642 | schema: { 643 | title: 'BB', 644 | 'x-disabled': true, 645 | }, 646 | }, 647 | otherwise: { 648 | schema: { 649 | title: 'Changed', 650 | 'x-disabled': false, 651 | }, 652 | }, 653 | }, 654 | ], 655 | }, 656 | }, 657 | }, 658 | right: { 659 | type: 'void', 660 | 'x-component': 'Space', 661 | properties: { 662 | remove: { 663 | type: 'void', 664 | 'x-component': 'ArrayItems.Remove', 665 | }, 666 | moveUp: { 667 | type: 'void', 668 | 'x-component': 'ArrayItems.MoveUp', 669 | }, 670 | moveDown: { 671 | type: 'void', 672 | 'x-component': 'ArrayItems.MoveDown', 673 | }, 674 | }, 675 | }, 676 | }, 677 | }, 678 | properties: { 679 | addition: { 680 | type: 'void', 681 | title: 'Add entry', 682 | 'x-component': 'ArrayItems.Addition', 683 | }, 684 | }, 685 | }, 686 | }, 687 | } 688 | 689 | export default () => { 690 | return ( 691 | <FormProvider form={form}> 692 | <SchemaField schema={schema} /> 693 | <FormButtonGroup> 694 | <Submit onSubmit={console.log}>Submit</Submit> 695 | </FormButtonGroup> 696 | </FormProvider> 697 | ) 698 | } 699 | ``` 700 | 701 | ## API 702 | 703 | ### ArrayItems 704 | 705 | Extended attributes 706 | 707 | | Property name | Type | Description | Default value | 708 | | ------------- | ------------------------- | --------------- | ------------- | 709 | | onAdd | `(index: number) => void` | add method | | 710 | | onRemove | `(index: number) => void` | remove method | | 711 | | onCopy | `(index: number) => void` | copy method | | 712 | | onMoveUp | `(index: number) => void` | moveUp method | | 713 | | onMoveDown | `(index: number) => void` | moveDown method | | 714 | 715 | Other Inherit HTMLDivElement Props 716 | 717 | ### ArrayItems.Item 718 | 719 | > List block 720 | 721 | Inherit HTMLDivElement Props 722 | 723 | Extended attributes 724 | 725 | | Property name | Type | Description | Default value | 726 | | ------------- | ------------------- | --------------------- | ------------- | 727 | | type | `'card' \|'divide'` | card or dividing line | | 728 | 729 | ### ArrayItems.SortHandle 730 | 731 | > Drag handle 732 | 733 | Reference https://ant.design/components/icon-cn/ 734 | 735 | ### ArrayItems.Addition 736 | 737 | > Add button 738 | 739 | Extended attributes 740 | 741 | | Property name | Type | Description | Default value | 742 | | ------------- | -------------------- | ------------- | ------------- | 743 | | title | ReactText | Copywriting | | 744 | | method | `'push' \|'unshift'` | add method | `'push'` | 745 | | defaultValue | `any` | Default value | | 746 | 747 | Other references https://ant.design/components/button-cn/ 748 | 749 | Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective 750 | 751 | Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. 752 | 753 | ### ArrayItems.Copy 754 | 755 | > Copy button 756 | 757 | Extended attributes 758 | 759 | | Property name | Type | Description | Default value | 760 | | ------------- | -------------------- | ----------- | ------------- | 761 | | title | ReactText | Copywriting | | 762 | | method | `'push' \|'unshift'` | Copy method | `'push'` | 763 | 764 | Other references https://ant.design/components/button-cn/ 765 | 766 | Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective 767 | 768 | Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. 769 | 770 | ### ArrayItems.Remove 771 | 772 | > Delete button 773 | 774 | | Property name | Type | Description | Default value | 775 | | ------------- | --------- | ----------- | ------------- | 776 | | title | ReactText | Copywriting | | 777 | 778 | Other references https://ant.design/components/icon-cn/ 779 | 780 | Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective 781 | 782 | Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. 783 | 784 | ### ArrayItems.MoveDown 785 | 786 | > Move down button 787 | 788 | | Property name | Type | Description | Default value | 789 | | ------------- | --------- | ----------- | ------------- | 790 | | title | ReactText | Copywriting | | 791 | 792 | Other references https://ant.design/components/icon-cn/ 793 | 794 | Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective 795 | 796 | Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. 797 | 798 | ### ArrayItems.MoveUp 799 | 800 | > Move up button 801 | 802 | | Property name | Type | Description | Default value | 803 | | ------------- | --------- | ----------- | ------------- | 804 | | title | ReactText | Copywriting | | 805 | 806 | Other references https://ant.design/components/icon-cn/ 807 | 808 | Note: The title attribute can receive the title mapping in the Field model, that is, uploading the title in the Field is also effective 809 | 810 | Note: You can disable default behavior with `onClick={e => e.preventDefault()}` in props. 811 | 812 | ### ArrayItems.Index 813 | 814 | > Index Renderer 815 | 816 | No attributes 817 | 818 | ### ArrayItems.useIndex 819 | 820 | > Read the React Hook of the current rendering row index 821 | 822 | ### ArrayItems.useRecord 823 | 824 | > Read the React Hook of the current rendering row 825 | ``` -------------------------------------------------------------------------------- /packages/core/src/__tests__/array.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { createForm } from '../' 2 | import { 3 | onFieldValueChange, 4 | onFormInitialValuesChange, 5 | onFormValuesChange, 6 | } from '../effects' 7 | import { DataField } from '../types' 8 | import { attach } from './shared' 9 | 10 | test('create array field', () => { 11 | const form = attach(createForm()) 12 | const array = attach( 13 | form.createArrayField({ 14 | name: 'array', 15 | }) 16 | ) 17 | expect(array.value).toEqual([]) 18 | expect(array.push).toBeDefined() 19 | expect(array.pop).toBeDefined() 20 | expect(array.shift).toBeDefined() 21 | expect(array.unshift).toBeDefined() 22 | expect(array.move).toBeDefined() 23 | expect(array.moveDown).toBeDefined() 24 | expect(array.moveUp).toBeDefined() 25 | expect(array.insert).toBeDefined() 26 | expect(array.remove).toBeDefined() 27 | }) 28 | 29 | test('array field methods', () => { 30 | const form = attach(createForm()) 31 | const array = attach( 32 | form.createArrayField({ 33 | name: 'array', 34 | value: [], 35 | }) 36 | ) 37 | array.push({ aa: 11 }, { bb: 22 }) 38 | expect(array.value).toEqual([{ aa: 11 }, { bb: 22 }]) 39 | array.pop() 40 | expect(array.value).toEqual([{ aa: 11 }]) 41 | array.unshift({ cc: 33 }) 42 | expect(array.value).toEqual([{ cc: 33 }, { aa: 11 }]) 43 | array.remove(1) 44 | expect(array.value).toEqual([{ cc: 33 }]) 45 | array.insert(1, { dd: 44 }, { ee: 55 }) 46 | expect(array.value).toEqual([{ cc: 33 }, { dd: 44 }, { ee: 55 }]) 47 | array.move(0, 2) 48 | expect(array.value).toEqual([{ dd: 44 }, { ee: 55 }, { cc: 33 }]) 49 | array.shift() 50 | expect(array.value).toEqual([{ ee: 55 }, { cc: 33 }]) 51 | array.moveDown(0) 52 | expect(array.value).toEqual([{ cc: 33 }, { ee: 55 }]) 53 | array.moveUp(1) 54 | expect(array.value).toEqual([{ ee: 55 }, { cc: 33 }]) 55 | array.move(1, 0) 56 | expect(array.value).toEqual([{ cc: 33 }, { ee: 55 }]) 57 | }) 58 | 59 | test('array field children state exchanges', () => { 60 | //注意:插入新节点,如果指定位置有节点,会丢弃,需要重新插入节点,主要是为了防止上一个节点状态对新节点状态产生污染 61 | const form = attach(createForm()) 62 | const array = attach( 63 | form.createArrayField({ 64 | name: 'array', 65 | }) 66 | ) 67 | attach( 68 | form.createField({ 69 | name: 'other', 70 | basePath: 'array', 71 | }) 72 | ) 73 | array.push({ value: 11 }, { value: 22 }) 74 | attach( 75 | form.createField({ 76 | name: 'value', 77 | basePath: 'array.0', 78 | }) 79 | ) 80 | attach( 81 | form.createField({ 82 | name: 'value', 83 | basePath: 'array.1', 84 | }) 85 | ) 86 | expect(array.value).toEqual([{ value: 11 }, { value: 22 }]) 87 | expect(form.query('array.0.value').get('value')).toEqual(11) 88 | expect(form.query('array.1.value').get('value')).toEqual(22) 89 | expect(Object.keys(form.fields).sort()).toEqual([ 90 | 'array', 91 | 'array.0.value', 92 | 'array.1.value', 93 | 'array.other', 94 | ]) 95 | array.pop() 96 | expect(array.value).toEqual([{ value: 11 }]) 97 | expect(form.query('array.0.value').get('value')).toEqual(11) 98 | expect(form.query('array.1.value').get('value')).toBeUndefined() 99 | array.unshift({ value: 33 }) 100 | attach( 101 | form.createField({ 102 | name: 'value', 103 | basePath: 'array.0', 104 | }) 105 | ) 106 | attach( 107 | form.createField({ 108 | name: 'value', 109 | basePath: 'array.1', 110 | }) 111 | ) 112 | expect(array.value).toEqual([{ value: 33 }, { value: 11 }]) 113 | expect(form.query('array.0.value').get('value')).toEqual(33) 114 | expect(form.query('array.1.value').get('value')).toEqual(11) 115 | array.remove(1) 116 | expect(array.value).toEqual([{ value: 33 }]) 117 | expect(form.query('array.0.value').get('value')).toEqual(33) 118 | expect(form.query('array.1.value').get('value')).toBeUndefined() 119 | array.insert(1, { value: 44 }, { value: 55 }) 120 | attach( 121 | form.createField({ 122 | name: 'value', 123 | basePath: 'array.1', 124 | }) 125 | ) 126 | attach( 127 | form.createField({ 128 | name: 'value', 129 | basePath: 'array.2', 130 | }) 131 | ) 132 | expect(array.value).toEqual([{ value: 33 }, { value: 44 }, { value: 55 }]) 133 | expect(form.query('array.0.value').get('value')).toEqual(33) 134 | expect(form.query('array.1.value').get('value')).toEqual(44) 135 | expect(form.query('array.2.value').get('value')).toEqual(55) 136 | array.move(0, 2) 137 | expect(array.value).toEqual([{ value: 44 }, { value: 55 }, { value: 33 }]) 138 | expect(form.query('array.0.value').get('value')).toEqual(44) 139 | expect(form.query('array.1.value').get('value')).toEqual(55) 140 | expect(form.query('array.2.value').get('value')).toEqual(33) 141 | array.move(2, 0) 142 | expect(array.value).toEqual([{ value: 33 }, { value: 44 }, { value: 55 }]) 143 | expect(form.query('array.0.value').get('value')).toEqual(33) 144 | expect(form.query('array.1.value').get('value')).toEqual(44) 145 | expect(form.query('array.2.value').get('value')).toEqual(55) 146 | }) 147 | 148 | test('array field move up/down then fields move', () => { 149 | const form = attach(createForm()) 150 | const array = attach( 151 | form.createArrayField({ 152 | name: 'array', 153 | }) 154 | ) 155 | attach( 156 | form.createField({ 157 | name: 'value', 158 | basePath: 'array.0', 159 | }) 160 | ) 161 | attach( 162 | form.createField({ 163 | name: 'value', 164 | basePath: 'array.1', 165 | }) 166 | ) 167 | attach( 168 | form.createField({ 169 | name: 'value', 170 | basePath: 'array.2', 171 | }) 172 | ) 173 | attach( 174 | form.createField({ 175 | name: 'value', 176 | basePath: 'array.3', 177 | }) 178 | ) 179 | const line0 = form.fields['array.0.value'] 180 | const line1 = form.fields['array.1.value'] 181 | const line2 = form.fields['array.2.value'] 182 | const line3 = form.fields['array.3.value'] 183 | 184 | array.push({ value: '0' }, { value: '1' }, { value: '2' }, { value: '3' }) 185 | 186 | array.move(0, 3) 187 | 188 | // 1,2,3,0 189 | expect(form.fields['array.0.value']).toBe(line1) 190 | expect(form.fields['array.1.value']).toBe(line2) 191 | expect(form.fields['array.2.value']).toBe(line3) 192 | expect(form.fields['array.3.value']).toBe(line0) 193 | 194 | array.move(3, 1) 195 | 196 | // 1,0,2,3 197 | expect(form.fields['array.0.value']).toBe(line1) 198 | expect(form.fields['array.1.value']).toBe(line0) 199 | expect(form.fields['array.2.value']).toBe(line2) 200 | expect(form.fields['array.3.value']).toBe(line3) 201 | }) 202 | 203 | // 重现 issues #3932 , 补全 PR #3992 测试用例 204 | test('lazy array field query each', () => { 205 | const form = attach(createForm()) 206 | const array = attach( 207 | form.createArrayField({ 208 | name: 'array', 209 | }) 210 | ) 211 | 212 | const init = Array.from({ length: 6 }).map((_, i) => ({ value: i })) 213 | array.setValue(init) 214 | 215 | // page1: 0, 1 216 | // page2: 2, 3 untouch 217 | // page3: 4, 5 218 | init.forEach((item) => { 219 | const len = item.value 220 | //2, 3 221 | if (len >= 2 && len <= 3) { 222 | } else { 223 | // 0, 1, 4, 5 224 | attach( 225 | form.createField({ 226 | name: 'value', 227 | basePath: 'array.' + len, 228 | }) 229 | ) 230 | } 231 | }) 232 | 233 | array.insert(1, { value: '11' }) 234 | expect(() => form.query('*').take()).not.toThrowError() 235 | expect(Object.keys(form.fields)).toEqual([ 236 | 'array', 237 | 'array.0.value', 238 | 'array.5.value', 239 | 'array.2.value', 240 | 'array.6.value', 241 | ]) 242 | }) 243 | 244 | test('void children', () => { 245 | const form = attach(createForm()) 246 | const array = attach( 247 | form.createArrayField({ 248 | name: 'array', 249 | }) 250 | ) 251 | attach( 252 | form.createField({ 253 | name: 'other', 254 | basePath: 'array', 255 | }) 256 | ) 257 | attach( 258 | form.createVoidField({ 259 | name: 0, 260 | basePath: 'array', 261 | }) 262 | ) 263 | const aaa = attach( 264 | form.createField({ 265 | name: 'aaa', 266 | basePath: 'array.0', 267 | value: 123, 268 | }) 269 | ) 270 | expect(aaa.value).toEqual(123) 271 | expect(array.value).toEqual([123]) 272 | }) 273 | 274 | test('exchange children', () => { 275 | const form = attach(createForm()) 276 | const array = attach( 277 | form.createArrayField({ 278 | name: 'array', 279 | }) 280 | ) 281 | attach( 282 | form.createField({ 283 | name: 'other', 284 | basePath: 'array', 285 | }) 286 | ) 287 | attach( 288 | form.createField({ 289 | name: '0.aaa', 290 | basePath: 'array', 291 | value: '123', 292 | }) 293 | ) 294 | attach( 295 | form.createField({ 296 | name: '0.bbb', 297 | basePath: 'array', 298 | value: '321', 299 | }) 300 | ) 301 | attach( 302 | form.createField({ 303 | name: '1.bbb', 304 | basePath: 'array', 305 | value: 'kkk', 306 | }) 307 | ) 308 | expect(array.value).toEqual([{ aaa: '123', bbb: '321' }, { bbb: 'kkk' }]) 309 | array.move(0, 1) 310 | expect(array.value).toEqual([{ bbb: 'kkk' }, { aaa: '123', bbb: '321' }]) 311 | expect(form.query('array.0.aaa').take()).toBeUndefined() 312 | }) 313 | 314 | test('fault tolerance', () => { 315 | const form = attach(createForm()) 316 | const array = attach( 317 | form.createArrayField({ 318 | name: 'array', 319 | }) 320 | ) 321 | const array2 = attach( 322 | form.createArrayField({ 323 | name: 'array2', 324 | value: [1, 2], 325 | }) 326 | ) 327 | array.setValue({} as any) 328 | array.push(11) 329 | expect(array.value).toEqual([11]) 330 | array.pop() 331 | expect(array.value).toEqual([]) 332 | array.remove(1) 333 | expect(array.value).toEqual([]) 334 | array.shift() 335 | expect(array.value).toEqual([]) 336 | array.unshift(1) 337 | expect(array.value).toEqual([1]) 338 | array.move(0, 1) 339 | expect(array.value).toEqual([1]) 340 | array.moveUp(1) 341 | expect(array.value).toEqual([1]) 342 | array.moveDown(1) 343 | expect(array.value).toEqual([1]) 344 | array.insert(1) 345 | expect(array.value).toEqual([1]) 346 | array2.move(1, 1) 347 | expect(array2.value).toEqual([1, 2]) 348 | array2.push(3) 349 | array2.moveUp(2) 350 | expect(array2.value).toEqual([1, 3, 2]) 351 | array2.moveUp(0) 352 | expect(array2.value).toEqual([3, 2, 1]) 353 | array2.moveDown(0) 354 | expect(array2.value).toEqual([2, 3, 1]) 355 | array2.moveDown(1) 356 | expect(array2.value).toEqual([2, 1, 3]) 357 | array2.moveDown(2) 358 | expect(array2.value).toEqual([3, 2, 1]) 359 | }) 360 | 361 | test('mutation fault tolerance', () => { 362 | const form = attach(createForm()) 363 | const pushArray = attach( 364 | form.createArrayField({ 365 | name: 'array1', 366 | }) 367 | ) 368 | const popArray = attach( 369 | form.createArrayField({ 370 | name: 'array2', 371 | }) 372 | ) 373 | const insertArray = attach( 374 | form.createArrayField({ 375 | name: 'array3', 376 | }) 377 | ) 378 | const removeArray = attach( 379 | form.createArrayField({ 380 | name: 'array4', 381 | }) 382 | ) 383 | const shiftArray = attach( 384 | form.createArrayField({ 385 | name: 'array5', 386 | }) 387 | ) 388 | const unshiftArray = attach( 389 | form.createArrayField({ 390 | name: 'array6', 391 | }) 392 | ) 393 | const moveArray = attach( 394 | form.createArrayField({ 395 | name: 'array7', 396 | }) 397 | ) 398 | const moveUpArray = attach( 399 | form.createArrayField({ 400 | name: 'array8', 401 | }) 402 | ) 403 | const moveDownArray = attach( 404 | form.createArrayField({ 405 | name: 'array9', 406 | }) 407 | ) 408 | pushArray.setValue({} as any) 409 | pushArray.push(123) 410 | expect(pushArray.value).toEqual([123]) 411 | popArray.setValue({} as any) 412 | popArray.pop() 413 | expect(popArray.value).toEqual({}) 414 | insertArray.setValue({} as any) 415 | insertArray.insert(0, 123) 416 | expect(insertArray.value).toEqual([123]) 417 | removeArray.setValue({} as any) 418 | removeArray.remove(0) 419 | expect(removeArray.value).toEqual({}) 420 | shiftArray.setValue({} as any) 421 | shiftArray.shift() 422 | expect(shiftArray.value).toEqual({}) 423 | unshiftArray.setValue({} as any) 424 | unshiftArray.unshift(123) 425 | expect(unshiftArray.value).toEqual([123]) 426 | moveArray.setValue({} as any) 427 | moveArray.move(0, 1) 428 | expect(moveArray.value).toEqual({}) 429 | moveUpArray.setValue({} as any) 430 | moveUpArray.moveUp(0) 431 | expect(moveUpArray.value).toEqual({}) 432 | moveDownArray.setValue({} as any) 433 | moveDownArray.moveDown(1) 434 | expect(moveDownArray.value).toEqual({}) 435 | }) 436 | 437 | test('array field move api with children', async () => { 438 | const form = attach(createForm()) 439 | attach( 440 | form.createField({ 441 | name: 'other', 442 | }) 443 | ) 444 | const array = attach( 445 | form.createArrayField({ 446 | name: 'array', 447 | }) 448 | ) 449 | attach( 450 | form.createArrayField({ 451 | name: '0', 452 | basePath: 'array', 453 | }) 454 | ) 455 | attach( 456 | form.createArrayField({ 457 | name: '1', 458 | basePath: 'array', 459 | }) 460 | ) 461 | attach( 462 | form.createArrayField({ 463 | name: '2', 464 | basePath: 'array', 465 | }) 466 | ) 467 | attach( 468 | form.createArrayField({ 469 | name: 'name', 470 | basePath: 'array.2', 471 | }) 472 | ) 473 | await array.move(0, 2) 474 | expect(form.fields['array.0.name']).toBeUndefined() 475 | expect(form.fields['array.2.name']).toBeUndefined() 476 | expect(form.fields['array.1.name']).not.toBeUndefined() 477 | }) 478 | 479 | test('array field remove memo leak', async () => { 480 | const handler = jest.fn() 481 | const valuesChange = jest.fn() 482 | const initialValuesChange = jest.fn() 483 | const form = attach( 484 | createForm({ 485 | effects() { 486 | onFormValuesChange(valuesChange) 487 | onFormInitialValuesChange(initialValuesChange) 488 | onFieldValueChange('*', handler) 489 | }, 490 | }) 491 | ) 492 | const array = attach( 493 | form.createArrayField({ 494 | name: 'array', 495 | }) 496 | ) 497 | await array.push('') 498 | attach( 499 | form.createField({ 500 | name: '0', 501 | basePath: 'array', 502 | }) 503 | ) 504 | await array.remove(0) 505 | await array.push('') 506 | attach( 507 | form.createField({ 508 | name: '0', 509 | basePath: 'array', 510 | }) 511 | ) 512 | expect(handler).toBeCalledTimes(0) 513 | expect(valuesChange).toBeCalledTimes(4) 514 | expect(initialValuesChange).toBeCalledTimes(0) 515 | }) 516 | 517 | test('nest array remove', async () => { 518 | const form = attach(createForm()) 519 | 520 | const metrics = attach( 521 | form.createArrayField({ 522 | name: 'metrics', 523 | }) 524 | ) 525 | 526 | attach( 527 | form.createObjectField({ 528 | name: '0', 529 | basePath: 'metrics', 530 | }) 531 | ) 532 | 533 | attach( 534 | form.createObjectField({ 535 | name: '1', 536 | basePath: 'metrics', 537 | }) 538 | ) 539 | 540 | attach( 541 | form.createArrayField({ 542 | name: 'content', 543 | basePath: 'metrics.0', 544 | }) 545 | ) 546 | 547 | attach( 548 | form.createArrayField({ 549 | name: 'content', 550 | basePath: 'metrics.1', 551 | }) 552 | ) 553 | 554 | const obj00 = attach( 555 | form.createObjectField({ 556 | name: '0', 557 | basePath: 'metrics.0.content', 558 | }) 559 | ) 560 | 561 | const obj10 = attach( 562 | form.createObjectField({ 563 | name: '0', 564 | basePath: 'metrics.1.content', 565 | }) 566 | ) 567 | 568 | attach( 569 | form.createField({ 570 | name: 'attr', 571 | basePath: 'metrics.0.content.0', 572 | initialValue: '123', 573 | }) 574 | ) 575 | 576 | attach( 577 | form.createField({ 578 | name: 'attr', 579 | basePath: 'metrics.1.content.0', 580 | initialValue: '123', 581 | }) 582 | ) 583 | expect(obj00.indexes[0]).toBe(0) 584 | expect(obj00.index).toBe(0) 585 | expect(obj10.index).toBe(0) 586 | expect(obj10.indexes[0]).toBe(1) 587 | await (form.query('metrics.1.content').take() as any).remove(0) 588 | expect(form.fields['metrics.0.content.0.attr']).not.toBeUndefined() 589 | await metrics.remove(1) 590 | expect(form.fields['metrics.0.content.0.attr']).not.toBeUndefined() 591 | expect( 592 | form.initialValues.metrics?.[1]?.content?.[0]?.attr 593 | ).not.toBeUndefined() 594 | }) 595 | 596 | test('indexes: nest path need exclude incomplete number', () => { 597 | const form = attach(createForm()) 598 | 599 | const objPathIncludeNum = attach( 600 | form.createField({ 601 | name: 'attr', 602 | basePath: 'metrics.0.a.10.iconWidth50', 603 | }) 604 | ) 605 | 606 | expect(objPathIncludeNum.indexes.length).toBe(2) 607 | expect(objPathIncludeNum.indexes).toEqual([0, 10]) 608 | expect(objPathIncludeNum.index).toBe(10) 609 | }) 610 | 611 | test('incomplete insertion of array elements', async () => { 612 | const form = attach( 613 | createForm({ 614 | values: { 615 | array: [{ aa: 1 }, { aa: 2 }, { aa: 3 }], 616 | }, 617 | }) 618 | ) 619 | const array = attach( 620 | form.createArrayField({ 621 | name: 'array', 622 | }) 623 | ) 624 | attach( 625 | form.createObjectField({ 626 | name: '0', 627 | basePath: 'array', 628 | }) 629 | ) 630 | attach( 631 | form.createField({ 632 | name: 'aa', 633 | basePath: 'array.0', 634 | }) 635 | ) 636 | attach( 637 | form.createObjectField({ 638 | name: '2', 639 | basePath: 'array', 640 | }) 641 | ) 642 | attach( 643 | form.createField({ 644 | name: 'aa', 645 | basePath: 'array.2', 646 | }) 647 | ) 648 | expect(form.fields['array.0.aa']).not.toBeUndefined() 649 | expect(form.fields['array.1.aa']).toBeUndefined() 650 | expect(form.fields['array.2.aa']).not.toBeUndefined() 651 | await array.unshift({}) 652 | expect(form.fields['array.0.aa']).toBeUndefined() 653 | expect(form.fields['array.1.aa']).not.toBeUndefined() 654 | expect(form.fields['array.2.aa']).toBeUndefined() 655 | expect(form.fields['array.3.aa']).not.toBeUndefined() 656 | }) 657 | 658 | test('void array items need skip data', () => { 659 | const form = attach(createForm()) 660 | const array = attach( 661 | form.createArrayField({ 662 | name: 'array', 663 | }) 664 | ) 665 | const array2 = attach( 666 | form.createArrayField({ 667 | name: 'array2', 668 | }) 669 | ) 670 | attach( 671 | form.createVoidField({ 672 | name: '0', 673 | basePath: 'array', 674 | }) 675 | ) 676 | attach( 677 | form.createVoidField({ 678 | name: '0', 679 | basePath: 'array2', 680 | }) 681 | ) 682 | attach( 683 | form.createVoidField({ 684 | name: 'space', 685 | basePath: 'array.0', 686 | }) 687 | ) 688 | const select = attach( 689 | form.createField({ 690 | name: 'select', 691 | basePath: 'array.0.space', 692 | }) 693 | ) 694 | const select2 = attach( 695 | form.createField({ 696 | name: 'select2', 697 | basePath: 'array2.0', 698 | }) 699 | ) 700 | 701 | select.value = 123 702 | select2.value = 123 703 | expect(array.value).toEqual([123]) 704 | expect(array2.value).toEqual([123]) 705 | }) 706 | 707 | test('array field reset', () => { 708 | const form = attach(createForm()) 709 | const array = attach( 710 | form.createArrayField({ 711 | name: 'array', 712 | }) 713 | ) 714 | attach( 715 | form.createObjectField({ 716 | name: '0', 717 | basePath: 'array', 718 | }) 719 | ) 720 | attach( 721 | form.createField({ 722 | name: 'input', 723 | initialValue: '123', 724 | basePath: 'array.0', 725 | }) 726 | ) 727 | form.reset('*', { forceClear: true }) 728 | expect(form.values).toEqual({ array: [] }) 729 | expect(array.value).toEqual([]) 730 | }) 731 | 732 | test('array field remove can not memory leak', async () => { 733 | const handler = jest.fn() 734 | const form = attach( 735 | createForm({ 736 | values: { 737 | array: [{ aa: 1 }, { aa: 2 }], 738 | }, 739 | effects() { 740 | onFieldValueChange('array.*.aa', handler) 741 | }, 742 | }) 743 | ) 744 | const array = attach( 745 | form.createArrayField({ 746 | name: 'array', 747 | }) 748 | ) 749 | attach( 750 | form.createObjectField({ 751 | name: '0', 752 | basePath: 'array', 753 | }) 754 | ) 755 | attach( 756 | form.createField({ 757 | name: 'aa', 758 | basePath: 'array.0', 759 | }) 760 | ) 761 | attach( 762 | form.createObjectField({ 763 | name: '1', 764 | basePath: 'array', 765 | }) 766 | ) 767 | attach( 768 | form.createField({ 769 | name: 'aa', 770 | basePath: 'array.1', 771 | }) 772 | ) 773 | const bb = attach( 774 | form.createField({ 775 | name: 'bb', 776 | basePath: 'array.1', 777 | reactions: (field) => { 778 | field.visible = field.query('.aa').value() === '123' 779 | }, 780 | }) 781 | ) 782 | expect(bb.visible).toBeFalsy() 783 | await array.remove(0) 784 | form.query('array.0.aa').take((field) => { 785 | ;(field as DataField).value = '123' 786 | }) 787 | expect(bb.visible).toBeTruthy() 788 | expect(handler).toBeCalledTimes(1) 789 | }) 790 | 791 | test('array field patch values', async () => { 792 | const form = attach(createForm()) 793 | 794 | const arr = attach( 795 | form.createArrayField({ 796 | name: 'a', 797 | }) 798 | ) 799 | 800 | await arr.unshift({}) 801 | attach( 802 | form.createObjectField({ 803 | name: '0', 804 | basePath: 'a', 805 | }) 806 | ) 807 | attach( 808 | form.createField({ 809 | name: 'c', 810 | initialValue: 'A', 811 | basePath: 'a.0', 812 | }) 813 | ) 814 | expect(form.values).toEqual({ a: [{ c: 'A' }] }) 815 | await arr.unshift({}) 816 | attach( 817 | form.createObjectField({ 818 | name: '0', 819 | basePath: 'a', 820 | }) 821 | ) 822 | attach( 823 | form.createField({ 824 | name: 'c', 825 | initialValue: 'A', 826 | basePath: 'a.0', 827 | }) 828 | ) 829 | attach( 830 | form.createObjectField({ 831 | name: '1', 832 | basePath: 'a', 833 | }) 834 | ) 835 | attach( 836 | form.createField({ 837 | name: 'c', 838 | initialValue: 'A', 839 | basePath: 'a.1', 840 | }) 841 | ) 842 | expect(form.values).toEqual({ a: [{ c: 'A' }, { c: 'A' }] }) 843 | }) 844 | 845 | test('array remove with initialValues', async () => { 846 | const form = attach( 847 | createForm({ 848 | initialValues: { 849 | array: [{ a: 1 }, { a: 2 }], 850 | }, 851 | }) 852 | ) 853 | const array = attach( 854 | form.createArrayField({ 855 | name: 'array', 856 | }) 857 | ) 858 | attach( 859 | form.createObjectField({ 860 | name: '0', 861 | basePath: 'array', 862 | }) 863 | ) 864 | attach( 865 | form.createObjectField({ 866 | name: '1', 867 | basePath: 'array', 868 | }) 869 | ) 870 | attach( 871 | form.createField({ 872 | name: 'a', 873 | basePath: 'array.0', 874 | }) 875 | ) 876 | attach( 877 | form.createField({ 878 | name: 'a', 879 | basePath: 'array.1', 880 | }) 881 | ) 882 | expect(form.values).toEqual({ array: [{ a: 1 }, { a: 2 }] }) 883 | await array.remove(1) 884 | expect(form.values).toEqual({ array: [{ a: 1 }] }) 885 | expect(form.initialValues).toEqual({ array: [{ a: 1 }, { a: 2 }] }) 886 | await array.reset() 887 | attach( 888 | form.createObjectField({ 889 | name: '1', 890 | basePath: 'array', 891 | }) 892 | ) 893 | attach( 894 | form.createField({ 895 | name: 'a', 896 | basePath: 'array.0', 897 | }) 898 | ) 899 | attach( 900 | form.createField({ 901 | name: 'a', 902 | basePath: 'array.1', 903 | }) 904 | ) 905 | expect(form.values).toEqual({ array: [{ a: 1 }, { a: 2 }] }) 906 | expect(form.initialValues).toEqual({ array: [{ a: 1 }, { a: 2 }] }) 907 | }) 908 | 909 | test('records: find array fields', () => { 910 | const form = attach( 911 | createForm({ 912 | initialValues: { 913 | array: [{ a: 1 }, { a: 2 }], 914 | }, 915 | }) 916 | ) 917 | 918 | attach( 919 | form.createArrayField({ 920 | name: 'array', 921 | }) 922 | ) 923 | 924 | attach( 925 | form.createObjectField({ 926 | name: '0', 927 | basePath: 'array', 928 | }) 929 | ) 930 | attach( 931 | form.createObjectField({ 932 | name: '1', 933 | basePath: 'array', 934 | }) 935 | ) 936 | const field0 = attach( 937 | form.createField({ 938 | name: 'a', 939 | basePath: 'array.0', 940 | }) 941 | ) 942 | const field1 = attach( 943 | form.createField({ 944 | name: 'a', 945 | basePath: 'array.1', 946 | }) 947 | ) 948 | 949 | expect(field0.records.length).toBe(2) 950 | expect(field0.record).toEqual({ a: 1 }) 951 | expect(field1.record).toEqual({ a: 2 }) 952 | }) 953 | 954 | test('record: find array nest field record', () => { 955 | const form = attach( 956 | createForm({ 957 | initialValues: { 958 | array: [{ a: { b: { c: 1, d: 1 } } }, { a: { b: { c: 2, d: 2 } } }], 959 | }, 960 | }) 961 | ) 962 | 963 | attach( 964 | form.createArrayField({ 965 | name: 'array', 966 | }) 967 | ) 968 | 969 | attach( 970 | form.createObjectField({ 971 | name: '0', 972 | basePath: 'array', 973 | }) 974 | ) 975 | attach( 976 | form.createObjectField({ 977 | name: '1', 978 | basePath: 'array', 979 | }) 980 | ) 981 | 982 | attach( 983 | form.createObjectField({ 984 | name: 'a', 985 | basePath: 'array.0', 986 | }) 987 | ) 988 | attach( 989 | form.createObjectField({ 990 | name: 'a', 991 | basePath: 'array.1', 992 | }) 993 | ) 994 | 995 | attach( 996 | form.createObjectField({ 997 | name: 'b', 998 | basePath: 'array.0.a', 999 | }) 1000 | ) 1001 | 1002 | attach( 1003 | form.createObjectField({ 1004 | name: 'b', 1005 | basePath: 'array.1.a', 1006 | }) 1007 | ) 1008 | 1009 | const field0 = attach( 1010 | form.createField({ 1011 | name: 'c', 1012 | basePath: 'array.0.a.b', 1013 | }) 1014 | ) 1015 | 1016 | const field1 = attach( 1017 | form.createField({ 1018 | name: 'c', 1019 | basePath: 'array.1.a.b', 1020 | }) 1021 | ) 1022 | 1023 | const field2 = attach( 1024 | form.createField({ 1025 | name: 'cc', 1026 | basePath: 'array.1.a.b.c', 1027 | }) 1028 | ) 1029 | 1030 | expect(field0.records.length).toBe(2) 1031 | expect(field1.records.length).toBe(2) 1032 | expect(field1.records).toEqual([ 1033 | { a: { b: { c: 1, d: 1 } } }, 1034 | { a: { b: { c: 2, d: 2 } } }, 1035 | ]) 1036 | expect(field0.record).toEqual({ c: 1, d: 1 }) 1037 | expect(field1.record).toEqual({ c: 2, d: 2 }) 1038 | expect(field2.record).toEqual({ c: 2, d: 2 }) 1039 | }) 1040 | 1041 | test('record: find array field record', () => { 1042 | const form = attach( 1043 | createForm({ 1044 | initialValues: { 1045 | array: [1, 2, 3], 1046 | }, 1047 | }) 1048 | ) 1049 | 1050 | attach( 1051 | form.createArrayField({ 1052 | name: 'array', 1053 | }) 1054 | ) 1055 | 1056 | const field = attach( 1057 | form.createField({ 1058 | basePath: 'array', 1059 | name: '0', 1060 | }) 1061 | ) 1062 | 1063 | expect(field.records.length).toBe(3) 1064 | expect(field.record).toEqual(1) 1065 | }) 1066 | 1067 | test('record: find object field record', () => { 1068 | const form = attach( 1069 | createForm({ 1070 | initialValues: { 1071 | a: { 1072 | b: { 1073 | c: 1, 1074 | d: 1, 1075 | }, 1076 | }, 1077 | }, 1078 | }) 1079 | ) 1080 | 1081 | attach( 1082 | form.createArrayField({ 1083 | name: 'a', 1084 | }) 1085 | ) 1086 | 1087 | attach( 1088 | form.createObjectField({ 1089 | name: 'b', 1090 | basePath: 'a', 1091 | }) 1092 | ) 1093 | 1094 | const fieldc = attach( 1095 | form.createObjectField({ 1096 | name: 'c', 1097 | basePath: 'a.b', 1098 | }) 1099 | ) 1100 | 1101 | expect(fieldc.records).toEqual(undefined) 1102 | expect(fieldc.record).toEqual({ 1103 | c: 1, 1104 | d: 1, 1105 | }) 1106 | }) 1107 | 1108 | test('record: find form fields', () => { 1109 | const form = attach( 1110 | createForm({ 1111 | initialValues: { 1112 | array: [{ a: 1 }, { a: 2 }], 1113 | }, 1114 | }) 1115 | ) 1116 | 1117 | const array = attach( 1118 | form.createArrayField({ 1119 | name: 'array', 1120 | }) 1121 | ) 1122 | 1123 | expect(array.record).toEqual({ array: [{ a: 1 }, { a: 2 }] }) 1124 | }) 1125 | ``` -------------------------------------------------------------------------------- /docs/guide/index.md: -------------------------------------------------------------------------------- ```markdown 1 | # Introduction 2 | 3 | ## Problem 4 | 5 | As we all know, the form scene has always been the most complex scene in the front-end and back-end fields. What is the main complexity of it? 6 | 7 | - There are a lot of fields, how can the performance not deteriorate with the increase of the number of fields? 8 | - Field association logic is complex, how to implement complex linkage logic more simply? How to ensure that the form performance is not affected when the field is associated with the field? 9 | 10 | - One-to-Many (asynchronous) 11 | - Many-to-One (asynchronous) 12 | - Many-to-Many (asynchronous) 13 | 14 | - Complex form data management 15 | - Form value conversion logic is complex (front and back formats are inconsistent) 16 | - The logic of merging synchronous and asynchronous default values is complicated 17 | - Cross-form data communication, how to keep the performance from deteriorating with the increase in the number of fields? 18 | - Complex form state management 19 | - Focusing on the self-incrementing list scenario, how to make the array data move, and the field status can follow the move during the deletion process? 20 | - Scene reuse of forms 21 | - Query list 22 | - Dialog/Drawer form 23 | - Step form 24 | - Tab form 25 | - Dynamic rendering requirements are very strong 26 | - Field configuration allows non-professional front-ends to quickly build complex forms 27 | - Cross-terminal rendering, a JSON Schema, multi-terminal adaptation 28 | - How to describe the layout in the form protocol? 29 | - Vertical layout 30 | - Horizontal layout 31 | - Grid layout 32 | - Flexible layout 33 | - Free layout 34 | - How to describe the logic in the form protocol? 35 | 36 | So many problems, how to solve them, think about it, But we still have to find a solution,Not only to solve but also to solve elegantly, The Alibaba digital supply chain team, after experiencing a lot of middle and back-office practice and exploration, finally precipitated **Formily form solution**. All the problems mentioned above, after going through UForm to Formily1.x, until Formily2.x finally achieved the degree of **elegant solution**. So how does Formily 2.x solve these problems? 37 | 38 | ## Solution 39 | 40 | In order to solve the above problems, we can further refine the problem and come up with a breakthrough direction. 41 | 42 | ### Accurate Rendering 43 | 44 | In the React scenario, to realize a form requirement, most of them use setState to realize field data collection. because form data needs to be collected and some linkage requirements are realized.This implementation is very simple and the mental cost is very low, but it also introduces performance problems, because each input will cause all fields to be rendered in full. Although there is diff at the DOM update level, diff also has a computational cost, which wastes a lot of computational resources. In terms of time complexity, the initial rendering of the form is O(n), and the field input is also O(n), which is obviously unreasonable. 45 | 46 | Historical experience is always helpful to mankind. Decades ago, humans created the MVVM design pattern. The core of this design pattern is to abstract the view model and consume it at the DSL template layer.SL uses a certain dependency collection mechanism, and then uniformly schedules in the view model to ensure that each input is accurately rendered. This is the industrial-grade GUI form! 47 | 48 | It just so happened that the github community abstracted a state management solution called Mobx for such MVVM models. The core capabilities of [Mobx](https://github.com/mobxjs/mobx) are its dependency tracking mechanism and the abstraction capabilities of responsive models. 49 | 50 | Therefore, with the help of Mobx, the O(n) problem in the form field input process can be completely solved, and it can be solved very elegantly. However, during the implementation of Formily 2.x, it was discovered that Mobx still has some problems that are not compatible with Formily's core ideas. In the end, we only can reinvent one wheel,[@formily/reactive](https://reactive.formilyjs.org) which continues the core idea of Mobx. 51 | 52 | Mention here [react-hook-form](https://github.com/react-hook-form/react-hook-form) , Very popular, known as the industry’s top performance form solution, let’s take a look at its simplest case: 53 | 54 | ```tsx pure 55 | import React from 'react' 56 | import ReactDOM from 'react-dom' 57 | import { useForm } from 'react-hook-form' 58 | 59 | function App() { 60 | const { register, handleSubmit, errors } = useForm() // initialize the hook 61 | const onSubmit = (data) => { 62 | console.log(data) 63 | } 64 | 65 | return ( 66 | <form onSubmit={handleSubmit(onSubmit)}> 67 | <input name="firstname" ref={register} /> {/* register an input */} 68 | <input name="lastname" ref={register({ required: true })} /> 69 | {errors.lastname && 'Last name is required.'} 70 | <input name="age" ref={register({ pattern: /\d+/ })} /> 71 | {errors.age && 'Please enter number for age.'} 72 | <input type="submit" /> 73 | </form> 74 | ) 75 | } 76 | 77 | ReactDOM.render(<App />, document.getElementById('root')) 78 | ``` 79 | 80 | Although the value management achieves accurate rendering, when the verification is triggered, the form will still be rendered in full. Because of the update of the errors state, the overall controlled rendering is necessary to achieve synchronization. This is only the full rendering of the verification meeting. In fact, there is linkage. To achieve linkage with react-hook-form, it also requires overall controlled rendering to achieve linkage. Therefore, if you want to truly achieve accurate rendering, it must be Reactive! 81 | 82 | ### Domain Model 83 | 84 | As mentioned in the previous question, the linkage of forms is very complicated, including various relationships between fields. Let’s imagine that most form linkages are basically linkages triggered based on the values of certain fields. However, actual business requirements may be sophisticated. It is not only necessary to trigger linkage based on certain field values, but also based on other side-effect values, such as application status, server data status, page URL, internal data of a UI component of a field, and current Other data status of the field itself, some special asynchronous events, etc. Use a picture to describe: 85 | 86 |  87 | 88 | As you can see from the above figure, in order to achieve a linkage relationship, the core is to associate certain state attributes of the field with certain data. Some data here can be external data or own data. For example, the display/hide of a field is associated with certain data, the value of a field is associated with certain data, and the disabling/editing of a field is associated with certain data. Here are three examples. We have actually abstracted it. One of the simplest Field model: 89 | 90 | ```typescript 91 | interface Field { 92 | value: any 93 | visible: boolean 94 | disabled: boolean 95 | } 96 | ``` 97 | 98 | Of course, does the Field model only have these 3 attributes? Definitely not, if we want to express a field, then the path of the field must have, Because we want to describe the entire form tree structure, at the same time, we also need to manage the properties of the field corresponding to the UI component. For example, Input and Select have their properties. For example, the placeholder of Input is associated with some data, or the drop-down option of Select is associated with some data, so you can understand it. So, our Field model can look like this: 99 | 100 | ```typescript 101 | interface Field { 102 | path: string[] 103 | value: any 104 | visible: boolean 105 | disabled: boolean 106 | component: [Component, ComponentProps] 107 | } 108 | ``` 109 | 110 | We have added the component attribute, which represents the UI component and UI component attribute corresponding to the field, so that the ability to associate certain data with the field component attribute, or even the field component, is realized. Are there any more? Of course, there are also, such as the outer package container of the field, usually we call it FormItem, which is mainly responsible for the interactive style of the field, such as the field title, the style of error prompts, etc., If we want to include more linkage, such as the linkage between certain data and FormItem, then we have to add the outer package container. There are many other attributes, which are not listed here. 111 | 112 | From the above ideas, we can see that in order to solve the linkage problem, no matter how abstract we are, the field model will eventually be abstracted. It contains all the states related to the field. As long as these states are manipulated, linkage can be triggered. 113 | 114 | Regarding accurate rendering, we have determined that we can choose a Reactive solution similar to Mobx. Although it is a reinvention of a wheel, the Reactive model is still very suitable for abstract responsive models. So based on the ability of Reactive, Formily, after constant trial and error and correction, finally designed a truly elegant form model. Such a form model solves the problem of the form domain, so it is also called a domain model. With such a domain model, we can make the linkage of the form enumerable and predictable, which also lays a solid foundation for the linkage of the protocol description to be discussed later. 115 | 116 | ### Path System 117 | 118 | The field model in the form domain model was mentioned earlier. If the design is more complete, it is not only a field model, but also a form model as the top-level model. The top-level model manages all the field models, and each field has its own Path. How to find these fields? The linkage relationship mentioned earlier is more of a passive dependency, but in some scenarios, we just need to modify the state of a field based on an asynchronous event action. Here is how to find a field elegantly. The same It has also undergone a lot of trial and error and correction. Formily's original path system @formily/path solves this problem very well. It not only makes the field lookup elegant, but it can also deal with the disgusting problem of inconsistent front-end and back-end data structures through destructuring expressions. 119 | 120 | ### Life Cycle 121 | 122 | With the help of Mobx and the path system, we have created a relatively complete form scheme, but after this abstraction, our scheme is like a black box, and the outside world cannot perceive the internal state flow process of the scheme. If you want to implement some logic in a certain process stage, you cannot achieve it. So, here we need another concept, the life cycle. As long as we expose the entire form life cycle as an event hook to the outside world, we can achieve an abstract but flexible form solution. 123 | 124 | ### Protocol Driven 125 | 126 | If you want to implement a dynamically configurable form, you must make the form structure serializable. 127 | There are many ways to serialize, which can be a UI description protocol based on the UI, or a data description protocol based on the data. Because the form itself is to maintain a copy of data, it is natural that for the form scenario, the data protocol is the most suitable. To describe the data structure, [JSON-Schema](https://json-schema.org/) is now the most popular in the industry. Because the JSON Schema protocol itself has many verification-related attributes, this is naturally associated with form verification. Is the UI description protocol really not suitable for describing forms? No, the UI description protocol is suitable for more general UI expressions. Of course, the description form is not a problem, but it will be more front-end protocol. On the contrary, JSON-Schema is expressible at the back-end model layer, and is more versatile in describing data. Therefore, the two protocols have their own strengths, but in the field of pure forms, JSON-Schema will be more domain-oriented. 128 | 129 | So, if we choose JSON-Schema, how do we describe the UI and how do we describe the logic? It is not realistic to simply describe the data and output the form pages available for actual business. 130 | 131 | The solution of [react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema-form) is that data is data and UI is UI. The advantage of this is that each protocol is a very pure protocol, but it brings a large maintenance cost and understanding cost. 132 | To develop a form, users need to constantly switch between the two protocols mentally. Therefore, if you look at such a split from a technical perspective, it is very reasonable, but from a product perspective, the split is to throw the cost to the user. Therefore, Formily's form protocol will be more inclined to expand on JSON-Schema. 133 | 134 | So, how to expand? In order not to pollute the standard JSON-Schema attributes, we uniformly express the extended attributes in the x-\* format: 135 | 136 | ```json 137 | { 138 | "type": "string", 139 | "title": "String", 140 | "description": "This is a string", 141 | "x-component": "Input", 142 | "x-component-props": { 143 | "placeholder": "please enter" 144 | } 145 | } 146 | ``` 147 | 148 | In this way, the UI protocol and the data protocol are mixed together. As long as there is a unified extension agreement, the responsibilities of the two protocols can still be guaranteed to be single. 149 | 150 | Then, what if you want to wrap a UI container on certain fields? Here, Formily defines a new schema type called `void`. No stranger to void, there is also void element in W3C specification, and void keyword in js. The former represents virtual elements, and the latter represents virtual pointers. Therefore, in JSON Schema, void is introduced to represent a virtual data node, which means that the node does not occupy the actual data structure. So, we can do this: 151 | 152 | ```json 153 | { 154 | "type": "void", 155 | "title": "card", 156 | "description": "This is a card", 157 | "x-component": "Card", 158 | "properties": { 159 | "string": { 160 | "type": "string", 161 | "title": "String", 162 | "description": "This is a string", 163 | "x-component": "Input", 164 | "x-component-props": { 165 | "placeholder": "please enter" 166 | } 167 | } 168 | } 169 | } 170 | ``` 171 | 172 | In this way, a UI container can be described. Because the UI container can be described, we can easily encapsulate a scene-based component, such as FormStep. So how do we describe the linkage between fields? For example, one field needs to control the display and hide of another field. We can do this: 173 | 174 | ```json 175 | { 176 | "type": "object", 177 | "properties": { 178 | "source": { 179 | "type": "string", 180 | "title": "Source", 181 | "x-component": "Input", 182 | "x-component-props": { 183 | "placeholder": "please enter" 184 | } 185 | }, 186 | "target": { 187 | "type": "string", 188 | "title": "Target", 189 | "x-component": "Input", 190 | "x-component-props": { 191 | "placeholder": "please enter" 192 | }, 193 | "x-reactions": [ 194 | { 195 | "dependencies": ["source"], 196 | "when": "{{$deps[0] == '123'}}", 197 | "fulfill": { 198 | "state": { 199 | "visible": true 200 | } 201 | }, 202 | "otherwise": { 203 | "state": { 204 | "visible": false 205 | } 206 | } 207 | } 208 | ] 209 | } 210 | } 211 | } 212 | ``` 213 | 214 | The target field is described with the help of `x-reactions`, which depends on the value of the source field. If the value is `'123'`, the target field is displayed, otherwise it is hidden. This linkage method is a passive linkage. What if we want to achieve active linkage ? It can be like this: 215 | 216 | ```json 217 | { 218 | "type": "object", 219 | "properties": { 220 | "source": { 221 | "type": "string", 222 | "title": "Source", 223 | "x-component": "Input", 224 | "x-component-props": { 225 | "placeholder": "please enter" 226 | }, 227 | "x-reactions": [ 228 | { 229 | "when": "{{$self.value == '123'}}", 230 | "target": "target", 231 | "fulfill": { 232 | "state": { 233 | "visible": true 234 | } 235 | }, 236 | "otherwise": { 237 | "state": { 238 | "visible": false 239 | } 240 | } 241 | } 242 | ] 243 | }, 244 | "target": { 245 | "type": "string", 246 | "title": "Target", 247 | "x-component": "Input", 248 | "x-component-props": { 249 | "placeholder": "please enter" 250 | } 251 | } 252 | } 253 | } 254 | ``` 255 | 256 | Just change the location of `x-reactions`, put it on the source field, and then specify a target. 257 | 258 | It can be seen that our linkage is actually based on: 259 | 260 | - condition 261 | - Condition-satisfied action 262 | - Unsatisfied action 263 | 264 | To achieve. Because the internal state management uses the [@formily/reactive](https://reactive.formilyjs.org) solution similar to Mobx, Formily easily realizes passive and active linkage scenarios, covering most business needs. 265 | 266 | Therefore, our form can be described by protocol, and it can be configurable no matter how complicated the layout is or the linkage is very complicated. 267 | 268 | ### Layered Architecture 269 | 270 | I talked about the solutions to various problems at the beginning, so how do we design now to make Formily more self-consistent and elegant? 271 | 272 |  273 | 274 | This picture mainly divides Formily into the kernel layer, UI bridge layer, extended component layer, and configuration application layer. 275 | 276 | The kernel layer is UI-independent. It ensures that the logic and state of user management are not coupled to any framework. This has several advantages: 277 | 278 | - Logic and UI framework are decoupled, and framework-level migration will be done in the future, without extensive refactoring of business code. 279 | - The learning cost is uniform. If the user uses @formily/react, the business will be migrated to @formily/vue in the future, and the user does not need to learn again. 280 | 281 | JSON Schema exists independently and is consumed by the UI bridging layer, ensuring the absolute consistency of protocol drivers under different UI frameworks, and there is no need to repeatedly implement protocol parsing logic. 282 | 283 | Extend the component layer to provide a series of form scene components to ensure that users can use it out of the box. No need to spend a lot of time for secondary development. 284 | 285 | ## Competitive Product Comparison 286 | 287 | ```tsx 288 | /** 289 | * inline: true 290 | */ 291 | import React from 'react' 292 | import { Table, Tooltip } from 'antd' 293 | import { QuestionCircleOutlined } from '@ant-design/icons' 294 | 295 | const text = (content, tooltips) => { 296 | if (tooltips) { 297 | return ( 298 | <div> 299 | {content} 300 | <Tooltip title={tooltips}> 301 | <QuestionCircleOutlined style={{ marginLeft: 3 }} /> 302 | </Tooltip> 303 | </div> 304 | ) 305 | } 306 | return content 307 | } 308 | 309 | const dataSource = [ 310 | { 311 | feature: 'Custom component access cost', 312 | antd: '4.x low access cost', 313 | fusion: 'high', 314 | formik: 'low', 315 | finalForm: 'low', 316 | schemaForm: text('high', 'Because of coupling bootstrap'), 317 | hookForm: text('high', 'Because of coupling React Ref'), 318 | 'formily1.x': 'low', 319 | 'formily2.x': 'low', 320 | }, 321 | { 322 | feature: 'performance', 323 | antd: text( 324 | '4.x performance is better', 325 | 'Only solved the value synchronization and accurate rendering' 326 | ), 327 | fusion: 'bad', 328 | formik: 'bad', 329 | finalForm: text( 330 | 'better', 331 | 'But only solved the value synchronization and accurate rendering' 332 | ), 333 | schemaForm: 'bad', 334 | hookForm: text( 335 | 'good', 336 | 'But only solved the value synchronization and accurate rendering' 337 | ), 338 | 'formily1.x': text( 339 | 'excellent', 340 | 'Can solve the precise rendering in the linkage process' 341 | ), 342 | 'formily2.x': text( 343 | 'excellent', 344 | 'Can solve the precise rendering in the linkage process' 345 | ), 346 | }, 347 | { 348 | feature: 'Whether to support dynamic rendering', 349 | antd: 'no', 350 | fusion: 'no', 351 | formik: 'no', 352 | finalForm: 'no', 353 | schemaForm: 'yes', 354 | hookForm: 'no', 355 | 'formily1.x': 'yes', 356 | 'formily2.x': 'yes', 357 | }, 358 | { 359 | feature: 'Whether to use out of the box', 360 | antd: 'yes', 361 | fusion: 'yes', 362 | formik: 'no', 363 | finalForm: 'no', 364 | schemaForm: 'yes', 365 | hookForm: 'no', 366 | 'formily1.x': 'yes', 367 | 'formily2.x': 'yes', 368 | }, 369 | { 370 | feature: 'Whether to support cross-terminal', 371 | antd: 'no', 372 | fusion: 'no', 373 | formik: 'no', 374 | finalForm: 'no', 375 | schemaForm: 'no', 376 | hookForm: 'no', 377 | 'formily1.x': 'yes', 378 | 'formily2.x': 'yes', 379 | }, 380 | { 381 | feature: 'Development efficiency', 382 | antd: 'general', 383 | fusion: 'generalv', 384 | formik: 'general', 385 | finalForm: 'general', 386 | schemaForm: text( 387 | 'low', 388 | 'Source code development requires manual maintenance of JSON' 389 | ), 390 | hookForm: 'general', 391 | 'formily1.x': 'high', 392 | 'formily2.x': 'high', 393 | }, 394 | { 395 | feature: 'Learning cost', 396 | antd: 'easy', 397 | fusion: 'easy', 398 | formik: 'easy', 399 | finalForm: 'hard', 400 | schemaForm: 'hard', 401 | hookForm: 'easy', 402 | 'formily1.x': 'very hard', 403 | 'formily2.x': text('hard', 'The concept is drastically reduced'), 404 | }, 405 | { 406 | feature: 'View code maintainability', 407 | antd: text('bad', 'Lots of conditional expressions'), 408 | fusion: text('bad', 'Lots of conditional expressions'), 409 | formik: text('bad', 'Lots of conditional expressions'), 410 | finalForm: text('bad', 'Lots of conditional expressions'), 411 | schemaForm: 'good', 412 | hookForm: text('bad', 'Lots of conditional expressions'), 413 | 'formily1.x': 'good', 414 | 'formily2.x': 'good', 415 | }, 416 | { 417 | feature: 'Scenario-based packaging capabilities', 418 | antd: 'no', 419 | fusion: 'no', 420 | formik: 'no', 421 | finalForm: 'no', 422 | schemaForm: 'yes', 423 | hookForm: 'no', 424 | 'formily1.x': 'yes', 425 | 'formily2.x': 'yes', 426 | }, 427 | { 428 | feature: 'Whether to support form preview', 429 | antd: 'no', 430 | fusion: 'yes', 431 | formik: 'no', 432 | finalForm: 'no', 433 | schemaForm: 'no', 434 | hookForm: 'no', 435 | 'formily1.x': 'yes', 436 | 'formily2.x': 'yes', 437 | }, 438 | ] 439 | 440 | export default () => { 441 | return ( 442 | <Table 443 | dataSource={dataSource} 444 | pagination={false} 445 | bordered 446 | scroll={{ x: 1600 }} 447 | size="small" 448 | > 449 | <Table.Column title="ability" dataIndex="feature" width={160} /> 450 | <Table.Column title="Ant Design Form" dataIndex="antd" width={160} /> 451 | <Table.Column title="Fusion Form" dataIndex="fusion" width={160} /> 452 | <Table.Column title="Formik" dataIndex="formik" width={160} /> 453 | <Table.Column 454 | title="React Final Form" 455 | dataIndex="finalForm" 456 | width={160} 457 | /> 458 | <Table.Column 459 | title="React Schema Form" 460 | dataIndex="schemaForm" 461 | width={160} 462 | /> 463 | <Table.Column title="React Hook Form" dataIndex="hookForm" width={160} /> 464 | <Table.Column title="Formily1.x" dataIndex="formily1.x" width={160} /> 465 | <Table.Column title="Formily2.x" dataIndex="formily2.x" width={160} /> 466 | </Table> 467 | ) 468 | } 469 | ``` 470 | 471 | ## Core Advantages 472 | 473 | - high performance 474 | - Out of the box 475 | - Linkage logic to achieve high efficiencyv 476 | - Cross-terminal capability, logic can be cross-frame, cross-terminal reuse 477 | - Dynamic rendering capability 478 | 479 | ## Core Disadvantage 480 | 481 | - The learning cost is relatively high. Although 2.x has already converged a large number of concepts, there is still a certain learning cost. 482 | 483 | ## Who is using it? 484 | 485 | - Alibaba 486 | - Tencent 487 | - ByteDance 488 | 489 | ## Q/A 490 | 491 | Q: Now that I have Vue, why do I still need to provide @formily/vue? 492 | 493 | Answer: Vue is a UI framework. The problem it solves is a wider range of UI problems. Although its reactive ability is outstanding in form scenarios, at least it is more convenient than native React to write forms, but if it is in more complex form scenarios , We still need to do a lot of abstraction and encapsulation, so @formily/vue is to help you do these abstract encapsulation things, really let you develop super-complex form applications efficiently and conveniently. 494 | 495 | Q: What is the biggest advantage of Formily2.x compared to 1.x? 496 | 497 | Answer: The cost of learning, yes, the core is to allow users to understand Formily more quickly. We have tried our best to avoid all kinds of obscure logic and boundary problems during the 2.x design process. 498 | 499 | Q: What is the browser compatibility of Formily 2.x? 500 | 501 | Answer: IE is not supported, because the implementation of Reactive strongly relies on Proxy. 502 | ```