This is page 24 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 -------------------------------------------------------------------------------- /docs/guide/upgrade.md: -------------------------------------------------------------------------------- ```markdown 1 | # V2 Upgrade Guide 2 | 3 | It is important to mention here that Formily2 is very different from Formily1.x, and there are a lot of Break Changes. 4 | 5 | Therefore, for old users, they basically need to learn again, and V1 and V2 cannot be upgraded smoothly. 6 | 7 | But the original intention of the Formily2 project is to reduce everyone's learning costs, because the old users themselves have a certain understanding of Formily's core ideas. In order to help old users learn Formily2 more quickly, this article will list the core differences between V1 and V2. , and will not list the new capabilities. 8 | 9 | ## Kernel Difference 10 | 11 | > This mainly refers to the difference between @formily/core 12 | 13 | Because Formily1.x users mainly use setFieldState/setFormState and getFieldState/getFormState when using the core APIs, these APIs are retained in V2, but the internal model properties are semantically different. The differences are as follows: 14 | 15 | **modified** 16 | 17 | - V1: Represent whether the field has been changed, in fact, it is of no use, because the initialization of the field means that it has been changed. 18 | - V2: Indicates whether the field is manually modified, that is, it will be set to true when the component triggers the onChange event. 19 | 20 | **inputed** 21 | 22 | - V1: Represent Whether the field has been manually modified 23 | - V2: Remove, use modified uniformly 24 | 25 | **pristine** 26 | 27 | - V1:Represent whether the field value is equal to initialValue 28 | - V2: Remove, user manual judgment, this attribute will cause a lot of dirty checks 29 | 30 | **display** 31 | 32 | - V1: Represent whether the field is displayed, if it is false, the field value will not be removed 33 | - V2: Represent the field display mode, the value is `"none" | "visible" | "hidden"` 34 | 35 | **touched** 36 | 37 | - V1: Redundant field 38 | - V2: Remove 39 | 40 | **validating** 41 | 42 | - V1: Whether the representative field is being verified 43 | - V2: Remove, use validateStatus uniformly 44 | 45 | **effectErrors/effectWarnings** 46 | 47 | - V1: Errors and warnings that represent the manual operation of the user 48 | - V2: Remove, use feedbacks uniformly 49 | 50 | **ruleErrors/ruleWarnings** 51 | 52 | - V1: Errors and warnings representing the verification operation of the validator 53 | - V2: Remove, use feedbacks uniformly 54 | 55 | **values** 56 | 57 | - V1: Represent all the parameters returned by the onChange event 58 | - V2: Remove, use inputValues uniformly 59 | 60 | **rules** 61 | 62 | - V1: Represent verification rules 63 | - V2: Remove, use validator uniformly, because rules literally means rules, but the meaning of rules is very big, not limited to verification rules 64 | 65 | **props** 66 | 67 | - V1: Represent the extended attributes of the component, and the positioning is very unclear. In the pure JSX scenario, it represents the collection of component attributes and FormItem attributes. In the Schema scenario, it represents the attributes of the Schema field. 68 | - V2: Remove, use decorator and component uniformly 69 | 70 | **VirtualField** 71 | 72 | - V1: Represents a virtual field 73 | - V2: Renamed and use [VoidField](https://core.formilyjs.org/api/models/void-field) uniformly 74 | 75 | ## Bridge layer differences 76 | 77 | > This mainly refers to the difference between @formily/react and @formily/react-schema-renderer. 78 | 79 | **createFormActions/createAsyncFormActions** 80 | 81 | - V1 Create a Form operator, you can call the setFieldState/setFormState method. 82 | - V2 is removed, and the operation status of the Form instance created by [createForm](https://core.formilyjs.org/api/entry/create-form) in @formily/core is used uniformly. 83 | 84 | **Form** 85 | 86 | - V1 will create a Form instance inside, which can control the transfer of values/initialValues attributes, etc. 87 | - V2 removed, unified use of [FormProvider](https://react.formilyjs.org/api/components/form-provider) 88 | 89 | **SchemaForm** 90 | 91 | - V1 will parse the json-schema protocol internally, create a Form instance, support controlled mode, and render it. 92 | - V2 is removed, the SchemaField component created by [createSchemaField](https://react.formilyjs.org/api/components/schema-field) is used uniformly, and the controlled mode is not supported. 93 | 94 | **Field** 95 | 96 | - V1 supports controlled mode, which requires the use of render props for component state mapping. 97 | - V2 does not support controlled mode, you can quickly implement state mapping by passing in the decorator/component property. 98 | 99 | **VirtualField** 100 | 101 | - V1 supports controlled mode, which requires the use of render props for component state mapping. 102 | - V2 does not support controlled mode, renamed [VoidField](https://react.formilyjs.org/api/components/void-field), and passed in the decorator/component property to quickly implement state mapping. 103 | 104 | **FieldList** 105 | 106 | - V1 Represent auto-incremented field control component 107 | - V2 Renamed to [ArrayField](https://react.formilyjs.org/api/components/array-field) 108 | 109 | **FormSpy** 110 | 111 | - V1 Monitor all life cycle triggers and re-render 112 | - V2 Remove and use [FormConsumer](https://react.formilyjs.org/api/components/form-consumer) 113 | 114 | **SchemaMarkupField** 115 | 116 | - V1 Stands for Schema description label component 117 | - V2 Remove, unified use the description label component created by the [createSchemaField](https://react.formilyjs.org/api/components/schema-field) 118 | 119 | **useFormQuery** 120 | 121 | - V1 Fast Hook for realizing form query, supporting middleware mechanism 122 | - V2 Temporarily remove 123 | 124 | **useForm** 125 | 126 | - V1 Represents the creation of a Form instance 127 | - V2 Represents the Form instance in the consumption context, if you want to create it, please use [createForm](https://react.formilyjs.org/api/entry/create-form) 128 | 129 | **useField** 130 | 131 | - V1 Represents the creation of a Field instance 132 | - V2 Represents the Field instance in the consumption context, if you want to create it, please call [form.createField](https://core.formilyjs.org/api/models/form#createfield) 133 | 134 | **useVirtualField** 135 | 136 | - V1 Represents the creation of a VirtualField instance 137 | - V2 Remove, if you want to create, please call [form.createVoidField](https://core.formilyjs.org/api/models/form#createvoidfield) 138 | 139 | **useFormState** 140 | 141 | - V1 Form state in consumption context 142 | - V2 Remove, use [useForm](https://react.formilyjs.org/api/hooks/use-form) uniformly 143 | 144 | **useFieldState** 145 | 146 | - V1 consume Field status in context 147 | - V2 Remove, use [useField](https://react.formilyjs.org/api/hooks/use-field) 148 | 149 | **useFormSpy** 150 | 151 | - V1 Create a lifecycle listener and trigger a re-render 152 | - V2 Remove 153 | 154 | **useSchemaProps** 155 | 156 | - V1Cconsume rops of SchemaField in context 157 | - V2 Remove, use [useFieldSchema](https://react.formilyjs.org/api/hooks/use-field-schema) uniformly 158 | 159 | **connect** 160 | 161 | - V1 Standard HOC 162 | - V2 The higher-order function is changed to 1st order, and the properties have changed dramatically. See the [connect document](https://react.formilyjs.org/api/shared/connect) for details 163 | 164 | **registerFormField/registerVirtaulBox/registerFormComponent/registerFormItemComponent** 165 | 166 | - V1 Globally registered components 167 | - V2 Remove, global registration is no longer supported 168 | 169 | **FormEffectHooks** 170 | 171 | - V1 RxJS lifecycle hook 172 | - V2 Remove, export from @formily/core uniformly, and will not return RxJS Observable object 173 | 174 | **effects** 175 | 176 | - V1 Support callback function`$` selector 177 | - V2 Remove`$`selector 178 | 179 | ## Protocol layer differences 180 | 181 | > This mainly refers to the difference in the JSON Schema protocol 182 | 183 | **editable** 184 | 185 | - V1 is directly in the Schema description, indicating whether the field can be edited 186 | - V2 Renamed x-editable 187 | 188 | **visible** 189 | 190 | - V1 Indicates whether the field is displayed 191 | - V2 Renamed x-visible 192 | 193 | **display** 194 | 195 | - V1 Represent whether the field is displayed or not, if it is false, it represents the hidden behavior without deleting the value 196 | - V2 Renamed x-display, which represents the field display mode, and the value is`"none" | "visible" | "hidden"` 197 | 198 | **triggerType** 199 | 200 | - V1 Represent the field verification timing 201 | - V2 Remove, please use`x-validator:[{triggerType:"onBlur",validator:()=>...}]` 202 | 203 | **x-props** 204 | 205 | - V1 Represents the FormItem property 206 | - V2 Remove, please use x-decorator-props 207 | 208 | **x-rules** 209 | 210 | - V1 Represent field verification rules 211 | - V2 Renamed x-validator 212 | 213 | **x-linkages** 214 | 215 | - V1 Represent field linkage 216 | - V2 Remove, use x-reactions uniformly 217 | 218 | **x-mega-props** 219 | 220 | - V1 Represent the sub-component properties of the MegaLayout component 221 | - V2 Remove 222 | 223 | ## Component library differences 224 | 225 | In Formily 1.x, we mainly use @formily/antd and @formily/antd-components, or @formily/next and @formily/next-components. 226 | 227 | In V2, we have the following changes: 228 | 229 | - @formily/antd and @formily/antd-components were merged into @formily/antd, and the directory structure was changed to that of a pure component library. 230 | 231 | - The internal API of @formily/react @formily/core will no longer be exported. 232 | - Almost all components have been rewritten and cannot be smoothly upgraded. 233 | - Remove styled-components. 234 | ``` -------------------------------------------------------------------------------- /packages/next/docs/components/Space.md: -------------------------------------------------------------------------------- ```markdown 1 | # Space 2 | 3 | > Super convenient Flex layout component, can help users quickly realize the layout of any element side by side next to each other 4 | 5 | ## Markup Schema example 6 | 7 | ```tsx 8 | import React from 'react' 9 | import { 10 | Input, 11 | FormItem, 12 | FormLayout, 13 | FormButtonGroup, 14 | Submit, 15 | Space, 16 | } from '@formily/next' 17 | import { createForm } from '@formily/core' 18 | import { FormProvider, createSchemaField } from '@formily/react' 19 | 20 | const SchemaField = createSchemaField({ 21 | components: { 22 | Input, 23 | FormItem, 24 | Space, 25 | }, 26 | }) 27 | 28 | const form = createForm() 29 | 30 | export default () => ( 31 | <FormProvider form={form}> 32 | <FormLayout labelCol={6} wrapperCol={16}> 33 | <SchemaField> 34 | <SchemaField.Void 35 | title="name" 36 | x-decorator="FormItem" 37 | x-decorator-props={{ 38 | asterisk: true, 39 | feedbackLayout: 'none', 40 | }} 41 | x-component="Space" 42 | > 43 | <SchemaField.String 44 | name="firstName" 45 | x-decorator="FormItem" 46 | x-component="Input" 47 | required 48 | /> 49 | <SchemaField.String 50 | name="lastName" 51 | x-decorator="FormItem" 52 | x-component="Input" 53 | x-visible="{{$values.firstName === '123'}}" 54 | required 55 | /> 56 | <SchemaField.String 57 | name="kk" 58 | x-decorator="FormItem" 59 | x-component="Input" 60 | x-decorator-props={{ 61 | addonAfter: 'Unit', 62 | }} 63 | required 64 | /> 65 | </SchemaField.Void> 66 | <SchemaField.Void 67 | title="Text concatenation" 68 | x-decorator="FormItem" 69 | x-decorator-props={{ 70 | asterisk: true, 71 | feedbackLayout: 'none', 72 | }} 73 | x-component="Space" 74 | > 75 | <SchemaField.String 76 | name="aa" 77 | x-decorator="FormItem" 78 | x-component="Input" 79 | x-decorator-props={{ 80 | addonAfter: 'Unit', 81 | }} 82 | required 83 | /> 84 | <SchemaField.String 85 | name="bb" 86 | x-decorator="FormItem" 87 | x-component="Input" 88 | x-decorator-props={{ 89 | addonAfter: 'Unit', 90 | }} 91 | required 92 | /> 93 | <SchemaField.String 94 | name="cc" 95 | x-decorator="FormItem" 96 | x-component="Input" 97 | x-decorator-props={{ 98 | addonAfter: 'Unit', 99 | }} 100 | required 101 | /> 102 | </SchemaField.Void> 103 | <SchemaField.String 104 | name="textarea" 105 | title="text box" 106 | x-decorator="FormItem" 107 | required 108 | x-component="Input.TextArea" 109 | x-component-props={{ 110 | style: { 111 | width: 400, 112 | }, 113 | }} 114 | /> 115 | </SchemaField> 116 | <FormButtonGroup.FormItem> 117 | <Submit onSubmit={console.log}>Submit</Submit> 118 | </FormButtonGroup.FormItem> 119 | </FormLayout> 120 | </FormProvider> 121 | ) 122 | ``` 123 | 124 | ## JSON Schema case 125 | 126 | ```tsx 127 | import React from 'react' 128 | import { 129 | Input, 130 | FormItem, 131 | FormLayout, 132 | FormButtonGroup, 133 | Submit, 134 | Space, 135 | } from '@formily/next' 136 | import { createForm } from '@formily/core' 137 | import { FormProvider, createSchemaField } from '@formily/react' 138 | 139 | const SchemaField = createSchemaField({ 140 | components: { 141 | Input, 142 | FormItem, 143 | Space, 144 | }, 145 | }) 146 | 147 | const form = createForm() 148 | 149 | const schema = { 150 | type: 'object', 151 | properties: { 152 | name: { 153 | type: 'void', 154 | title: 'Name', 155 | 'x-decorator': 'FormItem', 156 | 'x-decorator-props': { 157 | asterisk: true, 158 | feedbackLayout: 'none', 159 | }, 160 | 'x-component': 'Space', 161 | properties: { 162 | firstName: { 163 | type: 'string', 164 | 'x-decorator': 'FormItem', 165 | 'x-component': 'Input', 166 | required: true, 167 | }, 168 | lastName: { 169 | type: 'string', 170 | 'x-decorator': 'FormItem', 171 | 'x-component': 'Input', 172 | required: true, 173 | }, 174 | }, 175 | }, 176 | texts: { 177 | type: 'void', 178 | title: 'Text concatenation', 179 | 'x-decorator': 'FormItem', 180 | 'x-decorator-props': { 181 | asterisk: true, 182 | feedbackLayout: 'none', 183 | }, 184 | 'x-component': 'Space', 185 | properties: { 186 | aa: { 187 | type: 'string', 188 | 'x-decorator': 'FormItem', 189 | 'x-decorator-props': { 190 | addonAfter: 'Unit', 191 | }, 192 | 'x-component': 'Input', 193 | required: true, 194 | }, 195 | bb: { 196 | type: 'string', 197 | 'x-decorator': 'FormItem', 198 | 'x-decorator-props': { 199 | addonAfter: 'Unit', 200 | }, 201 | 'x-component': 'Input', 202 | required: true, 203 | }, 204 | cc: { 205 | type: 'string', 206 | 'x-decorator': 'FormItem', 207 | 'x-decorator-props': { 208 | addonAfter: 'Unit', 209 | }, 210 | 'x-component': 'Input', 211 | required: true, 212 | }, 213 | }, 214 | }, 215 | 216 | textarea: { 217 | type: 'string', 218 | title: 'Text box', 219 | 'x-decorator': 'FormItem', 220 | 'x-component': 'Input.TextArea', 221 | 'x-component-props': { 222 | style: { 223 | width: 400, 224 | }, 225 | }, 226 | required: true, 227 | }, 228 | }, 229 | } 230 | 231 | export default () => ( 232 | <FormProvider form={form}> 233 | <FormLayout labelCol={6} wrapperCol={16}> 234 | <SchemaField schema={schema} /> 235 | <FormButtonGroup.FormItem> 236 | <Submit onSubmit={console.log}>Submit</Submit> 237 | </FormButtonGroup.FormItem> 238 | </FormLayout> 239 | </FormProvider> 240 | ) 241 | ``` 242 | 243 | ## Pure JSX case 244 | 245 | ```tsx 246 | import React from 'react' 247 | import { 248 | Input, 249 | FormItem, 250 | FormLayout, 251 | FormButtonGroup, 252 | Submit, 253 | Space, 254 | } from '@formily/next' 255 | import { createForm } from '@formily/core' 256 | import { FormProvider, Field, VoidField } from '@formily/react' 257 | 258 | const form = createForm() 259 | 260 | export default () => ( 261 | <FormProvider form={form}> 262 | <FormLayout labelCol={6} wrapperCol={16}> 263 | <VoidField 264 | name="name" 265 | title="name" 266 | decorator={[ 267 | FormItem, 268 | { 269 | asterisk: true, 270 | feedbackLayout: 'none', 271 | }, 272 | ]} 273 | component={[Space]} 274 | > 275 | <Field 276 | name="firstName" 277 | decorator={[FormItem]} 278 | component={[Input]} 279 | required 280 | /> 281 | <Field 282 | name="lastName" 283 | decorator={[FormItem]} 284 | component={[Input]} 285 | required 286 | /> 287 | </VoidField> 288 | <VoidField 289 | name="texts" 290 | title="Text concatenation" 291 | decorator={[ 292 | FormItem, 293 | { 294 | asterisk: true, 295 | feedbackLayout: 'none', 296 | }, 297 | ]} 298 | component={[Space]} 299 | > 300 | <Field 301 | name="aa" 302 | decorator={[ 303 | FormItem, 304 | { 305 | addonAfter: 'Unit', 306 | }, 307 | ]} 308 | component={[Input]} 309 | required 310 | /> 311 | <Field 312 | name="bb" 313 | decorator={[ 314 | FormItem, 315 | { 316 | addonAfter: 'Unit', 317 | }, 318 | ]} 319 | component={[Input]} 320 | required 321 | /> 322 | <Field 323 | name="cc" 324 | decorator={[ 325 | FormItem, 326 | { 327 | addonAfter: 'Unit', 328 | }, 329 | ]} 330 | component={[Input]} 331 | required 332 | /> 333 | </VoidField> 334 | <Field 335 | name="textarea" 336 | title="text box" 337 | decorator={[FormItem]} 338 | component={[ 339 | Input.TextArea, 340 | { 341 | style: { 342 | width: 400, 343 | }, 344 | }, 345 | ]} 346 | required 347 | /> 348 | <FormButtonGroup.FormItem> 349 | <Submit onSubmit={console.log}>Submit</Submit> 350 | </FormButtonGroup.FormItem> 351 | </FormLayout> 352 | </FormProvider> 353 | ) 354 | ``` 355 | 356 | ## API 357 | 358 | | Property name | Type | Description | Default value | 359 | | ------------- | ----------------------------------------- | --------------- | ------------- | 360 | | style | CSSProperties | Style | - | 361 | | className | string | class name | - | 362 | | prefix | string | style prefix | true | 363 | | size | `number \|'small' \|'large' \|'middle'` | interval size | 8px | 364 | | direction | `'horizontal' \|'vertical'` | direction | - | 365 | | align | `'start' \|'end' \|'center' \|'baseline'` | align | `'start'` | 366 | | wrap | boolean | Whether to wrap | false | 367 | ``` -------------------------------------------------------------------------------- /packages/next/src/form-item/main.scss: -------------------------------------------------------------------------------- ```scss 1 | @import '~@alifd/next/lib/core/index-noreset.scss'; 2 | @import './scss/variable.scss'; 3 | @import './grid.scss'; 4 | @import './animation.scss'; 5 | 6 | .#{$form-item-cls} { 7 | display: flex; 8 | margin-bottom: $form-item-m-margin-b + 2; 9 | position: relative; 10 | line-height: $form-element-medium-height; 11 | font-size: $form-element-medium-font-size; 12 | 13 | &-layout-vertical { 14 | display: block; 15 | } 16 | 17 | &-label-content { 18 | min-height: $form-element-medium-height; 19 | } 20 | 21 | &-control-content-component { 22 | line-height: $form-element-medium-height; 23 | } 24 | 25 | &-inset { 26 | padding: 0 8px; 27 | border: 1px solid $input-border-color; 28 | border-radius: $form-element-medium-corner; 29 | width: 100%; 30 | 31 | .#{$css-prefix}input { 32 | border: none !important; 33 | box-shadow: none !important; 34 | outline: none; 35 | } 36 | } 37 | 38 | &-bordered-none:not(.#{$form-item-cls}-inset) { 39 | .#{$css-prefix}input { 40 | border: none !important; 41 | box-shadow: none !important; 42 | outline: none; 43 | } 44 | } 45 | } 46 | 47 | .#{$form-item-cls}-label { 48 | position: relative; 49 | display: flex; 50 | &-content { 51 | overflow: hidden; 52 | text-overflow: ellipsis; 53 | white-space: nowrap; 54 | } 55 | 56 | &-tooltip { 57 | cursor: help; 58 | 59 | .#{$form-item-cls}-colon { 60 | display: flex; 61 | } 62 | 63 | * { 64 | cursor: help; 65 | } 66 | 67 | &-colon { 68 | cursor: help; 69 | } 70 | 71 | label { 72 | border-bottom: 1px dashed currentColor; 73 | } 74 | } 75 | } 76 | 77 | .#{$form-item-cls}-label label { 78 | cursor: text; 79 | color: rgba(0, 0, 0, 0.85); 80 | } 81 | 82 | .#{$form-item-cls}-label-align-left { 83 | > .#{$form-item-cls}-label { 84 | justify-content: flex-start; 85 | } 86 | } 87 | 88 | .#{$form-item-cls}-label-align-right { 89 | > .#{$form-item-cls}-label { 90 | justify-content: flex-end; 91 | } 92 | } 93 | 94 | .#{$form-item-cls}-label-wrap { 95 | .#{$form-item-cls}-label { 96 | label { 97 | white-space: pre-line; 98 | word-break: break-all; 99 | } 100 | } 101 | } 102 | 103 | .#{$form-item-cls}-feedback-layout-terse { 104 | margin-bottom: 8px; 105 | 106 | &.#{$form-item-cls}-feedback-has-text:not(.#{$form-item-cls}-inset) { 107 | margin-bottom: 0; 108 | } 109 | } 110 | 111 | .#{$form-item-cls}-feedback-layout-loose { 112 | margin-bottom: $form-item-m-margin-b + 4; 113 | 114 | &.#{$form-item-cls}-feedback-has-text:not(.#{$form-item-cls}-inset) { 115 | margin-bottom: 0; 116 | } 117 | } 118 | 119 | .#{$form-item-cls}-feedback-layout-none { 120 | margin-bottom: 0px; 121 | 122 | &.#{$form-item-cls}-feedback-has-text:not(.#{$form-item-cls}-inset) { 123 | margin-bottom: 0; 124 | } 125 | } 126 | 127 | .#{$form-item-cls}-control { 128 | flex: 1; 129 | max-width: 100%; 130 | 131 | .#{$form-item-cls}-control-content { 132 | display: flex; 133 | 134 | .#{$form-item-cls}-control-content-component { 135 | width: 100%; 136 | min-height: $form-element-medium-height; 137 | line-height: $form-element-medium-height; 138 | 139 | &-has-feedback-icon { 140 | flex: 1; 141 | position: relative; 142 | display: flex; 143 | align-items: center; 144 | 145 | .#{$css-prefix}input { 146 | height: 100%; 147 | border: none !important; 148 | box-shadow: none !important; 149 | outline: none; 150 | 151 | .#{$css-prefix}input-control { 152 | margin-right: 0; 153 | padding-right: 0; 154 | 155 | .#{$css-prefix}icon { 156 | display: none; 157 | } 158 | 159 | .#{$css-prefix}input-hint-wrap { 160 | .#{$css-prefix}icon { 161 | display: block; 162 | } 163 | } 164 | } 165 | } 166 | } 167 | 168 | .#{$css-prefix}range { 169 | height: 100%; 170 | padding-left: 2px; 171 | } 172 | 173 | .#{$css-prefix}transfer { 174 | display: flex; 175 | align-items: center; 176 | } 177 | } 178 | 179 | .#{$form-item-cls}-addon-before { 180 | margin-right: 8px; 181 | display: inline-flex; 182 | align-items: center; 183 | min-height: $form-element-medium-height; 184 | flex-shrink: 0; 185 | } 186 | 187 | .#{$form-item-cls}-addon-after { 188 | margin-left: 8px; 189 | display: inline-flex; 190 | align-items: center; 191 | min-height: $form-element-medium-height; 192 | flex-shrink: 0; 193 | } 194 | } 195 | } 196 | 197 | .#{$form-item-cls}-size-small { 198 | font-size: $form-element-small-font-size; 199 | line-height: $form-element-small-height; 200 | 201 | .#{$form-item-cls}-label-content { 202 | min-height: $form-element-small-height; 203 | } 204 | 205 | .#{$form-item-cls}-control-content { 206 | .#{$form-item-cls}-control-content-component { 207 | line-height: $form-element-small-height; 208 | min-height: $form-element-small-height; 209 | } 210 | 211 | .#{$form-item-cls}-addon-before { 212 | min-height: $form-element-small-height; 213 | } 214 | 215 | .#{$form-item-cls}-addon-after { 216 | min-height: $form-element-small-height; 217 | } 218 | } 219 | 220 | &-inset { 221 | border-radius: $form-element-small-corner; 222 | } 223 | 224 | &.#{$form-item-cls}-feedback-layout-terse { 225 | margin-bottom: 8px; 226 | 227 | &.#{$form-item-cls}-feedback-has-text:not(.#{$form-item-cls}-inset) { 228 | margin-bottom: 0; 229 | } 230 | } 231 | 232 | &.#{$form-item-cls}-feedback-layout-loose { 233 | margin-bottom: $form-item-s-margin-b + 8; 234 | 235 | &.#{$form-item-cls}-feedback-has-text:not(.#{$form-item-cls}-inset) { 236 | margin-bottom: 0; 237 | } 238 | } 239 | 240 | .#{$form-item-cls}-help, 241 | .#{$form-item-cls}-extra { 242 | min-height: $form-element-small-font-size + 2; 243 | } 244 | 245 | .#{$form-item-cls}-control-content { 246 | min-height: $form-element-small-height; 247 | } 248 | 249 | .#{$form-item-cls}-label > label { 250 | height: $form-element-small-height; 251 | } 252 | } 253 | 254 | .#{$form-item-cls}-size-large { 255 | font-size: $form-element-large-font-size; 256 | line-height: $form-element-large-height; 257 | 258 | .#{$form-item-cls}-label-content { 259 | min-height: $form-element-large-height; 260 | } 261 | 262 | .#{$form-item-cls}-control-content { 263 | .#{$form-item-cls}-control-content-component { 264 | line-height: $form-element-large-height; 265 | min-height: $form-element-large-height; 266 | } 267 | 268 | .#{$form-item-cls}-addon-before { 269 | min-height: $form-element-large-height; 270 | } 271 | 272 | .#{$form-item-cls}-addon-after { 273 | min-height: $form-element-large-height; 274 | } 275 | } 276 | 277 | &-inset { 278 | border-radius: $form-element-large-corner; 279 | } 280 | 281 | .#{$form-item-cls}-help, 282 | .#{$form-item-cls}-extra { 283 | min-height: $form-element-large-font-size + 2; 284 | } 285 | 286 | &.#{$form-item-cls}-feedback-layout-loose { 287 | margin-bottom: $form-item-l-margin-b; 288 | 289 | &.#{$form-item-cls}-feedback-has-text:not(.#{$form-item-cls}-inset) { 290 | margin-bottom: 0; 291 | } 292 | } 293 | 294 | .#{$form-item-cls}-control-content { 295 | min-height: $form-element-large-height; 296 | } 297 | 298 | .#{$form-item-cls}-label > label { 299 | height: $form-element-large-height; 300 | } 301 | } 302 | 303 | .#{$form-item-cls}-feedback-layout-popover { 304 | margin-bottom: 8px; 305 | } 306 | 307 | .#{$form-item-cls}-label-tooltip-icon { 308 | margin-left: 4px; 309 | color: #00000073; 310 | cursor: pointer; 311 | max-height: $form-element-medium-height; 312 | } 313 | 314 | .#{$form-item-cls}-control-align-left { 315 | .#{$form-item-cls}-control-content { 316 | justify-content: flex-start; 317 | } 318 | } 319 | 320 | .#{$form-item-cls}-control-align-right { 321 | .#{$form-item-cls}-control-content { 322 | justify-content: flex-end; 323 | } 324 | } 325 | 326 | .#{$form-item-cls}-control-wrap { 327 | .#{$form-item-cls}-control { 328 | white-space: pre-line; 329 | } 330 | } 331 | 332 | .#{$form-item-cls}-asterisk { 333 | color: $form-error-color; 334 | margin-right: 4px; 335 | display: inline-block; 336 | font-family: SimSun, sans-serif; 337 | } 338 | 339 | .#{$form-item-cls}-colon { 340 | margin-left: 2px; 341 | margin-right: 8px; 342 | } 343 | 344 | .#{$form-item-cls}-help, 345 | .#{$form-item-cls}-extra { 346 | clear: both; 347 | line-height: $form-item-m-margin-b + 4; 348 | min-height: $form-item-m-margin-b; 349 | color: rgba(0, 0, 0, 0.45); 350 | transition: color 0.3s cubic-bezier(0.215, 0.61, 0.355, 1); 351 | padding-top: 0px; 352 | } 353 | 354 | .#{$form-item-cls}-fullness { 355 | > .#{$form-item-cls}-control { 356 | > .#{$form-item-cls}-control-content { 357 | > .#{$form-item-cls}-control-content-component { 358 | > *:first-child:not(.#{$css-prefix}switch):not(.#{$css-prefix}icon):not(.#{$css-prefix}formily-icon):not(.anticon):not(.#{$css-prefix}btn-text) { 359 | width: 100%; 360 | } 361 | } 362 | } 363 | } 364 | } 365 | 366 | .#{$form-item-cls}-control-content-component-has-feedback-icon { 367 | border-radius: 2px; 368 | border: 1px solid $input-border-color; 369 | transition: all 0.3s; 370 | touch-action: manipulation; 371 | outline: none; 372 | padding-right: 8px; 373 | 374 | .#{$css-prefix}input { 375 | border: none !important; 376 | } 377 | 378 | .#{$css-prefix}range-picker-trigger { 379 | border: none !important; 380 | } 381 | 382 | .#{$form-item-cls}-feedback-icon { 383 | display: flex; 384 | justify-content: center; 385 | align-items: center; 386 | margin-right: -2px; 387 | padding-left: 2px; 388 | 389 | .anticon { 390 | display: inline-block; 391 | color: inherit; 392 | font-style: normal; 393 | font-size: $form-element-medium-font-size; 394 | line-height: 0; 395 | text-align: center; 396 | text-transform: none; 397 | vertical-align: -0.125em; 398 | text-rendering: optimizeLegibility; 399 | } 400 | } 401 | } 402 | 403 | .#{$form-item-cls}-error-help { 404 | color: $form-error-color; 405 | } 406 | 407 | .#{$form-item-cls}-warning-help { 408 | color: $form-warning-color; 409 | } 410 | 411 | .#{$form-item-cls}-success-help { 412 | color: $form-success-color; 413 | } 414 | ``` -------------------------------------------------------------------------------- /packages/reactive/src/__tests__/annotations.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { observable, action, model, define } from '../' 2 | import { autorun, reaction } from '../autorun' 3 | import { observe } from '../observe' 4 | import { isObservable } from '../externals' 5 | import { untracked } from '../untracked' 6 | import { getObservableMaker } from '../internals' 7 | 8 | test('observable annotation', () => { 9 | const obs = observable<any>({ 10 | aa: 111, 11 | }) 12 | const handler = jest.fn() 13 | const handler1 = jest.fn() 14 | observe(obs, handler1) 15 | reaction(() => { 16 | handler(obs.aa) 17 | }) 18 | obs.aa = { bb: { cc: 123 } } 19 | obs.aa.bb = 333 20 | expect(handler).toBeCalledTimes(2) 21 | expect(handler1).toBeCalledTimes(2) 22 | 23 | const handler2 = jest.fn() 24 | const handler3 = jest.fn() 25 | const obsAnno = getObservableMaker(observable)({ value: obs }) 26 | 27 | observe(obsAnno, handler2) 28 | reaction(() => { 29 | handler3(obsAnno.aa) 30 | }) 31 | obsAnno.aa = { bb: { cc: 123 } } 32 | obsAnno.aa.bb = 333 33 | expect(handler2).toBeCalledTimes(2) 34 | expect(handler3).toBeCalledTimes(2) 35 | }) 36 | 37 | test('shallow annotation', () => { 38 | const obs = observable.shallow<any>({ 39 | aa: 111, 40 | }) 41 | const handler = jest.fn() 42 | const handler1 = jest.fn() 43 | observe(obs, handler1) 44 | reaction(() => { 45 | handler(obs.aa) 46 | }) 47 | obs.aa = { bb: { cc: 123 } } 48 | expect(isObservable(obs)).toBe(true) 49 | expect(isObservable(obs.aa)).toBe(false) 50 | expect(isObservable(obs.aa.bb)).toBe(false) 51 | obs.aa.bb = 333 52 | obs.cc = 444 53 | expect(handler).toBeCalledTimes(2) 54 | expect(handler1).toBeCalledTimes(2) 55 | }) 56 | 57 | test('box annotation', () => { 58 | const obs = observable.box(123) 59 | const handler = jest.fn() 60 | const handler1 = jest.fn() 61 | observe(obs, handler1) 62 | reaction(() => { 63 | handler(obs.get()) 64 | }) 65 | const boxValue = 333 66 | obs.set(boxValue) 67 | expect(handler1).toBeCalledTimes(1) 68 | expect(handler1.mock.calls[0][0]).toMatchObject({ 69 | value: boxValue, 70 | }) 71 | expect(handler).toBeCalledTimes(2) 72 | expect(handler.mock.calls[0][0]).toBe(123) 73 | expect(handler.mock.calls[1][0]).toBe(boxValue) 74 | }) 75 | 76 | test('ref annotation', () => { 77 | const obs = observable.ref(123) 78 | const handler = jest.fn() 79 | const handler1 = jest.fn() 80 | observe(obs, handler1) 81 | reaction(() => { 82 | handler(obs.value) 83 | }) 84 | obs.value = 333 85 | expect(handler).nthCalledWith(1, 123) 86 | expect(handler).nthCalledWith(2, 333) 87 | expect(handler1).toBeCalledTimes(1) 88 | }) 89 | 90 | test('action annotation', () => { 91 | const obs = observable<any>({}) 92 | const setData = action.bound(() => { 93 | obs.aa = 123 94 | obs.bb = 321 95 | }) 96 | const handler = jest.fn() 97 | reaction(() => { 98 | return [obs.aa, obs.bb] 99 | }, handler) 100 | setData() 101 | expect(handler).toBeCalledTimes(1) 102 | expect(handler).toBeCalledWith([123, 321], [undefined, undefined]) 103 | }) 104 | 105 | test('no action annotation', () => { 106 | const obs = observable<any>({}) 107 | const setData = () => { 108 | obs.aa = 123 109 | obs.bb = 321 110 | } 111 | const handler = jest.fn() 112 | reaction(() => { 113 | return [obs.aa, obs.bb] 114 | }, handler) 115 | setData() 116 | expect(handler).toBeCalledTimes(2) 117 | expect(handler).nthCalledWith(1, [123, undefined], [undefined, undefined]) 118 | expect(handler).nthCalledWith(2, [123, 321], [123, undefined]) 119 | }) 120 | 121 | test('computed annotation', () => { 122 | const obs = observable({ 123 | aa: 11, 124 | bb: 22, 125 | }) 126 | const handler = jest.fn(() => obs.aa + obs.bb) 127 | const runner1 = jest.fn() 128 | const runner2 = jest.fn() 129 | const runner3 = jest.fn() 130 | const compu = observable.computed(handler) 131 | expect(compu.value).toEqual(33) 132 | expect(handler).toBeCalledTimes(1) 133 | obs.aa = 22 134 | expect(handler).toBeCalledTimes(1) 135 | const dispose = autorun(() => { 136 | compu.value 137 | runner1() 138 | }) 139 | const dispose2 = autorun(() => { 140 | compu.value 141 | runner2() 142 | }) 143 | expect(compu.value).toEqual(44) 144 | expect(handler).toBeCalledTimes(2) 145 | obs.bb = 33 146 | expect(runner1).toBeCalledTimes(2) 147 | expect(runner2).toBeCalledTimes(2) 148 | expect(handler).toBeCalledTimes(3) 149 | expect(compu.value).toEqual(55) 150 | expect(handler).toBeCalledTimes(3) 151 | obs.aa = 11 152 | expect(runner1).toBeCalledTimes(3) 153 | expect(runner2).toBeCalledTimes(3) 154 | expect(handler).toBeCalledTimes(4) 155 | expect(compu.value).toEqual(44) 156 | expect(handler).toBeCalledTimes(4) 157 | dispose() 158 | obs.aa = 22 159 | expect(runner1).toBeCalledTimes(3) 160 | expect(runner2).toBeCalledTimes(4) 161 | expect(handler).toBeCalledTimes(5) 162 | expect(compu.value).toEqual(55) 163 | expect(handler).toBeCalledTimes(5) 164 | dispose2() 165 | obs.aa = 33 166 | expect(runner1).toBeCalledTimes(3) 167 | expect(runner2).toBeCalledTimes(4) 168 | expect(handler).toBeCalledTimes(5) 169 | expect(compu.value).toEqual(66) 170 | expect(handler).toBeCalledTimes(6) 171 | expect(compu.value).toEqual(66) 172 | expect(handler).toBeCalledTimes(6) 173 | autorun(() => { 174 | compu.value 175 | runner3() 176 | }) 177 | expect(compu.value).toEqual(66) 178 | expect(handler).toBeCalledTimes(6) 179 | expect(compu.value).toEqual(66) 180 | expect(handler).toBeCalledTimes(6) 181 | obs.aa = 11 182 | expect(handler).toBeCalledTimes(7) 183 | expect(compu.value).toEqual(44) 184 | expect(handler).toBeCalledTimes(7) 185 | }) 186 | 187 | test('computed chain annotation', () => { 188 | const obs = observable({ 189 | aa: 11, 190 | bb: 22, 191 | }) 192 | const handler = jest.fn(() => obs.aa + obs.bb) 193 | const compu1 = observable.computed(handler) 194 | const handler1 = jest.fn(() => compu1.value + 33) 195 | const compu2 = observable.computed(handler1) 196 | const dispose = autorun(() => { 197 | compu2.value 198 | }) 199 | expect(handler).toBeCalledTimes(1) 200 | expect(handler1).toBeCalledTimes(1) 201 | expect(compu2.value).toEqual(66) 202 | expect(handler).toBeCalledTimes(1) 203 | expect(handler1).toBeCalledTimes(1) 204 | obs.aa = 22 205 | expect(handler).toBeCalledTimes(2) 206 | expect(handler1).toBeCalledTimes(2) 207 | expect(compu2.value).toEqual(77) 208 | expect(handler).toBeCalledTimes(2) 209 | expect(handler1).toBeCalledTimes(2) 210 | dispose() 211 | obs.aa = 11 212 | expect(handler).toBeCalledTimes(2) 213 | expect(handler1).toBeCalledTimes(2) 214 | expect(compu2.value).toEqual(66) 215 | expect(handler).toBeCalledTimes(3) 216 | expect(handler1).toBeCalledTimes(3) 217 | }) 218 | 219 | test('computed with array length', () => { 220 | const obs = model({ 221 | arr: [], 222 | get isEmpty() { 223 | return this.arr.length === 0 224 | }, 225 | get isNotEmpty() { 226 | return !this.isEmpty 227 | }, 228 | }) 229 | const handler = jest.fn() 230 | autorun(() => { 231 | handler(obs.isEmpty) 232 | handler(obs.isNotEmpty) 233 | }) 234 | expect(handler).toBeCalledTimes(2) 235 | obs.arr = ['1'] 236 | obs.arr = [] 237 | expect(handler).toBeCalledTimes(6) 238 | }) 239 | 240 | test('computed with computed array length', () => { 241 | const obs = model({ 242 | arr: [], 243 | get arr2() { 244 | return this.arr.map((item: number) => item + 1) 245 | }, 246 | get isEmpty() { 247 | return this.arr2.length === 0 248 | }, 249 | get isNotEmpty() { 250 | return !this.isEmpty 251 | }, 252 | }) 253 | const handler = jest.fn() 254 | const handler2 = jest.fn() 255 | autorun(() => { 256 | handler(obs.isNotEmpty) 257 | handler2(obs.arr2) 258 | }) 259 | expect(handler).toBeCalledTimes(1) 260 | expect(handler).lastCalledWith(false) 261 | expect(handler2).toBeCalledTimes(1) 262 | expect(handler2.mock.calls[0][0]).toEqual([]) 263 | obs.arr.push(1) 264 | expect(handler).lastCalledWith(true) 265 | expect(handler2.mock.calls[1][0]).toEqual([2]) 266 | obs.arr = [] 267 | expect(handler).lastCalledWith(false) 268 | expect(handler2.mock.calls[2][0]).toEqual([]) 269 | }) 270 | 271 | test('computed recollect dependencies', () => { 272 | const computed = jest.fn() 273 | const obs = model({ 274 | aa: 'aaa', 275 | bb: 'bbb', 276 | cc: 'ccc', 277 | get compute() { 278 | computed() 279 | if (this.aa === 'aaa') { 280 | return this.bb 281 | } 282 | return this.cc 283 | }, 284 | }) 285 | const handler = jest.fn() 286 | autorun(() => { 287 | handler(obs.compute) 288 | }) 289 | obs.aa = '111' 290 | obs.bb = '222' 291 | expect(computed).toBeCalledTimes(2) 292 | }) 293 | 294 | test('computed no params', () => { 295 | observable.computed(null) 296 | }) 297 | 298 | test('computed object params', () => { 299 | observable.computed({ get: () => {} }) 300 | }) 301 | 302 | test('computed no track get', () => { 303 | const obs = observable({ aa: 123 }) 304 | const compu = observable.computed({ get: () => obs.aa }) 305 | untracked(() => { 306 | expect(compu.value).toBe(123) 307 | }) 308 | }) 309 | 310 | test('computed cache descriptor', () => { 311 | class A { 312 | _value = 0 313 | constructor() { 314 | define(this, { 315 | _value: observable.ref, 316 | value: observable.computed, 317 | }) 318 | } 319 | 320 | get value() { 321 | return this._value 322 | } 323 | } 324 | const obs1 = new A() 325 | const obs2 = new A() 326 | const handler1 = jest.fn() 327 | const handler2 = jest.fn() 328 | autorun(() => { 329 | handler1(obs1.value) 330 | }) 331 | autorun(() => { 332 | handler2(obs2.value) 333 | }) 334 | expect(handler1).toBeCalledTimes(1) 335 | expect(handler2).toBeCalledTimes(1) 336 | obs1._value = 123 337 | obs2._value = 123 338 | expect(handler1).toBeCalledTimes(2) 339 | expect(handler2).toBeCalledTimes(2) 340 | }) 341 | 342 | test('computed normal object', () => { 343 | const obs = define( 344 | { 345 | _value: 0, 346 | get value() { 347 | return this._value 348 | }, 349 | }, 350 | { 351 | _value: observable.ref, 352 | value: observable.computed, 353 | } 354 | ) 355 | const handler = jest.fn() 356 | autorun(() => { 357 | handler(obs.value) 358 | }) 359 | expect(handler).toBeCalledTimes(1) 360 | obs._value = 123 361 | expect(handler).toBeCalledTimes(2) 362 | }) 363 | ``` -------------------------------------------------------------------------------- /packages/react/src/__tests__/field.spec.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React from 'react' 2 | import { act } from 'react-dom/test-utils' 3 | import { render, fireEvent, waitFor } from '@testing-library/react' 4 | import { createForm, onFieldUnmount, isArrayField } from '@formily/core' 5 | import { 6 | isField, 7 | Field as FieldType, 8 | isVoidField, 9 | onFieldChange, 10 | } from '@formily/core' 11 | import { 12 | FormProvider, 13 | ArrayField, 14 | ObjectField, 15 | VoidField, 16 | Field, 17 | useField, 18 | useFormEffects, 19 | observer, 20 | connect, 21 | mapProps, 22 | mapReadPretty, 23 | } from '..' 24 | import { ReactiveField } from '../components/ReactiveField' 25 | import { expectThrowError } from './shared' 26 | 27 | type InputProps = { 28 | value?: string 29 | onChange?: (...args: any) => void 30 | } 31 | 32 | type CustomProps = { 33 | list?: string[] 34 | } 35 | 36 | const Decorator = (props) => <div>{props.children}</div> 37 | const Input: React.FC<React.PropsWithChildren<InputProps>> = (props) => ( 38 | <input 39 | {...props} 40 | value={props.value || ''} 41 | data-testid={useField().path.toString()} 42 | /> 43 | ) 44 | 45 | const Normal = () => <div></div> 46 | 47 | test('render field', async () => { 48 | const form = createForm() 49 | const onChange = jest.fn() 50 | const { getByTestId, queryByTestId, unmount } = render( 51 | <FormProvider form={form}> 52 | <Field 53 | name="aa" 54 | decorator={[Decorator]} 55 | component={[Input, { onChange }]} 56 | /> 57 | <ArrayField name="bb" decorator={[Decorator]}> 58 | <div data-testid="bb-children"></div> 59 | </ArrayField> 60 | <ObjectField name="cc" decorator={[Decorator]}> 61 | <Field name="mm" decorator={[Decorator]} component={[Input]} /> 62 | <ObjectField name="pp" decorator={[Decorator]} /> 63 | <ArrayField name="tt" decorator={[Decorator]} /> 64 | <VoidField name="ww" /> 65 | </ObjectField> 66 | <VoidField name="dd" decorator={[Decorator]}> 67 | {() => ( 68 | <div data-testid="dd-children"> 69 | <Field name="oo" decorator={[Decorator]} component={[Input]} /> 70 | </div> 71 | )} 72 | </VoidField> 73 | <VoidField name="xx" decorator={[Decorator]} component={[Normal]} /> 74 | <Field 75 | name="ee" 76 | visible={false} 77 | decorator={[Decorator]} 78 | component={[Input]} 79 | /> 80 | <Field name="ff" decorator={[]} component={[]} /> 81 | <Field name="gg" decorator={null} component={null} /> 82 | <Field name="hh" decorator={[null]} component={[null, null]} /> 83 | <Field 84 | name="kk" 85 | decorator={[Decorator]} 86 | component={[Input, { onChange: null }]} 87 | /> 88 | </FormProvider> 89 | ) 90 | expect(form.mounted).toBeTruthy() 91 | expect(form.query('aa').take().mounted).toBeTruthy() 92 | expect(form.query('bb').take().mounted).toBeTruthy() 93 | expect(form.query('cc').take().mounted).toBeTruthy() 94 | expect(form.query('dd').take().mounted).toBeTruthy() 95 | fireEvent.change(getByTestId('aa'), { 96 | target: { 97 | value: '123', 98 | }, 99 | }) 100 | fireEvent.change(getByTestId('kk'), { 101 | target: { 102 | value: '123', 103 | }, 104 | }) 105 | expect(onChange).toBeCalledTimes(1) 106 | expect(getByTestId('bb-children')).not.toBeUndefined() 107 | expect(getByTestId('dd-children')).not.toBeUndefined() 108 | expect(queryByTestId('ee')).toBeNull() 109 | expect(form.query('aa').get('value')).toEqual('123') 110 | expect(form.query('kk').get('value')).toEqual('123') 111 | unmount() 112 | }) 113 | 114 | test('render field no context', () => { 115 | expectThrowError(() => { 116 | return ( 117 | <> 118 | <Field name="aa">{() => <div></div>}</Field> 119 | <ArrayField name="bb"> 120 | <div></div> 121 | </ArrayField> 122 | <ObjectField name="cc" /> 123 | <VoidField name="dd" /> 124 | </> 125 | ) 126 | }) 127 | }) 128 | 129 | test('ReactiveField', () => { 130 | render(<ReactiveField field={null} />) 131 | render(<ReactiveField field={null}>{() => <div></div>}</ReactiveField>) 132 | }) 133 | 134 | test('useAttach basic', async () => { 135 | const form = createForm() 136 | const MyComponent = (props: any) => { 137 | return ( 138 | <FormProvider form={form}> 139 | <Field name={props.name} decorator={[Decorator]} component={[Input]} /> 140 | </FormProvider> 141 | ) 142 | } 143 | const { rerender } = render(<MyComponent name="aa" />) 144 | expect(form.query('aa').take().mounted).toBeTruthy() 145 | rerender(<MyComponent name="bb" />) 146 | await waitFor(() => { 147 | expect(form.query('aa').take().mounted).toBeFalsy() 148 | expect(form.query('bb').take().mounted).toBeTruthy() 149 | }) 150 | }) 151 | 152 | test('useAttach with array field', async () => { 153 | const form = createForm() 154 | const MyComponent = () => { 155 | return ( 156 | <FormProvider form={form}> 157 | <ArrayField 158 | name="array" 159 | initialValue={[{ input: '11' }, { input: '22' }]} 160 | > 161 | {(field) => { 162 | return field.value.map((val, index) => { 163 | return ( 164 | <Field 165 | key={index} 166 | name={index + '.input'} 167 | decorator={[Decorator]} 168 | component={[Input]} 169 | /> 170 | ) 171 | }) 172 | }} 173 | </ArrayField> 174 | </FormProvider> 175 | ) 176 | } 177 | render(<MyComponent />) 178 | await waitFor(() => { 179 | expect(form.query('array.0.input').take().mounted).toBeTruthy() 180 | expect(form.query('array.1.input').take().mounted).toBeTruthy() 181 | }) 182 | form.query('array').take((field) => { 183 | if (isArrayField(field)) { 184 | field.moveDown(0) 185 | } 186 | }) 187 | await waitFor(() => { 188 | expect(form.query('array.0.input').take().mounted).toBeTruthy() 189 | expect(form.query('array.1.input').take().mounted).toBeTruthy() 190 | }) 191 | }) 192 | 193 | test('useFormEffects', async () => { 194 | const form = createForm() 195 | const CustomField = observer(() => { 196 | const field = useField<FieldType>() 197 | useFormEffects(() => { 198 | onFieldChange('aa', ['value'], (target) => { 199 | if (isVoidField(target)) return 200 | field.setValue(target.value) 201 | }) 202 | }) 203 | return <div data-testid="custom-value">{field.value}</div> 204 | }) 205 | act(async () => { 206 | const { queryByTestId, rerender } = render( 207 | <FormProvider form={form}> 208 | <Field name="aa" decorator={[Decorator]} component={[Input]} /> 209 | <Field name="bb" component={[CustomField, { tag: 'xxx' }]} /> 210 | </FormProvider> 211 | ) 212 | 213 | expect(queryByTestId('custom-value')?.textContent).toEqual('') 214 | form.query('aa').take((aa) => { 215 | if (isField(aa)) { 216 | aa.setValue('123') 217 | } 218 | }) 219 | await waitFor(() => { 220 | expect(queryByTestId('custom-value')?.textContent).toEqual('123') 221 | }) 222 | rerender( 223 | <FormProvider form={form}> 224 | <Field name="aa" decorator={[Decorator]} component={[Input]} /> 225 | <Field name="bb" component={[CustomField, { tag: 'yyy' }]} /> 226 | </FormProvider> 227 | ) 228 | }) 229 | }) 230 | 231 | test('connect', async () => { 232 | const CustomField = connect( 233 | (props: CustomProps) => { 234 | return <div>{props.list}</div> 235 | }, 236 | mapProps({ value: 'list', loading: true }, (props, field) => { 237 | return { 238 | ...props, 239 | mounted: field.mounted ? 1 : 2, 240 | } 241 | }), 242 | mapReadPretty(() => <div>read pretty</div>) 243 | ) 244 | const BaseComponent = (props: any) => { 245 | return <div>{props.value}</div> 246 | } 247 | BaseComponent.displayName = 'BaseComponent' 248 | const CustomField2 = connect( 249 | BaseComponent, 250 | mapProps({ value: true, loading: true }), 251 | mapReadPretty(() => <div>read pretty</div>) 252 | ) 253 | const form = createForm() 254 | const MyComponent = () => { 255 | return ( 256 | <FormProvider form={form}> 257 | <Field name="aa" decorator={[Decorator]} component={[CustomField]} /> 258 | <Field name="bb" decorator={[Decorator]} component={[CustomField2]} /> 259 | </FormProvider> 260 | ) 261 | } 262 | const { queryByText } = render(<MyComponent />) 263 | form.query('aa').take((field) => { 264 | field.setState((state) => { 265 | state.value = '123' 266 | }) 267 | }) 268 | await waitFor(() => { 269 | expect(queryByText('123')).toBeVisible() 270 | }) 271 | 272 | form.query('aa').take((field) => { 273 | if (!isField(field)) return 274 | field.readPretty = true 275 | }) 276 | await waitFor(() => { 277 | expect(queryByText('123')).toBeNull() 278 | expect(queryByText('read pretty')).toBeVisible() 279 | }) 280 | }) 281 | 282 | test('fields unmount and validate', async () => { 283 | const fn = jest.fn() 284 | const form = createForm({ 285 | initialValues: { 286 | parent: { 287 | type: 'mounted', 288 | }, 289 | }, 290 | effects: () => { 291 | onFieldUnmount('parent.child', () => { 292 | fn() 293 | }) 294 | }, 295 | }) 296 | const Parent = observer(() => { 297 | const field = useField<FieldType>() 298 | if (field.value.type === 'mounted') { 299 | return ( 300 | <Field 301 | name="child" 302 | component={[Input]} 303 | validator={{ required: true }} 304 | /> 305 | ) 306 | } 307 | return <div data-testid="unmounted"></div> 308 | }) 309 | 310 | const MyComponent = () => { 311 | return ( 312 | <FormProvider form={form}> 313 | <Field name="parent" component={[Parent]} /> 314 | </FormProvider> 315 | ) 316 | } 317 | render(<MyComponent />) 318 | 319 | try { 320 | await form.validate() 321 | } catch {} 322 | 323 | expect(form.invalid).toBeTruthy() 324 | 325 | form.query('parent').take((field) => { 326 | field.setState((state) => { 327 | state.value.type = 'unmounted' 328 | }) 329 | }) 330 | 331 | await waitFor(() => { 332 | expect(fn.mock.calls.length).toBe(1) 333 | }) 334 | 335 | try { 336 | await form.validate() 337 | } catch {} 338 | expect(form.invalid).toBeTruthy() 339 | }) 340 | ``` -------------------------------------------------------------------------------- /packages/next/docs/components/FormDialog.md: -------------------------------------------------------------------------------- ```markdown 1 | # FormDialog 2 | 3 | > Pop-up form, mainly used in simple event to open the form scene 4 | 5 | ## Markup Schema example 6 | 7 | ```tsx 8 | import React from 'react' 9 | import { FormDialog, FormItem, Input, FormLayout } from '@formily/next' 10 | import { createSchemaField } from '@formily/react' 11 | import { Button } from '@alifd/next' 12 | 13 | const SchemaField = createSchemaField({ 14 | components: { 15 | FormItem, 16 | Input, 17 | }, 18 | }) 19 | 20 | export default () => { 21 | return ( 22 | <Button 23 | onClick={() => { 24 | FormDialog('Pop-up form', () => { 25 | return ( 26 | <FormLayout labelCol={6} wrapperCol={14}> 27 | <SchemaField> 28 | <SchemaField.String 29 | name="aaa" 30 | required 31 | title="input box 1" 32 | x-decorator="FormItem" 33 | x-component="Input" 34 | /> 35 | <SchemaField.String 36 | name="bbb" 37 | required 38 | title="input box 2" 39 | x-decorator="FormItem" 40 | x-component="Input" 41 | /> 42 | <SchemaField.String 43 | name="ccc" 44 | required 45 | title="input box 3" 46 | x-decorator="FormItem" 47 | x-component="Input" 48 | /> 49 | <SchemaField.String 50 | name="ddd" 51 | required 52 | title="input box 4" 53 | x-decorator="FormItem" 54 | x-component="Input" 55 | /> 56 | </SchemaField> 57 | <FormDialog.Footer> 58 | <span style={{ marginLeft: 4 }}>Extended copywriting</span> 59 | </FormDialog.Footer> 60 | </FormLayout> 61 | ) 62 | }) 63 | .open({ 64 | initialValues: { 65 | aaa: '123', 66 | }, 67 | }) 68 | .then(console.log) 69 | }} 70 | > 71 | Click me to open the form 72 | </Button> 73 | ) 74 | } 75 | ``` 76 | 77 | ## JSON Schema case 78 | 79 | ```tsx 80 | import React from 'react' 81 | import { FormDialog, FormItem, Input, FormLayout } from '@formily/next' 82 | import { createSchemaField } from '@formily/react' 83 | import { Button } from '@alifd/next' 84 | 85 | const SchemaField = createSchemaField({ 86 | components: { 87 | FormItem, 88 | Input, 89 | }, 90 | }) 91 | 92 | const schema = { 93 | type: 'object', 94 | properties: { 95 | aaa: { 96 | type: 'string', 97 | title: 'input box 1', 98 | required: true, 99 | 'x-decorator': 'FormItem', 100 | 'x-component': 'Input', 101 | }, 102 | bbb: { 103 | type: 'string', 104 | title: 'input box 2', 105 | required: true, 106 | 'x-decorator': 'FormItem', 107 | 'x-component': 'Input', 108 | }, 109 | ccc: { 110 | type: 'string', 111 | title: 'input box 3', 112 | required: true, 113 | 'x-decorator': 'FormItem', 114 | 'x-component': 'Input', 115 | }, 116 | ddd: { 117 | type: 'string', 118 | title: 'input box 4', 119 | required: true, 120 | 'x-decorator': 'FormItem', 121 | 'x-component': 'Input', 122 | }, 123 | }, 124 | } 125 | 126 | export default () => { 127 | return ( 128 | <Button 129 | onClick={() => { 130 | FormDialog('Pop-up form', () => { 131 | return ( 132 | <FormLayout labelCol={6} wrapperCol={14}> 133 | <SchemaField schema={schema} /> 134 | <FormDialog.Footer> 135 | <span style={{ marginLeft: 4 }}>Extended copywriting</span> 136 | </FormDialog.Footer> 137 | </FormLayout> 138 | ) 139 | }) 140 | .open({ 141 | initialValues: { 142 | aaa: '123', 143 | }, 144 | }) 145 | .then(console.log) 146 | }} 147 | > 148 | Click me to open the form 149 | </Button> 150 | ) 151 | } 152 | ``` 153 | 154 | ## Pure JSX case 155 | 156 | ```tsx 157 | import React from 'react' 158 | import { FormDialog, FormItem, Input, FormLayout } from '@formily/next' 159 | import { Field } from '@formily/react' 160 | import { Button } from '@alifd/next' 161 | 162 | export default () => { 163 | return ( 164 | <Button 165 | onClick={() => { 166 | FormDialog('Pop-up form', () => { 167 | return ( 168 | <FormLayout labelCol={6} wrapperCol={14}> 169 | <Field 170 | name="aaa" 171 | required 172 | title="input box 1" 173 | decorator={[FormItem]} 174 | component={[Input]} 175 | /> 176 | <Field 177 | name="bbb" 178 | required 179 | title="input box 2" 180 | decorator={[FormItem]} 181 | component={[Input]} 182 | /> 183 | <Field 184 | name="ccc" 185 | required 186 | title="input box 3" 187 | decorator={[FormItem]} 188 | component={[Input]} 189 | /> 190 | <Field 191 | name="ddd" 192 | required 193 | title="input box 4" 194 | decorator={[FormItem]} 195 | component={[Input]} 196 | /> 197 | <FormDialog.Footer> 198 | <span style={{ marginLeft: 4 }}>Extended copywriting</span> 199 | </FormDialog.Footer> 200 | </FormLayout> 201 | ) 202 | }) 203 | .open({ 204 | initialValues: { 205 | aaa: '123', 206 | }, 207 | }) 208 | .then(console.log) 209 | }} 210 | > 211 | Click me to open the form 212 | </Button> 213 | ) 214 | } 215 | ``` 216 | 217 | ## Use Fusion Context 218 | 219 | ```tsx 220 | import React from 'react' 221 | import { FormDialog, FormItem, Input, FormLayout } from '@formily/next' 222 | import { Field } from '@formily/react' 223 | import { Button, ConfigProvider } from '@alifd/next' 224 | 225 | export default () => { 226 | return ( 227 | <ConfigProvider 228 | locale={{ 229 | Dialog: { 230 | ok: 'OK', 231 | cancel: 'Cancel', 232 | }, 233 | }} 234 | defaultPropsConfig={{ 235 | Dialog: { 236 | isFullScreen: true, 237 | footerActions: ['cancel', 'ok'], 238 | }, 239 | }} 240 | > 241 | <Button 242 | onClick={() => { 243 | FormDialog('Pop-up form', () => { 244 | return ( 245 | <FormLayout labelCol={6} wrapperCol={14}> 246 | <Field 247 | name="aaa" 248 | required 249 | title="input box 1" 250 | decorator={[FormItem]} 251 | component={[Input]} 252 | /> 253 | <Field 254 | name="bbb" 255 | required 256 | title="input box 2" 257 | decorator={[FormItem]} 258 | component={[Input]} 259 | /> 260 | <Field 261 | name="ccc" 262 | required 263 | title="input box 3" 264 | decorator={[FormItem]} 265 | component={[Input]} 266 | /> 267 | <Field 268 | name="ddd" 269 | required 270 | title="input box 4" 271 | decorator={[FormItem]} 272 | component={[Input]} 273 | /> 274 | <FormDialog.Footer> 275 | <span style={{ marginLeft: 4 }}>Extended copywriting</span> 276 | </FormDialog.Footer> 277 | </FormLayout> 278 | ) 279 | }) 280 | .open({ 281 | initialValues: { 282 | aaa: '123', 283 | }, 284 | }) 285 | .then(console.log) 286 | }} 287 | > 288 | Click me to open the form 289 | </Button> 290 | </ConfigProvider> 291 | ) 292 | } 293 | ``` 294 | 295 | ## API 296 | 297 | ### FormDialog 298 | 299 | ```ts pure 300 | import { IFormProps, Form } from '@formily/core' 301 | 302 | type FormDialogRenderer = 303 | | React.ReactElement 304 | | ((form: Form) => React.ReactElement) 305 | 306 | interface IFormDialog { 307 | forOpen( 308 | middleware: ( 309 | props: IFormProps, 310 | next: (props?: IFormProps) => Promise<any> 311 | ) => any 312 | ): any //Middleware interceptor, can intercept Dialog to open 313 | forConfirm( 314 | middleware: (props: Form, next: (props?: Form) => Promise<any>) => any 315 | ): any //Middleware interceptor, which can intercept Dialog confirmation 316 | forCancel( 317 | middleware: (props: Form, next: (props?: Form) => Promise<any>) => any 318 | ): any //Middleware interceptor, can intercept Dialog to cancel 319 | //Open the pop-up window to receive form attributes, you can pass in initialValues/values/effects etc. 320 | open(props: IFormProps): Promise<any> //return form data 321 | //Close the pop-up window 322 | close(): void 323 | } 324 | 325 | interface IDialogProps extends DialogProps { 326 | onOk?: (event: React.MouseEvent) => void | boolean // return false can prevent onOk 327 | onCancel?: (event: React.MouseEvent) => void | boolean // return false can prevent onCancel 328 | loadingText?: React.ReactText 329 | } 330 | 331 | interface FormDialog { 332 | (title: IDialogProps, id: string, renderer: FormDialogRenderer): IFormDialog 333 | (title: IDialogProps, renderer: FormDialogRenderer): IFormDialog 334 | (title: ModalTitle, id: string, renderer: FormDialogRenderer): IFormDialog 335 | (title: ModalTitle, renderer: FormDialogRenderer): IFormDialog 336 | } 337 | ``` 338 | 339 | `DialogProps` type definition reference fusion [Dialog API](https://fusion.design/pc/component/dialog?themeid=2#API) 340 | 341 | ### FormDialog.Footer 342 | 343 | No attributes, only child nodes are received 344 | 345 | ### FormDialog.Portal 346 | 347 | Receive the optional id attribute, the default value is `form-dialog`, if there are multiple prefixCls in an application, and the prefixCls in the pop-up window of different regions are different, then it is recommended to specify the id as the region-level id 348 | ``` -------------------------------------------------------------------------------- /packages/core/docs/api/models/VoidField.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | order: 4 3 | --- 4 | 5 | # VoidField 6 | 7 | Call the VoidField model returned by [createVoidField](/api/models/form#createvoidfield). 8 | 9 | All model attributes are listed below. If the attribute is writable, then we can directly refer to it to modify the attribute, and @formily/reactive will respond to trigger the UI update. 10 | 11 | ## Attributes 12 | 13 | | Property | Description | Type | Read-only or not | Default value | 14 | | ----------- | ----------------------------------------- | --------------------------------------- | ---------------- | ------------- | 15 | | initialized | Whether the field has been initialized | Boolean | No | `false` | 16 | | mounted | Is the field mounted | Boolean | No | `false` | 17 | | unmounted | Is the field unmounted | Boolean | No | `false` | 18 | | address | Field node path | [FormPath](/api/entry/form-path) | Yes | | 19 | | path | Field data path | [FormPath](/api/entry/form-path) | Yes | | 20 | | title | Field Title | [FieldMessage](#fieldmessage) | No | `""` | 21 | | description | Field description | [FieldMessage](#fieldmessage) | No | `""` | 22 | | decorator | field decorator | Any[] | No | `null` | 23 | | component | Field component | Any[] | No | `null` | 24 | | parent | Parent field | [GeneralField](#generalfield) | yes | `null` | 25 | | display | Field display status | [FieldDisplayTypes](#fielddisplaytypes) | No | `"visible"` | 26 | | pattern | Field interaction mode | [FieldPatternTypes](#fieldpatterntypes) | No | `"editable"` | 27 | | hidden | Whether the field is hidden | Boolean | No | `false` | 28 | | visible | Whether the field is displayed | Boolean | No | `true` | 29 | | disabled | Whether the field is disabled | Boolean | No | `false` | 30 | | readOnly | Is the field read-only | Boolean | No | `false` | 31 | | readPretty | Whether the field is in the reading state | Boolean | No | `false` | 32 | | editable | Field is editable | Boolean | No | `true` | 33 | 34 | #### explain in detail 35 | 36 | **hidden** 37 | 38 | When true, display is hidden, when false, display is visible 39 | 40 | **visible** 41 | 42 | When true, display is visible, when false, display is none 43 | 44 | ## Method 45 | 46 | ### setTitle 47 | 48 | #### Description 49 | 50 | Set field title 51 | 52 | #### Signature 53 | 54 | ```ts 55 | interface setTitle { 56 | (title?: FieldMessage): void 57 | } 58 | ``` 59 | 60 | FieldMessage Reference [FieldMessage](#fieldmessage) 61 | 62 | ### setDescription 63 | 64 | #### Description 65 | 66 | Set field description information 67 | 68 | #### Signature 69 | 70 | ```ts 71 | interface setDescription { 72 | (title?: FieldMessage): void 73 | } 74 | ``` 75 | 76 | FieldMessage Reference [FieldMessage](#fieldmessage) 77 | 78 | ### setDisplay 79 | 80 | #### Description 81 | 82 | Set field display status 83 | 84 | #### Signature 85 | 86 | ```ts 87 | interface setDisplay { 88 | (display?: FieldDisplayTypes): void 89 | } 90 | ``` 91 | 92 | FieldDisplayTypes Reference [FieldDisplayTypes](#fielddisplaytypes) 93 | 94 | ### setPattern 95 | 96 | #### Description 97 | 98 | Set field interaction mode 99 | 100 | #### Signature 101 | 102 | ```ts 103 | interface setPattern { 104 | (pattern?: FieldPatternTypes): void 105 | } 106 | ``` 107 | 108 | FieldPatternTypes Reference [FieldPatternTypes](#fieldpatterntypes) 109 | 110 | ### setComponent 111 | 112 | #### Description 113 | 114 | Set field component 115 | 116 | #### Signature 117 | 118 | ```ts 119 | interface setComponent { 120 | (component?: FieldComponent, props?: any): void 121 | } 122 | ``` 123 | 124 | FieldComponent Reference [FieldComponent](#fieldcomponent) 125 | 126 | ### setComponentProps 127 | 128 | #### Description 129 | 130 | Set field component properties 131 | 132 | #### Signature 133 | 134 | ```ts 135 | interface setComponentProps { 136 | (props?: any): void 137 | } 138 | ``` 139 | 140 | ### setDecorator 141 | 142 | #### Description 143 | 144 | Set field decorator 145 | 146 | #### Signature 147 | 148 | ```ts 149 | interface setDecorator { 150 | (decorator?: FieldDecorator, props?: any): void 151 | } 152 | ``` 153 | 154 | FieldDecorator Reference [FieldDecorator](#fielddecorator) 155 | 156 | ### setDecoratorProps 157 | 158 | #### Description 159 | 160 | Set field decorator properties 161 | 162 | #### Signature 163 | 164 | ```ts 165 | interface setDecoratorProps { 166 | (props?: any): void 167 | } 168 | ``` 169 | 170 | ### setState 171 | 172 | #### Description 173 | 174 | Set field status 175 | 176 | #### Signature 177 | 178 | ```ts 179 | interface setState { 180 | (state: IVoidFieldState): void 181 | (callback: (state: IVoidFieldState) => void): void 182 | } 183 | ``` 184 | 185 | IVoidFieldState Reference [IVoidFieldState](#ifieldstate) 186 | 187 | ### getState 188 | 189 | #### Description 190 | 191 | Get field status 192 | 193 | #### Signature 194 | 195 | ```ts 196 | interface getState<T> { 197 | (): IVoidFieldState 198 | (callback: (state: IVoidFieldState) => T): T 199 | } 200 | ``` 201 | 202 | IVoidFieldState Reference [IVoidFieldState](#ifieldstate) 203 | 204 | ### setData 205 | 206 | #### Description 207 | 208 | set field data 209 | 210 | #### Signature 211 | 212 | ```ts 213 | interface setData { 214 | (data: any): void 215 | } 216 | ``` 217 | 218 | ### setContent 219 | 220 | #### Description 221 | 222 | set field content 223 | 224 | #### Signature 225 | 226 | ```ts 227 | interface setContent { 228 | (content: any): void 229 | } 230 | ``` 231 | 232 | ### onInit 233 | 234 | #### Description 235 | 236 | Trigger field initialization, no need to call manually 237 | 238 | #### Signature 239 | 240 | ```ts 241 | interface onInit { 242 | (): void 243 | } 244 | ``` 245 | 246 | ### onMount 247 | 248 | #### Description 249 | 250 | Trigger field mount 251 | 252 | #### Signature 253 | 254 | ```ts 255 | interface onMount { 256 | (): void 257 | } 258 | ``` 259 | 260 | ### onUnmount 261 | 262 | #### Description 263 | 264 | Trigger field unloading 265 | 266 | #### Signature 267 | 268 | ```ts 269 | interface onUnmount { 270 | (): void 271 | } 272 | ``` 273 | 274 | ### query 275 | 276 | #### Description 277 | 278 | Query field, you can query adjacent fields based on the current field 279 | 280 | #### Signature 281 | 282 | ```ts 283 | interface query { 284 | (pattern: FormPathPattern): Query 285 | } 286 | ``` 287 | 288 | FormPathPattern API Reference [FormPath](/api/entry/form-path#formpathpattern) 289 | 290 | Query object API reference [Query](/api/models/query) 291 | 292 | ### dispose 293 | 294 | #### Description 295 | 296 | Release observer, no need to release manually by default 297 | 298 | #### Signature 299 | 300 | ```ts 301 | interface dispose { 302 | (): void 303 | } 304 | ``` 305 | 306 | ### destroy 307 | 308 | #### Description 309 | 310 | Release observer, and remove current field model 311 | 312 | #### Signature 313 | 314 | ```ts 315 | interface destroy { 316 | (): void 317 | } 318 | ``` 319 | 320 | ### match 321 | 322 | #### Description 323 | 324 | Match fields based on path 325 | 326 | #### Signature 327 | 328 | ```ts 329 | interface match { 330 | (pattern: FormPathPattern): boolean 331 | } 332 | ``` 333 | 334 | FormPathPattern API Reference [FormPath](/api/entry/form-path#formpathpattern) 335 | 336 | ### inject 337 | 338 | #### Description 339 | 340 | Inject executable methods into field models 341 | 342 | #### Signature 343 | 344 | ```ts 345 | interface inject { 346 | (actions: Record<string, (...args: any[]) => any>): void 347 | } 348 | ``` 349 | 350 | ### invoke 351 | 352 | #### Description 353 | 354 | Invoke an executable method injected by the field model via inject 355 | 356 | #### Signature 357 | 358 | ```ts 359 | interface invoke { 360 | (name: string, ...args: any[]): any 361 | } 362 | ``` 363 | 364 | ## Types of 365 | 366 | <Alert> 367 | Note: If you want to manually consume the type, just export it directly from the package module 368 | </Alert> 369 | 370 | ### FieldMessage 371 | 372 | ```ts 373 | type FieldMessage = string | JSXElement 374 | ``` 375 | 376 | If under the UI framework that supports JSX, we can directly pass the Node of JSX, otherwise, we can only pass the string 377 | 378 | ### FieldComponent 379 | 380 | ```ts 381 | type FieldComponent = string | JSXComponentConstructor 382 | ``` 383 | 384 | Field component, if we use it in a framework that supports JSX, FieldComponent recommends to store the JSX component reference directly, otherwise it can store a component identification string and distribute it during actual rendering. 385 | 386 | ### FieldDecorator 387 | 388 | ```ts 389 | type FieldDecorator = string | JSXComponentConstructor 390 | ``` 391 | 392 | Field decorator, if we use it in a framework that supports JSX, FieldDecorator recommends to store the JSX component reference directly, otherwise it can store a component identification string and distribute it during actual rendering. 393 | 394 | ### FieldReaction 395 | 396 | ```ts 397 | type FieldReaction = (field: GeneralField) => void 398 | ``` 399 | 400 | ### FieldDisplayTypes 401 | 402 | ```ts 403 | type FieldDisplayTypes = 'none' | 'hidden' | 'visible' 404 | ``` 405 | 406 | ### FieldPatternTypes 407 | 408 | ```ts 409 | type FieldPatternTypes = 'editable' | 'disabled' | 'readOnly' | 'readPretty' 410 | ``` 411 | 412 | ### GeneralField 413 | 414 | ```ts 415 | type GeneralField = Field | VoidField | ArrayField | ObjectField 416 | ``` 417 | 418 | Field Reference [Field](/api/models/field) 419 | 420 | ArrayField Reference [ArrayField](/api/models/array-field) 421 | 422 | ObjectField Reference [ObjectField](/api/models/object-field) 423 | 424 | ### IVoidFieldState 425 | 426 | ```ts 427 | interface IVoidFieldState { 428 | hidden?: boolean 429 | visible?: boolean 430 | editable?: boolean 431 | readOnly?: boolean 432 | disabled?: boolean 433 | readPretty?: boolean 434 | title?: any 435 | description?: any 436 | modified?: boolean 437 | active?: boolean 438 | visited?: boolean 439 | initialized?: boolean 440 | mounted?: boolean 441 | unmounted?: boolean 442 | decorator?: FieldDecorator 443 | component?: FieldComponent 444 | readonly parent?: GeneralField 445 | display?: FieldDisplayTypes 446 | pattern?: FieldPatternTypes 447 | } 448 | ``` 449 | 450 | ### IGeneralFieldState 451 | 452 | ```ts 453 | type IGeneralFieldState = IVoidFieldState & IFieldState 454 | ``` 455 | 456 | IFieldState Reference [IFieldState](/api/models/field#ifieldstate) 457 | ``` -------------------------------------------------------------------------------- /packages/json-schema/src/__tests__/transformer.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Schema } from '../schema' 2 | import { createForm } from '@formily/core' 3 | import { isObservable } from '@formily/reactive' 4 | import { ISchema, ISchemaTransformerOptions } from '../types' 5 | 6 | const attach = <T extends { onMount: () => void }>(target: T): T => { 7 | target.onMount() 8 | return target 9 | } 10 | 11 | const getFormAndFields = ( 12 | field1SchemaProps: Omit<ISchema, 'name'> = {}, 13 | field2SchemaProps: Omit<ISchema, 'name'> = {}, 14 | options: ISchemaTransformerOptions = {} 15 | ) => { 16 | const filed1Schema = new Schema({ 17 | name: 'field1', 18 | ...field1SchemaProps, 19 | }).toFieldProps(options) 20 | 21 | const filed2Schema = new Schema({ 22 | name: 'field2', 23 | ...field2SchemaProps, 24 | }).toFieldProps(options) 25 | 26 | const form = createForm() 27 | const field1 = form.createField(filed1Schema) 28 | const field2 = form.createField(filed2Schema) 29 | 30 | return { 31 | form, 32 | field1, 33 | field2, 34 | } 35 | } 36 | 37 | test('baseReaction', () => { 38 | const { field1, field2 } = getFormAndFields( 39 | { 40 | title: 'field1Title', 41 | }, 42 | { 43 | title: 'field2Title', 44 | } 45 | ) 46 | 47 | expect(field1.title).toBe('field1Title') 48 | expect(field2.title).toBe('field2Title') 49 | }) 50 | 51 | test('baseReaction with scopes', () => { 52 | const scopeTitle = 'fieldTitle' 53 | const scopeDescription = 'fieldDescription' 54 | 55 | const { field1, field2 } = getFormAndFields( 56 | { 57 | title: '{{scopeTitle}}', 58 | }, 59 | { 60 | description: '{{scopeDescription}}', 61 | }, 62 | { 63 | scope: { 64 | scopeTitle, 65 | scopeDescription, 66 | }, 67 | } 68 | ) 69 | 70 | expect(field1.title).toBe(scopeTitle) 71 | expect(field2.description).toBe(scopeDescription) 72 | }) 73 | 74 | test('userReactions with target(state)', () => { 75 | const field2Title = 'field2Title' 76 | const { field2 } = getFormAndFields({ 77 | 'x-reactions': { 78 | target: 'field2', 79 | fulfill: { 80 | state: { 81 | title: field2Title, 82 | }, 83 | }, 84 | }, 85 | }) 86 | 87 | expect(field2.title).toBe(field2Title) 88 | }) 89 | 90 | test('userReactions with target(schema)', () => { 91 | const field2Data = 'fieldData' 92 | const { field2 } = getFormAndFields({ 93 | 'x-reactions': { 94 | target: 'field2', 95 | fulfill: { 96 | schema: { 97 | 'x-data': field2Data, 98 | }, 99 | }, 100 | }, 101 | }) 102 | 103 | expect(field2.data).toBe(field2Data) 104 | }) 105 | 106 | test('userReactions with target(runner)', () => { 107 | const mockFn = jest.fn() 108 | const field2Title = 'field2Title' 109 | const { field2 } = getFormAndFields( 110 | { 111 | 'x-reactions': { 112 | target: 'field2', 113 | fulfill: { 114 | run: `$target.title='${field2Title}';fn()`, 115 | }, 116 | }, 117 | }, 118 | {}, 119 | { 120 | scope: { 121 | fn: mockFn, 122 | }, 123 | } 124 | ) 125 | 126 | expect(mockFn).toBeCalledTimes(1) 127 | expect(field2.title).toBe(field2Title) 128 | }) 129 | 130 | test('userReactions without target(state)', () => { 131 | const field1Title = 'field1Title' 132 | const { field1 } = getFormAndFields({ 133 | 'x-reactions': { 134 | fulfill: { 135 | state: { 136 | title: field1Title, 137 | }, 138 | }, 139 | }, 140 | }) 141 | 142 | expect(field1.title).toBe(field1Title) 143 | }) 144 | 145 | test('userReactions without target(schema)', () => { 146 | const field1Data = 'fieldData' 147 | const { field1 } = getFormAndFields({ 148 | 'x-reactions': { 149 | fulfill: { 150 | schema: { 151 | 'x-data': field1Data, 152 | }, 153 | }, 154 | }, 155 | }) 156 | 157 | expect(field1.data).toBe(field1Data) 158 | }) 159 | 160 | test('userReactions without target(runner)', () => { 161 | const mockFn = jest.fn() 162 | const { field1 } = getFormAndFields( 163 | { 164 | 'x-reactions': { 165 | fulfill: { 166 | run: `$self.__target__=$target;fn()`, 167 | }, 168 | }, 169 | }, 170 | {}, 171 | { 172 | scope: { 173 | fn: mockFn, 174 | }, 175 | } 176 | ) 177 | 178 | expect(mockFn).toBeCalledTimes(1) 179 | expect((field1 as any).__target__).toBe(null) 180 | }) 181 | 182 | test('userReactions with condition', () => { 183 | const mockFn = jest.fn() 184 | const { field1 } = getFormAndFields( 185 | { 186 | 'x-value': true, 187 | 'x-reactions': { 188 | when: '$self.value===true', 189 | fulfill: { 190 | run: 'mockFn($self.value)', 191 | }, 192 | otherwise: { 193 | run: 'mockFn($self.value)', 194 | }, 195 | }, 196 | }, 197 | {}, 198 | { 199 | scope: { 200 | mockFn, 201 | }, 202 | } 203 | ) 204 | 205 | expect(mockFn).nthCalledWith(1, true) 206 | 207 | field1.value = false 208 | 209 | expect(mockFn).nthCalledWith(2, false) 210 | }) 211 | 212 | test('userReactions with condition(wrong type)', () => { 213 | const field1Value = 'field1Value' 214 | const mockFn = jest.fn() 215 | getFormAndFields( 216 | { 217 | 'x-value': field1Value, 218 | 'x-reactions': { 219 | dependencies: 'value', 220 | fulfill: { 221 | run: 'mockFn($deps, $dependencies)', 222 | }, 223 | }, 224 | }, 225 | {}, 226 | { 227 | scope: { 228 | mockFn, 229 | }, 230 | } 231 | ) 232 | 233 | expect(mockFn).nthCalledWith(1, [], []) 234 | }) 235 | 236 | test('userReactions with condition(array)', () => { 237 | const field1Value = 'field1Value' 238 | const field2Value = 'field2Value' 239 | const field1Title = 'field1Title' 240 | const field1Description = 'field1Description' 241 | const mockFn = jest.fn() 242 | 243 | getFormAndFields( 244 | { 245 | title: field1Title, 246 | description: field1Description, 247 | 'x-value': field1Value, 248 | }, 249 | { 250 | 'x-value': field2Value, 251 | 'x-reactions': { 252 | dependencies: [ 253 | 'field2', 254 | { 255 | name: 1, 256 | source: 'field1', 257 | }, 258 | { 259 | name: 2, 260 | source: 'field1#title', 261 | }, 262 | { 263 | name: 3, 264 | source: 'field1', 265 | property: 'description', 266 | }, 267 | ], 268 | fulfill: { 269 | run: `mockFn($deps)`, 270 | }, 271 | }, 272 | }, 273 | { 274 | scope: { 275 | mockFn, 276 | }, 277 | } 278 | ) 279 | 280 | expect(mockFn).nthCalledWith(1, [ 281 | field2Value, 282 | field1Value, 283 | field1Title, 284 | field1Description, 285 | ]) 286 | }) 287 | 288 | test('userReactions with condition(object)', () => { 289 | const field2Value = 'field2Value' 290 | const field1Title = 'field1Title' 291 | const mockFn = jest.fn() 292 | 293 | getFormAndFields( 294 | { 295 | title: field1Title, 296 | }, 297 | { 298 | 'x-value': field2Value, 299 | 'x-reactions': { 300 | dependencies: { 301 | key1: 'field1#title', 302 | key2: 'field2', 303 | }, 304 | fulfill: { 305 | run: `mockFn($deps)`, 306 | }, 307 | }, 308 | }, 309 | { 310 | scope: { 311 | mockFn, 312 | }, 313 | } 314 | ) 315 | 316 | expect(mockFn).nthCalledWith(1, { 317 | key1: field1Title, 318 | key2: field2Value, 319 | }) 320 | }) 321 | 322 | test('userReactions with user-defined effects', () => { 323 | const field2Value = 'field2Value' 324 | const field1Title = 'field1Title' 325 | const mockFn = jest.fn() 326 | 327 | const { field2 } = getFormAndFields( 328 | { 329 | title: field1Title, 330 | 'x-reactions': { 331 | target: 'field2', 332 | fulfill: { 333 | run: `mockFn($target.value)`, 334 | }, 335 | effects: ['onFieldInit'], 336 | }, 337 | }, 338 | { 339 | 'x-value': field2Value, 340 | }, 341 | { 342 | scope: { 343 | mockFn, 344 | }, 345 | } 346 | ) 347 | 348 | expect(mockFn).toBeCalledTimes(1) 349 | expect(mockFn).nthCalledWith(1, field2Value) 350 | 351 | field2.value = field1Title 352 | expect(mockFn).toBeCalledTimes(1) 353 | }) 354 | 355 | test('userReactions with function type', () => { 356 | const componentProps = { 357 | prop: 1, 358 | } 359 | let observable: any = {} 360 | const { field1 } = getFormAndFields({ 361 | 'x-reactions': (field, baseScope) => { 362 | baseScope.$props(componentProps) 363 | observable = baseScope.$observable({}) 364 | }, 365 | }) 366 | 367 | expect(field1.componentProps).toMatchObject(componentProps) 368 | expect(isObservable(observable)).toBe(true) 369 | }) 370 | 371 | test('userReactions with $lookup $record $records $index', () => { 372 | const initialValues = { 373 | array: [ 374 | { a: 1, b: 2 }, 375 | { a: 3, b: 4 }, 376 | ], 377 | } 378 | const form = attach( 379 | createForm({ 380 | initialValues, 381 | }) 382 | ) 383 | 384 | form.createArrayField({ 385 | name: 'array', 386 | }) 387 | form.createObjectField({ 388 | name: '0', 389 | basePath: 'array', 390 | }) 391 | form.createObjectField({ 392 | name: '1', 393 | basePath: 'array', 394 | }) 395 | 396 | const field0aSchema = new Schema({ 397 | name: 'array.0.a', 398 | 'x-reactions': `{{$self.title = $record.b}}`, 399 | }).toFieldProps({}) 400 | 401 | const field0bSchema = new Schema({ 402 | name: 'array.0.b', 403 | 'x-reactions': '{{$self.title = $lookup.array[0].a}}', 404 | }).toFieldProps({}) 405 | 406 | const field1aSchema = new Schema({ 407 | name: 'array.1.a', 408 | 'x-reactions': '{{$self.title = $records[$index].b}}', 409 | }).toFieldProps({}) 410 | 411 | const field1bSchema = new Schema({ 412 | name: 'array.1.b', 413 | 'x-reactions': `{{$self.title = $record.$lookup.array[$record.$index].a}}`, 414 | }).toFieldProps({}) 415 | 416 | const field0a = attach(form.createField(field0aSchema)) 417 | const field0b = attach(form.createField(field0bSchema)) 418 | const field1a = attach(form.createField(field1aSchema)) 419 | const field1b = attach(form.createField(field1bSchema)) 420 | 421 | expect(field0a.title).toEqual(2) 422 | expect(field0b.title).toEqual(1) 423 | expect(field1a.title).toEqual(4) 424 | expect(field1b.title).toEqual(3) 425 | }) 426 | 427 | test('userReactions with primary type record', () => { 428 | const initialValues = { 429 | array: [1, 2, 3], 430 | } 431 | 432 | const form = attach( 433 | createForm({ 434 | initialValues, 435 | }) 436 | ) 437 | 438 | const field0Schema = new Schema({ 439 | name: 'array.0', 440 | 'x-reactions': `{{$self.title = $record}}`, 441 | }).toFieldProps({}) 442 | 443 | const field1Schema = new Schema({ 444 | name: 'array.1', 445 | 'x-reactions': '{{$self.title = $record}}', 446 | }).toFieldProps({}) 447 | 448 | form.createArrayField({ 449 | name: 'array', 450 | }) 451 | const field0 = attach(form.createField(field0Schema)) 452 | const field1 = attach(form.createField(field1Schema)) 453 | expect(field0.title).toEqual(1) 454 | expect(field1.title).toEqual(2) 455 | }) 456 | ``` -------------------------------------------------------------------------------- /packages/next/docs/components/Select.zh-CN.md: -------------------------------------------------------------------------------- ```markdown 1 | # Select 2 | 3 | > 下拉框组件 4 | 5 | ## Markup Schema 同步数据源案例 6 | 7 | ```tsx 8 | import React from 'react' 9 | import { Select, FormItem, FormButtonGroup, Submit } from '@formily/next' 10 | import { createForm } from '@formily/core' 11 | import { FormProvider, createSchemaField } from '@formily/react' 12 | 13 | const SchemaField = createSchemaField({ 14 | components: { 15 | Select, 16 | FormItem, 17 | }, 18 | }) 19 | 20 | const form = createForm() 21 | 22 | export default () => ( 23 | <FormProvider form={form}> 24 | <SchemaField> 25 | <SchemaField.Number 26 | name="select" 27 | title="选择框" 28 | x-decorator="FormItem" 29 | x-component="Select" 30 | enum={[ 31 | { label: '选项1', value: 1 }, 32 | { label: '选项2', value: 2 }, 33 | ]} 34 | x-component-props={{ 35 | style: { 36 | width: 120, 37 | }, 38 | }} 39 | /> 40 | </SchemaField> 41 | <FormButtonGroup> 42 | <Submit onSubmit={console.log}>提交</Submit> 43 | </FormButtonGroup> 44 | </FormProvider> 45 | ) 46 | ``` 47 | 48 | ## Markup Schema 异步联动数据源案例 49 | 50 | ```tsx 51 | import React from 'react' 52 | import { Select, FormItem, FormButtonGroup, Submit } from '@formily/next' 53 | import { createForm, onFieldReact, FormPathPattern, Field } from '@formily/core' 54 | import { FormProvider, createSchemaField } from '@formily/react' 55 | import { action } from '@formily/reactive' 56 | 57 | const SchemaField = createSchemaField({ 58 | components: { 59 | Select, 60 | FormItem, 61 | }, 62 | }) 63 | 64 | const useAsyncDataSource = ( 65 | pattern: FormPathPattern, 66 | service: (field: Field) => Promise<{ label: string; value: any }[]> 67 | ) => { 68 | onFieldReact(pattern, (field) => { 69 | field.loading = true 70 | service(field).then( 71 | action.bound((data) => { 72 | field.dataSource = data 73 | field.loading = false 74 | }) 75 | ) 76 | }) 77 | } 78 | 79 | const form = createForm({ 80 | effects: () => { 81 | useAsyncDataSource('select', async (field) => { 82 | const linkage = field.query('linkage').get('value') 83 | if (!linkage) return [] 84 | return new Promise((resolve) => { 85 | setTimeout(() => { 86 | if (linkage === 1) { 87 | resolve([ 88 | { 89 | label: 'AAA', 90 | value: 'aaa', 91 | }, 92 | { 93 | label: 'BBB', 94 | value: 'ccc', 95 | }, 96 | ]) 97 | } else if (linkage === 2) { 98 | resolve([ 99 | { 100 | label: 'CCC', 101 | value: 'ccc', 102 | }, 103 | { 104 | label: 'DDD', 105 | value: 'ddd', 106 | }, 107 | ]) 108 | } 109 | }, 1500) 110 | }) 111 | }) 112 | }, 113 | }) 114 | 115 | export default () => ( 116 | <FormProvider form={form}> 117 | <SchemaField> 118 | <SchemaField.Number 119 | name="linkage" 120 | title="联动选择框" 121 | x-decorator="FormItem" 122 | x-component="Select" 123 | enum={[ 124 | { label: '发请求1', value: 1 }, 125 | { label: '发请求2', value: 2 }, 126 | ]} 127 | x-component-props={{ 128 | style: { 129 | width: 120, 130 | }, 131 | }} 132 | /> 133 | <SchemaField.String 134 | name="select" 135 | title="异步选择框" 136 | x-decorator="FormItem" 137 | x-component="Select" 138 | x-component-props={{ 139 | style: { 140 | width: 120, 141 | }, 142 | }} 143 | /> 144 | </SchemaField> 145 | <FormButtonGroup> 146 | <Submit onSubmit={console.log}>提交</Submit> 147 | </FormButtonGroup> 148 | </FormProvider> 149 | ) 150 | ``` 151 | 152 | ## JSON Schema 同步数据源案例 153 | 154 | ```tsx 155 | import React from 'react' 156 | import { Select, FormItem, FormButtonGroup, Submit } from '@formily/next' 157 | import { createForm } from '@formily/core' 158 | import { FormProvider, createSchemaField } from '@formily/react' 159 | 160 | const SchemaField = createSchemaField({ 161 | components: { 162 | Select, 163 | FormItem, 164 | }, 165 | }) 166 | 167 | const form = createForm() 168 | 169 | const schema = { 170 | type: 'object', 171 | properties: { 172 | select: { 173 | type: 'string', 174 | title: '选择框', 175 | 'x-decorator': 'FormItem', 176 | 'x-component': 'Select', 177 | enum: [ 178 | { label: '选项1', value: 1 }, 179 | { label: '选项2', value: 2 }, 180 | ], 181 | 'x-component-props': { 182 | style: { 183 | width: 120, 184 | }, 185 | }, 186 | }, 187 | }, 188 | } 189 | 190 | export default () => ( 191 | <FormProvider form={form}> 192 | <SchemaField schema={schema} /> 193 | <FormButtonGroup> 194 | <Submit onSubmit={console.log}>提交</Submit> 195 | </FormButtonGroup> 196 | </FormProvider> 197 | ) 198 | ``` 199 | 200 | ## JSON Schema 异步联动数据源案例 201 | 202 | ```tsx 203 | import React from 'react' 204 | import { Select, FormItem, FormButtonGroup, Submit } from '@formily/next' 205 | import { createForm } from '@formily/core' 206 | import { FormProvider, createSchemaField } from '@formily/react' 207 | import { action } from '@formily/reactive' 208 | 209 | const SchemaField = createSchemaField({ 210 | components: { 211 | Select, 212 | FormItem, 213 | }, 214 | }) 215 | 216 | const loadData = async (field) => { 217 | const linkage = field.query('linkage').get('value') 218 | if (!linkage) return [] 219 | return new Promise((resolve) => { 220 | setTimeout(() => { 221 | if (linkage === 1) { 222 | resolve([ 223 | { 224 | label: 'AAA', 225 | value: 'aaa', 226 | }, 227 | { 228 | label: 'BBB', 229 | value: 'ccc', 230 | }, 231 | ]) 232 | } else if (linkage === 2) { 233 | resolve([ 234 | { 235 | label: 'CCC', 236 | value: 'ccc', 237 | }, 238 | { 239 | label: 'DDD', 240 | value: 'ddd', 241 | }, 242 | ]) 243 | } 244 | }, 1500) 245 | }) 246 | } 247 | 248 | const useAsyncDataSource = (service) => (field) => { 249 | field.loading = true 250 | service(field).then( 251 | action.bound((data) => { 252 | field.dataSource = data 253 | field.loading = false 254 | }) 255 | ) 256 | } 257 | 258 | const form = createForm() 259 | 260 | const schema = { 261 | type: 'object', 262 | properties: { 263 | linkage: { 264 | type: 'string', 265 | title: '联动选择框', 266 | enum: [ 267 | { label: '发请求1', value: 1 }, 268 | { label: '发请求2', value: 2 }, 269 | ], 270 | 'x-decorator': 'FormItem', 271 | 'x-component': 'Select', 272 | 'x-component-props': { 273 | style: { 274 | width: 120, 275 | }, 276 | }, 277 | }, 278 | select: { 279 | type: 'string', 280 | title: '异步选择框', 281 | 'x-decorator': 'FormItem', 282 | 'x-component': 'Select', 283 | 'x-component-props': { 284 | style: { 285 | width: 120, 286 | }, 287 | }, 288 | 'x-reactions': ['{{useAsyncDataSource(loadData)}}'], 289 | }, 290 | }, 291 | } 292 | 293 | export default () => ( 294 | <FormProvider form={form}> 295 | <SchemaField schema={schema} scope={{ useAsyncDataSource, loadData }} /> 296 | <FormButtonGroup> 297 | <Submit onSubmit={console.log}>提交</Submit> 298 | </FormButtonGroup> 299 | </FormProvider> 300 | ) 301 | ``` 302 | 303 | ## 纯 JSX 同步数据源案例 304 | 305 | ```tsx 306 | import React from 'react' 307 | import { Select, FormItem, FormButtonGroup, Submit } from '@formily/next' 308 | import { createForm } from '@formily/core' 309 | import { FormProvider, Field } from '@formily/react' 310 | 311 | const form = createForm() 312 | 313 | export default () => ( 314 | <FormProvider form={form}> 315 | <Field 316 | name="select" 317 | title="选择框" 318 | dataSource={[ 319 | { label: '选项1', value: 1 }, 320 | { label: '选项2', value: 2 }, 321 | ]} 322 | decorator={[FormItem]} 323 | component={[ 324 | Select, 325 | { 326 | style: { 327 | width: 120, 328 | }, 329 | }, 330 | ]} 331 | /> 332 | <FormButtonGroup> 333 | <Submit onSubmit={console.log}>提交</Submit> 334 | </FormButtonGroup> 335 | </FormProvider> 336 | ) 337 | ``` 338 | 339 | ## 纯 JSX 异步联动数据源案例 340 | 341 | ```tsx 342 | import React from 'react' 343 | import { Select, FormItem, FormButtonGroup, Submit } from '@formily/next' 344 | import { 345 | createForm, 346 | onFieldReact, 347 | FormPathPattern, 348 | FieldType, 349 | } from '@formily/core' 350 | import { FormProvider, Field } from '@formily/react' 351 | import { action } from '@formily/reactive' 352 | 353 | const useAsyncDataSource = ( 354 | pattern: FormPathPattern, 355 | service: (field: FieldType) => Promise<{ label: string; value: any }[]> 356 | ) => { 357 | onFieldReact(pattern, (field) => { 358 | field.loading = true 359 | service(field).then( 360 | action.bound((data) => { 361 | field.dataSource = data 362 | field.loading = false 363 | }) 364 | ) 365 | }) 366 | } 367 | 368 | const form = createForm({ 369 | effects: () => { 370 | useAsyncDataSource('select', async (field) => { 371 | const linkage = field.query('linkage').get('value') 372 | if (!linkage) return [] 373 | return new Promise((resolve) => { 374 | setTimeout(() => { 375 | if (linkage === 1) { 376 | resolve([ 377 | { 378 | label: 'AAA', 379 | value: 'aaa', 380 | }, 381 | { 382 | label: 'BBB', 383 | value: 'ccc', 384 | }, 385 | ]) 386 | } else if (linkage === 2) { 387 | resolve([ 388 | { 389 | label: 'CCC', 390 | value: 'ccc', 391 | }, 392 | { 393 | label: 'DDD', 394 | value: 'ddd', 395 | }, 396 | ]) 397 | } 398 | }, 1500) 399 | }) 400 | }) 401 | }, 402 | }) 403 | 404 | export default () => ( 405 | <FormProvider form={form}> 406 | <Field 407 | name="linkage" 408 | title="联动选择框" 409 | dataSource={[ 410 | { label: '发请求1', value: 1 }, 411 | { label: '发请求2', value: 2 }, 412 | ]} 413 | decorator={[FormItem]} 414 | component={[ 415 | Select, 416 | { 417 | style: { 418 | width: 120, 419 | }, 420 | }, 421 | ]} 422 | /> 423 | <Field 424 | name="select" 425 | title="异步选择框" 426 | decorator={[FormItem]} 427 | component={[ 428 | Select, 429 | { 430 | style: { 431 | width: 120, 432 | }, 433 | }, 434 | ]} 435 | /> 436 | <FormButtonGroup> 437 | <Submit onSubmit={console.log}>提交</Submit> 438 | </FormButtonGroup> 439 | </FormProvider> 440 | ) 441 | ``` 442 | 443 | ## API 444 | 445 | 参考 https://fusion.design/pc/component/basic/select 446 | ```